diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 07120f15b0..d7a95ec095 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -37,7 +37,7 @@ env: GOLANG_VERSION: "1.23.x" KUBEBUILDER_VERSION: "2.3.1" KIND_VERSION: "v0.24.0" - ROOK_VERSION: "v1.15.2" + ROOK_VERSION: "v1.15.3" EXTERNAL_SNAPSHOTTER_VERSION: "v8.1.0" OPERATOR_IMAGE_NAME: "ghcr.io/${{ github.repository }}-testing" BUILD_PUSH_PROVENANCE: "" diff --git a/.wordlist-en-custom.txt b/.wordlist-en-custom.txt index 26de5d448c..b4188b65aa 100644 --- a/.wordlist-en-custom.txt +++ b/.wordlist-en-custom.txt @@ -500,6 +500,7 @@ allowPrivilegeEscalation allowVolumeExpansion amd angus +anonymization api apiGroup apiGroups @@ -795,6 +796,7 @@ http httpGet https hugepages +icu ident imageCatalogRef imageName diff --git a/api/v1/backup_funcs.go b/api/v1/backup_funcs.go index 9c56e8503d..c41e09ee12 100644 --- a/api/v1/backup_funcs.go +++ b/api/v1/backup_funcs.go @@ -24,6 +24,7 @@ import ( volumesnapshot "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -230,6 +231,17 @@ func (backup *Backup) GetVolumeSnapshotConfiguration( return config } +// EnsureGVKIsPresent ensures that the GroupVersionKind (GVK) metadata is present in the Backup object. +// This is necessary because informers do not automatically include metadata inside the object. +// By setting the GVK, we ensure that components such as the plugins have enough metadata to typecheck the object. +func (backup *Backup) EnsureGVKIsPresent() { + backup.SetGroupVersionKind(schema.GroupVersionKind{ + Group: GroupVersion.Group, + Version: GroupVersion.Version, + Kind: BackupKind, + }) +} + // IsEmpty checks if the plugin configuration is empty or not func (configuration *BackupPluginConfiguration) IsEmpty() bool { return configuration == nil || len(configuration.Name) == 0 diff --git a/api/v1/cluster_funcs.go b/api/v1/cluster_funcs.go index 41206f8f21..f3caf8d59c 100644 --- a/api/v1/cluster_funcs.go +++ b/api/v1/cluster_funcs.go @@ -28,13 +28,13 @@ import ( "github.com/cloudnative-pg/machinery/pkg/image/reference" "github.com/cloudnative-pg/machinery/pkg/log" "github.com/cloudnative-pg/machinery/pkg/postgres/version" + "github.com/cloudnative-pg/machinery/pkg/stringset" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "github.com/cloudnative-pg/cloudnative-pg/internal/configuration" - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" "github.com/cloudnative-pg/cloudnative-pg/pkg/system" "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" "github.com/cloudnative-pg/cloudnative-pg/pkg/versions" diff --git a/api/v1/cluster_funcs_test.go b/api/v1/cluster_funcs_test.go index 7d459faca0..8d6f0950ac 100644 --- a/api/v1/cluster_funcs_test.go +++ b/api/v1/cluster_funcs_test.go @@ -22,12 +22,12 @@ import ( barmanCatalog "github.com/cloudnative-pg/barman-cloud/pkg/catalog" "github.com/cloudnative-pg/machinery/pkg/postgres/version" + "github.com/cloudnative-pg/machinery/pkg/stringset" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" . "github.com/onsi/ginkgo/v2" diff --git a/api/v1/cluster_webhook.go b/api/v1/cluster_webhook.go index 16b8c73c52..197358d786 100644 --- a/api/v1/cluster_webhook.go +++ b/api/v1/cluster_webhook.go @@ -27,6 +27,7 @@ import ( "github.com/cloudnative-pg/machinery/pkg/image/reference" "github.com/cloudnative-pg/machinery/pkg/log" "github.com/cloudnative-pg/machinery/pkg/postgres/version" + "github.com/cloudnative-pg/machinery/pkg/stringset" "github.com/cloudnative-pg/machinery/pkg/types" storagesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" v1 "k8s.io/api/core/v1" @@ -44,7 +45,6 @@ import ( "github.com/cloudnative-pg/cloudnative-pg/internal/configuration" "github.com/cloudnative-pg/cloudnative-pg/pkg/postgres" - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" ) diff --git a/api/v1/database_types.go b/api/v1/database_types.go index 8cb52ad810..dd7bd58cf5 100644 --- a/api/v1/database_types.go +++ b/api/v1/database_types.go @@ -42,6 +42,9 @@ type DatabaseSpec struct { // The name inside PostgreSQL // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" + // +kubebuilder:validation:XValidation:rule="self != 'postgres'",message="the name postgres is reserved" + // +kubebuilder:validation:XValidation:rule="self != 'template0'",message="the name template0 is reserved" + // +kubebuilder:validation:XValidation:rule="self != 'template1'",message="the name template1 is reserved" Name string `json:"name"` // The owner @@ -57,6 +60,36 @@ type DatabaseSpec struct { // +optional Encoding string `json:"encoding,omitempty"` + // The locale (cannot be changed) + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="locale is immutable" + // +optional + Locale string `json:"locale,omitempty"` + + // The locale provider (cannot be changed) + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="locale_provider is immutable" + // +optional + LocaleProvider string `json:"locale_provider,omitempty"` + + // The LC_COLLATE (cannot be changed) + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="lc_collate is immutable" + // +optional + LcCollate string `json:"lc_collate,omitempty"` + + // The LC_CTYPE (cannot be changed) + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="lc_ctype is immutable" + // +optional + LcCtype string `json:"lc_ctype,omitempty"` + + // The ICU_LOCALE (cannot be changed) + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="icu_locale is immutable" + // +optional + IcuLocale string `json:"icu_locale,omitempty"` + + // The ICU_RULES (cannot be changed) + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="icu_rules is immutable" + // +optional + IcuRules string `json:"icu_rules,omitempty"` + // True when the database is a template // +optional IsTemplate *bool `json:"isTemplate,omitempty"` diff --git a/api/v1/pooler_webhook.go b/api/v1/pooler_webhook.go index c82205ed0c..24241a836a 100644 --- a/api/v1/pooler_webhook.go +++ b/api/v1/pooler_webhook.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/cloudnative-pg/machinery/pkg/log" + "github.com/cloudnative-pg/machinery/pkg/stringset" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -27,8 +28,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" ) var ( diff --git a/config/crd/bases/postgresql.cnpg.io_databases.yaml b/config/crd/bases/postgresql.cnpg.io_databases.yaml index b348c25dd9..f49202505e 100644 --- a/config/crd/bases/postgresql.cnpg.io_databases.yaml +++ b/config/crd/bases/postgresql.cnpg.io_databases.yaml @@ -93,15 +93,57 @@ spec: x-kubernetes-validations: - message: encoding is immutable rule: self == oldSelf + icu_locale: + description: The ICU_LOCALE (cannot be changed) + type: string + x-kubernetes-validations: + - message: icu_locale is immutable + rule: self == oldSelf + icu_rules: + description: The ICU_RULES (cannot be changed) + type: string + x-kubernetes-validations: + - message: icu_rules is immutable + rule: self == oldSelf isTemplate: description: True when the database is a template type: boolean + lc_collate: + description: The LC_COLLATE (cannot be changed) + type: string + x-kubernetes-validations: + - message: lc_collate is immutable + rule: self == oldSelf + lc_ctype: + description: The LC_CTYPE (cannot be changed) + type: string + x-kubernetes-validations: + - message: lc_ctype is immutable + rule: self == oldSelf + locale: + description: The locale (cannot be changed) + type: string + x-kubernetes-validations: + - message: locale is immutable + rule: self == oldSelf + locale_provider: + description: The locale provider (cannot be changed) + type: string + x-kubernetes-validations: + - message: locale_provider is immutable + rule: self == oldSelf name: description: The name inside PostgreSQL type: string x-kubernetes-validations: - message: name is immutable rule: self == oldSelf + - message: the name postgres is reserved + rule: self != 'postgres' + - message: the name template0 is reserved + rule: self != 'template0' + - message: the name template1 is reserved + rule: self != 'template1' owner: description: The owner type: string diff --git a/docs/src/bootstrap.md b/docs/src/bootstrap.md index 3cf3cb41b1..87525b4679 100644 --- a/docs/src/bootstrap.md +++ b/docs/src/bootstrap.md @@ -389,44 +389,59 @@ to the ["Recovery" section](recovery.md). ### Bootstrap from a live cluster (`pg_basebackup`) -The `pg_basebackup` bootstrap mode lets you create a new cluster (*target*) as -an exact physical copy of an existing and **binary compatible** PostgreSQL -instance (*source*), through a valid *streaming replication* connection. -The source instance can be either a primary or a standby PostgreSQL server. +The `pg_basebackup` bootstrap mode allows you to create a new cluster +(*target*) as an exact physical copy of an existing and **binary-compatible** +PostgreSQL instance (*source*) managed by CloudNativePG, using a valid +*streaming replication* connection. The source instance can either be a primary +or a standby PostgreSQL server. It’s crucial to thoroughly review the +requirements section below, as the pros and cons of PostgreSQL physical +replication fully apply. + +The primary use cases for this method include: + +- Reporting and business intelligence clusters that need to be regenerated + periodically (daily, weekly) +- Test databases containing live data that require periodic regeneration + (daily, weekly, monthly) and anonymization +- Rapid spin-up of a standalone replica cluster +- Physical migrations of CloudNativePG clusters to different namespaces or + Kubernetes clusters -The primary use case for this method is represented by **migrations** to CloudNativePG, -either from outside Kubernetes or within Kubernetes (e.g., from another operator). +!!! Important + Avoid using this method, based on physical replication, to migrate an + existing PostgreSQL cluster outside of Kubernetes into CloudNativePG unless you + are completely certain that all requirements are met and the operation has been + thoroughly tested. The CloudNativePG community does not endorse this approach + for such use cases and recommends using logical import instead. It is + exceedingly rare that all requirements for physical replication are met in a + way that seamlessly works with CloudNativePG. !!! Warning - The current implementation creates a *snapshot* of the origin PostgreSQL - instance when the cloning process terminates and immediately starts - the created cluster. See ["Current limitations"](#current-limitations) below for details. - -Similar to the case of the `recovery` bootstrap method, once the clone operation -completes, the operator will take ownership of the target cluster, starting from -the first instance. This includes overriding some configuration parameters, as -required by CloudNativePG, resetting the superuser password, creating -the `streaming_replica` user, managing the replicas, and so on. The resulting -cluster will be completely independent of the source instance. + In its current implementation, this method clones the source PostgreSQL + instance, thereby creating a *snapshot*. Once the cloning process has finished, + the new cluster is immediately started. + Refer to ["Current limitations"](#current-limitations) for more details. + +Similar to the `recovery` bootstrap method, once the cloning operation is +complete, the operator takes full ownership of the target cluster, starting +from the first instance. This includes overriding certain configuration +parameters as required by CloudNativePG, resetting the superuser password, +creating the `streaming_replica` user, managing replicas, and more. The +resulting cluster operates independently from the source instance. !!! Important - Configuring the network between the target instance and the source instance - goes beyond the scope of CloudNativePG documentation, as it depends - on the actual context and environment. + Configuring the network connection between the target and source instances + lies outside the scope of CloudNativePG documentation, as it depends heavily on + the specific context and environment. -The streaming replication client on the target instance, which will be -transparently managed by `pg_basebackup`, can authenticate itself on the source -instance in any of the following ways: +The streaming replication client on the target instance, managed transparently +by `pg_basebackup`, can authenticate on the source instance using one of the +following methods: -1. via [username/password](#usernamepassword-authentication) -2. via [TLS client certificate](#tls-certificate-authentication) +1. [Username/password](#usernamepassword-authentication) +2. [TLS client certificate](#tls-certificate-authentication) -The latter is the recommended one if you connect to a source managed -by CloudNativePG or configured for TLS authentication. -The first option is, however, the most common form of authentication to a -PostgreSQL server in general, and might be the easiest way if the source -instance is on a traditional environment outside Kubernetes. -Both cases are explained below. +Both authentication methods are detailed below. #### Requirements @@ -650,7 +665,7 @@ instance using a second connection (see the `--wal-method=stream` option for Once the backup is completed, the new instance will be started on a new timeline and diverge from the source. For this reason, it is advised to stop all write operations to the source database -before migrating to the target database in Kubernetes. +before migrating to the target database. !!! Important Before you attempt a migration, you must test both the procedure diff --git a/docs/src/cloudnative-pg.v1.md b/docs/src/cloudnative-pg.v1.md index 868b609256..22eb5d401e 100644 --- a/docs/src/cloudnative-pg.v1.md +++ b/docs/src/cloudnative-pg.v1.md @@ -2314,6 +2314,48 @@ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-

The encoding (cannot be changed)

+locale
+string + + +

The locale (cannot be changed)

+ + +locale_provider
+string + + +

The locale provider (cannot be changed)

+ + +lc_collate
+string + + +

The LC_COLLATE (cannot be changed)

+ + +lc_ctype
+string + + +

The LC_CTYPE (cannot be changed)

+ + +icu_locale
+string + + +

The ICU_LOCALE (cannot be changed)

+ + +icu_rules
+string + + +

The ICU_RULES (cannot be changed)

+ + isTemplate
bool diff --git a/docs/src/samples/database-example-icu.yaml b/docs/src/samples/database-example-icu.yaml new file mode 100644 index 0000000000..7a6bba7e4d --- /dev/null +++ b/docs/src/samples/database-example-icu.yaml @@ -0,0 +1,16 @@ +# NOTE: this manifest will only work properly if the Postgres version supports +# ICU locales and rules (version 16 and newer) +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: db-icu +spec: + name: declarative-icu + owner: app + encoding: UTF8 + locale_provider: icu + icu_locale: en + icu_rules: fr + template: template0 + cluster: + name: cluster-example diff --git a/go.mod b/go.mod index d430eac802..b3734a9ea5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/cloudnative-pg/cloudnative-pg -go 1.22.0 +go 1.23 toolchain go1.23.2 @@ -12,7 +12,7 @@ require ( github.com/cheynewallace/tabby v1.1.1 github.com/cloudnative-pg/barman-cloud v0.0.0-20240924124724-92831d48562a github.com/cloudnative-pg/cnpg-i v0.0.0-20241001103001-7e24b2eccd50 - github.com/cloudnative-pg/machinery v0.0.0-20241001153943-0e5ba4f9a0e1 + github.com/cloudnative-pg/machinery v0.0.0-20241007084552-267a543ce26f github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/evanphx/json-patch/v5 v5.9.0 github.com/go-logr/logr v1.4.2 @@ -27,7 +27,7 @@ require ( github.com/mitchellh/go-ps v1.0.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.2 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.77.1 github.com/prometheus/client_golang v1.20.4 github.com/robfig/cron v1.2.0 github.com/sethvargo/go-password v0.3.1 diff --git a/go.sum b/go.sum index f8365e307d..92f5c0833b 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/cloudnative-pg/barman-cloud v0.0.0-20240924124724-92831d48562a h1:0v1 github.com/cloudnative-pg/barman-cloud v0.0.0-20240924124724-92831d48562a/go.mod h1:Jm0tOp5oB7utpt8wz6RfSv31h1mThOtffjfyxVupriE= github.com/cloudnative-pg/cnpg-i v0.0.0-20241001103001-7e24b2eccd50 h1:Rm/bbC0GNCuWth5fHVMos99RzNczbWRVBdjubh3JMPs= github.com/cloudnative-pg/cnpg-i v0.0.0-20241001103001-7e24b2eccd50/go.mod h1:lTWPq8pluS0PSnRMwt0zShftbyssoRhTJ5zAip8unl8= -github.com/cloudnative-pg/machinery v0.0.0-20241001153943-0e5ba4f9a0e1 h1:qrxfp0vR+zqC+L1yTdQTqRHvnLLcVk4CdWB1RwLd8UE= -github.com/cloudnative-pg/machinery v0.0.0-20241001153943-0e5ba4f9a0e1/go.mod h1:bWp1Es5zlxElg4Z/c5f0RKOkDcyNvDHdYIvNcPQU4WM= +github.com/cloudnative-pg/machinery v0.0.0-20241007084552-267a543ce26f h1:tdh7vyJBadzToa2pYYC5gERr35kum4N2571VWtXnkPk= +github.com/cloudnative-pg/machinery v0.0.0-20241007084552-267a543ce26f/go.mod h1:bWp1Es5zlxElg4Z/c5f0RKOkDcyNvDHdYIvNcPQU4WM= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -157,8 +157,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.2 h1:6UsAv+jAevuGO2yZFU/BukV4o9NKnFMOuoouSA4G0ns= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.2/go.mod h1:XYrdZw5dW12Cjkt4ndbeNZZTBp4UCHtW0ccR9+sTtPU= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.77.1 h1:XGoEXT6WTTihO+MD8MAao+YaQIH905HbK0WK2lyo28k= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.77.1/go.mod h1:D0KY8md81DQKdaR/cXwnhoWB3MYYyc/UjvqE8GFkIvA= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= diff --git a/internal/cmd/plugin/status/status.go b/internal/cmd/plugin/status/status.go index 41489ea976..1889975c55 100644 --- a/internal/cmd/plugin/status/status.go +++ b/internal/cmd/plugin/status/status.go @@ -27,6 +27,7 @@ import ( "time" "github.com/cheynewallace/tabby" + "github.com/cloudnative-pg/machinery/pkg/stringset" types "github.com/cloudnative-pg/machinery/pkg/types" "github.com/logrusorgru/aurora/v4" corev1 "k8s.io/api/core/v1" @@ -44,7 +45,6 @@ import ( "github.com/cloudnative-pg/cloudnative-pg/pkg/postgres" "github.com/cloudnative-pg/cloudnative-pg/pkg/reconciler/hibernation" "github.com/cloudnative-pg/cloudnative-pg/pkg/specs" - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" ) diff --git a/internal/management/controller/database_controller_sql.go b/internal/management/controller/database_controller_sql.go index 1cf32d83a0..cd01a4f926 100644 --- a/internal/management/controller/database_controller_sql.go +++ b/internal/management/controller/database_controller_sql.go @@ -20,7 +20,9 @@ import ( "context" "database/sql" "fmt" + "strings" + "github.com/cloudnative-pg/machinery/pkg/log" "github.com/jackc/pgx/v5" apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" @@ -56,27 +58,54 @@ func createDatabase( db *sql.DB, obj *apiv1.Database, ) error { - sqlCreateDatabase := fmt.Sprintf("CREATE DATABASE %s ", pgx.Identifier{obj.Spec.Name}.Sanitize()) + var sqlCreateDatabase strings.Builder + sqlCreateDatabase.WriteString(fmt.Sprintf("CREATE DATABASE %s ", pgx.Identifier{obj.Spec.Name}.Sanitize())) if len(obj.Spec.Owner) > 0 { - sqlCreateDatabase += fmt.Sprintf(" OWNER %s", pgx.Identifier{obj.Spec.Owner}.Sanitize()) + sqlCreateDatabase.WriteString(fmt.Sprintf(" OWNER %s", pgx.Identifier{obj.Spec.Owner}.Sanitize())) } if len(obj.Spec.Template) > 0 { - sqlCreateDatabase += fmt.Sprintf(" TEMPLATE %s", pgx.Identifier{obj.Spec.Template}.Sanitize()) + sqlCreateDatabase.WriteString(fmt.Sprintf(" TEMPLATE %s", pgx.Identifier{obj.Spec.Template}.Sanitize())) } if len(obj.Spec.Tablespace) > 0 { - sqlCreateDatabase += fmt.Sprintf(" TABLESPACE %s", pgx.Identifier{obj.Spec.Tablespace}.Sanitize()) + sqlCreateDatabase.WriteString(fmt.Sprintf(" TABLESPACE %s", pgx.Identifier{obj.Spec.Tablespace}.Sanitize())) } if obj.Spec.AllowConnections != nil { - sqlCreateDatabase += fmt.Sprintf(" ALLOW_CONNECTIONS %v", *obj.Spec.AllowConnections) + sqlCreateDatabase.WriteString(fmt.Sprintf(" ALLOW_CONNECTIONS %v", *obj.Spec.AllowConnections)) } if obj.Spec.ConnectionLimit != nil { - sqlCreateDatabase += fmt.Sprintf(" CONNECTION LIMIT %v", *obj.Spec.ConnectionLimit) + sqlCreateDatabase.WriteString(fmt.Sprintf(" CONNECTION LIMIT %v", *obj.Spec.ConnectionLimit)) } if obj.Spec.IsTemplate != nil { - sqlCreateDatabase += fmt.Sprintf(" IS_TEMPLATE %v", *obj.Spec.IsTemplate) + sqlCreateDatabase.WriteString(fmt.Sprintf(" IS_TEMPLATE %v", *obj.Spec.IsTemplate)) + } + if obj.Spec.Encoding != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" ENCODING %s", pgx.Identifier{obj.Spec.Encoding}.Sanitize())) + } + if obj.Spec.Locale != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" LOCALE %s", pgx.Identifier{obj.Spec.Locale}.Sanitize())) + } + if obj.Spec.LocaleProvider != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" LOCALE_PROVIDER %s", pgx.Identifier{obj.Spec.LocaleProvider}.Sanitize())) + } + if obj.Spec.LcCollate != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" LC_COLLATE %s", pgx.Identifier{obj.Spec.LcCollate}.Sanitize())) + } + if obj.Spec.LcCtype != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" LC_CTYPE %s", pgx.Identifier{obj.Spec.LcCtype}.Sanitize())) + } + if obj.Spec.IcuLocale != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" ICU_LOCALE %s", pgx.Identifier{obj.Spec.IcuLocale}.Sanitize())) + } + if obj.Spec.IcuRules != "" { + sqlCreateDatabase.WriteString(fmt.Sprintf(" ICU_RULES %s", pgx.Identifier{obj.Spec.IcuRules}.Sanitize())) } - _, err := db.ExecContext(ctx, sqlCreateDatabase) + contextLogger, ctx := log.SetupLogger(ctx) + + _, err := db.ExecContext(ctx, sqlCreateDatabase.String()) + if err != nil { + contextLogger.Error(err, "while creating database", "query", sqlCreateDatabase.String()) + } return err } @@ -86,6 +115,8 @@ func updateDatabase( db *sql.DB, obj *apiv1.Database, ) error { + contextLogger, ctx := log.SetupLogger(ctx) + if obj.Spec.AllowConnections != nil { changeAllowConnectionsSQL := fmt.Sprintf( "ALTER DATABASE %s WITH ALLOW_CONNECTIONS %v", @@ -93,6 +124,7 @@ func updateDatabase( *obj.Spec.AllowConnections) if _, err := db.ExecContext(ctx, changeAllowConnectionsSQL); err != nil { + contextLogger.Error(err, "while altering database", "query", changeAllowConnectionsSQL) return fmt.Errorf("while altering database %q with allow_connections %t: %w", obj.Spec.Name, *obj.Spec.AllowConnections, err) } @@ -105,6 +137,7 @@ func updateDatabase( *obj.Spec.ConnectionLimit) if _, err := db.ExecContext(ctx, changeConnectionsLimitSQL); err != nil { + contextLogger.Error(err, "while altering database", "query", changeConnectionsLimitSQL) return fmt.Errorf("while altering database %q with connection limit %d: %w", obj.Spec.Name, *obj.Spec.ConnectionLimit, err) } @@ -117,6 +150,7 @@ func updateDatabase( *obj.Spec.IsTemplate) if _, err := db.ExecContext(ctx, changeIsTemplateSQL); err != nil { + contextLogger.Error(err, "while altering database", "query", changeIsTemplateSQL) return fmt.Errorf("while altering database %q with is_template %t: %w", obj.Spec.Name, *obj.Spec.IsTemplate, err) } @@ -129,6 +163,7 @@ func updateDatabase( pgx.Identifier{obj.Spec.Owner}.Sanitize()) if _, err := db.ExecContext(ctx, changeOwnerSQL); err != nil { + contextLogger.Error(err, "while altering database", "query", changeOwnerSQL) return fmt.Errorf("while altering database %q owner %s to: %w", obj.Spec.Name, obj.Spec.Owner, err) } @@ -141,6 +176,7 @@ func updateDatabase( pgx.Identifier{obj.Spec.Tablespace}.Sanitize()) if _, err := db.ExecContext(ctx, changeTablespaceSQL); err != nil { + contextLogger.Error(err, "while altering database", "query", changeTablespaceSQL) return fmt.Errorf("while altering database %q tablespace %s: %w", obj.Spec.Name, obj.Spec.Tablespace, err) } @@ -154,11 +190,13 @@ func dropDatabase( db *sql.DB, obj *apiv1.Database, ) error { + contextLogger, ctx := log.SetupLogger(ctx) + query := fmt.Sprintf("DROP DATABASE IF EXISTS %s", pgx.Identifier{obj.Spec.Name}.Sanitize()) _, err := db.ExecContext( ctx, - fmt.Sprintf("DROP DATABASE IF EXISTS %s", pgx.Identifier{obj.Spec.Name}.Sanitize()), - ) + query) if err != nil { + contextLogger.Error(err, "while dropping database", "query", query) return fmt.Errorf("while dropping database %q: %w", obj.Spec.Name, err) } diff --git a/internal/management/controller/database_controller_sql_test.go b/internal/management/controller/database_controller_sql_test.go index 444267b36e..b95a13e076 100644 --- a/internal/management/controller/database_controller_sql_test.go +++ b/internal/management/controller/database_controller_sql_test.go @@ -107,6 +107,32 @@ var _ = Describe("Managed Database SQL", func() { err = createDatabase(ctx, db, database) Expect(err).ToNot(HaveOccurred()) }) + + It("should create a new Database with locale and encoding kind fields", func(ctx SpecContext) { + database.Spec.Locale = "POSIX" + database.Spec.LocaleProvider = "icu" + database.Spec.LcCtype = "en_US.utf8" + database.Spec.LcCollate = "C" + database.Spec.Encoding = "LATIN1" + database.Spec.IcuLocale = "en" + database.Spec.IcuRules = "fr" + + expectedValue := sqlmock.NewResult(0, 1) + expectedQuery := fmt.Sprintf( + "CREATE DATABASE %s OWNER %s "+ + "ENCODING %s LOCALE %s LOCALE_PROVIDER %s LC_COLLATE %s LC_CTYPE %s "+ + "ICU_LOCALE %s ICU_RULES %s", + pgx.Identifier{database.Spec.Name}.Sanitize(), pgx.Identifier{database.Spec.Owner}.Sanitize(), + pgx.Identifier{database.Spec.Encoding}.Sanitize(), pgx.Identifier{database.Spec.Locale}.Sanitize(), + pgx.Identifier{database.Spec.LocaleProvider}.Sanitize(), pgx.Identifier{database.Spec.LcCollate}.Sanitize(), + pgx.Identifier{database.Spec.LcCtype}.Sanitize(), + pgx.Identifier{database.Spec.IcuLocale}.Sanitize(), pgx.Identifier{database.Spec.IcuRules}.Sanitize(), + ) + dbMock.ExpectExec(expectedQuery).WillReturnResult(expectedValue) + + err = createDatabase(ctx, db, database) + Expect(err).ToNot(HaveOccurred()) + }) }) Context("updateDatabase", func() { diff --git a/pkg/configfile/configfile.go b/pkg/configfile/configfile.go index 14ac64bcc1..9b5aa1b584 100644 --- a/pkg/configfile/configfile.go +++ b/pkg/configfile/configfile.go @@ -23,9 +23,8 @@ import ( "strings" "github.com/cloudnative-pg/machinery/pkg/fileutils" + "github.com/cloudnative-pg/machinery/pkg/stringset" "github.com/lib/pq" - - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" ) // UpdatePostgresConfigurationFile search and replace options in a Postgres configuration file. diff --git a/pkg/management/postgres/webserver/local.go b/pkg/management/postgres/webserver/local.go index 89579382d6..61e99860e8 100644 --- a/pkg/management/postgres/webserver/local.go +++ b/pkg/management/postgres/webserver/local.go @@ -227,7 +227,5 @@ func (ws *localWebserverEndpoints) startPluginBackup( cluster *apiv1.Cluster, backup *apiv1.Backup, ) { - cmd := NewPluginBackupCommand(cluster, backup, ws.typedClient, ws.eventRecorder) - cmd.Start(ctx) - cmd.Close() + NewPluginBackupCommand(cluster, backup, ws.typedClient, ws.eventRecorder).Start(ctx) } diff --git a/pkg/management/postgres/webserver/plugin_backup.go b/pkg/management/postgres/webserver/plugin_backup.go index bbd1c993bd..0c0d18acdf 100644 --- a/pkg/management/postgres/webserver/plugin_backup.go +++ b/pkg/management/postgres/webserver/plugin_backup.go @@ -43,8 +43,6 @@ type PluginBackupCommand struct { Backup *apiv1.Backup Client client.Client Recorder record.EventRecorder - Log log.Logger - Plugins repository.Interface } // NewPluginBackupCommand initializes a BackupCommand object, taking a physical @@ -55,23 +53,13 @@ func NewPluginBackupCommand( client client.Client, recorder record.EventRecorder, ) *PluginBackupCommand { - logger := log.WithValues( - "pluginConfiguration", backup.Spec.PluginConfiguration, - "backupName", backup.Name, - "backupNamespace", backup.Name) - - plugins := repository.New() - if err := plugins.RegisterUnixSocketPluginsInPath(configuration.Current.PluginSocketDir); err != nil { - logger.Error(err, "Error while discovering plugins") - } + backup.EnsureGVKIsPresent() return &PluginBackupCommand{ Cluster: cluster, Backup: backup, Client: client, Recorder: recorder, - Log: logger, - Plugins: plugins, } } @@ -80,31 +68,33 @@ func (b *PluginBackupCommand) Start(ctx context.Context) { go b.invokeStart(ctx) } -// Close closes all the connections to the plugins -func (b *PluginBackupCommand) Close() { - b.Plugins.Close() -} - func (b *PluginBackupCommand) invokeStart(ctx context.Context) { - backupLog := b.Log.WithValues( + contextLogger := log.FromContext(ctx).WithValues( + "pluginConfiguration", b.Backup.Spec.PluginConfiguration, "backupName", b.Backup.Name, "backupNamespace", b.Backup.Name) - cli, err := pluginClient.WithPlugins(ctx, b.Plugins, b.Cluster.Spec.Plugins.GetEnabledPluginNames()...) + plugins := repository.New() + if err := plugins.RegisterUnixSocketPluginsInPath(configuration.Current.PluginSocketDir); err != nil { + contextLogger.Error(err, "Error while discovering plugins") + } + defer plugins.Close() + + cli, err := pluginClient.WithPlugins(ctx, plugins, b.Cluster.Spec.Plugins.GetEnabledPluginNames()...) if err != nil { b.markBackupAsFailed(ctx, err) return } // record the backup beginning - backupLog.Info("Plugin backup started") + contextLogger.Info("Plugin backup started") b.Recorder.Event(b.Backup, "Normal", "Starting", "Backup started") // Update backup status in cluster conditions on startup if err := b.retryWithRefreshedCluster(ctx, func() error { return conditions.Patch(ctx, b.Client, b.Cluster, apiv1.BackupStartingCondition) }); err != nil { - backupLog.Error(err, "Error changing backup condition (backup started)") + contextLogger.Error(err, "Error changing backup condition (backup started)") // We do not terminate here because we could still have a good backup // even if we are unable to communicate with the Kubernetes API server } @@ -120,7 +110,7 @@ func (b *PluginBackupCommand) invokeStart(ctx context.Context) { return } - backupLog.Info("Backup completed") + contextLogger.Info("Backup completed") b.Recorder.Event(b.Backup, "Normal", "Completed", "Backup completed") // Set the status to completed @@ -146,28 +136,30 @@ func (b *PluginBackupCommand) invokeStart(ctx context.Context) { } if err := postgres.PatchBackupStatusAndRetry(ctx, b.Client, b.Backup); err != nil { - backupLog.Error(err, "Can't set backup status as completed") + contextLogger.Error(err, "Can't set backup status as completed") } // Update backup status in cluster conditions on backup completion if err := b.retryWithRefreshedCluster(ctx, func() error { return conditions.Patch(ctx, b.Client, b.Cluster, apiv1.BackupSucceededCondition) }); err != nil { - b.Log.Error(err, "Can't update the cluster with the completed backup data") + contextLogger.Error(err, "Can't update the cluster with the completed backup data") } } func (b *PluginBackupCommand) markBackupAsFailed(ctx context.Context, failure error) { + contextLogger := log.FromContext(ctx) + backupStatus := b.Backup.GetStatus() // record the failure - b.Log.Error(failure, "Backup failed") + contextLogger.Error(failure, "Backup failed") b.Recorder.Event(b.Backup, "Normal", "Failed", "Backup failed") // update backup status as failed backupStatus.SetAsFailed(failure) if err := postgres.PatchBackupStatusAndRetry(ctx, b.Client, b.Backup); err != nil { - b.Log.Error(err, "Can't mark backup as failed") + contextLogger.Error(err, "Can't mark backup as failed") // We do not terminate here because we still want to set the condition on the cluster. } @@ -180,7 +172,7 @@ func (b *PluginBackupCommand) markBackupAsFailed(ctx context.Context, failure er b.Cluster.Status.LastFailedBackup = utils.GetCurrentTimestampWithFormat(time.RFC3339) return b.Client.Status().Patch(ctx, b.Cluster, client.MergeFrom(origCluster)) }); failErr != nil { - b.Log.Error(failErr, "while setting cluster condition for failed backup") + contextLogger.Error(failErr, "while setting cluster condition for failed backup") } } diff --git a/pkg/multicache/multinamespaced_cache.go b/pkg/multicache/multinamespaced_cache.go index 80f69eecfb..a3222085db 100644 --- a/pkg/multicache/multinamespaced_cache.go +++ b/pkg/multicache/multinamespaced_cache.go @@ -24,12 +24,11 @@ import ( "fmt" "github.com/cloudnative-pg/machinery/pkg/log" + "github.com/cloudnative-pg/machinery/pkg/stringset" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" ) type multiNamespaceCache struct { diff --git a/pkg/specs/roles.go b/pkg/specs/roles.go index e48a3b6ad4..f0d9bf4cb1 100644 --- a/pkg/specs/roles.go +++ b/pkg/specs/roles.go @@ -19,11 +19,11 @@ package specs import ( "slices" + "github.com/cloudnative-pg/machinery/pkg/stringset" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" ) // CreateRole create a role with the permissions needed by the instance manager diff --git a/pkg/stringset/stringset.go b/pkg/stringset/stringset.go deleted file mode 100644 index f5678ec4a0..0000000000 --- a/pkg/stringset/stringset.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright The CloudNativePG Contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package stringset implements a basic set of strings -package stringset - -import ( - "slices" -) - -// Data represent a set of strings -type Data struct { - innerMap map[string]struct{} -} - -// New create a new empty set of strings -func New() *Data { - return &Data{ - innerMap: make(map[string]struct{}), - } -} - -// From create a empty set of strings given -// a slice of strings -func From(strings []string) *Data { - result := New() - for _, value := range strings { - result.Put(value) - } - return result -} - -// FromKeys create a string set from the -// keys of a map -func FromKeys[T any](v map[string]T) *Data { - result := New() - for key := range v { - result.Put(key) - } - return result -} - -// Put a string in the set -func (set *Data) Put(key string) { - set.innerMap[key] = struct{}{} -} - -// Delete deletes a string from the set. If the string doesn't exist -// this is a no-op -func (set *Data) Delete(key string) { - delete(set.innerMap, key) -} - -// Has checks if a string is in the set or not -func (set *Data) Has(key string) bool { - _, ok := set.innerMap[key] - return ok -} - -// Len returns the map of the set -func (set *Data) Len() int { - return len(set.innerMap) -} - -// ToList returns the strings contained in this set as -// a string slice -func (set *Data) ToList() (result []string) { - result = make([]string, 0, len(set.innerMap)) - for key := range set.innerMap { - result = append(result, key) - } - return -} - -// ToSortedList returns the string container in this set -// as a sorted string slice -func (set *Data) ToSortedList() []string { - result := set.ToList() - slices.Sort(result) - return result -} - -// Eq compares two string sets for equality -func (set *Data) Eq(other *Data) bool { - if set == nil || other == nil { - return false - } - - if set.Len() != other.Len() { - return false - } - - for key := range set.innerMap { - if !other.Has(key) { - return false - } - } - - return true -} diff --git a/pkg/stringset/stringset_test.go b/pkg/stringset/stringset_test.go deleted file mode 100644 index abf6e5548b..0000000000 --- a/pkg/stringset/stringset_test.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright The CloudNativePG Contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package stringset - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("String set", func() { - It("starts as an empty set", func() { - Expect(New().Len()).To(Equal(0)) - }) - - It("starts with a list of strings", func() { - Expect(From([]string{"one", "two"}).Len()).To(Equal(2)) - Expect(From([]string{"one", "two", "two"}).Len()).To(Equal(2)) - }) - - It("store string keys", func() { - set := New() - Expect(set.Has("test")).To(BeFalse()) - Expect(set.Has("test2")).To(BeFalse()) - - set.Put("test") - Expect(set.Has("test")).To(BeTrue()) - Expect(set.Has("test2")).To(BeFalse()) - }) - - It("removes string keys", func() { - set := From([]string{"one", "two"}) - set.Delete("one") - Expect(set.ToList()).To(Equal([]string{"two"})) - }) - - It("constructs a string slice given a set", func() { - Expect(From([]string{"one", "two"}).ToList()).To(ContainElements("one", "two")) - }) - - It("compares two string set for equality", func() { - Expect(From([]string{"one", "two"}).Eq(From([]string{"one", "two"}))).To(BeTrue()) - Expect(From([]string{"one", "two"}).Eq(From([]string{"two", "three"}))).To(BeFalse()) - Expect(From([]string{"one", "two"}).Eq(From([]string{"one", "two", "three"}))).To(BeFalse()) - Expect(From([]string{"one", "two", "three"}).Eq(From([]string{"one", "two"}))).To(BeFalse()) - }) - - It("constructs a sorted string slice given a set", func() { - Expect(From([]string{"one", "two", "three", "four"}).ToSortedList()).To( - HaveExactElements("four", "one", "three", "two")) - Expect(New().ToList()).To(BeEmpty()) - }) - - It("constructs a string set from a map having string as keys", func() { - Expect(FromKeys(map[string]int{ - "one": 1, - "two": 2, - "three": 3, - }).ToSortedList()).To( - HaveExactElements("one", "three", "two"), - ) - }) -}) diff --git a/pkg/stringset/suite_test.go b/pkg/stringset/suite_test.go deleted file mode 100644 index bb29e64601..0000000000 --- a/pkg/stringset/suite_test.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright The CloudNativePG Contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package stringset - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -func TestConfigFile(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs(t, "Configuration File Parsing Suite") -} diff --git a/pkg/utils/fencing.go b/pkg/utils/fencing.go index 7cedb1cbf5..c7ec6c37aa 100644 --- a/pkg/utils/fencing.go +++ b/pkg/utils/fencing.go @@ -24,12 +24,11 @@ import ( "slices" "sort" + "github.com/cloudnative-pg/machinery/pkg/stringset" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/cloudnative-pg/cloudnative-pg/pkg/stringset" ) var ( diff --git a/tests/e2e/declarative_database_management_test.go b/tests/e2e/declarative_database_management_test.go index aee596b3cc..ec702a6084 100644 --- a/tests/e2e/declarative_database_management_test.go +++ b/tests/e2e/declarative_database_management_test.go @@ -17,6 +17,7 @@ limitations under the License. package e2e import ( + "fmt" "time" "k8s.io/apimachinery/pkg/types" @@ -50,19 +51,17 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke Context("plain vanilla cluster", Ordered, func() { const ( - namespacePrefix = "declarative-db" - databaseCrdName = "db-declarative" - databaseWithDeleteRetainPolicyCrdName = "db-declarative-delete" - dbname = "declarative" + namespacePrefix = "declarative-db" + dbname = "declarative" ) var ( - clusterName, namespace string - database *apiv1.Database - databaseWithDeleteRetainPolicy *apiv1.Database + clusterName, namespace, databaseObjectName string + database *apiv1.Database + databaseWithDeleteRetainPolicy *apiv1.Database + err error ) BeforeAll(func() { - var err error // Create a cluster in a namespace we'll delete after the test namespace, err = env.CreateUniqueTestNamespace(namespacePrefix) Expect(err).ToNot(HaveOccurred()) @@ -93,11 +92,28 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke }, 300).Should(Succeed()) } + assertDatabaseHasExpectedFields := func(namespace, primaryPod string, db apiv1.Database) { + query := fmt.Sprintf("select count(*) from pg_database where datname = '%s' "+ + "and encoding = %s and datctype = '%s' and datcollate = '%s'", + db.Spec.Name, db.Spec.Encoding, db.Spec.LcCtype, db.Spec.LcCollate) + Eventually(func(g Gomega) { + stdout, _, err := env.ExecQueryInInstancePod( + utils.PodLocator{ + Namespace: namespace, + PodName: primaryPod, + }, + "postgres", + query) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(stdout).Should(ContainSubstring("1")) + }, 30).Should(Succeed()) + } + When("Database CRD reclaim policy is set to retain (default) inside spec", func() { It("can add a declarative database", func() { By("applying Database CRD manifest", func() { CreateResourceFromFile(namespace, databaseManifest) - _, err := env.GetResourceNameFromYAML(databaseManifest) + databaseObjectName, err = env.GetResourceNameFromYAML(databaseManifest) Expect(err).NotTo(HaveOccurred()) }) By("ensuring the Database CRD succeeded reconciliation", func() { @@ -105,7 +121,7 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke database = &apiv1.Database{} databaseNamespacedName := types.NamespacedName{ Namespace: namespace, - Name: databaseCrdName, + Name: databaseObjectName, } Eventually(func(g Gomega) { @@ -115,11 +131,25 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke }, 300).WithPolling(10 * time.Second).Should(Succeed()) }) - By("verifying the db exists", func() { + By("verifying new database has been created with the expected fields", func() { primaryPodInfo, err := env.GetClusterPrimary(namespace, clusterName) Expect(err).ToNot(HaveOccurred()) assertDatabaseExists(namespace, primaryPodInfo.Name, dbname, true) + + // NOTE: the `pg_database` table in Postgres does not contain fields + // for the owner nor the template. + // Its fields are dependent on the version of Postgres, so we pick + // a subset that is available to check even on PG v12 + expectedDatabaseFields := apiv1.Database{ + Spec: apiv1.DatabaseSpec{ + Name: "declarative", + LcCtype: "en_US.utf8", + LcCollate: "C", // this is the default value + Encoding: "0", // corresponds to SQL_ASCII + }, + } + assertDatabaseHasExpectedFields(namespace, primaryPodInfo.Name, expectedDatabaseFields) }) }) @@ -140,7 +170,7 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke It("can add a declarative database", func() { By("applying Database CRD manifest", func() { CreateResourceFromFile(namespace, databaseManifestWithDeleteReclaimPolicy) - _, err := env.GetResourceNameFromYAML(databaseManifestWithDeleteReclaimPolicy) + databaseObjectName, err = env.GetResourceNameFromYAML(databaseManifestWithDeleteReclaimPolicy) Expect(err).NotTo(HaveOccurred()) }) By("ensuring the Database CRD succeeded reconciliation", func() { @@ -148,7 +178,7 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke databaseWithDeleteRetainPolicy = &apiv1.Database{} databaseNamespacedName := types.NamespacedName{ Namespace: namespace, - Name: databaseWithDeleteRetainPolicyCrdName, + Name: databaseObjectName, } Eventually(func(g Gomega) { @@ -183,7 +213,7 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke "will be deleted", func() { By("applying Database CRD manifest", func() { CreateResourceFromFile(namespace, databaseManifestWithDeleteReclaimPolicy) - _, err := env.GetResourceNameFromYAML(databaseManifestWithDeleteReclaimPolicy) + databaseObjectName, err = env.GetResourceNameFromYAML(databaseManifestWithDeleteReclaimPolicy) Expect(err).NotTo(HaveOccurred()) }) By("ensuring the Database CRD succeeded reconciliation", func() { @@ -191,7 +221,7 @@ var _ = Describe("Declarative databases management test", Label(tests.LabelSmoke databaseWithDeleteRetainPolicy = &apiv1.Database{} databaseNamespacedName := types.NamespacedName{ Namespace: namespace, - Name: databaseWithDeleteRetainPolicyCrdName, + Name: databaseObjectName, } Eventually(func(g Gomega) { diff --git a/tests/e2e/fixtures/declarative_databases/database.yaml.template b/tests/e2e/fixtures/declarative_databases/database.yaml.template index afa83d0ccd..3ded03c50a 100644 --- a/tests/e2e/fixtures/declarative_databases/database.yaml.template +++ b/tests/e2e/fixtures/declarative_databases/database.yaml.template @@ -5,5 +5,8 @@ metadata: spec: name: declarative owner: app + lc_ctype: "en_US.utf8" + encoding: SQL_ASCII + template: template0 cluster: name: cluster-with-declarative-databases