From 4a96e0c9a7176026bf3275ec27b01cf5fd6efda0 Mon Sep 17 00:00:00 2001 From: dpishchenkov Date: Tue, 3 Mar 2026 16:59:57 +0100 Subject: [PATCH 1/3] add configmaps and secret --- api/v4/postgrescluster_types.go | 17 ++ .../controller/postgrescluster_controller.go | 187 +++++++++++++++++- 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/api/v4/postgrescluster_types.go b/api/v4/postgrescluster_types.go index f6ae81ea7..a84a0828b 100644 --- a/api/v4/postgrescluster_types.go +++ b/api/v4/postgrescluster_types.go @@ -111,6 +111,19 @@ type PostgresClusterSpec struct { ManagedRoles []ManagedRole `json:"managedRoles,omitempty"` } +// PostgresClusterResources defines references to Kubernetes resources related to the PostgresCluster, such as ConfigMaps and Secrets. +type PostgresClusterResources struct { + // ConfigMapRef references the ConfigMap with connection endpoints. + // Contains: CLUSTER_ENDPOINTS, POOLER_ENDPOINTS (if connection pooler enabled) + // +optional + ConfigMapRef *corev1.LocalObjectReference `json:"configMapRef,omitempty"` + + // SecretRef references the Secret with superuser credentials. + // Contains: passwords for superuser + // +optional + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` +} + // PostgresClusterStatus defines the observed state of PostgresCluster. type PostgresClusterStatus struct { // Phase represents the current phase of the PostgresCluster. @@ -135,6 +148,10 @@ type PostgresClusterStatus struct { // ManagedRolesStatus tracks the reconciliation status of managed roles. // +optional ManagedRolesStatus *ManagedRolesStatus `json:"managedRolesStatus,omitempty"` + + // Resources contains references to related Kubernetes resources like ConfigMaps and Secrets. + // +optional + Resources *PostgresClusterResources `json:"resources,omitempty"` } // ManagedRolesStatus tracks the state of managed PostgreSQL roles. diff --git a/internal/controller/postgrescluster_controller.go b/internal/controller/postgrescluster_controller.go index c6ef54f28..4a0d670c4 100644 --- a/internal/controller/postgrescluster_controller.go +++ b/internal/controller/postgrescluster_controller.go @@ -122,9 +122,9 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ } return ctrl.Result{}, err } - cnpgCluster = existingCNPG // 6. If CNPG Cluster exists, compare the current spec with the desired spec and update if necessary. + cnpgCluster = existingCNPG currentNormalizedSpec := normalizeCNPGClusterSpec(cnpgCluster.Spec, mergedConfig.PostgreSQLConfig) desiredNormalizedSpec := normalizeCNPGClusterSpec(desiredSpec, mergedConfig.PostgreSQLConfig) @@ -238,6 +238,27 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ } } + // Step 7: If CNPG is ready, generate ConfigMap and Secret with connection info + if cnpgCluster.Status.Phase == cnpgv1.PhaseHealthy { + logger.Info("CNPG Cluster is ready, generating connection resources") + + // Reconcile ConfigMap + if err := r.reconcileConfigMap(ctx, postgresCluster, cnpgCluster); err != nil { + logger.Error(err, "Failed to reconcile ConfigMap") + r.setCondition(postgresCluster, metav1.ConditionFalse, "ConfigMapFailed", err.Error()) + return ctrl.Result{}, err + } + + // Reconcile Secret + if err := r.reconcileSecret(ctx, postgresCluster, cnpgCluster); err != nil { + logger.Error(err, "Failed to reconcile Secret") + r.setCondition(postgresCluster, metav1.ConditionFalse, "SecretFailed", err.Error()) + return ctrl.Result{}, err + } + + logger.Info("Connection resources created successfully") + } + // 8. Report progress back to the user and manage the reconciliation lifecycle. if err := r.syncStatus(ctx, postgresCluster, cnpgCluster); err != nil { logger.Error(err, "Failed to sync final status") @@ -791,6 +812,170 @@ func normalizeCNPGClusterSpec(spec cnpgv1.ClusterSpec, customDefinedParameters m return normalizedConf } +func (r *PostgresClusterReconciler) reconcileConfigMap(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) error { + logger := logs.FromContext(ctx) + + configMap := r.generateConfigMap(postgresCluster, cnpgCluster) + existingConfigMap := &corev1.ConfigMap{} + err := r.Get(ctx, types.NamespacedName{Namespace: configMap.Namespace, Name: configMap.Name}, existingConfigMap) + if err != nil && apierrors.IsNotFound(err) { + logger.Info("ConfigMap resource not found, creating one") + if err := r.Create(ctx, configMap); err != nil { + return err + } + if postgresCluster.Status.Resources == nil { + postgresCluster.Status.Resources = &enterprisev4.PostgresClusterResources{} + } + postgresCluster.Status.Resources.ConfigMapRef = &corev1.LocalObjectReference{Name: configMap.Name} + if err := r.Status().Update(ctx, postgresCluster); err != nil { + logger.Error(err, "Failed to update PostgresCluster status with ConfigMap reference") + return err + } + return nil + } else if err != nil { + logger.Error(err, "Failed to fetch existing ConfigMap") + return err + } + existingConfigMap.Data = configMap.Data + if err := r.Update(ctx, existingConfigMap); err != nil { + logger.Error(err, "Failed to update ConfigMap") + return err + } + return nil +} + +func (r *PostgresClusterReconciler) reconcileSecret(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) error { + logger := logs.FromContext(ctx) + + // Generate the desired Secret + secret, err := r.generateSecret(ctx, postgresCluster, cnpgCluster) + if err != nil { + return fmt.Errorf("failed to generate secret: %w", err) + } + + // Try to get existing Secret + existingSecret := &corev1.Secret{} + err = r.Get(ctx, types.NamespacedName{ + Namespace: secret.Namespace, + Name: secret.Name, + }, existingSecret) + + if err != nil && apierrors.IsNotFound(err) { + // Secret doesn't exist, create it + logger.Info("Secret not found, creating", "name", secret.Name) + if err := r.Create(ctx, secret); err != nil { + logger.Error(err, "Failed to create Secret") + return err + } + + // Update PostgresCluster status to reference the Secret + if postgresCluster.Status.Resources == nil { + postgresCluster.Status.Resources = &enterprisev4.PostgresClusterResources{} + } + postgresCluster.Status.Resources.SecretRef = &corev1.LocalObjectReference{Name: secret.Name} + + if err := r.Status().Update(ctx, postgresCluster); err != nil { + logger.Error(err, "Failed to update PostgresCluster status with Secret reference") + return err + } + + logger.Info("Secret created successfully", "name", secret.Name) + return nil + } else if err != nil { + // Some other error occurred + logger.Error(err, "Failed to get Secret") + return err + } + + // Secret exists, update its data + logger.Info("Secret exists, updating data", "name", secret.Name) + existingSecret.Data = secret.Data + if err := r.Update(ctx, existingSecret); err != nil { + logger.Error(err, "Failed to update Secret") + return err + } + + logger.Info("Secret updated successfully", "name", secret.Name) + return nil +} + +func (r *PostgresClusterReconciler) generateConfigMap(postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) *corev1.ConfigMap { + if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.ConfigMapRef != nil { + // If ConfigMap already exists, keep the same name to update it with new data + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: postgresCluster.Status.Resources.ConfigMapRef.Name, + Namespace: postgresCluster.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "postgrescluster-controller", + }, + }, + Data: map[string]string{ + "POSTGRES_CLUSTER_SERVICE_RW": cnpgCluster.Status.WriteService, + "POSTGRES_CLUSTER_SERVICE_RO": cnpgCluster.Status.ReadService, + "POSTGRES_CLUSTER_PORT": "5432", + }, + } + } + + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-connection", postgresCluster.Name), + Namespace: postgresCluster.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "postgrescluster-controller", + }, + }, + Data: map[string]string{ + "POSTGRES_CLUSTER_SERVICE_RW": cnpgCluster.Status.WriteService, + "POSTGRES_CLUSTER_SERVICE_RO": cnpgCluster.Status.ReadService, + "POSTGRES_CLUSTER_PORT": "5432", + + }, + } + ctrl.SetControllerReference(postgresCluster, configMap, r.Scheme) + return configMap +} + +func (r *PostgresClusterReconciler) generateSecret(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) (*corev1.Secret, error) { + // Fetch CNPG secret (contains the "postgres" user credentials) + appSecret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("%s-app", cnpgCluster.Name), + Namespace: cnpgCluster.Namespace, + }, appSecret) + if err != nil { + return nil, fmt.Errorf("failed to get CNPG app secret: %w", err) + } + + // Build Secret Data - use postgres user credentials for all databases + // Note: CNPG doesn't create separate superuser secrets by default + secretData := map[string][]byte{ + "username": appSecret.Data["username"], // "postgres" user + "password": appSecret.Data["password"], // postgres user password + } + + // Create Secret object + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-credentials", postgresCluster.Name), + Namespace: postgresCluster.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "postgrescluster-controller", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: secretData, + } + + // Set owner reference + if err := ctrl.SetControllerReference(postgresCluster, secret, r.Scheme); err != nil { + return nil, fmt.Errorf("failed to set controller reference: %w", err) + } + + return secret, nil +} + // SetupWithManager sets up the controller with the Manager. func (r *PostgresClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). From 6f1225168c2494678ed1dd9eefb1f0942793d0d3 Mon Sep 17 00:00:00 2001 From: dpishchenkov Date: Wed, 4 Mar 2026 19:08:30 +0100 Subject: [PATCH 2/3] PostgresCluster configMaps and secrets generated --- api/v4/postgrescluster_types.go | 2 +- api/v4/postgresclusterclass_types.go | 1 - go.mod | 2 +- .../controller/postgrescluster_controller.go | 310 ++++++++++-------- .../postgresoperator_common_types.go | 20 +- 5 files changed, 185 insertions(+), 150 deletions(-) diff --git a/api/v4/postgrescluster_types.go b/api/v4/postgrescluster_types.go index a84a0828b..de4e1b240 100644 --- a/api/v4/postgrescluster_types.go +++ b/api/v4/postgrescluster_types.go @@ -45,7 +45,7 @@ type ManagedRole struct { // Validation rules ensure immutability of Class, and that Storage and PostgresVersion can only be set once and cannot be removed or downgraded. // +kubebuilder:validation:XValidation:rule="!has(oldSelf.postgresVersion) || (has(self.postgresVersion) && int(self.postgresVersion.split('.')[0]) >= int(oldSelf.postgresVersion.split('.')[0]))",messageExpression="!has(self.postgresVersion) ? 'postgresVersion cannot be removed once set (was: ' + oldSelf.postgresVersion + ')' : 'postgresVersion major version cannot be downgraded (from: ' + oldSelf.postgresVersion + ', to: ' + self.postgresVersion + ')'" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.storage) || (has(self.storage) && quantity(self.storage).compareTo(quantity(oldSelf.storage)) >= 0)",messageExpression="!has(self.storage) ? 'storage cannot be removed once set (was: ' + string(oldSelf.storage) + ')' : 'storage size cannot be decreased (from: ' + string(oldSelf.storage) + ', to: ' + string(self.storage) + ')'" -// +kubebuilder:validation:XValidation:rule="!self.connectionPoolerEnabled || self.connectionPoolerConfig != null || (self.cnpg != null && self.cnpg.connectionPooler != null)",message="connectionPoolerConfig must be set in cluster spec or class when connectionPoolerEnabled is true" +// +kubebuilder:validation:XValidation:rule="!self.connectionPoolerEnabled || self.connectionPoolerConfig != null",message="connectionPoolerConfig must be set when connectionPoolerEnabled is true" type PostgresClusterSpec struct { // This field is IMMUTABLE after creation. // +kubebuilder:validation:Required diff --git a/api/v4/postgresclusterclass_types.go b/api/v4/postgresclusterclass_types.go index 9c4fcef44..430e3409e 100644 --- a/api/v4/postgresclusterclass_types.go +++ b/api/v4/postgresclusterclass_types.go @@ -114,7 +114,6 @@ const ( // ConnectionPoolerConfig defines PgBouncer connection pooler configuration. // When enabled, creates RW and RO pooler deployments for clusters using this class. -// +kubebuilder:validation:XValidation:rule="!self.connectionPoolerEnabled || self.connectionPoolerConfig != null || (self.cnpg != null && self.cnpg.connectionPooler != null)",message="connectionPoolerConfig must be set in cluster spec or class when connectionPoolerEnabled is true" type ConnectionPoolerConfig struct { // Instances is the number of PgBouncer pod replicas. // Higher values provide better availability and load distribution. diff --git a/go.mod b/go.mod index e5917beaa..f26c17001 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/kubectl v0.26.2 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.22.4 ) @@ -183,7 +184,6 @@ require ( k8s.io/component-base v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/internal/controller/postgrescluster_controller.go b/internal/controller/postgrescluster_controller.go index 4a0d670c4..77ee37dc3 100644 --- a/internal/controller/postgrescluster_controller.go +++ b/internal/controller/postgrescluster_controller.go @@ -18,7 +18,11 @@ package controller import ( "context" + "crypto/rand" "fmt" + "strings" + "time" + cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" enterprisev4 "github.com/splunk/splunk-operator/api/v4" corev1 "k8s.io/api/core/v1" @@ -28,6 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" client "sigs.k8s.io/controller-runtime/pkg/client" logs "sigs.k8s.io/controller-runtime/pkg/log" @@ -92,17 +97,49 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, mergeErr } - // 4. Build the desired CNPG Cluster spec based on the merged configuration. - desiredSpec := r.buildCNPGClusterSpec(mergedConfig) + // 4. Ensure PostgresCluster secret exists before creating CNPG cluster. + + var postgresSecretName string + // Check if we already have a secret referenced in the status + + if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.SecretRef != nil { + postgresSecretName = postgresCluster.Status.Resources.SecretRef.Name + } else { + // If not, check if we have an orphaned secret (created, but status update failed previously) + secretList := &corev1.SecretList{} + if err := r.List(ctx, secretList, client.InNamespace(postgresCluster.Namespace)); err == nil { + for _, s := range secretList.Items { + if metav1.IsControlledBy(&s, postgresCluster) && s.Type == corev1.SecretTypeOpaque { + postgresSecretName = s.Name + break + } + } + } + // Generate secret using the cluster name and a random suffix to avoid collisions. + if postgresSecretName == "" { + postgresSecretName = fmt.Sprintf("%s%s%s", postgresCluster.Name, defaultSecretSuffix, generateRandomSuffix()) + } + } + logger.Info("Creating PostgresCluster secret", "name", postgresSecretName) + if err := r.generateSecret(ctx, postgresCluster, postgresSecretName); err != nil { + logger.Error(err, "Failed to ensure PostgresCluster secret", "name", postgresSecretName) + if statusErr := updateStatus(clusterReady, metav1.ConditionFalse, reasonUserSecretFailed, fmt.Sprintf("Failed to generate PostgresCluster secret: %v", err), failedClusterPhase); statusErr != nil { + logger.Error(statusErr, "Failed to update status") + } + return ctrl.Result{}, err + } + + // 5. Build the desired CNPG Cluster spec based on the merged configuration. + desiredSpec := r.buildCNPGClusterSpec(mergedConfig, postgresSecretName) - // 5. Fetch existing CNPG Cluster or create it if it doesn't exist yet. + // 6. Fetch existing CNPG Cluster or create it if it doesn't exist yet. existingCNPG := &cnpgv1.Cluster{} err := r.Get(ctx, types.NamespacedName{Name: postgresCluster.Name, Namespace: postgresCluster.Namespace}, existingCNPG) switch { case apierrors.IsNotFound(err): // CNPG Cluster doesn't exist, create it and requeue for status update. logger.Info("CNPG Cluster not found, creating", "name", postgresCluster.Name) - newCluster := r.buildCNPGCluster(postgresCluster, mergedConfig) + newCluster := r.buildCNPGCluster(postgresCluster, mergedConfig, postgresSecretName) if err = r.Create(ctx, newCluster); err != nil { logger.Error(err, "Failed to create CNPG Cluster") if statusErr := updateStatus(clusterReady, metav1.ConditionFalse, reasonClusterBuildFailed, fmt.Sprintf("Failed to create CNPG Cluster: %v", err), failedClusterPhase); statusErr != nil { @@ -123,10 +160,10 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - // 6. If CNPG Cluster exists, compare the current spec with the desired spec and update if necessary. + // 7. If CNPG Cluster exists, compare the current spec with the desired spec and update if necessary. cnpgCluster = existingCNPG - currentNormalizedSpec := normalizeCNPGClusterSpec(cnpgCluster.Spec, mergedConfig.PostgreSQLConfig) - desiredNormalizedSpec := normalizeCNPGClusterSpec(desiredSpec, mergedConfig.PostgreSQLConfig) + currentNormalizedSpec := normalizeCNPGClusterSpec(cnpgCluster.Spec, mergedConfig.PostgreSQLConfig, postgresSecretName) + desiredNormalizedSpec := normalizeCNPGClusterSpec(desiredSpec, mergedConfig.PostgreSQLConfig, postgresSecretName) if !equality.Semantic.DeepEqual(currentNormalizedSpec, desiredNormalizedSpec) { logger.Info("Detected drift in CNPG Cluster spec, patching", "name", cnpgCluster.Name) @@ -238,30 +275,27 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ } } - // Step 7: If CNPG is ready, generate ConfigMap and Secret with connection info + // 8. If CNPG is ready, generate ConfigMap if cnpgCluster.Status.Phase == cnpgv1.PhaseHealthy { - logger.Info("CNPG Cluster is ready, generating connection resources") + logger.Info("CNPG Cluster is ready, generating ConfigMap for connection details") // Reconcile ConfigMap - if err := r.reconcileConfigMap(ctx, postgresCluster, cnpgCluster); err != nil { + if err := r.reconcileConfigMap(ctx, postgresCluster, cnpgCluster, postgresSecretName); err != nil { logger.Error(err, "Failed to reconcile ConfigMap") - r.setCondition(postgresCluster, metav1.ConditionFalse, "ConfigMapFailed", err.Error()) - return ctrl.Result{}, err - } - - // Reconcile Secret - if err := r.reconcileSecret(ctx, postgresCluster, cnpgCluster); err != nil { - logger.Error(err, "Failed to reconcile Secret") - r.setCondition(postgresCluster, metav1.ConditionFalse, "SecretFailed", err.Error()) + if statusErr := updateStatus(clusterReady, metav1.ConditionFalse, reasonConfigMapFailed, fmt.Sprintf("Failed to reconcile ConfigMap: %v", err), failedClusterPhase); statusErr != nil { + logger.Error(statusErr, "Failed to update status") + } return ctrl.Result{}, err } - - logger.Info("Connection resources created successfully") + logger.Info("ConfigMap created successfully") } - // 8. Report progress back to the user and manage the reconciliation lifecycle. + // 9. Report progress back to the user and manage the reconciliation lifecycle. if err := r.syncStatus(ctx, postgresCluster, cnpgCluster); err != nil { logger.Error(err, "Failed to sync final status") + if statusErr := updateStatus(clusterReady, metav1.ConditionFalse, reasonStatusSyncFailed, fmt.Sprintf("Failed to sync final status: %v", err), failedClusterPhase); statusErr != nil { + logger.Error(statusErr, "Failed to update status") + } return ctrl.Result{}, err } return ctrl.Result{}, nil @@ -327,7 +361,7 @@ func (r *PostgresClusterReconciler) getMergedConfig(clusterClass *enterprisev4.P // buildCNPGClusterSpec builds the desired CNPG ClusterSpec. // IMPORTANT: any field added here must also be added to normalizedCNPGClusterSpec and normalizeCNPGClusterSpec, // otherwise it will not be included in drift detection and changes will be silently ignored. -func (r *PostgresClusterReconciler) buildCNPGClusterSpec(mergedConfig *enterprisev4.PostgresClusterSpec) cnpgv1.ClusterSpec { +func (r *PostgresClusterReconciler) buildCNPGClusterSpec(mergedConfig *enterprisev4.PostgresClusterSpec, secretName string) cnpgv1.ClusterSpec { // 3. Build the Spec spec := cnpgv1.ClusterSpec{ @@ -337,6 +371,11 @@ func (r *PostgresClusterReconciler) buildCNPGClusterSpec(mergedConfig *enterpris Parameters: mergedConfig.PostgreSQLConfig, PgHBA: mergedConfig.PgHBA, }, + SuperuserSecret: &cnpgv1.LocalObjectReference{ + Name: secretName, + }, + EnableSuperuserAccess: ptr.To(true), + Bootstrap: &cnpgv1.BootstrapConfiguration{ InitDB: &cnpgv1.BootstrapInitDB{ Database: defaultDatabaseName, @@ -356,13 +395,14 @@ func (r *PostgresClusterReconciler) buildCNPGClusterSpec(mergedConfig *enterpris func (r *PostgresClusterReconciler) buildCNPGCluster( postgresCluster *enterprisev4.PostgresCluster, mergedConfig *enterprisev4.PostgresClusterSpec, + secretName string, ) *cnpgv1.Cluster { cnpgCluster := &cnpgv1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: postgresCluster.Name, Namespace: postgresCluster.Namespace, }, - Spec: r.buildCNPGClusterSpec(mergedConfig), + Spec: r.buildCNPGClusterSpec(mergedConfig, secretName), } ctrl.SetControllerReference(postgresCluster, cnpgCluster, r.Scheme) return cnpgCluster @@ -370,7 +410,7 @@ func (r *PostgresClusterReconciler) buildCNPGCluster( // poolerResourceName returns the CNPG Pooler resource name for a given cluster and type (rw/ro). func poolerResourceName(clusterName, poolerType string) string { - return fmt.Sprintf("%s-pooler-%s", clusterName, poolerType) + return fmt.Sprintf("%s%s%s", clusterName, defaultPoolerSuffix, poolerType) } // createOrUpdateConnectionPooler creates or updates CNPG Pooler resources. @@ -641,7 +681,6 @@ func (r *PostgresClusterReconciler) updateStatus( Message: message, ObservedGeneration: postgresCluster.Generation, }) - if err := r.Status().Update(ctx, postgresCluster); err != nil { return fmt.Errorf("failed to update PostgresCluster status: %w", err) } @@ -674,14 +713,16 @@ func (r *PostgresClusterReconciler) syncPoolerStatus(ctx context.Context, postgr rwDesired, rwScheduled := r.getPoolerInstanceCount(rwPooler) roDesired, roScheduled := r.getPoolerInstanceCount(roPooler) - r.updateStatus( + if err := r.updateStatus( ctx, postgresCluster, poolerReady, metav1.ConditionTrue, reasonAllInstancesReady, fmt.Sprintf("%s: %d/%d, %s: %d/%d", readWriteEndpoint, rwScheduled, rwDesired, readOnlyEndpoint, roScheduled, roDesired), - readyClusterPhase) // Not sure if we should use this phase here + readyClusterPhase); err != nil { + return err + } return nil } @@ -786,13 +827,17 @@ func (r *PostgresClusterReconciler) reconcileManagedRoles(ctx context.Context, p return nil } -func normalizeCNPGClusterSpec(spec cnpgv1.ClusterSpec, customDefinedParameters map[string]string) normalizedCNPGClusterSpec { +func normalizeCNPGClusterSpec(spec cnpgv1.ClusterSpec, customDefinedParameters map[string]string, secretName string) normalizedCNPGClusterSpec { normalizedConf := normalizedCNPGClusterSpec{ ImageName: spec.ImageName, Instances: spec.Instances, // Parameters intentionally excluded — CNPG injects defaults that we don't change StorageSize: spec.StorageConfiguration.Size, Resources: spec.Resources, + SuperuserSecret: cnpgv1.LocalObjectReference{ + Name: secretName, + }, + EnableSuperuserAccess: *ptr.To(true), } if len(customDefinedParameters) > 0 { @@ -812,10 +857,10 @@ func normalizeCNPGClusterSpec(spec cnpgv1.ClusterSpec, customDefinedParameters m return normalizedConf } -func (r *PostgresClusterReconciler) reconcileConfigMap(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) error { +func (r *PostgresClusterReconciler) reconcileConfigMap(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster, secretName string) error { logger := logs.FromContext(ctx) - configMap := r.generateConfigMap(postgresCluster, cnpgCluster) + configMap := r.generateConfigMap(ctx, postgresCluster, cnpgCluster, secretName) existingConfigMap := &corev1.ConfigMap{} err := r.Get(ctx, types.NamespacedName{Namespace: configMap.Namespace, Name: configMap.Name}, existingConfigMap) if err != nil && apierrors.IsNotFound(err) { @@ -823,14 +868,17 @@ func (r *PostgresClusterReconciler) reconcileConfigMap(ctx context.Context, post if err := r.Create(ctx, configMap); err != nil { return err } + if err := r.Get(ctx, types.NamespacedName{ + Name: postgresCluster.Name, + Namespace: postgresCluster.Namespace, + }, postgresCluster); err != nil { + logger.Error(err, "Failed to re-fetch PostgresCluster after creating ConfigMap") + return err + } if postgresCluster.Status.Resources == nil { postgresCluster.Status.Resources = &enterprisev4.PostgresClusterResources{} } postgresCluster.Status.Resources.ConfigMapRef = &corev1.LocalObjectReference{Name: configMap.Name} - if err := r.Status().Update(ctx, postgresCluster); err != nil { - logger.Error(err, "Failed to update PostgresCluster status with ConfigMap reference") - return err - } return nil } else if err != nil { logger.Error(err, "Failed to fetch existing ConfigMap") @@ -844,136 +892,114 @@ func (r *PostgresClusterReconciler) reconcileConfigMap(ctx context.Context, post return nil } -func (r *PostgresClusterReconciler) reconcileSecret(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) error { - logger := logs.FromContext(ctx) +// generateConfigMap generates a ConfigMap with connection details for the PostgresCluster. +func (r *PostgresClusterReconciler) generateConfigMap(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster, secretName string) *corev1.ConfigMap { + // Reuse existing name from status, or generate a new one with a random suffix + var name string - // Generate the desired Secret - secret, err := r.generateSecret(ctx, postgresCluster, cnpgCluster) - if err != nil { - return fmt.Errorf("failed to generate secret: %w", err) - } - - // Try to get existing Secret - existingSecret := &corev1.Secret{} - err = r.Get(ctx, types.NamespacedName{ - Namespace: secret.Namespace, - Name: secret.Name, - }, existingSecret) - - if err != nil && apierrors.IsNotFound(err) { - // Secret doesn't exist, create it - logger.Info("Secret not found, creating", "name", secret.Name) - if err := r.Create(ctx, secret); err != nil { - logger.Error(err, "Failed to create Secret") - return err - } - - // Update PostgresCluster status to reference the Secret - if postgresCluster.Status.Resources == nil { - postgresCluster.Status.Resources = &enterprisev4.PostgresClusterResources{} + if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.ConfigMapRef != nil { + name = postgresCluster.Status.Resources.ConfigMapRef.Name + } else { + // Attempt to find existing orphaned ConfigMap + cmList := &corev1.ConfigMapList{} + if err := r.List(ctx, cmList, client.InNamespace(postgresCluster.Namespace)); err == nil { + for _, cm := range cmList.Items { + if metav1.IsControlledBy(&cm, postgresCluster) && strings.HasPrefix(cm.Name, postgresCluster.Name+defaultConfigSuffix) { + name = cm.Name + break + } + } } - postgresCluster.Status.Resources.SecretRef = &corev1.LocalObjectReference{Name: secret.Name} - - if err := r.Status().Update(ctx, postgresCluster); err != nil { - logger.Error(err, "Failed to update PostgresCluster status with Secret reference") - return err + if name == "" { + name = fmt.Sprintf("%s%s%s", postgresCluster.Name, defaultConfigSuffix, generateRandomSuffix()) } - - logger.Info("Secret created successfully", "name", secret.Name) - return nil - } else if err != nil { - // Some other error occurred - logger.Error(err, "Failed to get Secret") - return err } - - // Secret exists, update its data - logger.Info("Secret exists, updating data", "name", secret.Name) - existingSecret.Data = secret.Data - if err := r.Update(ctx, existingSecret); err != nil { - logger.Error(err, "Failed to update Secret") - return err + if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.ConfigMapRef != nil { + name = postgresCluster.Status.Resources.ConfigMapRef.Name } - logger.Info("Secret updated successfully", "name", secret.Name) - return nil -} - -func (r *PostgresClusterReconciler) generateConfigMap(postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) *corev1.ConfigMap { - if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.ConfigMapRef != nil { - // If ConfigMap already exists, keep the same name to update it with new data - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: postgresCluster.Status.Resources.ConfigMapRef.Name, - Namespace: postgresCluster.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "postgrescluster-controller", - }, - }, - Data: map[string]string{ - "POSTGRES_CLUSTER_SERVICE_RW": cnpgCluster.Status.WriteService, - "POSTGRES_CLUSTER_SERVICE_RO": cnpgCluster.Status.ReadService, - "POSTGRES_CLUSTER_PORT": "5432", - }, - } + data := map[string]string{ + "CLUSTER_RW_ENDPOINT": fmt.Sprintf("%s.%s.svc", cnpgCluster.Status.WriteService, postgresCluster.Namespace), + "CLUSTER_R_ENDPOINT": fmt.Sprintf("%s.%s.svc", cnpgCluster.Status.ReadService, postgresCluster.Namespace), + "CLUSTER_RO_ENDPOINT": fmt.Sprintf("%so.%s.svc", cnpgCluster.Status.ReadService, postgresCluster.Namespace), + "DEFAULT_CLUSTER_PORT": defaultPort, + "DEFAULT_USER": defaultUsername, + "DEFAULT_USER_SECRET_REF": secretName, + } + if r.poolerExists(ctx, postgresCluster, readWriteEndpoint) && r.poolerExists(ctx, postgresCluster, readOnlyEndpoint) { + data["CLUSTER_POOLER_RW_ENDPOINT"] = fmt.Sprintf("%s.%s.svc", poolerResourceName(postgresCluster.Name, readWriteEndpoint), postgresCluster.Namespace) + data["CLUSTER_POOLER_RO_ENDPOINT"] = fmt.Sprintf("%s.%s.svc", poolerResourceName(postgresCluster.Name, readOnlyEndpoint), postgresCluster.Namespace) } configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-connection", postgresCluster.Name), + Name: name, Namespace: postgresCluster.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "postgrescluster-controller", - }, - }, - Data: map[string]string{ - "POSTGRES_CLUSTER_SERVICE_RW": cnpgCluster.Status.WriteService, - "POSTGRES_CLUSTER_SERVICE_RO": cnpgCluster.Status.ReadService, - "POSTGRES_CLUSTER_PORT": "5432", - + Labels: map[string]string{"app.kubernetes.io/managed-by": "postgrescluster-controller"}, }, + Data: data, } ctrl.SetControllerReference(postgresCluster, configMap, r.Scheme) return configMap } -func (r *PostgresClusterReconciler) generateSecret(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster) (*corev1.Secret, error) { - // Fetch CNPG secret (contains the "postgres" user credentials) - appSecret := &corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{ - Name: fmt.Sprintf("%s-app", cnpgCluster.Name), - Namespace: cnpgCluster.Namespace, - }, appSecret) - if err != nil { - return nil, fmt.Errorf("failed to get CNPG app secret: %w", err) - } +// generateSecret creates a Kubernetes Secret with credentials for the default postgres user if it doesn't already exist. +func (r *PostgresClusterReconciler) generateSecret(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, secretName string) error { + existing := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: postgresCluster.Namespace}, existing) - // Build Secret Data - use postgres user credentials for all databases - // Note: CNPG doesn't create separate superuser secrets by default - secretData := map[string][]byte{ - "username": appSecret.Data["username"], // "postgres" user - "password": appSecret.Data["password"], // postgres user password - } - - // Create Secret object - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-credentials", postgresCluster.Name), - Namespace: postgresCluster.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "postgrescluster-controller", + // If secret does not exist, create it + if apierrors.IsNotFound(err) { + password := generatePassword() + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: postgresCluster.Namespace, }, - }, - Type: corev1.SecretTypeOpaque, - Data: secretData, + StringData: map[string]string{ + "username": defaultUsername, + "password": password, + }, + Type: corev1.SecretTypeOpaque, + } + // Set owner reference so we can find it later if status update fails + if err := ctrl.SetControllerReference(postgresCluster, secret, r.Scheme); err != nil { + return err + } + if err := r.Create(ctx, secret); err != nil { + return err + } + } else if err != nil { + return err + } + if postgresCluster.Status.Resources == nil { + postgresCluster.Status.Resources = &enterprisev4.PostgresClusterResources{} } + postgresCluster.Status.Resources.SecretRef = &corev1.LocalObjectReference{Name: secretName} - // Set owner reference - if err := ctrl.SetControllerReference(postgresCluster, secret, r.Scheme); err != nil { - return nil, fmt.Errorf("failed to set controller reference: %w", err) + return nil +} + +// generateRandomSuffix returns a short random alphanumeric suffix. +func generateRandomSuffix() string { + b := make([]byte, 4) + if _, err := rand.Read(b); err != nil { + return fmt.Sprintf("%d", time.Now().UnixNano())[:8] } + return fmt.Sprintf("%x", b) +} - return secret, nil +// generatePassword creates a random 16-character alphanumeric password for the default postgres user. +func generatePassword() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "postgres" + } + for i := range b { + b[i] = charset[int(b[i])%len(charset)] + } + return string(b) } // SetupWithManager sets up the controller with the Manager. @@ -982,6 +1008,8 @@ func (r *PostgresClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&enterprisev4.PostgresCluster{}). Owns(&cnpgv1.Cluster{}). Owns(&cnpgv1.Pooler{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.Secret{}). Named("postgresCluster"). Complete(r) } diff --git a/internal/controller/postgresoperator_common_types.go b/internal/controller/postgresoperator_common_types.go index 771824c6f..e54bc47a6 100644 --- a/internal/controller/postgresoperator_common_types.go +++ b/internal/controller/postgresoperator_common_types.go @@ -1,6 +1,7 @@ package controller import ( + cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" corev1 "k8s.io/api/core/v1" "time" ) @@ -18,6 +19,8 @@ type normalizedCNPGClusterSpec struct { Owner string StorageSize string Resources corev1.ResourceRequirements + SuperuserSecret cnpgv1.LocalObjectReference + EnableSuperuserAccess bool } type reconcilePhases string @@ -27,14 +30,16 @@ type conditionReasons string type clusterReadyStatus string const ( - // default requeue delay - retryDelay = time.Second * 15 - // cluster endpoint suffixes - readOnlyEndpoint string = "ro" - readWriteEndpoint string = "rw" - // default database name + // defaults + retryDelay = time.Second * 15 + defaultPort = "5432" + readOnlyEndpoint string = "ro" + readWriteEndpoint string = "rw" defaultDatabaseName string = "postgres" defaultUsername string = "postgres" + defaultSecretSuffix string = "-secret-" + defaultConfigSuffix string = "-config-" + defaultPoolerSuffix string = "-pooler-" // phases ready reconcilePhases = "Ready" @@ -76,6 +81,9 @@ const ( reasonClusterGetFailed conditionReasons = "ClusterGetFailed" reasonClusterPatchFailed conditionReasons = "ClusterPatchFailed" reasonInvalidConfiguration conditionReasons = "InvalidConfiguration" + reasonConfigMapFailed conditionReasons = "ConfigMapReconciliationFailed" + reasonStatusSyncFailed conditionReasons = "StatusSyncFailed" + reasonUserSecretFailed conditionReasons = "UserSecretReconciliationFailed" // Additional condition reasons for poolerReady conditionType reasonPoolerReconciliationFailed conditionReasons = "PoolerReconciliationFailed" From 62295ef2437267f6d83dbe275e4e926e3c5ddca9 Mon Sep 17 00:00:00 2001 From: dpishchenkov Date: Thu, 5 Mar 2026 19:18:12 +0100 Subject: [PATCH 3/3] fix configmap and secrets logic --- ...nterprise.splunk.com_postgresclusters.yaml | 84 ++++++++++++++++++- config/rbac/role.yaml | 2 + go.mod | 1 + go.sum | 2 + .../controller/postgrescluster_controller.go | 75 ++++++++++------- .../postgresoperator_common_types.go | 10 +-- 6 files changed, 135 insertions(+), 39 deletions(-) diff --git a/config/crd/bases/enterprise.splunk.com_postgresclusters.yaml b/config/crd/bases/enterprise.splunk.com_postgresclusters.yaml index 205ce8438..45d6f6c5d 100644 --- a/config/crd/bases/enterprise.splunk.com_postgresclusters.yaml +++ b/config/crd/bases/enterprise.splunk.com_postgresclusters.yaml @@ -58,6 +58,37 @@ spec: x-kubernetes-validations: - message: class is immutable rule: self == oldSelf + connectionPoolerConfig: + description: |- + ConnectionPoolerConfig overrides the connection pooler configuration from the class. + Only takes effect when connection pooling is enabled. + properties: + config: + additionalProperties: + type: string + description: |- + Config contains PgBouncer configuration parameters. + Passed directly to CNPG Pooler spec.pgbouncer.parameters. + See: https://cloudnative-pg.io/docs/1.28/connection_pooling/#pgbouncer-configuration-options + type: object + instances: + default: 3 + description: |- + Instances is the number of PgBouncer pod replicas. + Higher values provide better availability and load distribution. + format: int32 + maximum: 10 + minimum: 1 + type: integer + mode: + default: transaction + description: Mode defines the connection pooling strategy. + enum: + - session + - transaction + - statement + type: string + type: object connectionPoolerEnabled: default: false description: |- @@ -128,7 +159,6 @@ spec: type: string type: array postgresVersion: - default: "18" description: |- PostgresVersion is the PostgreSQL version (major or major.minor). Examples: "18" (latest 18.x), "18.1" (specific minor), "17", "16" @@ -216,9 +246,22 @@ spec: - class type: object x-kubernetes-validations: - - message: Storage size cannot be removed and can only be increased + - messageExpression: '!has(self.postgresVersion) ? ''postgresVersion cannot + be removed once set (was: '' + oldSelf.postgresVersion + '')'' : ''postgresVersion + major version cannot be downgraded (from: '' + oldSelf.postgresVersion + + '', to: '' + self.postgresVersion + '')''' + rule: '!has(oldSelf.postgresVersion) || (has(self.postgresVersion) && + int(self.postgresVersion.split(''.'')[0]) >= int(oldSelf.postgresVersion.split(''.'')[0]))' + - messageExpression: '!has(self.storage) ? ''storage cannot be removed + once set (was: '' + string(oldSelf.storage) + '')'' : ''storage size + cannot be decreased (from: '' + string(oldSelf.storage) + '', to: + '' + string(self.storage) + '')''' rule: '!has(oldSelf.storage) || (has(self.storage) && quantity(self.storage).compareTo(quantity(oldSelf.storage)) >= 0)' + - message: connectionPoolerConfig must be set when connectionPoolerEnabled + is true + rule: '!self.connectionPoolerEnabled || self.connectionPoolerConfig + != null' status: description: PostgresClusterStatus defines the observed state of PostgresCluster. properties: @@ -363,6 +406,43 @@ spec: type: string type: object x-kubernetes-map-type: atomic + resources: + description: Resources contains references to related Kubernetes resources + like ConfigMaps and Secrets. + properties: + configMapRef: + description: |- + ConfigMapRef references the ConfigMap with connection endpoints. + Contains: CLUSTER_ENDPOINTS, POOLER_ENDPOINTS (if connection pooler enabled) + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + secretRef: + description: |- + SecretRef references the Secret with superuser credentials. + Contains: passwords for superuser + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object type: object type: object served: true diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f1506faf9..e7f4b73e1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -110,6 +110,7 @@ rules: resources: - clusters - databases + - poolers verbs: - create - delete @@ -122,5 +123,6 @@ rules: - postgresql.cnpg.io resources: - clusters/status + - poolers/status verbs: - get diff --git a/go.mod b/go.mod index f26c17001..3e39b2ebb 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/onsi/gomega v1.39.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 + github.com/sethvargo/go-password v0.3.1 github.com/stretchr/testify v1.11.1 github.com/wk8/go-ordered-map/v2 v2.1.7 go.uber.org/zap v1.27.1 diff --git a/go.sum b/go.sum index d908caadc..bfada64f1 100644 --- a/go.sum +++ b/go.sum @@ -305,6 +305,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= +github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/internal/controller/postgrescluster_controller.go b/internal/controller/postgrescluster_controller.go index 77ee37dc3..b97ff0c7a 100644 --- a/internal/controller/postgrescluster_controller.go +++ b/internal/controller/postgrescluster_controller.go @@ -18,12 +18,11 @@ package controller import ( "context" - "crypto/rand" "fmt" "strings" - "time" cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" + "github.com/sethvargo/go-password/password" enterprisev4 "github.com/splunk/splunk-operator/api/v4" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -117,7 +116,15 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // Generate secret using the cluster name and a random suffix to avoid collisions. if postgresSecretName == "" { - postgresSecretName = fmt.Sprintf("%s%s%s", postgresCluster.Name, defaultSecretSuffix, generateRandomSuffix()) + suffix, err := generateRandomSuffix() + if err != nil { + logger.Error(err, "Failed to generate random suffix for PostgresCluster secret") + if statusErr := updateStatus(clusterReady, metav1.ConditionFalse, reasonUserSecretFailed, fmt.Sprintf("Failed to generate random suffix for PostgresCluster secret: %v", err), failedClusterPhase); statusErr != nil { + logger.Error(statusErr, "Failed to update status") + } + return ctrl.Result{}, err + } + postgresSecretName = fmt.Sprintf("%s%s%s", postgresCluster.Name, defaultSecretSuffix, suffix) } } logger.Info("Creating PostgresCluster secret", "name", postgresSecretName) @@ -278,8 +285,6 @@ func (r *PostgresClusterReconciler) Reconcile(ctx context.Context, req ctrl.Requ // 8. If CNPG is ready, generate ConfigMap if cnpgCluster.Status.Phase == cnpgv1.PhaseHealthy { logger.Info("CNPG Cluster is ready, generating ConfigMap for connection details") - - // Reconcile ConfigMap if err := r.reconcileConfigMap(ctx, postgresCluster, cnpgCluster, postgresSecretName); err != nil { logger.Error(err, "Failed to reconcile ConfigMap") if statusErr := updateStatus(clusterReady, metav1.ConditionFalse, reasonConfigMapFailed, fmt.Sprintf("Failed to reconcile ConfigMap: %v", err), failedClusterPhase); statusErr != nil { @@ -379,7 +384,7 @@ func (r *PostgresClusterReconciler) buildCNPGClusterSpec(mergedConfig *enterpris Bootstrap: &cnpgv1.BootstrapConfiguration{ InitDB: &cnpgv1.BootstrapInitDB{ Database: defaultDatabaseName, - Owner: defaultUsername, + Owner: superUsername, }, }, StorageConfiguration: cnpgv1.StorageConfiguration{ @@ -837,7 +842,7 @@ func normalizeCNPGClusterSpec(spec cnpgv1.ClusterSpec, customDefinedParameters m SuperuserSecret: cnpgv1.LocalObjectReference{ Name: secretName, }, - EnableSuperuserAccess: *ptr.To(true), + EnableSuperuserAccess: ptr.To(true), } if len(customDefinedParameters) > 0 { @@ -894,28 +899,33 @@ func (r *PostgresClusterReconciler) reconcileConfigMap(ctx context.Context, post // generateConfigMap generates a ConfigMap with connection details for the PostgresCluster. func (r *PostgresClusterReconciler) generateConfigMap(ctx context.Context, postgresCluster *enterprisev4.PostgresCluster, cnpgCluster *cnpgv1.Cluster, secretName string) *corev1.ConfigMap { - // Reuse existing name from status, or generate a new one with a random suffix - var name string + logger := logs.FromContext(ctx) + var configMapName string if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.ConfigMapRef != nil { - name = postgresCluster.Status.Resources.ConfigMapRef.Name + configMapName = postgresCluster.Status.Resources.ConfigMapRef.Name } else { // Attempt to find existing orphaned ConfigMap cmList := &corev1.ConfigMapList{} if err := r.List(ctx, cmList, client.InNamespace(postgresCluster.Namespace)); err == nil { for _, cm := range cmList.Items { if metav1.IsControlledBy(&cm, postgresCluster) && strings.HasPrefix(cm.Name, postgresCluster.Name+defaultConfigSuffix) { - name = cm.Name + configMapName = cm.Name break } } } - if name == "" { - name = fmt.Sprintf("%s%s%s", postgresCluster.Name, defaultConfigSuffix, generateRandomSuffix()) + if configMapName == "" { + suffix, err := generateRandomSuffix() + if err != nil { + logger.Error(err, "Failed to generate random suffix for ConfigMap") + return nil + } + configMapName = fmt.Sprintf("%s%s%s", postgresCluster.Name, defaultConfigSuffix, suffix) } } if postgresCluster.Status.Resources != nil && postgresCluster.Status.Resources.ConfigMapRef != nil { - name = postgresCluster.Status.Resources.ConfigMapRef.Name + configMapName = postgresCluster.Status.Resources.ConfigMapRef.Name } data := map[string]string{ @@ -923,7 +933,7 @@ func (r *PostgresClusterReconciler) generateConfigMap(ctx context.Context, postg "CLUSTER_R_ENDPOINT": fmt.Sprintf("%s.%s.svc", cnpgCluster.Status.ReadService, postgresCluster.Namespace), "CLUSTER_RO_ENDPOINT": fmt.Sprintf("%so.%s.svc", cnpgCluster.Status.ReadService, postgresCluster.Namespace), "DEFAULT_CLUSTER_PORT": defaultPort, - "DEFAULT_USER": defaultUsername, + "SUPER_USER_NAME": superUsername, "DEFAULT_USER_SECRET_REF": secretName, } if r.poolerExists(ctx, postgresCluster, readWriteEndpoint) && r.poolerExists(ctx, postgresCluster, readOnlyEndpoint) { @@ -933,7 +943,7 @@ func (r *PostgresClusterReconciler) generateConfigMap(ctx context.Context, postg configMap := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: name, + Name: configMapName, Namespace: postgresCluster.Namespace, Labels: map[string]string{"app.kubernetes.io/managed-by": "postgrescluster-controller"}, }, @@ -950,14 +960,17 @@ func (r *PostgresClusterReconciler) generateSecret(ctx context.Context, postgres // If secret does not exist, create it if apierrors.IsNotFound(err) { - password := generatePassword() + password, err := generatePassword() + if err != nil { + return err + } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: postgresCluster.Namespace, }, StringData: map[string]string{ - "username": defaultUsername, + "username": superUsername, "password": password, }, Type: corev1.SecretTypeOpaque, @@ -981,25 +994,23 @@ func (r *PostgresClusterReconciler) generateSecret(ctx context.Context, postgres } // generateRandomSuffix returns a short random alphanumeric suffix. -func generateRandomSuffix() string { - b := make([]byte, 4) - if _, err := rand.Read(b); err != nil { - return fmt.Sprintf("%d", time.Now().UnixNano())[:8] +func generateRandomSuffix() (string, error) { + + suff, err := password.Generate(5, 2, 0, false, false) + if err != nil { + fmt.Printf("Error generating random suffix: %v", err) + return "", err } - return fmt.Sprintf("%x", b) + return suff, nil } // generatePassword creates a random 16-character alphanumeric password for the default postgres user. -func generatePassword() string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, 16) - if _, err := rand.Read(b); err != nil { - return "postgres" - } - for i := range b { - b[i] = charset[int(b[i])%len(charset)] +func generatePassword() (string, error) { + pass, err := password.Generate(16, 4, 0, false, false) + if err != nil { + return "", err } - return string(b) + return pass, nil } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/controller/postgresoperator_common_types.go b/internal/controller/postgresoperator_common_types.go index e54bc47a6..1baa1cee6 100644 --- a/internal/controller/postgresoperator_common_types.go +++ b/internal/controller/postgresoperator_common_types.go @@ -20,7 +20,7 @@ type normalizedCNPGClusterSpec struct { StorageSize string Resources corev1.ResourceRequirements SuperuserSecret cnpgv1.LocalObjectReference - EnableSuperuserAccess bool + EnableSuperuserAccess *bool } type reconcilePhases string @@ -36,10 +36,10 @@ const ( readOnlyEndpoint string = "ro" readWriteEndpoint string = "rw" defaultDatabaseName string = "postgres" - defaultUsername string = "postgres" - defaultSecretSuffix string = "-secret-" - defaultConfigSuffix string = "-config-" - defaultPoolerSuffix string = "-pooler-" + superUsername string = "postgres" + defaultSecretSuffix string = "-secret-" + defaultConfigSuffix string = "-config-" + defaultPoolerSuffix string = "-pooler-" // phases ready reconcilePhases = "Ready"