Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions internal/controller/postgresrole_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ package controller
import (
"context"
"fmt"
"math/rand/v2"
"time"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -80,10 +83,19 @@ func (r *PostgresRoleReconciler) Reconcile(ctx context.Context, req ctrl.Request
resourceSecret := &corev1.Secret{}

if err := r.Client.Get(ctx, secretNamespacedName, resourceSecret); err != nil {
return ctrlFailResult, client.IgnoreNotFound(err)
// 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)
}
} else {
rolePassword = string(resourceSecret.Data["password"])
}

rolePassword = string(resourceSecret.Data["password"])
}

desiredRole := postgresql.Role{
Expand Down Expand Up @@ -125,6 +137,26 @@ 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 {
Expand Down Expand Up @@ -281,3 +313,33 @@ func (r *PostgresRoleReconciler) reconcileRoleMembership(role string, desiredMem

return err
}

func (r *PostgresRoleReconciler) generatePasswordSecret(secretName *types.NamespacedName) (password string, err error) {
// Generate password
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
random := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64()))

result := make([]byte, 30)
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
}
132 changes: 132 additions & 0 deletions internal/controller/postgresrole_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,62 @@ var _ = Describe("PostgresRole Controller", func() {
})
})

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",
}),
)

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})

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")
Expand Down Expand Up @@ -402,6 +458,82 @@ var _ = Describe("PostgresRole Controller", func() {
})
})

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() {
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"),
},
}
Expect(k8sClient.Create(ctx, resourceSecret)).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",
}).
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,
})

Expect(err).NotTo(HaveOccurred())
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{
Expand Down