From 582cf364c3ccc84eb3a1a92fa15ea1b749c7c685 Mon Sep 17 00:00:00 2001 From: doodgeMatvey Date: Fri, 30 Jun 2023 12:04:49 +0300 Subject: [PATCH] issue-135, saving and updating the PostgreSQL default password were implemented --- apis/clusters/v1beta1/postgresql_types.go | 37 ++- .../clusters.instaclustr.com_postgresqls.yaml | 2 - .../cassandrauser_controller.go | 4 +- controllers/clusters/postgresql_controller.go | 269 ++++++++++++------ pkg/models/kafka_user_apv2.go | 5 - pkg/models/operator.go | 39 +-- 6 files changed, 234 insertions(+), 122 deletions(-) diff --git a/apis/clusters/v1beta1/postgresql_types.go b/apis/clusters/v1beta1/postgresql_types.go index 45a598825..1a8758e7a 100644 --- a/apis/clusters/v1beta1/postgresql_types.go +++ b/apis/clusters/v1beta1/postgresql_types.go @@ -93,8 +93,7 @@ type PgSpec struct { // PgStatus defines the observed state of PostgreSQL type PgStatus struct { - ClusterStatus `json:",inline"` - DefaultUserSecretName string `json:"defaultUserSecretName,omitempty"` + ClusterStatus `json:",inline"` } //+kubebuilder:object:root=true @@ -303,18 +302,18 @@ func (pdc *PgDataCentre) ArePGBouncersEqual(iPGBs []*PgBouncer) bool { } func (pg *PostgreSQL) GetUserPassword(secret *k8sCore.Secret) string { - password := secret.Data[models.DefaultUserPassword] + password := secret.Data[models.Password] if len(password) == 0 { return "" } - return string(password[:len(password)-1]) + return string(password) } func (pg *PostgreSQL) GetUserSecret(ctx context.Context, k8sClient client.Client) (*k8sCore.Secret, error) { userSecret := &k8sCore.Secret{} userSecretNamespacedName := types.NamespacedName{ - Name: pg.Status.DefaultUserSecretName, + Name: fmt.Sprintf(models.DefaultUserSecretNameTemplate, models.DefaultUserSecretPrefix, pg.Name), Namespace: pg.Namespace, } err := k8sClient.Get(ctx, userSecretNamespacedName, userSecret) @@ -347,18 +346,24 @@ func (pg *PostgreSQL) GetUserSecretName(ctx context.Context, k8sClient client.Cl return userSecretList.Items[0].Name, nil } -func (pg *PostgreSQL) NewUserSecret() *k8sCore.Secret { +func (pg *PostgreSQL) NewUserSecret(defaultUserPassword string) *k8sCore.Secret { return &k8sCore.Secret{ TypeMeta: metav1.TypeMeta{ Kind: models.SecretKind, APIVersion: models.K8sAPIVersionV1, }, ObjectMeta: metav1.ObjectMeta{ - Name: models.DefaultUserSecretPrefix + pg.Name, + Name: fmt.Sprintf(models.DefaultUserSecretNameTemplate, models.DefaultUserSecretPrefix, pg.Name), Namespace: pg.Namespace, - Labels: map[string]string{models.ControlledByLabel: pg.Name}, + Labels: map[string]string{ + models.ControlledByLabel: pg.Name, + models.DefaultSecretLabel: "true", + }, + }, + StringData: map[string]string{ + models.Username: models.DefaultPgUsernameValue, + models.Password: defaultUserPassword, }, - StringData: map[string]string{models.DefaultUserPassword: ""}, } } @@ -519,6 +524,20 @@ func (pg *PostgreSQL) FromInstAPI(iData []byte) (*PostgreSQL, error) { }, nil } +func (pg *PostgreSQL) DefaultPasswordFromInstAPI(iData []byte) (string, error) { + type defaultPasswordResponse struct { + DefaultUserPassword string `json:"defaultUserPassword,omitempty"` + } + + dpr := &defaultPasswordResponse{} + err := json.Unmarshal(iData, dpr) + if err != nil { + return "", err + } + + return dpr.DefaultUserPassword, nil +} + func (pgs *PgSpec) FromInstAPI(iPg *models.PGCluster) PgSpec { return PgSpec{ Cluster: Cluster{ diff --git a/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml b/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml index d5fd6f9cf..a01959df6 100644 --- a/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml +++ b/config/crd/bases/clusters.instaclustr.com_postgresqls.yaml @@ -228,8 +228,6 @@ spec: type: string type: object type: array - defaultUserSecretName: - type: string id: type: string maintenanceEvents: diff --git a/controllers/clusterresources/cassandrauser_controller.go b/controllers/clusterresources/cassandrauser_controller.go index 595848b3a..4bc3c6456 100644 --- a/controllers/clusterresources/cassandrauser_controller.go +++ b/controllers/clusterresources/cassandrauser_controller.go @@ -205,8 +205,8 @@ func (r *CassandraUserReconciler) Reconcile(ctx context.Context, req ctrl.Reques } func (r *CassandraUserReconciler) getUserCreds(secret *k8sCore.Secret) (username, password string, err error) { - password = string(secret.Data["password"]) - username = string(secret.Data["username"]) + password = string(secret.Data[models.Password]) + username = string(secret.Data[models.Username]) if len(username) == 0 || len(password) == 0 { return "", "", models.ErrMissingSecretKeys diff --git a/controllers/clusters/postgresql_controller.go b/controllers/clusters/postgresql_controller.go index 1484aace1..c23db4e4f 100644 --- a/controllers/clusters/postgresql_controller.go +++ b/controllers/clusters/postgresql_controller.go @@ -90,11 +90,13 @@ func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) switch pg.Annotations[models.ResourceStateAnnotation] { case models.CreatingEvent: - return r.HandleCreateCluster(ctx, pg, logger), nil + return r.handleCreateCluster(ctx, pg, logger), nil case models.UpdatingEvent: - return r.HandleUpdateCluster(ctx, pg, logger), nil + return r.handleUpdateCluster(ctx, pg, logger), nil case models.DeletingEvent: - return r.HandleDeleteCluster(ctx, pg, logger), nil + return r.handleDeleteCluster(ctx, pg, logger), nil + case models.SecretEvent: + return r.handleUpdateDefaultUserPassword(ctx, pg, logger), nil case models.GenericEvent: logger.Info("PostgreSQL resource generic event isn't handled", "cluster name", pg.Spec.Name, @@ -112,7 +114,7 @@ func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } -func (r *PostgreSQLReconciler) HandleCreateCluster( +func (r *PostgreSQLReconciler) handleCreateCluster( ctx context.Context, pg *v1beta1.PostgreSQL, logger logr.Logger, @@ -123,57 +125,6 @@ func (r *PostgreSQLReconciler) HandleCreateCluster( var err error patch := pg.NewPatch() - if pg.Status.DefaultUserSecretName == "" { - secretName, err := pg.GetUserSecretName(ctx, r.Client) - if err != nil { - logger.Error(err, "Cannot get PostgreSQL secret name", - "cluster name", pg.Spec.Name, - "cluster ID", pg.Status.ID, - ) - - r.EventRecorder.Eventf( - pg, models.Warning, models.FetchSecretFailed, - "Default user secret fetch is failed. Reason: %v", - err, - ) - - return models.ReconcileRequeue - } - - pg.Status.DefaultUserSecretName = secretName - - if secretName == "" { - secret := pg.NewUserSecret() - err = r.Client.Create(ctx, secret) - if err != nil { - logger.Error(err, "Cannot create PostgreSQL default user secret", - "cluster ID", pg.Status.ID, - ) - - r.EventRecorder.Eventf( - pg, models.Warning, models.CreationFailed, - "Default user secret creation is failed. Reason: %v", - err, - ) - - return models.ReconcileRequeue - } - - pg.Status.DefaultUserSecretName = secret.Name - - logger.Info("PostgreSQL default user secret was created", - "secret name", secret.Name, - "cluster ID", pg.Status.ID, - ) - - r.EventRecorder.Eventf( - pg, models.Normal, models.Created, - "Default user secret is created. Secret name: %s", - pg.Status.DefaultUserSecretName, - ) - } - } - if pg.Status.ID == "" { if pg.Spec.HasRestore() { logger.Info( @@ -319,14 +270,26 @@ func (r *PostgreSQLReconciler) HandleCreateCluster( "Cluster backups check job is started", ) - if pg.Annotations[models.ResourceStateAnnotation] == models.UpdatingEvent { - return reconcile.Result{Requeue: true} + err = r.createDefaultPassword(pg, logger) + if err != nil { + logger.Error(err, "Cannot create default password for PostgreSQL", + "cluster name", pg.Spec.Name, + "clusterID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.CreationFailed, + "Default user secret creation on the Instaclustr is failed. Reason: %v", + err, + ) + + return models.ReconcileRequeue } return models.ExitReconcile } -func (r *PostgreSQLReconciler) HandleUpdateCluster( +func (r *PostgreSQLReconciler) handleUpdateCluster( ctx context.Context, pg *v1beta1.PostgreSQL, logger logr.Logger, @@ -456,21 +419,6 @@ func (r *PostgreSQLReconciler) HandleUpdateCluster( return models.ReconcileRequeue } - err = r.updateDefaultUserPassword(ctx, pg) - if err != nil { - logger.Error(err, "Cannot update PostgreSQL default user password", - "cluster ID", pg.Status.ID, - "secret name", pg.Status.DefaultUserSecretName, - ) - - r.EventRecorder.Eventf( - pg, models.Warning, models.UpdateFailed, - "Default user password update is failed. Reason: %v", - err, - ) - return models.ReconcileRequeue - } - pg.Annotations[models.ResourceStateAnnotation] = models.UpdatedEvent err = r.patchClusterMetadata(ctx, pg, logger) if err != nil { @@ -538,7 +486,7 @@ func (r *PostgreSQLReconciler) handleExternalChanges(pg, iPg *v1beta1.PostgreSQL } } -func (r *PostgreSQLReconciler) HandleDeleteCluster( +func (r *PostgreSQLReconciler) handleDeleteCluster( ctx context.Context, pg *v1beta1.PostgreSQL, logger logr.Logger, @@ -611,7 +559,6 @@ func (r *PostgreSQLReconciler) HandleDeleteCluster( logger.Info("PostgreSQL cluster is being deleted. Deleting PostgreSQL default user secret", "cluster ID", pg.Status.ID, - "secret name", pg.Status.DefaultUserSecretName, ) err = r.deleteSecret(ctx, pg) @@ -623,7 +570,6 @@ func (r *PostgreSQLReconciler) HandleDeleteCluster( r.EventRecorder.Eventf( pg, models.Warning, models.DeletionFailed, "Default user secret deletion is failed. Secret name: %s. Reason: %v", - pg.Status.DefaultUserSecretName, err, ) return models.ReconcileRequeue @@ -636,7 +582,6 @@ func (r *PostgreSQLReconciler) HandleDeleteCluster( r.EventRecorder.Eventf( pg, models.Normal, models.Deleted, "Default user secret is deleted. Secret name: %s", - pg.Status.DefaultUserSecretName, ) logger.Info("Deleting cluster backup resources", @@ -708,32 +653,90 @@ func (r *PostgreSQLReconciler) HandleDeleteCluster( return models.ExitReconcile } -func (r *PostgreSQLReconciler) updateDefaultUserPassword( +func (r *PostgreSQLReconciler) handleUpdateDefaultUserPassword( ctx context.Context, pg *v1beta1.PostgreSQL, -) error { + logger logr.Logger, +) reconcile.Result { + logger = logger.WithName("PostgreSQL default user password updating event") + secret, err := pg.GetUserSecret(ctx, r.Client) if err != nil { - return err - } + logger.Error(err, "Cannot get the default secret for the PostgreSQL cluster", + "cluster name", pg.Spec.Name, + "cluster ID", pg.Status.ID, + ) - if secret.Generation == 0 { - return nil + r.EventRecorder.Eventf( + pg, models.Warning, models.FetchFailed, + "Fetch default user secret is failed. Reason: %v", + err, + ) + + return models.ReconcileRequeue } password := pg.GetUserPassword(secret) isValid := pg.ValidateDefaultUserPassword(password) if !isValid { - return models.ErrNotValidPassword + logger.Error(err, "Default PostgreSQL user password is not valid. This field must be at least 8 characters long. Must contain characters from at least 3 of the following 4 categories: Uppercase, Lowercase, Numbers, Special Characters", + "cluster name", pg.Spec.Name, + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.ValidationFailed, + "Validation for default user secret is failed. Reason: %v", + err, + ) + + return models.ReconcileRequeue } err = r.API.UpdatePostgreSQLDefaultUserPassword(pg.Status.ID, password) if err != nil { - return err + logger.Error(err, "Cannot update default PostgreSQL user password", + "cluster name", pg.Spec.Name, + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.UpdateFailed, + "Default user password update on the Instaclustr API is failed. Reason: %v", + err, + ) + + return models.ReconcileRequeue } - return nil + pg.Annotations[models.ResourceStateAnnotation] = models.UpdatedEvent + err = r.patchClusterMetadata(ctx, pg, logger) + if err != nil { + logger.Error(err, "Cannot patch PostgreSQL resource metadata", + "cluster name", pg.Spec.Name, + "cluster metadata", pg.ObjectMeta, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.PatchFailed, + "Cluster resource patch is failed. Reason: %v", + err, + ) + return models.ReconcileRequeue + } + + logger.Info("PostgreSQL default user password was updated", + "cluster name", pg.Spec.Name, + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Normal, models.UpdatedEvent, + "Cluster default user password is updated", + ) + + return models.ExitReconcile } func (r *PostgreSQLReconciler) startClusterStatusJob(pg *v1beta1.PostgreSQL) error { @@ -930,6 +933,90 @@ func (r *PostgreSQLReconciler) newWatchStatusJob(pg *v1beta1.PostgreSQL) schedul } } +func (r *PostgreSQLReconciler) createDefaultPassword(pg *v1beta1.PostgreSQL, l logr.Logger) error { + iData, err := r.API.GetPostgreSQL(pg.Status.ID) + if err != nil { + l.Error( + err, "Cannot get PostgreSQL cluster status from the Instaclustr API", + "cluster name", pg.Spec.Name, + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.FetchFailed, + "Cluster fetch from the Instaclustr API is failed. Reason: %v", + err, + ) + return err + } + + secret, err := pg.GetUserSecret(context.TODO(), r.Client) + if err != nil && !k8serrors.IsNotFound(err) { + r.EventRecorder.Eventf( + pg, models.Warning, models.FetchFailed, + "Default user secret fetch is failed. Reason: %v", + err, + ) + + return err + } + + if secret != nil { + l.Info("Default user secret for PostgreSQL cluster already exists", + "cluster name", pg.Spec.Name, + "clusterID", pg.Status.ID, + ) + + return nil + } + + defaultUserPassword, err := pg.DefaultPasswordFromInstAPI(iData) + if err != nil { + l.Error(err, "Cannot get default user creds for PostgreSQL cluster from the Instaclustr API", + "cluster name", pg.Spec.Name, + "clusterID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.FetchFailed, + "Default user password fetch from the Instaclustr API is failed. Reason: %v", + err, + ) + + return err + } + + secret = pg.NewUserSecret(defaultUserPassword) + err = r.Client.Create(context.TODO(), secret) + if err != nil { + l.Error(err, "Cannot create PostgreSQL default user secret", + "cluster ID", pg.Status.ID, + "secret name", secret.Name, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.CreationFailed, + "Default user secret creation is failed. Reason: %v", + err, + ) + + return err + } + + l.Info("PostgreSQL default user secret was created", + "secret name", secret.Name, + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Normal, models.Created, + "Default user secret is created. Secret name: %s", + secret.Name, + ) + + return nil +} + func (r *PostgreSQLReconciler) newWatchBackupsJob(pg *v1beta1.PostgreSQL) scheduler.Job { l := log.Log.WithValues("component", "postgreSQLBackupsClusterJob") @@ -1197,6 +1284,12 @@ func (r *PostgreSQLReconciler) updateDescriptionAndTwoFactorDelete(pgCluster *v1 } func (r *PostgreSQLReconciler) findSecretObject(secret client.Object) []reconcile.Request { + s := secret.(*k8sCore.Secret) + + if s.Labels[models.DefaultSecretLabel] != "true" { + return []reconcile.Request{} + } + pg := &v1beta1.PostgreSQL{} pgNamespacedName := types.NamespacedName{ Namespace: secret.GetNamespace(), @@ -1212,7 +1305,7 @@ func (r *PostgreSQLReconciler) findSecretObject(secret client.Object) []reconcil } patch := pg.NewPatch() - pg.Annotations[models.ResourceStateAnnotation] = models.UpdatingEvent + pg.Annotations[models.ResourceStateAnnotation] = models.SecretEvent err = r.Patch(context.TODO(), pg, patch) if err != nil { return []reconcile.Request{} @@ -1264,7 +1357,7 @@ func (r *PostgreSQLReconciler) SetupWithManager(mgr ctrl.Manager) error { handler.EnqueueRequestsFromMapFunc(r.findSecretObject), builder.WithPredicates(predicate.Funcs{ CreateFunc: func(createEvent event.CreateEvent) bool { - return createEvent.Object.GetGeneration() == 0 + return false }, }), ). diff --git a/pkg/models/kafka_user_apv2.go b/pkg/models/kafka_user_apv2.go index 08a21febe..70303b716 100644 --- a/pkg/models/kafka_user_apv2.go +++ b/pkg/models/kafka_user_apv2.go @@ -16,11 +16,6 @@ limitations under the License. package models -const ( - Username = "Username" - Password = "Password" -) - type KafkaUser struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` diff --git a/pkg/models/operator.go b/pkg/models/operator.go index e4c1b1dbc..1a5ea801c 100644 --- a/pkg/models/operator.go +++ b/pkg/models/operator.go @@ -30,6 +30,7 @@ const ( DeletionFinalizer = "instaclustr.com/deletionFinalizer" StartTimestampAnnotation = "instaclustr.com/startTimestamp" + DefaultSecretLabel = "instaclustr.com/defaultSecret" ControlledByLabel = "instaclustr.com/controlledBy" ClusterIDLabel = "instaclustr.com/clusterID" ClusterNameLabel = "instaclustr.com/clusterName" @@ -61,18 +62,19 @@ const ( Triggered = "triggered" - ClusterBackupKind = "ClusterBackup" - PgClusterKind = "PostgreSQL" - RedisClusterKind = "Redis" - OsClusterKind = "OpenSearch" - CassandraClusterKind = "Cassandra" - ZookeeperClusterKind = "Zookeeper" - SecretKind = "Secret" - PgBackupEventType = "postgresql-backup" - SnapshotUploadEventType = "snapshot-upload" - PgBackupPrefix = "postgresql-backup-" - SnapshotUploadPrefix = "snapshot-upload-" - DefaultUserSecretPrefix = "default-user-password-" + ClusterBackupKind = "ClusterBackup" + PgClusterKind = "PostgreSQL" + RedisClusterKind = "Redis" + OsClusterKind = "OpenSearch" + CassandraClusterKind = "Cassandra" + ZookeeperClusterKind = "Zookeeper" + SecretKind = "Secret" + PgBackupEventType = "postgresql-backup" + SnapshotUploadEventType = "snapshot-upload" + PgBackupPrefix = "postgresql-backup-" + SnapshotUploadPrefix = "snapshot-upload-" + DefaultUserSecretPrefix = "default-user-password" + DefaultUserSecretNameTemplate = "%s-%s" CassandraConnectionPort = 9042 CadenceConnectionPort = 7933 @@ -101,6 +103,8 @@ const ( KafkaConnectAppType = "KAFKA_CONNECT" CassandraAppType = "APACHE_CASSANDRA" SparkAppType = "SPARK" + + DefaultPgUsernameValue = "icpostgresql" ) const ( @@ -124,6 +128,7 @@ const ( FetchFailed = "FetchFailed" FetchSecretFailed = "FetchSecretFailed" ConvertionFailed = "ConvertionFailed" + ValidationFailed = "ValidationFailed" UpdateFailed = "UpdateFailed" UpdatedSecret = "UpdatedSecret" ExternalChanges = "ExternalChanges" @@ -133,10 +138,12 @@ const ( ) const ( - ReplaceOperation = "replace" - AnnotationsPath = "/metadata/annotations" - FinalizersPath = "/metadata/finalizers" - DefaultUserPassword = "defaultUserPassword" + ReplaceOperation = "replace" + AnnotationsPath = "/metadata/annotations" + FinalizersPath = "/metadata/finalizers" + + Username = "username" + Password = "password" ) const Requeue60 = time.Second * 60