diff --git a/api/v1alpha1/postgresrole_types.go b/api/v1alpha1/postgresrole_types.go index 2646311..9268b9b 100644 --- a/api/v1alpha1/postgresrole_types.go +++ b/api/v1alpha1/postgresrole_types.go @@ -20,6 +20,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type PostgresRolePasswordFromSecret struct { + // +kubebuilder:validation:Required + Name string `json:"name"` + // +kubebuilder:validation:Required + Key string `json:"key"` +} + // PostgresRoleSpec defines the desired state of PostgresRole. type PostgresRoleSpec struct { // PostgreSQL role name @@ -35,7 +42,9 @@ type PostgresRoleSpec struct { Replication bool `json:"replication,omitempty"` BypassRLS bool `json:"bypassRLS,omitempty"` - PasswordSecretName string `json:"passwordSecretName,omitempty"` + PasswordFromSecret *PostgresRolePasswordFromSecret `json:"passwordFromSecret,omitempty"` + SecretName string `json:"secretName,omitempty"` + SecretTemplate map[string]string `json:"secretTemplate,omitempty"` MemberOfRoles []string `json:"memberOfRoles,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 887cfa5..16154c1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -199,9 +199,36 @@ func (in *PostgresRoleList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostgresRolePasswordFromSecret) DeepCopyInto(out *PostgresRolePasswordFromSecret) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgresRolePasswordFromSecret. +func (in *PostgresRolePasswordFromSecret) DeepCopy() *PostgresRolePasswordFromSecret { + if in == nil { + return nil + } + out := new(PostgresRolePasswordFromSecret) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PostgresRoleSpec) DeepCopyInto(out *PostgresRoleSpec) { *out = *in + if in.PasswordFromSecret != nil { + in, out := &in.PasswordFromSecret, &out.PasswordFromSecret + *out = new(PostgresRolePasswordFromSecret) + **out = **in + } + if in.SecretTemplate != nil { + in, out := &in.SecretTemplate, &out.SecretTemplate + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.MemberOfRoles != nil { in, out := &in.MemberOfRoles, &out.MemberOfRoles *out = make([]string, len(*in)) diff --git a/config/crd/bases/managed-postgres-operator.hoppscale.com_postgresroles.yaml b/config/crd/bases/managed-postgres-operator.hoppscale.com_postgresroles.yaml index 23ca90b..e2e1b45 100644 --- a/config/crd/bases/managed-postgres-operator.hoppscale.com_postgresroles.yaml +++ b/config/crd/bases/managed-postgres-operator.hoppscale.com_postgresroles.yaml @@ -59,10 +59,24 @@ spec: x-kubernetes-validations: - message: name is immutable rule: self == oldSelf - passwordSecretName: - type: string + passwordFromSecret: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object replication: type: boolean + secretName: + type: string + secretTemplate: + additionalProperties: + type: string + type: object superUser: type: boolean required: diff --git a/internal/controller/postgresrole_controller.go b/internal/controller/postgresrole_controller.go index aa4b75c..973380d 100644 --- a/internal/controller/postgresrole_controller.go +++ b/internal/controller/postgresrole_controller.go @@ -17,9 +17,11 @@ limitations under the License. package controller import ( + "bytes" "context" "fmt" "math/rand/v2" + "text/template" "time" corev1 "k8s.io/api/core/v1" @@ -36,6 +38,7 @@ import ( managedpostgresoperatorhoppscalecomv1alpha1 "github.com/hoppscale/managed-postgres-operator/api/v1alpha1" "github.com/hoppscale/managed-postgres-operator/internal/postgresql" "github.com/hoppscale/managed-postgres-operator/internal/utils" + "github.com/jackc/pgx/v5" ) const PostgresRoleFinalizer = "postgresrole.managed-postgres-operator.hoppscale.com/finalizer" @@ -74,27 +77,27 @@ func (r *PostgresRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request rolePassword := "" - if resource.Spec.PasswordSecretName != "" { + // Retrieve password from user-provided Secret or generate it + if resource.Spec.PasswordFromSecret != nil { secretNamespacedName := types.NamespacedName{ Namespace: resource.ObjectMeta.Namespace, - Name: resource.Spec.PasswordSecretName, + Name: resource.Spec.PasswordFromSecret.Name, } resourceSecret := &corev1.Secret{} - if err := r.Client.Get(ctx, secretNamespacedName, resourceSecret); err != nil { - // If the secret doesn't exist, it creates it and generates a password - if errors.IsNotFound(err) { - rolePassword, err = r.generatePasswordSecret(&secretNamespacedName) - if err != nil { - return ctrlFailResult, err - } - r.logging.Info(fmt.Sprintf("Password has been generated and secret created for role \"%s\"", resource.Spec.Name)) - } else { - return ctrlFailResult, client.IgnoreNotFound(err) - } + err := r.Client.Get(ctx, secretNamespacedName, resourceSecret) + if err != nil { + return ctrlFailResult, fmt.Errorf("failed to retrieve password from secret: %s", err) + } + + rolePassword = string(resourceSecret.Data[resource.Spec.PasswordFromSecret.Key]) + } else { + // If no password is cached, we create a new one + if val, ok := r.CacheRolePasswords[resource.Spec.Name]; ok { + rolePassword = val } else { - rolePassword = string(resourceSecret.Data["password"]) + rolePassword = r.generatePassword(64) } } @@ -137,26 +140,6 @@ func (r *PostgresRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrlFailResult, err } - // Remove associated secret if created by operator - if resource.Spec.PasswordSecretName != "" { - secretNamespacedName := types.NamespacedName{ - Namespace: resource.ObjectMeta.Namespace, - Name: resource.Spec.PasswordSecretName, - } - - resourceSecret := &corev1.Secret{} - if err := r.Client.Get(ctx, secretNamespacedName, resourceSecret); err != nil { - return ctrlFailResult, client.IgnoreNotFound(err) - } - - if val, ok := resourceSecret.Labels["app.kubernetes.io/managed-by"]; ok && val == "managed-postgres-operator.hoppscale.com" { - err = r.Client.Delete(ctx, resourceSecret) - if err != nil { - return ctrlFailResult, client.IgnoreNotFound(err) - } - } - } - // Remove our finalizer from the list and update it. controllerutil.RemoveFinalizer(resource, PostgresRoleFinalizer) if err := r.Update(ctx, resource); err != nil { @@ -181,6 +164,17 @@ func (r *PostgresRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrlFailResult, err } + err = r.reconcileRoleSecret( + resource.ObjectMeta.Namespace, + resource.Spec.SecretName, + resource.Spec.SecretTemplate, + &desiredRole, + r.PGPools.Default.Config().ConnConfig, + ) + if err != nil { + return ctrlFailResult, err + } + if !resource.Status.Succeeded { resource.Status.Succeeded = true if err = r.Client.Status().Update(context.Background(), resource); err != nil { @@ -314,32 +308,115 @@ func (r *PostgresRoleReconciler) reconcileRoleMembership(role string, desiredMem return err } -func (r *PostgresRoleReconciler) generatePasswordSecret(secretName *types.NamespacedName) (password string, err error) { +func (r *PostgresRoleReconciler) reconcileRoleSecret(secretNamespace, secretName string, secretTemplate map[string]string, role *postgresql.Role, pgConfig *pgx.ConnConfig) (err error) { + // Do not create Secret if no name provided by the user + if secretName == "" { + return err + } + + secretNamespacedName := types.NamespacedName{ + Namespace: secretNamespace, + Name: secretName, + } + + resourceSecret := &corev1.Secret{} + + secretDataTemplateVars := struct { + Role string + Password string + Host string + Port string + Database string + }{ + Role: role.Name, + Password: role.Password, + Host: pgConfig.Host, + Port: fmt.Sprintf("%d", pgConfig.Port), + Database: pgConfig.Database, + } + + desiredSecretData := map[string][]byte{ + "PGUSER": []byte(secretDataTemplateVars.Role), + "PGPASSWORD": []byte(secretDataTemplateVars.Password), + "PGHOST": []byte(secretDataTemplateVars.Host), + "PGPORT": []byte(secretDataTemplateVars.Port), + "PGDATABASE": []byte(secretDataTemplateVars.Database), + } + + for secretKey, secretValue := range secretTemplate { + t := template.Must(template.New("secret").Parse(secretValue)) + var tpl bytes.Buffer + if err := t.Execute(&tpl, secretDataTemplateVars); err != nil { + return fmt.Errorf("failed to render secret template: %s", err) + } + desiredSecretData[secretKey] = tpl.Bytes() + } + + // Retrieve Secret + err = r.Client.Get(context.Background(), secretNamespacedName, resourceSecret) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to retrieve secret: %s", err) + } + + // If Secret is not found, create it + if errors.IsNotFound(err) { + resourceSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: secretNamespace, + Name: secretName, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "managed-postgres-operator.hoppscale.com", + }, + }, + Type: "Opaque", + Data: desiredSecretData, + } + + err = r.Client.Create(context.Background(), resourceSecret) + if err != nil { + return fmt.Errorf("failed to create secret: %s", err) + } + + r.logging.Info("Role's secret has been created") + + return err + } + + // Update secret if needed + toUpdate := false + if val, ok := resourceSecret.ObjectMeta.Labels["app.kubernetes.io/managed-by"]; !ok || val != "managed-postgres-operator.hoppscale.com" { + if resourceSecret.ObjectMeta.Labels == nil { + resourceSecret.ObjectMeta.Labels = make(map[string]string) + } + resourceSecret.ObjectMeta.Labels["app.kubernetes.io/managed-by"] = "managed-postgres-operator.hoppscale.com" + toUpdate = true + } + + if fmt.Sprint(resourceSecret.Data) != fmt.Sprint(desiredSecretData) { + toUpdate = true + resourceSecret.Data = desiredSecretData + } + + if toUpdate { + err = r.Client.Update(context.Background(), resourceSecret) + if err != nil { + return fmt.Errorf("failed to update secret: %s", err) + } + r.logging.Info("Role's secret has been updated") + } + + return err +} + +func (r *PostgresRoleReconciler) generatePassword(length int) (password string) { // Generate password const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" random := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64())) - result := make([]byte, 30) + result := make([]byte, length) for i := range result { result[i] = charset[random.IntN(len(charset))] } - // Create K8S secret - resourceSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: secretName.Namespace, - Name: secretName.Name, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "managed-postgres-operator.hoppscale.com", - }, - }, - Type: "Opaque", - Data: map[string][]byte{ - "password": result, - }, - } - - err = r.Client.Create(context.Background(), resourceSecret) - password = string(result) - return + return string(result) } diff --git a/internal/controller/postgresrole_controller_test.go b/internal/controller/postgresrole_controller_test.go index c100bd3..f31ac3d 100644 --- a/internal/controller/postgresrole_controller_test.go +++ b/internal/controller/postgresrole_controller_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -21,6 +21,7 @@ import ( "fmt" "regexp" + "github.com/jackc/pgx/v5" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" pgxmock "github.com/pashagolub/pgxmock/v4" @@ -61,7 +62,7 @@ var _ = Describe("PostgresRole Controller", func() { Namespace: "default", }, Spec: managedpostgresoperatorhoppscalecomv1alpha1.PostgresRoleSpec{ - Name: "foo", + Name: "myrole", CreateRole: true, CreateDB: true, }, @@ -71,11 +72,11 @@ var _ = Describe("PostgresRole Controller", func() { resourceSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "db-foo", + Name: "myrole-password", }, Type: "Opaque", Data: map[string][]byte{ - "password": []byte("password"), + "password": []byte("mypassword"), }, } Expect(k8sClient.Create(ctx, resourceSecret)).To(Succeed()) @@ -100,18 +101,45 @@ var _ = Describe("PostgresRole Controller", func() { pool.Close() } - // Delete Secret - resourceSecret := &corev1.Secret{ + // Delete Secret (passwordFromSecret) + resourceSecretInput := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "db-foo", + Name: "myrole-password", }, } - Expect(k8sClient.Delete(ctx, resourceSecret)).To(Succeed()) + typeSecretInputNamespacedName := types.NamespacedName{ + Namespace: "default", + Name: "myrole-password", + } + err := k8sClient.Get(ctx, typeSecretInputNamespacedName, resourceSecretInput) + if err == nil { + Expect(k8sClient.Delete(ctx, resourceSecretInput)).To(Succeed()) + } else if !errors.IsNotFound(err) { + Fail(err.Error()) + } + + // Delete Secret (secretName) + resourceSecretOutput := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "db-config-myrole", + }, + } + typeSecretOutputNamespacedName := types.NamespacedName{ + Namespace: "default", + Name: "db-config-myrole", + } + err = k8sClient.Get(ctx, typeSecretOutputNamespacedName, resourceSecretOutput) + if err == nil { + Expect(k8sClient.Delete(ctx, resourceSecretOutput)).To(Succeed()) + } else if !errors.IsNotFound(err) { + Fail(err.Error()) + } // Delete CustomResource resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) + err = k8sClient.Get(ctx, typeNamespacedName, resource) if err != nil && errors.IsNotFound(err) { return } @@ -123,399 +151,444 @@ var _ = Describe("PostgresRole Controller", func() { Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) }) - When("the resource is managed by the operator's instance", func() { - It("should continue to reconcile the resource and create the role", func() { - resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} - Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) - resource.ObjectMeta.Annotations = map[string]string{ - utils.OperatorInstanceAnnotationName: "foo", - } - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - OperatorInstanceName: "foo", - CacheRolePasswords: make(map[string]string), - } - - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }), - ) - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`CREATE ROLE "foo" WITH NOSUPERUSER NOINHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "group_role", - }), - ) - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + When("the resource is created and managed by the operator's instance", func() { + When("the role doesn't exist", func() { + When("no password is provided", func() { + It("should continue to reconcile the resource and create the role", func() { + resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.ObjectMeta.Annotations = map[string]string{ + utils.OperatorInstanceAnnotationName: "foo", + } + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + OperatorInstanceName: "foo", + CacheRolePasswords: make(map[string]string), + } + + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "rolname", + "rolsuper", + "rolinherit", + "rolcreaterole", + "rolcreatedb", + "rolcanlogin", + "rolreplication", + "rolbypassrls", + }), + ) + pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s '.*'$", regexp.QuoteMeta(`CREATE ROLE "myrole" WITH NOSUPERUSER NOINHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD`))). + WillReturnResult(pgxmock.NewResult("CREATE ROLE", 1)) + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "group_role", + }), + ) + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } + }) }) - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - }) - }) - - When("the resource is not managed by the operator's instance", func() { - It("should skip reconciliation", func() { - resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} - Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) - resource.ObjectMeta.Annotations = map[string]string{ - utils.OperatorInstanceAnnotationName: "bar", - } - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - OperatorInstanceName: "foo", - CacheRolePasswords: make(map[string]string), - } - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + When("a secretName is provided", func() { + It("should create a Secret with PostgreSQL connection information", func() { + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + OperatorInstanceName: "foo", + CacheRolePasswords: make(map[string]string), + } + + role := postgresql.Role{ + Name: "myrole", + Password: "mypassword", + } + + pgConfig, err := pgx.ParseConfig("postgres://localhost:5432/mydatabase") + Expect(err).NotTo(HaveOccurred()) + + err = controllerReconciler.reconcileRoleSecret( + "default", + "db-config-myrole", + make(map[string]string), + &role, + pgConfig, + ) + + Expect(err).NotTo(HaveOccurred()) + + outputSecretNamespacedName := types.NamespacedName{ + Namespace: "default", + Name: "db-config-myrole", + } + outputSecret := &corev1.Secret{} + Expect(k8sClient.Get(ctx, outputSecretNamespacedName, outputSecret)).To(Succeed()) + Expect(outputSecret.Data["PGUSER"]).To(Equal([]byte("myrole"))) + Expect(outputSecret.Data["PGPASSWORD"]).To(Equal([]byte("mypassword"))) + Expect(outputSecret.Data["PGHOST"]).To(Equal([]byte("localhost"))) + Expect(outputSecret.Data["PGPORT"]).To(Equal([]byte("5432"))) + Expect(outputSecret.Data["PGDATABASE"]).To(Equal([]byte("mydatabase"))) + Expect(outputSecret.Data).To(HaveLen(5)) + }) }) - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - }) - }) - - When("the resource is created without password and no role exists", func() { - It("should reconcile the resource and create the role", func() { - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: make(map[string]string), - } - - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }), - ) - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`CREATE ROLE "foo" WITH NOSUPERUSER NOINHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "group_role", - }), - ) - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + When("a secretName and a secretTemplate are provided", func() { + It("should create a Secret with PostgreSQL connection information with the defined templating", func() { + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + OperatorInstanceName: "foo", + CacheRolePasswords: make(map[string]string), + } + + role := postgresql.Role{ + Name: "myrole", + Password: "mypassword", + } + + pgConfig, err := pgx.ParseConfig("postgres://localhost:5432/mydatabase") + Expect(err).NotTo(HaveOccurred()) + + secretTemplate := map[string]string{ + "PGDATABASE": "fake", + "JDBC_URL": "jdbc:postgresql://{{ .Host }}:{{ .Port }}/fake?user={{ .Role }}&password={{ .Password }}", + } + err = controllerReconciler.reconcileRoleSecret( + "default", + "db-config-myrole", + secretTemplate, + &role, + pgConfig, + ) + + Expect(err).NotTo(HaveOccurred()) + if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } + + outputSecretNamespacedName := types.NamespacedName{ + Namespace: "default", + Name: "db-config-myrole", + } + outputSecret := &corev1.Secret{} + Expect(k8sClient.Get(ctx, outputSecretNamespacedName, outputSecret)).To(Succeed()) + Expect(outputSecret.Data["PGUSER"]).To(Equal([]byte("myrole"))) + Expect(outputSecret.Data["PGPASSWORD"]).To(Equal([]byte("mypassword"))) + Expect(outputSecret.Data["PGHOST"]).To(Equal([]byte("localhost"))) + Expect(outputSecret.Data["PGPORT"]).To(Equal([]byte("5432"))) + Expect(outputSecret.Data["PGDATABASE"]).To(Equal([]byte("fake"))) + Expect(outputSecret.Data["JDBC_URL"]).To(Equal([]byte("jdbc:postgresql://localhost:5432/fake?user=myrole&password=mypassword"))) + Expect(outputSecret.Data).To(HaveLen(6)) + }) }) - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - }) - }) - - When("the resource is created but the role already exists", func() { - It("should not try to create role and match existing role", func() { - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: make(map[string]string), - } - - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }). - AddRow( - "foo", - false, - false, - true, - true, - false, - false, - false, - ), - ) - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "group_role", - }), - ) - - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + When("a password is provided", func() { + It("should retrieve the password from the secret and create the role", func() { + resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.ObjectMeta.Annotations = map[string]string{ + utils.OperatorInstanceAnnotationName: "foo", + } + resource.Spec.PasswordFromSecret = &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRolePasswordFromSecret{ + Name: "myrole-password", + Key: "password", + } + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + OperatorInstanceName: "foo", + CacheRolePasswords: make(map[string]string), + } + + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "rolname", + "rolsuper", + "rolinherit", + "rolcreaterole", + "rolcreatedb", + "rolcanlogin", + "rolreplication", + "rolbypassrls", + }), + ) + pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`CREATE ROLE "myrole" WITH NOSUPERUSER NOINHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'mypassword'`))). + WillReturnResult(pgxmock.NewResult("CREATE ROLE", 1)) + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "group_role", + }), + ) + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } + }) }) - - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } }) - }) - - When("the resource is created with a password and no role exists", func() { - It("should reconcile the resource and create the role", func() { - resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} - Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) - resource.Spec.PasswordSecretName = "db-foo" - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: make(map[string]string), - } - - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }), - ) - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`CREATE ROLE "foo" WITH NOSUPERUSER NOINHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'password'`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "group_role", - }), - ) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + When("the role already exists", func() { + When("there is no difference between the existing role and the resource", func() { + It("should retrieve the role and do nothing", func() { + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + CacheRolePasswords: map[string]string{ + "myrole": "mypassword", + }, + } + + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "rolname", + "rolsuper", + "rolinherit", + "rolcreaterole", + "rolcreatedb", + "rolcanlogin", + "rolreplication", + "rolbypassrls", + }). + AddRow( + "myrole", + false, + false, + true, + true, + false, + false, + false, + ), + ) + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "group_role", + }), + ) + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } + }) }) - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - }) - }) - - When("the resource is created without a password but a secret and no role exists", func() { - It("should reconcile the resource, create a secret, generate a password and create the role", func() { - resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} - Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) - resource.Spec.PasswordSecretName = "db-config" - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: make(map[string]string), - } - - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }), - ) - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s[a-zA-Z0-9]{30}'$", regexp.QuoteMeta(`CREATE ROLE "foo" WITH NOSUPERUSER NOINHERIT CREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD '`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "group_role", - }), - ) + When("the role is member of a role which has been changed", func() { + It("should revoke membership on the old role and grant membership on the new one", func() { + resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.Spec.MemberOfRoles = []string{ + "role_to_add", + } + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + CacheRolePasswords: map[string]string{ + "myrole": "mypassword", + }, + } + + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "rolname", + "rolsuper", + "rolinherit", + "rolcreaterole", + "rolcreatedb", + "rolcanlogin", + "rolreplication", + "rolbypassrls", + }). + AddRow( + "myrole", + false, + false, + true, + true, + false, + false, + false, + ), + ) + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleMembershipStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "group_role", + }). + AddRow( + "role_to_remove", + ), + ) + + pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`REVOKE "role_to_remove" FROM "myrole"`))). + WillReturnResult(pgxmock.NewResult("REVOKE", 1)) + + pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`GRANT "role_to_add" TO "myrole"`))). + WillReturnResult(pgxmock.NewResult("GRANT", 1)) + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } + }) + }) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + When("the output secret exists", func() { + When("the output secret's label has been removed and PGUSER is missing", func() { + It("should update the output secret to add the 'managed-by' label and add PGUSER field", func() { + existingOutputSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "db-config-myrole", + }, + Type: "Opaque", + Data: map[string][]byte{ + "PGPASSWORD": []byte("mypassword"), + "PGHOST": []byte("localhost"), + "PGPORT": []byte("5432"), + "PGDATABASE": []byte("mydatabase"), + }, + } + Expect(k8sClient.Create(ctx, existingOutputSecret)).To(Succeed()) + + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + OperatorInstanceName: "foo", + CacheRolePasswords: make(map[string]string), + } + + role := postgresql.Role{ + Name: "myrole", + Password: "mypassword", + } + + pgConfig, err := pgx.ParseConfig("postgres://localhost:5432/mydatabase") + Expect(err).NotTo(HaveOccurred()) + + err = controllerReconciler.reconcileRoleSecret( + "default", + "db-config-myrole", + make(map[string]string), + &role, + pgConfig, + ) + + Expect(err).NotTo(HaveOccurred()) + + outputSecretNamespacedName := types.NamespacedName{ + Namespace: "default", + Name: "db-config-myrole", + } + outputSecret := &corev1.Secret{} + Expect(k8sClient.Get(ctx, outputSecretNamespacedName, outputSecret)).To(Succeed()) + Expect(outputSecret.Data["PGUSER"]).To(Equal([]byte("myrole"))) + Expect(outputSecret.Data["PGPASSWORD"]).To(Equal([]byte("mypassword"))) + Expect(outputSecret.Data["PGHOST"]).To(Equal([]byte("localhost"))) + Expect(outputSecret.Data["PGPORT"]).To(Equal([]byte("5432"))) + Expect(outputSecret.Data["PGDATABASE"]).To(Equal([]byte("mydatabase"))) + Expect(outputSecret.Data).To(HaveLen(5)) + }) + }) }) - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - secretPassword := &corev1.Secret{} - secretPasswordName := types.NamespacedName{ - Namespace: "default", - Name: "db-config", - } - Expect(k8sClient.Get(ctx, secretPasswordName, secretPassword)).To(Succeed()) - Expect(k8sClient.Delete(ctx, secretPassword)).To(Succeed()) }) - }) - - When("the resource is deleted", func() { - It("should successfully reconcile the resource on deletion", func() { - By("Reconciling the deleted resource") - resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} - Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) - controllerutil.AddFinalizer(resource, PostgresRoleFinalizer) - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + When("the resource has been changed and the role needs to be updated", func() { + It("should alter role to apply the changes", func() { + existingRole := &postgresql.Role{ + Name: "myrole", + } + desiredRole := &postgresql.Role{ + Name: "myrole", + CreateDB: true, + } - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: make(map[string]string), - } + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + CacheRolePasswords: map[string]string{}, + } - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }). - AddRow( - "foo", - false, - false, - true, - true, - false, - false, - false, - ), - ) - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`DROP ROLE "foo"`))). - WillReturnResult(pgxmock.NewResult("", 1)) + pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`ALTER ROLE "myrole" WITH NOSUPERUSER NOINHERIT NOCREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS`))). + WillReturnResult(pgxmock.NewResult("foo", 1)) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + err := controllerReconciler.reconcileOnCreation(existingRole, desiredRole) + Expect(err).NotTo(HaveOccurred()) + for _, poolMock := range pgpoolsMock { + if err := poolMock.ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } + } }) - - Expect(err).NotTo(HaveOccurred()) - if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } }) }) - When("the resource is deleted and an associated secret exists", func() { - It("should successfully reconcile the resource on deletion and delete the associated secret", func() { + When("the resource is not managed by the operator's instance", func() { + It("should skip reconciliation", func() { resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) - controllerutil.AddFinalizer(resource, PostgresRoleFinalizer) - resource.Spec.PasswordSecretName = "db-config" - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) - - resourceSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "db-config", - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "managed-postgres-operator.hoppscale.com", - }, - }, - Type: "Opaque", - Data: map[string][]byte{ - "password": []byte("password"), - }, + resource.ObjectMeta.Annotations = map[string]string{ + utils.OperatorInstanceAnnotationName: "bar", } - Expect(k8sClient.Create(ctx, resourceSecret)).To(Succeed()) + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: make(map[string]string), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + OperatorInstanceName: "foo", + CacheRolePasswords: make(map[string]string), } - pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). - WithArgs("foo"). - WillReturnRows( - pgxmock.NewRows([]string{ - "rolname", - "rolsuper", - "rolinherit", - "rolcreaterole", - "rolcreatedb", - "rolcanlogin", - "rolreplication", - "rolbypassrls", - }). - AddRow( - "foo", - false, - false, - true, - true, - false, - false, - false, - ), - ) - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`DROP ROLE "foo"`))). - WillReturnResult(pgxmock.NewResult("", 1)) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) @@ -524,167 +597,82 @@ var _ = Describe("PostgresRole Controller", func() { if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { Fail(err.Error()) } - secretNamespacedName := types.NamespacedName{ - Namespace: "default", - Name: "db-config", - } - err = k8sClient.Get(ctx, secretNamespacedName, &corev1.Secret{}) - Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) }) }) - When("reconciling on creation", func() { - It("should not create role if already exists", func() { - existingRole := &postgresql.Role{ - Name: "foo", - } - desiredRole := &postgresql.Role{ - Name: "foo", - } - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: map[string]string{}, - } - - err := controllerReconciler.reconcileOnCreation(existingRole, desiredRole) - Expect(err).NotTo(HaveOccurred()) - for _, poolMock := range pgpoolsMock { - if err := poolMock.ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - } - }) - - It("should create role if it doesn't exist", func() { - var existingRole *postgresql.Role = nil - desiredRole := &postgresql.Role{ - Name: "foo", - } - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: map[string]string{}, - } - - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`CREATE ROLE "foo" WITH NOSUPERUSER NOINHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - - err := controllerReconciler.reconcileOnCreation(existingRole, desiredRole) - Expect(err).NotTo(HaveOccurred()) - for _, poolMock := range pgpoolsMock { - if err := poolMock.ExpectationsWereMet(); err != nil { - Fail(err.Error()) + When("the resource is deleted", func() { + When("the role exists", func() { + It("should successfully drop the role", func() { + resource := &managedpostgresoperatorhoppscalecomv1alpha1.PostgresRole{} + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + controllerutil.AddFinalizer(resource, PostgresRoleFinalizer) + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + CacheRolePasswords: make(map[string]string), } - } - }) - - It("should update role if the desired role's password is different than the cached password", func() { - existingRole := &postgresql.Role{ - Name: "foo", - } - desiredRole := &postgresql.Role{ - Name: "foo", - Password: "fake", - } - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: map[string]string{ - "foo": "foo", - }, - } - - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`ALTER ROLE "foo" WITH NOSUPERUSER NOINHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'fake'`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - err := controllerReconciler.reconcileOnCreation(existingRole, desiredRole) - Expect(err).NotTo(HaveOccurred()) - for _, poolMock := range pgpoolsMock { - if err := poolMock.ExpectationsWereMet(); err != nil { + pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(postgresql.GetRoleSQLStatement))). + WithArgs("myrole"). + WillReturnRows( + pgxmock.NewRows([]string{ + "rolname", + "rolsuper", + "rolinherit", + "rolcreaterole", + "rolcreatedb", + "rolcanlogin", + "rolreplication", + "rolbypassrls", + }). + AddRow( + "myrole", + false, + false, + true, + true, + false, + false, + false, + ), + ) + pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`DROP ROLE "myrole"`))). + WillReturnResult(pgxmock.NewResult("DROP ROLE", 1)) + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + Expect(err).NotTo(HaveOccurred()) + if err := pgpoolsMock["default"].ExpectationsWereMet(); err != nil { Fail(err.Error()) } - } + }) }) - - It("should update role if the desired role is different than the existing role", func() { - existingRole := &postgresql.Role{ - Name: "foo", - } - desiredRole := &postgresql.Role{ - Name: "foo", - CreateDB: true, - } - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: map[string]string{}, - } - - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`ALTER ROLE "foo" WITH NOSUPERUSER NOINHERIT NOCREATEROLE CREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS`))). - WillReturnResult(pgxmock.NewResult("foo", 1)) - - err := controllerReconciler.reconcileOnCreation(existingRole, desiredRole) - Expect(err).NotTo(HaveOccurred()) - for _, poolMock := range pgpoolsMock { - if err := poolMock.ExpectationsWereMet(); err != nil { - Fail(err.Error()) + When("no role exists", func() { + It("should return immediately", func() { + var existingRole *postgresql.Role + + controllerReconciler := &PostgresRoleReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + PGPools: pgpools, + CacheRolePasswords: map[string]string{}, } - } - }) - }) - When("reconciling on deletion", func() { - It("should return immediately if no role exists", func() { - var existingRole *postgresql.Role - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: map[string]string{}, - } - - err := controllerReconciler.reconcileOnDeletion(existingRole) - Expect(err).NotTo(HaveOccurred()) - for _, poolMock := range pgpoolsMock { - if err := poolMock.ExpectationsWereMet(); err != nil { - Fail(err.Error()) + err := controllerReconciler.reconcileOnDeletion(existingRole) + Expect(err).NotTo(HaveOccurred()) + for _, poolMock := range pgpoolsMock { + if err := poolMock.ExpectationsWereMet(); err != nil { + Fail(err.Error()) + } } - } - }) - - It("should drop database successfully", func() { - existingRole := &postgresql.Role{ - Name: "foo", - } - - controllerReconciler := &PostgresRoleReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - PGPools: pgpools, - CacheRolePasswords: map[string]string{}, - } - - pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`DROP ROLE "foo"`))). - WillReturnResult(pgxmock.NewResult("", 1)) + }) - err := controllerReconciler.reconcileOnDeletion(existingRole) - Expect(err).NotTo(HaveOccurred()) - for _, poolMock := range pgpoolsMock { - if err := poolMock.ExpectationsWereMet(); err != nil { - Fail(err.Error()) - } - } }) }) })