From ebfeecab56d291289c6c7938fc27cfb7df312b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:47:19 +0200 Subject: [PATCH] feat: add watch for user/role secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jesús Fernández <7312236+fernandezcuesta@users.noreply.github.com> --- .../cluster/mssql/user/reconciler.go | 17 +++ .../cluster/mysql/user/reconciler.go | 17 +++ .../cluster/postgresql/role/reconciler.go | 17 +++ .../namespaced/mssql/user/reconciler.go | 19 ++++ .../namespaced/mysql/user/reconciler.go | 19 ++++ .../namespaced/postgresql/role/reconciler.go | 19 ++++ pkg/controller/secretwatch/secretwatch.go | 90 +++++++++++++++ .../secretwatch/secretwatch_test.go | 107 ++++++++++++++++++ 8 files changed, 305 insertions(+) create mode 100644 pkg/controller/secretwatch/secretwatch.go create mode 100644 pkg/controller/secretwatch/secretwatch_test.go diff --git a/pkg/controller/cluster/mssql/user/reconciler.go b/pkg/controller/cluster/mssql/user/reconciler.go index e83f8c47..e849a991 100644 --- a/pkg/controller/cluster/mssql/user/reconciler.go +++ b/pkg/controller/cluster/mssql/user/reconciler.go @@ -41,6 +41,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/cluster/mssql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients/mssql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" + "github.com/crossplane-contrib/provider-sql/pkg/controller/secretwatch" ) const ( @@ -79,6 +80,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { if o.Features.Enabled(feature.EnableBetaManagementPolicies) { reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) } + h, err := secretwatch.PasswordSecretRef(mgr, o.Logger, &v1alpha1.User{}, &v1alpha1.UserList{}, passwordSecretRef) + if err != nil { + return err + } r := managed.NewReconciler(mgr, resource.ManagedKind(v1alpha1.UserGroupVersionKind), reconcilerOptions..., @@ -92,6 +97,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.User{}). + Watches(&corev1.Secret{}, h). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -349,3 +355,14 @@ func (c *external) Delete(ctx context.Context, mg *v1alpha1.User) (managed.Exter return managed.ExternalDelete{}, nil } + +// passwordSecretRef returns the Secret referenced by the passwordSecretRef of +// a User, and false if it references none. +func passwordSecretRef(o client.Object) (types.NamespacedName, bool) { + cr, ok := o.(*v1alpha1.User) + if !ok || cr.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + ref := cr.Spec.ForProvider.PasswordSecretRef + return types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, true +} diff --git a/pkg/controller/cluster/mysql/user/reconciler.go b/pkg/controller/cluster/mysql/user/reconciler.go index 93b46d2f..f8b5855e 100644 --- a/pkg/controller/cluster/mysql/user/reconciler.go +++ b/pkg/controller/cluster/mysql/user/reconciler.go @@ -42,6 +42,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/cluster/mysql/tls" + "github.com/crossplane-contrib/provider-sql/pkg/controller/secretwatch" ) const ( @@ -75,6 +76,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { if o.Features.Enabled(feature.EnableBetaManagementPolicies) { reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) } + h, err := secretwatch.PasswordSecretRef(mgr, o.Logger, &v1alpha1.User{}, &v1alpha1.UserList{}, passwordSecretRef) + if err != nil { + return err + } r := managed.NewReconciler(mgr, resource.ManagedKind(v1alpha1.UserGroupVersionKind), reconcilerOptions..., @@ -88,6 +93,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.User{}). + Watches(&corev1.Secret{}, h). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -577,3 +583,14 @@ func upToDate(observed *v1alpha1.UserParameters, desired *v1alpha1.UserParameter } return true } + +// passwordSecretRef returns the Secret referenced by the passwordSecretRef of +// a User, and false if it references none. +func passwordSecretRef(o client.Object) (types.NamespacedName, bool) { + cr, ok := o.(*v1alpha1.User) + if !ok || cr.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + ref := cr.Spec.ForProvider.PasswordSecretRef + return types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, true +} diff --git a/pkg/controller/cluster/postgresql/role/reconciler.go b/pkg/controller/cluster/postgresql/role/reconciler.go index 2da5d98f..fd7b684e 100644 --- a/pkg/controller/cluster/postgresql/role/reconciler.go +++ b/pkg/controller/cluster/postgresql/role/reconciler.go @@ -47,6 +47,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/clients" "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" + "github.com/crossplane-contrib/provider-sql/pkg/controller/secretwatch" ) const ( @@ -81,6 +82,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { if o.Features.Enabled(feature.EnableBetaManagementPolicies) { reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) } + h, err := secretwatch.PasswordSecretRef(mgr, o.Logger, &v1alpha1.Role{}, &v1alpha1.RoleList{}, passwordSecretRef) + if err != nil { + return err + } r := managed.NewReconciler(mgr, resource.ManagedKind(v1alpha1.RoleGroupVersionKind), reconcilerOptions..., @@ -94,6 +99,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Role{}). + Watches(&corev1.Secret{}, h). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -507,3 +513,14 @@ func lateInit(observed *v1alpha1.RoleParameters, desired *v1alpha1.RoleParameter func (c *external) Disconnect(ctx context.Context) error { return nil } + +// passwordSecretRef returns the Secret referenced by the passwordSecretRef of +// a Role, and false if it references none. +func passwordSecretRef(o client.Object) (types.NamespacedName, bool) { + cr, ok := o.(*v1alpha1.Role) + if !ok || cr.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + ref := cr.Spec.ForProvider.PasswordSecretRef + return types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, true +} diff --git a/pkg/controller/namespaced/mssql/user/reconciler.go b/pkg/controller/namespaced/mssql/user/reconciler.go index 405a9cfd..62438702 100644 --- a/pkg/controller/namespaced/mssql/user/reconciler.go +++ b/pkg/controller/namespaced/mssql/user/reconciler.go @@ -22,6 +22,8 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,6 +42,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/clients/mssql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/mssql/provider" + "github.com/crossplane-contrib/provider-sql/pkg/controller/secretwatch" ) const ( @@ -75,6 +78,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { if o.Features.Enabled(feature.EnableBetaManagementPolicies) { reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) } + h, err := secretwatch.PasswordSecretRef(mgr, o.Logger, &namespacedv1alpha1.User{}, &namespacedv1alpha1.UserList{}, passwordSecretRef) + if err != nil { + return err + } r := managed.NewReconciler(mgr, resource.ManagedKind(namespacedv1alpha1.UserGroupVersionKind), reconcilerOptions..., @@ -88,6 +95,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). For(&namespacedv1alpha1.User{}). + Watches(&corev1.Secret{}, h). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -333,3 +341,14 @@ func (c *external) Delete(ctx context.Context, mg *namespacedv1alpha1.User) (man return managed.ExternalDelete{}, nil } + +// passwordSecretRef returns the Secret referenced by the passwordSecretRef of +// a User, and false if it references none. +func passwordSecretRef(o client.Object) (types.NamespacedName, bool) { + cr, ok := o.(*namespacedv1alpha1.User) + if !ok || cr.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + ref := cr.Spec.ForProvider.PasswordSecretRef + return types.NamespacedName{Namespace: o.GetNamespace(), Name: ref.Name}, true +} diff --git a/pkg/controller/namespaced/mysql/user/reconciler.go b/pkg/controller/namespaced/mysql/user/reconciler.go index 0ebdc073..fd861f7f 100644 --- a/pkg/controller/namespaced/mysql/user/reconciler.go +++ b/pkg/controller/namespaced/mysql/user/reconciler.go @@ -23,6 +23,8 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -41,6 +43,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/mysql/provider" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/mysql/tls" + "github.com/crossplane-contrib/provider-sql/pkg/controller/secretwatch" ) const ( @@ -72,6 +75,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { if o.Features.Enabled(feature.EnableBetaManagementPolicies) { reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) } + h, err := secretwatch.PasswordSecretRef(mgr, o.Logger, &namespacedv1alpha1.User{}, &namespacedv1alpha1.UserList{}, passwordSecretRef) + if err != nil { + return err + } r := managed.NewReconciler(mgr, resource.ManagedKind(namespacedv1alpha1.UserGroupVersionKind), reconcilerOptions..., @@ -85,6 +92,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). For(&namespacedv1alpha1.User{}). + Watches(&corev1.Secret{}, h). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -558,3 +566,14 @@ func upToDate(observed *namespacedv1alpha1.UserParameters, desired *namespacedv1 } return true } + +// passwordSecretRef returns the Secret referenced by the passwordSecretRef of +// a User, and false if it references none. +func passwordSecretRef(o client.Object) (types.NamespacedName, bool) { + cr, ok := o.(*namespacedv1alpha1.User) + if !ok || cr.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + ref := cr.Spec.ForProvider.PasswordSecretRef + return types.NamespacedName{Namespace: o.GetNamespace(), Name: ref.Name}, true +} diff --git a/pkg/controller/namespaced/postgresql/role/reconciler.go b/pkg/controller/namespaced/postgresql/role/reconciler.go index 41990982..19f65e57 100644 --- a/pkg/controller/namespaced/postgresql/role/reconciler.go +++ b/pkg/controller/namespaced/postgresql/role/reconciler.go @@ -27,7 +27,9 @@ import ( "github.com/crossplane/crossplane-runtime/v2/pkg/statemetrics" "github.com/lib/pq" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -46,6 +48,7 @@ import ( "github.com/crossplane-contrib/provider-sql/pkg/clients/postgresql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" "github.com/crossplane-contrib/provider-sql/pkg/controller/namespaced/postgresql/provider" + "github.com/crossplane-contrib/provider-sql/pkg/controller/secretwatch" ) const ( @@ -78,6 +81,10 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { if o.Features.Enabled(feature.EnableBetaManagementPolicies) { reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) } + h, err := secretwatch.PasswordSecretRef(mgr, o.Logger, &namespacedv1alpha1.Role{}, &namespacedv1alpha1.RoleList{}, passwordSecretRef) + if err != nil { + return err + } r := managed.NewReconciler(mgr, resource.ManagedKind(namespacedv1alpha1.RoleGroupVersionKind), reconcilerOptions..., @@ -91,6 +98,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { return ctrl.NewControllerManagedBy(mgr). Named(name). For(&namespacedv1alpha1.Role{}). + Watches(&corev1.Secret{}, h). WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrency, }). @@ -490,3 +498,14 @@ func lateInit(observed *namespacedv1alpha1.RoleParameters, desired *namespacedv1 func (c *external) Disconnect(ctx context.Context) error { return nil } + +// passwordSecretRef returns the Secret referenced by the passwordSecretRef of +// a Role, and false if it references none. +func passwordSecretRef(o client.Object) (types.NamespacedName, bool) { + cr, ok := o.(*namespacedv1alpha1.Role) + if !ok || cr.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + ref := cr.Spec.ForProvider.PasswordSecretRef + return types.NamespacedName{Namespace: o.GetNamespace(), Name: ref.Name}, true +} diff --git a/pkg/controller/secretwatch/secretwatch.go b/pkg/controller/secretwatch/secretwatch.go new file mode 100644 index 00000000..13a34b8e --- /dev/null +++ b/pkg/controller/secretwatch/secretwatch.go @@ -0,0 +1,90 @@ +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package secretwatch reconciles managed resources when the Secret referenced +// by their passwordSecretRef changes. +package secretwatch + +import ( + "context" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" +) + +const ( + indexKey = "spec.forProvider.passwordSecretRef" + + errIndex = "cannot index managed resources by passwordSecretRef" + errList = "cannot list managed resources referencing secret" +) + +// SecretRefFn returns the Secret referenced by the passwordSecretRef of obj, +// and false if obj references none. +type SecretRefFn func(obj client.Object) (types.NamespacedName, bool) + +// PasswordSecretRef indexes objects of the type of obj by the Secret their +// passwordSecretRef points at, and returns a handler for Secret events that +// enqueues every object in list referencing the changed Secret. +func PasswordSecretRef(mgr ctrl.Manager, log logging.Logger, obj client.Object, list client.ObjectList, ref SecretRefFn) (handler.EventHandler, error) { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), obj, indexKey, indexer(ref)); err != nil { + return nil, errors.Wrap(err, errIndex) + } + return handler.EnqueueRequestsFromMapFunc(enqueueReferencing(mgr.GetClient(), log, list)), nil +} + +func indexer(ref SecretRefFn) client.IndexerFunc { + return func(obj client.Object) []string { + nn, ok := ref(obj) + if !ok { + return nil + } + return []string{nn.String()} + } +} + +func enqueueReferencing(c client.Reader, log logging.Logger, list client.ObjectList) handler.MapFunc { + return func(ctx context.Context, s client.Object) []reconcile.Request { + key := types.NamespacedName{Namespace: s.GetNamespace(), Name: s.GetName()}.String() + l := list.DeepCopyObject().(client.ObjectList) + if err := c.List(ctx, l, client.MatchingFields{indexKey: key}); err != nil { + // The next poll still picks up the change. + log.Info(errList, "secret", key, "error", err) + return nil + } + items, err := meta.ExtractList(l) + if err != nil { + log.Info(errList, "secret", key, "error", err) + return nil + } + reqs := make([]reconcile.Request, 0, len(items)) + for _, i := range items { + o, ok := i.(client.Object) + if !ok { + continue + } + reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: o.GetNamespace(), Name: o.GetName()}}) + } + return reqs + } +} diff --git a/pkg/controller/secretwatch/secretwatch_test.go b/pkg/controller/secretwatch/secretwatch_test.go new file mode 100644 index 00000000..6224efeb --- /dev/null +++ b/pkg/controller/secretwatch/secretwatch_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2026 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package secretwatch + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/crossplane/crossplane-runtime/v2/pkg/logging" + xpv2 "github.com/crossplane/crossplane/apis/v2/core/v2" + + "github.com/crossplane-contrib/provider-sql/apis/namespaced/postgresql/v1alpha1" +) + +func role(ns, name, secret string) *v1alpha1.Role { + r := &v1alpha1.Role{ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}} + if secret != "" { + r.Spec.ForProvider.PasswordSecretRef = &xpv2.LocalSecretKeySelector{ + LocalSecretReference: xpv2.LocalSecretReference{Name: secret}, + Key: "password", + } + } + return r +} + +func ref(o client.Object) (types.NamespacedName, bool) { + r := o.(*v1alpha1.Role) + if r.Spec.ForProvider.PasswordSecretRef == nil { + return types.NamespacedName{}, false + } + return types.NamespacedName{Namespace: r.GetNamespace(), Name: r.Spec.ForProvider.PasswordSecretRef.Name}, true +} + +func TestEnqueueReferencing(t *testing.T) { + s := runtime.NewScheme() + if err := v1alpha1.SchemeBuilder.AddToScheme(s); err != nil { + t.Fatal(err) + } + c := fake.NewClientBuilder(). + WithScheme(s). + WithIndex(&v1alpha1.Role{}, indexKey, indexer(ref)). + WithObjects( + role("a", "r1", "pw"), + role("a", "r2", "pw"), + role("a", "r3", "other"), + role("a", "r4", ""), + role("b", "r5", "pw"), + ). + Build() + fn := enqueueReferencing(c, logging.NewNopLogger(), &v1alpha1.RoleList{}) + + cases := map[string]struct { + secret *corev1.Secret + want []reconcile.Request + }{ + "ReferencedByTwo": { + secret: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "a", Name: "pw"}}, + want: []reconcile.Request{ + {NamespacedName: types.NamespacedName{Namespace: "a", Name: "r1"}}, + {NamespacedName: types.NamespacedName{Namespace: "a", Name: "r2"}}, + }, + }, + "OtherNamespace": { + secret: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "b", Name: "pw"}}, + want: []reconcile.Request{ + {NamespacedName: types.NamespacedName{Namespace: "b", Name: "r5"}}, + }, + }, + "Unreferenced": { + secret: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "a", Name: "conn"}}, + want: []reconcile.Request{}, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := fn(context.Background(), tc.secret) + sortReqs := cmpopts.SortSlices(func(a, b reconcile.Request) bool { return a.Name < b.Name }) + if diff := cmp.Diff(tc.want, got, sortReqs); diff != "" { + t.Errorf("enqueueReferencing(...): -want, +got:\n%s", diff) + } + }) + } +}