diff --git a/README.md b/README.md index f325431e..37af0acd 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ None * Creates Kubernetes secret with postgres_uri in the same namespace as CR * Support for AWS RDS and Azure Database for PostgresSQL * Support for managing CRs in dynamically created namespaces +* Template secret values ## Cloud specific configuration @@ -173,6 +174,8 @@ spec: privileges: OWNER # Can be OWNER/READ/WRITE annotations: # Annotations to be propagated to the secrets metadata section (optional) foo: "bar" + secretTemplate: # Output secrets can be customized using standard Go templates + PQ_URL: "host={{.Host}} user={{.Role}} password={{.Password}} dbname={{.Database}}" ``` This creates a user role `username-` and grants role `test-db-group`, `test-db-writer` or `test-db-reader` depending on `privileges` property. Its credentials are put in secret `my-secret-my-db-user` (unless `KEEP_SECRET_NAME` is enabled). @@ -203,6 +206,21 @@ With the help of annotations it is possible to create annotation-based copies of For more information and an example, see [kubernetes-replicator#pull-based-replication](https://github.com/mittwald/kubernetes-replicator#pull-based-replication) +#### Template Use Case + +Users can specify the structure and content of secrets based on their unique requirements using standard +[Go templates](https://pkg.go.dev/text/template#hdr-Actions). This flexibility allows for a more tailored approach to +meeting the specific needs of different applications. + +Available context: + +| Variable | Meaning | +|-------------|--------------------------| +| `.Host` | Database host | +| `.Role` | Generated user/role name | +| `.Database` | Referenced database name | +| `.Password` | Generated role password | + ### Contribution You can contribute to this project by opening a PR to merge to `master`, or one of the `vX.X.X` branches. diff --git a/charts/ext-postgres-operator/crds/db.movetokube.com_postgresusers_crd.yaml b/charts/ext-postgres-operator/crds/db.movetokube.com_postgresusers_crd.yaml index 8f2b2c2f..475d86b5 100644 --- a/charts/ext-postgres-operator/crds/db.movetokube.com_postgresusers_crd.yaml +++ b/charts/ext-postgres-operator/crds/db.movetokube.com_postgresusers_crd.yaml @@ -31,6 +31,10 @@ spec: spec: description: PostgresUserSpec defines the desired state of PostgresUser properties: + annotations: + additionalProperties: + type: string + type: object database: type: string privileges: @@ -39,6 +43,10 @@ spec: type: string secretName: type: string + template: + additionalProperties: + type: string + type: object required: - database - role diff --git a/deploy/crds/db.movetokube.com_postgresusers_crd.yaml b/deploy/crds/db.movetokube.com_postgresusers_crd.yaml index 8f2b2c2f..1d9a5355 100644 --- a/deploy/crds/db.movetokube.com_postgresusers_crd.yaml +++ b/deploy/crds/db.movetokube.com_postgresusers_crd.yaml @@ -31,6 +31,10 @@ spec: spec: description: PostgresUserSpec defines the desired state of PostgresUser properties: + annotations: + additionalProperties: + type: string + type: object database: type: string privileges: @@ -39,6 +43,10 @@ spec: type: string secretName: type: string + secretTemplate: + additionalProperties: + type: string + type: object required: - database - role diff --git a/pkg/apis/db/v1alpha1/postgresuser_types.go b/pkg/apis/db/v1alpha1/postgresuser_types.go index de20372b..80e70666 100644 --- a/pkg/apis/db/v1alpha1/postgresuser_types.go +++ b/pkg/apis/db/v1alpha1/postgresuser_types.go @@ -14,6 +14,8 @@ type PostgresUserSpec struct { Database string `json:"database"` SecretName string `json:"secretName"` // +optional + SecretTemplate map[string]string `json:"secretTemplate,omitempty"` // key-value, where key is secret field, value is go template + // +optional Privileges string `json:"privileges"` // +optional Annotations map[string]string `json:"annotations,omitempty"` diff --git a/pkg/apis/db/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/db/v1alpha1/zz_generated.deepcopy.go index 8fdd7f05..b8d36b75 100644 --- a/pkg/apis/db/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/db/v1alpha1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated // Code generated by operator-sdk. DO NOT EDIT. @@ -143,7 +144,7 @@ func (in *PostgresUser) DeepCopyInto(out *PostgresUser) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -202,6 +203,20 @@ func (in *PostgresUserList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PostgresUserSpec) DeepCopyInto(out *PostgresUserSpec) { *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.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/controller/postgresuser/postgresuser_controller.go b/pkg/controller/postgresuser/postgresuser_controller.go index ffd04ec2..ff88927c 100644 --- a/pkg/controller/postgresuser/postgresuser_controller.go +++ b/pkg/controller/postgresuser/postgresuser_controller.go @@ -1,9 +1,11 @@ package postgresuser import ( + "bytes" "context" goerr "errors" "fmt" + "text/template" "github.com/movetokube/postgres-operator/pkg/config" @@ -221,7 +223,10 @@ func (r *ReconcilePostgresUser) Reconcile(request reconcile.Request) (reconcile. return r.requeue(instance, err) } - secret := r.newSecretForCR(instance, role, password, login) + secret, err := r.newSecretForCR(instance, role, password, login) + if err != nil { + return r.requeue(instance, err) + } // Set PostgresUser instance as the owner and controller if err := controllerutil.SetControllerReference(instance, secret, r.scheme); err != nil { @@ -270,7 +275,7 @@ func (r *ReconcilePostgresUser) addFinalizer(reqLogger logr.Logger, m *dbv1alpha return nil } -func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role, password, login string) *corev1.Secret { +func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role, password, login string) (*corev1.Secret, error) { pgUserUrl := fmt.Sprintf("postgresql://%s:%s@%s/%s", role, password, r.pgHost, cr.Status.DatabaseName) pgJDBCUrl := fmt.Sprintf("jdbc:postgresql://%s/%s", r.pgHost, cr.Status.DatabaseName) pgDotnetUrl := fmt.Sprintf("User ID=%s;Password=%s;Host=%s;Port=5432;Database=%s;", role, password, r.pgHost, cr.Status.DatabaseName) @@ -283,6 +288,31 @@ func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role name = cr.Spec.SecretName } + templateData, err := renderTemplate(cr.Spec.SecretTemplate, templateContext{ + Role: role, + Host: r.pgHost, + Database: cr.Status.DatabaseName, + Password: password, + }) + if err != nil { + return nil, fmt.Errorf("render templated keys: %w", err) + } + + data := map[string][]byte{ + "POSTGRES_URL": []byte(pgUserUrl), + "POSTGRES_JDBC_URL": []byte(pgJDBCUrl), + "POSTGRES_DOTNET_URL": []byte(pgDotnetUrl), + "HOST": []byte(r.pgHost), + "DATABASE_NAME": []byte(cr.Status.DatabaseName), + "ROLE": []byte(role), + "PASSWORD": []byte(password), + "LOGIN": []byte(login), + } + // templates may override standard keys + for k, v := range templateData { + data[k] = v + } + return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -290,17 +320,8 @@ func (r *ReconcilePostgresUser) newSecretForCR(cr *dbv1alpha1.PostgresUser, role Labels: labels, Annotations: annotations, }, - Data: map[string][]byte{ - "POSTGRES_URL": []byte(pgUserUrl), - "POSTGRES_JDBC_URL": []byte(pgJDBCUrl), - "POSTGRES_DOTNET_URL": []byte(pgDotnetUrl), - "HOST": []byte(r.pgHost), - "DATABASE_NAME": []byte(cr.Status.DatabaseName), - "ROLE": []byte(role), - "PASSWORD": []byte(password), - "LOGIN": []byte(login), - }, - } + Data: data, + }, nil } func (r *ReconcilePostgresUser) requeue(cr *dbv1alpha1.PostgresUser, reason error) (reconcile.Result, error) { @@ -354,3 +375,29 @@ func (r *ReconcilePostgresUser) addOwnerRef(reqLogger logr.Logger, instance *dbv err = r.client.Update(context.TODO(), instance) return err } + +type templateContext struct { + Host string + Role string + Database string + Password string +} + +func renderTemplate(data map[string]string, tc templateContext) (map[string][]byte, error) { + if len(data) == 0 { + return nil, nil + } + var out = make(map[string][]byte, len(data)) + for key, templ := range data { + parsed, err := template.New("").Parse(templ) + if err != nil { + return nil, fmt.Errorf("parse template %q: %w", key, err) + } + var content bytes.Buffer + if err := parsed.Execute(&content, tc); err != nil { + return nil, fmt.Errorf("execute template %q: %w", key, err) + } + out[key] = content.Bytes() + } + return out, nil +}