From e0ac35c406fec299a57a683053aca51fe812aa80 Mon Sep 17 00:00:00 2001 From: doodgeMatvey Date: Mon, 25 Sep 2023 20:36:04 +0300 Subject: [PATCH] issue-558, PostgreSQL user deletion flow was implemented --- .../cassandrauser_controller.go | 2 +- .../postgresqluser_controller.go | 166 ++++++++++++++-- controllers/clusters/postgresql_controller.go | 188 +++++++++++++++--- main.go | 1 + 4 files changed, 309 insertions(+), 48 deletions(-) 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..146f340fc 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" ) @@ -41,6 +42,7 @@ import ( type PostgreSQLUserReconciler struct { client.Client Scheme *runtime.Scheme + API instaclustr.API EventRecorder record.EventRecorder } @@ -170,10 +172,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 +263,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) + defaultUsername, defaultPassword, 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,14 +300,14 @@ 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) } } } } 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) @@ -259,12 +321,72 @@ func (r *PostgreSQLUserReconciler) createUser( createUserQuery := fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, newUserName, newPassword) _, err = conn.Exec(ctx, createUserQuery) if err != nil { - return fmt.Errorf("cannot execute creation user query in postgresql, err: %w", err) + return fmt.Errorf("cannot execute creation user query in PostgreSQL, err: %w", err) } return nil } +func (r *PostgreSQLUserReconciler) deleteUser( + ctx context.Context, + newUserName string, + defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, +) error { + defaultUsername, defaultPassword, clusterName, err := r.getDefaultPostgreSQLUserCreds(ctx, defaultUserSecretNamespacedName) + if err != nil { + return err + } + + serviceName := fmt.Sprintf(models.ExposeServiceNameTemplate, clusterName) + 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) + + conn, err := pgx.Connect(ctx, dbURL) + if err != nil { + return fmt.Errorf("cannot establish a connection with a PostgreSQL server, err: %w", err) + } + defer conn.Close(ctx) + + createUserQuery := fmt.Sprintf(`DROP USER IF EXISTS "%s"`, newUserName) + _, err = conn.Exec(ctx, createUserQuery) + if err != nil { + return fmt.Errorf("cannot execute creation user query in PostgreSQL, err: %w", err) + } + + return nil +} + +func (r *PostgreSQLUserReconciler) getDefaultPostgreSQLUserCreds( + ctx context.Context, + defaultUserSecretNamespacedName clusterresourcesv1beta1.NamespacedName, +) (string, string, 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 "", "", "", 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] + + return defaultUsername, defaultPassword, 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/main.go b/main.go index ac0ecfd6c..9b5a55a69 100644 --- a/main.go +++ b/main.go @@ -424,6 +424,7 @@ func main() { if err = (&clusterresourcescontrollers.PostgreSQLUserReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + API: instaClient, EventRecorder: eventRecorder, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "PostgreSQLUser")