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
11 changes: 10 additions & 1 deletion api/v1alpha1/postgresrole_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}
Expand Down
27 changes: 27 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
185 changes: 131 additions & 54 deletions internal/controller/postgresrole_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Loading
Loading