From 8ade5550ad661a8d1499b092bcbcfb18141b6301 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Sun, 17 Aug 2025 18:04:51 -0700 Subject: [PATCH] Add ability to add uri args to secret templates --- README.md | 5 +++ .../controller/postgresuser_controller.go | 9 ++++-- .../postgresuser_controller_test.go | 31 +++++++++++++++++-- pkg/utils/template.go | 21 ++++++++++++- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 687a7f29..8f5d8bd3 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ Every PostgresUser has a generated Kubernetes secret attached to it, which conta |----------------------|---------------------| | `DATABASE_NAME` | Name of the database, same as in `Postgres` CR, copied for convenience | | `HOST` | PostgreSQL server host (including port number) | +| `URI_ARGS` | URI Args, same as in `Postgres` CR, copied for convenience | | `PASSWORD` | Autogenerated password for user | | `ROLE` | Autogenerated role with login enabled (user) | | `LOGIN` | Same as `ROLE`. In case `POSTGRES_CLOUD_PROVIDER` is set to "Azure", `LOGIN` it will be set to `{role}@{serverName}`, serverName is extracted from `POSTGRES_USER` from operator's config. | @@ -203,6 +204,10 @@ Every PostgresUser has a generated Kubernetes secret attached to it, which conta | `HOSTNAME` | The PostgreSQL server hostname (without port) | | `PORT` | The PostgreSQL server port | +| Functions | Meaning | +|----------------|-------------------------------------------------------------------| +| `mergeUriArgs` | Merge any provided uri args with any set in the `Postgres` CR | + ### Multiple operator support Run multiple operator instances by setting unique POSTGRES_INSTANCE values and using annotations in your CRs to assign them. diff --git a/internal/controller/postgresuser_controller.go b/internal/controller/postgresuser_controller.go index 78aabd3e..da94df08 100644 --- a/internal/controller/postgresuser_controller.go +++ b/internal/controller/postgresuser_controller.go @@ -6,6 +6,7 @@ import ( "maps" "net" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,7 +18,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/go-logr/logr" dbv1alpha1 "github.com/movetokube/postgres-operator/api/v1alpha1" "github.com/movetokube/postgres-operator/pkg/config" "github.com/movetokube/postgres-operator/pkg/postgres" @@ -30,6 +30,7 @@ type PostgresUserReconciler struct { Scheme *runtime.Scheme pg postgres.PG pgHost string + pgUriArgs string instanceFilter string keepSecretName bool // use secret name as defined in PostgresUserSpec } @@ -41,6 +42,7 @@ func NewPostgresUserReconciler(mgr manager.Manager, cfg *config.Cfg, pg postgres Scheme: mgr.GetScheme(), pg: pg, pgHost: cfg.PostgresHost, + pgUriArgs: cfg.PostgresUriArgs, instanceFilter: cfg.AnnotationFilter, keepSecretName: cfg.KeepSecretName, } @@ -259,6 +261,7 @@ func (r *PostgresUserReconciler) newSecretForCR(reqLogger logr.Logger, cr *dbv1a templateData, err := utils.RenderTemplate(cr.Spec.SecretTemplate, utils.TemplateContext{ Role: role, Host: r.pgHost, + UriArgs: r.pgUriArgs, Database: cr.Status.DatabaseName, Password: password, Hostname: hostname, @@ -274,6 +277,7 @@ func (r *PostgresUserReconciler) newSecretForCR(reqLogger logr.Logger, cr *dbv1a "POSTGRES_DOTNET_URL": []byte(pgDotnetUrl), "HOST": []byte(r.pgHost), "DATABASE_NAME": []byte(cr.Status.DatabaseName), + "URI_ARGS": []byte(r.pgUriArgs), "ROLE": []byte(role), "PASSWORD": []byte(password), "LOGIN": []byte(login), @@ -310,7 +314,8 @@ func (r *PostgresUserReconciler) addFinalizer(ctx context.Context, reqLogger log } return nil } -func (r *PostgresUserReconciler) addOwnerRef(ctx context.Context, reqLogger logr.Logger, instance *dbv1alpha1.PostgresUser) error { + +func (r *PostgresUserReconciler) addOwnerRef(ctx context.Context, _ logr.Logger, instance *dbv1alpha1.PostgresUser) error { // Search postgres database CR pg, err := r.getPostgresCR(ctx, instance) if err != nil { diff --git a/internal/controller/postgresuser_controller_test.go b/internal/controller/postgresuser_controller_test.go index 7bb04582..f54789e3 100644 --- a/internal/controller/postgresuser_controller_test.go +++ b/internal/controller/postgresuser_controller_test.go @@ -436,12 +436,15 @@ var _ = Describe("PostgresUser Controller", func() { BeforeEach(func() { userWithTemplate := postgresUser.DeepCopy() userWithTemplate.Spec.SecretTemplate = map[string]string{ - "CUSTOM_KEY": "User: {{.Role}}, DB: {{.Database}}", - "PGPASSWORD": "{{.Password}}", + "CUSTOM_KEY": "User: {{.Role}}, DB: {{.Database}}", + "PGPASSWORD": "{{.Password}}", + "URIARGSFILTER": `postgres://foobar?{{ "sslmode=no-verify" | mergeUriArgs }}`, + "URIARGSFILTER_COMBINED": `postgres://foobar?{{ "logging=true" | mergeUriArgs }}`, + "URIARGSFILTER_EMPTYSTRING": `postgres://foobar?{{ "" | mergeUriArgs }}`, } - initClient(postgresDB, userWithTemplate, false) }) + AfterEach(func() { // Clean up any created secrets secretList := &corev1.SecretList{} @@ -458,6 +461,8 @@ var _ = Describe("PostgresUser Controller", func() { pg.EXPECT().GrantRole(gomock.Any(), gomock.Any()).Return(nil) pg.EXPECT().AlterDefaultLoginRole(gomock.Any(), gomock.Any()).Return(nil) + rp.pgUriArgs = "sslmode=disable" + // Call Reconcile err := runReconcile(rp, ctx, req) Expect(err).NotTo(HaveOccurred()) @@ -492,6 +497,10 @@ var _ = Describe("PostgresUser Controller", func() { pgUrl := string(foundSecret.Data["POSTGRES_URL"]) Expect(pgUrl).To(ContainSubstring(actualRole)) + // Check if URI_ARGS contains the uri args from the secret + uriArgs := string(foundSecret.Data["URI_ARGS"]) + Expect(uriArgs).To(Equal("sslmode=disable")) + // Check if the template was applied using the data in the actual secret // Directly check the custom keys we're expecting Expect(foundSecret.Data).To(HaveKey("CUSTOM_KEY")) @@ -503,6 +512,22 @@ var _ = Describe("PostgresUser Controller", func() { Expect(foundSecret.Data).To(HaveKey("PGPASSWORD")) pgPassword := string(foundSecret.Data["PGPASSWORD"]) Expect(pgPassword).NotTo(BeEmpty()) + + // Check that uri parameters are copied + Expect(foundSecret.Data).To(HaveKey("URIARGSFILTER")) + uriArgsFilter := string(foundSecret.Data["URIARGSFILTER"]) + Expect(uriArgsFilter).To(Equal("postgres://foobar?sslmode=disable")) + + // Check that uri parameters are merged with none in the templates + Expect(foundSecret.Data).To(HaveKey("URIARGSFILTER_EMPTYSTRING")) + uriArgsFilterEmptyString := string(foundSecret.Data["URIARGSFILTER_EMPTYSTRING"]) + Expect(uriArgsFilterEmptyString).To(Equal("postgres://foobar?sslmode=disable")) + + // Check that uri parameters are merged + Expect(foundSecret.Data).To(HaveKey("URIARGSFILTER_COMBINED")) + uriArgsFilterCombined := string(foundSecret.Data["URIARGSFILTER_COMBINED"]) + Expect(uriArgsFilterCombined).To(Equal("postgres://foobar?logging=true&sslmode=disable")) + }) }) }) diff --git a/pkg/utils/template.go b/pkg/utils/template.go index ac65ba3f..00f5a7e5 100644 --- a/pkg/utils/template.go +++ b/pkg/utils/template.go @@ -3,6 +3,7 @@ package utils import ( "bytes" "fmt" + "net/url" "text/template" ) @@ -13,6 +14,7 @@ type TemplateContext struct { Password string Hostname string // Hostname is different from Host as it does not contain the port number. Port string + UriArgs string } func RenderTemplate(data map[string]string, tc TemplateContext) (map[string][]byte, error) { @@ -21,7 +23,24 @@ func RenderTemplate(data map[string]string, tc TemplateContext) (map[string][]by } var out = make(map[string][]byte, len(data)) for key, templ := range data { - parsed, err := template.New("").Parse(templ) + tmplObj := template.New("") + tmplObj.Funcs(template.FuncMap{ + "mergeUriArgs": func(uriArgs string) (string, error) { + inputArgs, err := url.ParseQuery(uriArgs) + if err != nil { + return uriArgs, fmt.Errorf("unable to parse input uri args: %w", err) + } + pgArgs, err := url.ParseQuery(tc.UriArgs) + if err != nil { + return uriArgs, fmt.Errorf("unable to parse pg uri args: %w", err) + } + for argName, values := range pgArgs { + inputArgs.Set(argName, values[0]) + } + return inputArgs.Encode(), nil + }, + }) + parsed, err := tmplObj.Parse(templ) if err != nil { return nil, fmt.Errorf("parse template %q: %w", key, err) }