Skip to content

Add support for template secrets #143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 20, 2023
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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-<hash>` 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).
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -39,6 +43,10 @@ spec:
type: string
secretName:
type: string
template:
additionalProperties:
type: string
type: object
required:
- database
- role
Expand Down
8 changes: 8 additions & 0 deletions deploy/crds/db.movetokube.com_postgresusers_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -39,6 +43,10 @@ spec:
type: string
secretName:
type: string
secretTemplate:
additionalProperties:
type: string
type: object
required:
- database
- role
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/db/v1alpha1/postgresuser_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
17 changes: 16 additions & 1 deletion pkg/apis/db/v1alpha1/zz_generated.deepcopy.go

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

73 changes: 60 additions & 13 deletions pkg/controller/postgresuser/postgresuser_controller.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package postgresuser

import (
"bytes"
"context"
goerr "errors"
"fmt"
"text/template"

"github.com/movetokube/postgres-operator/pkg/config"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -283,24 +288,40 @@ 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,
Namespace: cr.Namespace,
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) {
Expand Down Expand Up @@ -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
}