diff --git a/controllers/clusterresources/cassandrauser_controller.go b/controllers/clusterresources/cassandrauser_controller.go index 76ddf6c5d..1742bb1ba 100644 --- a/controllers/clusterresources/cassandrauser_controller.go +++ b/controllers/clusterresources/cassandrauser_controller.go @@ -195,7 +195,7 @@ func (r *CassandraUserReconciler) Reconcile(ctx context.Context, req ctrl.Reques l.Error(err, "Cannot detach clusterID from the Cassandra user resource", "cluster ID", clusterID) r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, - "Detaching clusterID from the OpenSearch user resource has been failed. Reason: %v", err) + "Detaching clusterID from the Cassandra user resource has been failed. Reason: %v", err) return models.ReconcileRequeue, nil } diff --git a/controllers/clusterresources/postgresqluser_controller.go b/controllers/clusterresources/postgresqluser_controller.go index 735ed585c..4fb208531 100644 --- a/controllers/clusterresources/postgresqluser_controller.go +++ b/controllers/clusterresources/postgresqluser_controller.go @@ -34,6 +34,7 @@ import ( clusterresourcesv1beta1 "github.com/instaclustr/operator/apis/clusterresources/v1beta1" "github.com/instaclustr/operator/pkg/exposeservice" + "github.com/instaclustr/operator/pkg/instaclustr" "github.com/instaclustr/operator/pkg/models" ) @@ -170,10 +171,86 @@ func (r *PostgreSQLUserReconciler) Reconcile(ctx context.Context, req ctrl.Reque continue } - // TODO: implement user deletion logic on this event + if clusterInfo.Event == models.DeletingEvent { + l.Info("Deleting user from a cluster", "cluster ID", clusterID) + + err = r.deleteUser(ctx, newUsername, clusterInfo.DefaultSecretNamespacedName) + if err != nil { + l.Error(err, "Cannot delete PostgreSQL user") + r.EventRecorder.Eventf(u, models.Warning, models.DeletingEvent, + "Cannot delete user. Reason: %v", err) + + return models.ReconcileRequeue, nil + } + + l.Info("User has been deleted for cluster", "username", newUsername, + "cluster ID", clusterID) + r.EventRecorder.Eventf(u, models.Normal, models.Deleted, + "User has been deleted for a cluster. Cluster ID: %s, username: %s", + clusterID, newUsername) + + delete(u.Status.ClustersInfo, clusterID) + + err = r.Status().Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot patch PostgreSQL user status") + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Resource patch is failed. Reason: %v", err) + + return models.ReconcileRequeue, nil + } + + continue + } + + if clusterInfo.Event == models.ClusterDeletingEvent { + delete(u.Status.ClustersInfo, clusterID) + + err = r.Status().Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot detach clusterID from PostgreSQL user resource", + "cluster ID", clusterID) + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Detaching clusterID from the PostgreSQL user resource has been failed. Reason: %v", err) + return models.ReconcileRequeue, nil + } + + l.Info("PostgreSQL user has been detached from the cluster", "cluster ID", clusterID) + r.EventRecorder.Eventf(u, models.Normal, models.Deleted, + "User has been detached from the cluster. ClusterID: %v", clusterID) + } } - // TODO: add logic for Deletion case + if u.DeletionTimestamp != nil { + if u.Status.ClustersInfo != nil { + l.Error(models.ErrUserStillExist, instaclustr.MsgDeleteUser) + r.EventRecorder.Event(u, models.Warning, models.DeletingEvent, instaclustr.MsgDeleteUser) + + return models.ExitReconcile, nil + } + + controllerutil.RemoveFinalizer(s, u.GetDeletionFinalizer()) + err = r.Update(ctx, s) + if err != nil { + l.Error(err, "Cannot delete finalizer from the user's secret") + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Deleting finalizer from the user's secret has been failed. Reason: %v", err) + return models.ReconcileRequeue, nil + } + + controllerutil.RemoveFinalizer(u, u.GetDeletionFinalizer()) + err = r.Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot delete finalizer from the PostgreSQL user resource") + r.EventRecorder.Eventf(u, models.Warning, models.PatchFailed, + "Deleting finalizer from the PostgreSQL user resource has been failed. Reason: %v", err) + return models.ReconcileRequeue, nil + } + + l.Info("PostgreSQL user resource has been deleted") + + return models.ExitReconcile, nil + } return models.ExitReconcile, nil } @@ -185,27 +262,11 @@ func (r *PostgreSQLUserReconciler) createUser( newPassword string, defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, ) error { - defaultUserSecret := &k8sCore.Secret{} - - namespacedName := types.NamespacedName{ - Namespace: defaultUserSecretNamespacedName.Namespace, - Name: defaultUserSecretNamespacedName.Name, - } - err := r.Get(ctx, namespacedName, defaultUserSecret) + defaultCreds, clusterName, err := r.getDefaultPostgreSQLUserCreds(ctx, defaultUserSecretNamespacedName) if err != nil { - if k8sErrors.IsNotFound(err) { - return fmt.Errorf("cannot get default PostgreSQL user secret, user reference: %v, err: %w", defaultUserSecretNamespacedName, err) - } - return err } - defaultUsername, defaultPassword, err := getUserCreds(defaultUserSecret) - if err != nil { - return fmt.Errorf("cannot get default PostgreSQL user credentials, user reference: %v, err: %w", defaultUserSecretNamespacedName, err) - } - - clusterName := defaultUserSecret.Labels[models.ControlledByLabel] exposeServiceList, err := exposeservice.GetExposeService(r.Client, clusterName, defaultUserSecretNamespacedName.Namespace) if err != nil { return fmt.Errorf("cannot list expose services for cluster: %s, err: %w", clusterID, err) @@ -238,17 +299,52 @@ func (r *PostgreSQLUserReconciler) createUser( if nodeAddress.Type == k8sCore.NodeExternalIP { err := r.createPostgreSQLFirewallRule(ctx, node.Name, clusterID, defaultUserSecretNamespacedName.Namespace, nodeAddress.Address) if err != nil { - return fmt.Errorf("cannot create postgreSQL firewall rule, err: %w", err) + return fmt.Errorf("cannot create PostgreSQL firewall rule, err: %w", err) } } } } + createUserQuery := fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, newUserName, newPassword) + err = r.ExecPostgreSQLQuery(ctx, createUserQuery, defaultCreds, clusterName, defaultUserSecretNamespacedName) + if err != nil { + return err + } + + return nil +} + +func (r *PostgreSQLUserReconciler) deleteUser( + ctx context.Context, + newUserName string, + defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, +) error { + defaultCreds, clusterName, err := r.getDefaultPostgreSQLUserCreds(ctx, defaultUserSecretNamespacedName) + if err != nil { + return err + } + + deleteUserQuery := fmt.Sprintf(`DROP USER IF EXISTS "%s"`, newUserName) + err = r.ExecPostgreSQLQuery(ctx, deleteUserQuery, defaultCreds, clusterName, defaultUserSecretNamespacedName) + if err != nil { + return err + } + + return nil +} + +func (r *PostgreSQLUserReconciler) ExecPostgreSQLQuery( + ctx context.Context, + query string, + defaultCreds *models.Credentials, + clusterName string, + defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, +) error { serviceName := fmt.Sprintf(models.ExposeServiceNameTemplate, clusterName) - host := fmt.Sprintf("%s.%s", serviceName, exposeServiceList.Items[0].Namespace) + host := fmt.Sprintf("%s.%s", serviceName, defaultUserSecretNamespacedName.Namespace) dbURL := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?target_session_attrs=read-write", - defaultUsername, defaultPassword, host, models.DefaultPgDbPortValue, models.DefaultPgDbNameValue) + defaultCreds.Username, defaultCreds.Password, host, models.DefaultPgDbPortValue, models.DefaultPgDbNameValue) conn, err := pgx.Connect(ctx, dbURL) if err != nil { @@ -256,15 +352,48 @@ func (r *PostgreSQLUserReconciler) createUser( } defer conn.Close(ctx) - createUserQuery := fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, newUserName, newPassword) - _, err = conn.Exec(ctx, createUserQuery) + _, err = conn.Exec(ctx, query) if err != nil { - return fmt.Errorf("cannot execute creation user query in postgresql, err: %w", err) + return fmt.Errorf("cannot execute query in PostgreSQL, err: %w", err) } return nil } +func (r *PostgreSQLUserReconciler) getDefaultPostgreSQLUserCreds( + ctx context.Context, + defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, +) (*models.Credentials, string, error) { + defaultUserSecret := &k8sCore.Secret{} + + namespacedName := types.NamespacedName{ + Namespace: defaultUserSecretNamespacedName.Namespace, + Name: defaultUserSecretNamespacedName.Name, + } + err := r.Get(ctx, namespacedName, defaultUserSecret) + if err != nil { + if k8sErrors.IsNotFound(err) { + return nil, "", fmt.Errorf("cannot get default PostgreSQL user secret, user reference: %v, err: %w", defaultUserSecretNamespacedName, err) + } + + return nil, "", err + } + + defaultUsername, defaultPassword, err := getUserCreds(defaultUserSecret) + if err != nil { + return nil, "", fmt.Errorf("cannot get default PostgreSQL user credentials, user reference: %v, err: %w", defaultUserSecretNamespacedName, err) + } + + clusterName := defaultUserSecret.Labels[models.ControlledByLabel] + + defaultPostgreSQLUserCreds := &models.Credentials{ + Username: defaultUsername, + Password: defaultPassword, + } + + return defaultPostgreSQLUserCreds, clusterName, nil +} + func (r *PostgreSQLUserReconciler) createPostgreSQLFirewallRule( ctx context.Context, nodeName string, diff --git a/controllers/clusters/postgresql_controller.go b/controllers/clusters/postgresql_controller.go index 406f69f2d..056076927 100644 --- a/controllers/clusters/postgresql_controller.go +++ b/controllers/clusters/postgresql_controller.go @@ -569,6 +569,129 @@ func (r *PostgreSQLReconciler) createUser( return nil } +func (r *PostgreSQLReconciler) handleUsersDelete( + ctx context.Context, + l logr.Logger, + c *v1beta1.PostgreSQL, + uRef *v1beta1.UserReference, +) error { + req := types.NamespacedName{ + Namespace: uRef.Namespace, + Name: uRef.Name, + } + + u := &clusterresourcesv1beta1.PostgreSQLUser{} + err := r.Get(ctx, req, u) + if err != nil { + if k8serrors.IsNotFound(err) { + l.Error(err, "Cannot delete a PostgreSQL user, the user is not found", "request", req) + r.EventRecorder.Eventf(c, models.Warning, models.NotFound, + "Cannot delete a PostgreSQL user, the user %v is not found", req) + return nil + } + + l.Error(err, "Cannot get PostgreSQL user", "user", req) + r.EventRecorder.Eventf(c, models.Warning, models.DeletionFailed, + "Cannot get PostgreSQL user. user reference: %v", req) + return err + } + + if _, exist := u.Status.ClustersInfo[c.Status.ID]; !exist { + l.Info("User is not existing on the cluster", + "user reference", uRef) + r.EventRecorder.Eventf(c, models.Normal, models.DeletionFailed, + "User is not existing on the cluster. User reference: %v", req) + + return nil + } + + patch := u.NewPatch() + + defaultSecretNamespacedName := u.Status.ClustersInfo[c.Status.ID].DefaultSecretNamespacedName + + u.Status.ClustersInfo[c.Status.ID] = clusterresourcesv1beta1.ClusterInfo{ + DefaultSecretNamespacedName: clusterresourcesv1beta1.NamespacedName{ + Namespace: defaultSecretNamespacedName.Namespace, + Name: defaultSecretNamespacedName.Name, + }, + Event: models.DeletingEvent, + } + + err = r.Status().Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot patch the PostgreSQL User status with the DeletingEvent", + "cluster name", c.Spec.Name, "cluster ID", c.Status.ID) + r.EventRecorder.Eventf(c, models.Warning, models.DeletionFailed, + "Cannot patch the PostgreSQL User status with the DeletingEvent. Reason: %v", err) + return err + } + + l.Info("User has been added to the queue for deletion", + "User resource", u.Namespace+"/"+u.Name, + "PostgreSQL resource", c.Namespace+"/"+c.Name) + + return nil +} + +func (r *PostgreSQLReconciler) handleUsersDetach( + ctx context.Context, + l logr.Logger, + c *v1beta1.PostgreSQL, + uRef *v1beta1.UserReference, +) error { + req := types.NamespacedName{ + Namespace: uRef.Namespace, + Name: uRef.Name, + } + + u := &clusterresourcesv1beta1.PostgreSQLUser{} + err := r.Get(ctx, req, u) + if err != nil { + if k8serrors.IsNotFound(err) { + l.Error(err, "Cannot detach a PostgreSQL user, the user is not found", "request", req) + r.EventRecorder.Eventf(c, models.Warning, models.NotFound, + "Cannot detach a PostgreSQL user, the user %v is not found", req) + return nil + } + + l.Error(err, "Cannot get PostgreSQL user", "user", req) + r.EventRecorder.Eventf(c, models.Warning, models.DeletionFailed, + "Cannot get PostgreSQL user. user reference: %v", req) + return err + } + + if _, exist := u.Status.ClustersInfo[c.Status.ID]; !exist { + l.Info("User is not existing in the cluster", "user reference", uRef) + r.EventRecorder.Eventf(c, models.Normal, models.DeletionFailed, + "User is not existing in the cluster. User reference: %v", uRef) + return nil + } + + defaultSecretNamespacedName := u.Status.ClustersInfo[c.Status.ID].DefaultSecretNamespacedName + + patch := u.NewPatch() + u.Status.ClustersInfo[c.Status.ID] = clusterresourcesv1beta1.ClusterInfo{ + DefaultSecretNamespacedName: clusterresourcesv1beta1.NamespacedName{ + Namespace: defaultSecretNamespacedName.Namespace, + Name: defaultSecretNamespacedName.Name, + }, + Event: models.DeletingEvent, + } + + err = r.Status().Patch(ctx, u, patch) + if err != nil { + l.Error(err, "Cannot patch the PostgreSQL user status with the ClusterDeletingEvent", + "cluster name", c.Spec.Name, "cluster ID", c.Status.ID) + r.EventRecorder.Eventf(c, models.Warning, models.DeletionFailed, + "Cannot patch the PostgreSQL user status with the ClusterDeletingEvent. Reason: %v", err) + return err + } + + l.Info("User has been added to the queue for detaching", "username", u.Name) + + return nil +} + func (r *PostgreSQLReconciler) handleUserEvent( newObj *v1beta1.PostgreSQL, oldUsers []*v1beta1.UserReference, @@ -614,7 +737,12 @@ func (r *PostgreSQLReconciler) handleUserEvent( continue } - // TODO: implement user deletion + err := r.handleUsersDelete(ctx, l, newObj, oldUser) + if err != nil { + l.Error(err, "Cannot delete Cassandra user", "user", oldUser) + r.EventRecorder.Eventf(newObj, models.Warning, models.CreatingEvent, + "Cannot delete user from cluster. Reason: %v", err) + } } } @@ -730,30 +858,6 @@ func (r *PostgreSQLReconciler) handleDeleteCluster( "cluster ID", pg.Status.ID, ) - err = r.deleteSecret(ctx, pg) - if client.IgnoreNotFound(err) != nil { - logger.Error(err, "Cannot delete PostgreSQL default user secret", - "cluster ID", pg.Status.ID, - ) - - r.EventRecorder.Eventf( - pg, models.Warning, models.DeletionFailed, - "Default user secret deletion is failed. Reason: %v", - err, - ) - return models.ReconcileRequeue - } - - logger.Info("Cluster PostgreSQL default user secret was deleted", - "cluster ID", pg.Status.ID, - ) - - r.EventRecorder.Eventf( - pg, models.Normal, models.Deleted, - "Default user secret is deleted. Cluster ID: %s", - pg.Status.ID, - ) - logger.Info("Deleting cluster backup resources", "cluster ID", pg.Status.ID, ) @@ -780,8 +884,17 @@ func (r *PostgreSQLReconciler) handleDeleteCluster( "Cluster backup resources are deleted", ) + r.Scheduler.RemoveJob(pg.GetJobID(scheduler.UserCreator)) r.Scheduler.RemoveJob(pg.GetJobID(scheduler.BackupsChecker)) r.Scheduler.RemoveJob(pg.GetJobID(scheduler.StatusChecker)) + + for _, ref := range pg.Spec.UserRefs { + err = r.handleUsersDetach(ctx, logger, pg, ref) + if err != nil { + return models.ReconcileRequeue + } + } + controllerutil.RemoveFinalizer(pg, models.DeletionFinalizer) pg.Annotations[models.ResourceStateAnnotation] = models.DeletedEvent err = r.patchClusterMetadata(ctx, pg, logger) @@ -800,6 +913,30 @@ func (r *PostgreSQLReconciler) handleDeleteCluster( return models.ReconcileRequeue } + err = r.deleteSecret(ctx, pg) + if client.IgnoreNotFound(err) != nil { + logger.Error(err, "Cannot delete PostgreSQL default user secret", + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Warning, models.DeletionFailed, + "Default user secret deletion is failed. Reason: %v", + err, + ) + return models.ReconcileRequeue + } + + logger.Info("Cluster PostgreSQL default user secret was deleted", + "cluster ID", pg.Status.ID, + ) + + r.EventRecorder.Eventf( + pg, models.Normal, models.Deleted, + "Default user secret is deleted. Cluster ID: %s", + pg.Status.ID, + ) + err = exposeservice.Delete(r.Client, pg.Name, pg.Namespace) if err != nil { logger.Error(err, "Cannot delete PostgreSQL cluster expose service", @@ -952,6 +1089,7 @@ func (r *PostgreSQLReconciler) newWatchStatusJob(pg *v1beta1.PostgreSQL) schedul "namespaced name", namespacedName) r.Scheduler.RemoveJob(pg.GetJobID(scheduler.BackupsChecker)) r.Scheduler.RemoveJob(pg.GetJobID(scheduler.StatusChecker)) + r.Scheduler.RemoveJob(pg.GetJobID(scheduler.UserCreator)) return nil } if err != nil { diff --git a/pkg/models/apiv2.go b/pkg/models/apiv2.go index 548e9b1a6..f1b92a412 100644 --- a/pkg/models/apiv2.go +++ b/pkg/models/apiv2.go @@ -169,6 +169,11 @@ type AddonBundle struct { Options *BundleOptions `json:"options"` } +type Credentials struct { + Username string + Password string +} + func (cb *ClusterBackup) GetBackupEvents(clusterKind string) map[int]*BackupEvent { var eventType string