From 7c184706767682abfbc6c808fdef456e3a9dabd5 Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Tue, 15 Jul 2025 15:34:30 +0100 Subject: [PATCH] Introduce label selector for watching ConfigMaps and Secrets Signed-off-by: Matheus Pimenta --- api/go.mod | 2 +- api/go.sum | 4 +- docs/spec/v2/helmreleases.md | 30 +++++++ go.mod | 8 +- go.sum | 16 ++-- internal/controller/helmrelease_controller.go | 86 +++++++++++++++++++ main.go | 7 ++ 7 files changed, 138 insertions(+), 15 deletions(-) diff --git a/api/go.mod b/api/go.mod index b4c1c8b..2afba51 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/fluxcd/pkg/apis/kustomize v1.11.0 - github.com/fluxcd/pkg/apis/meta v1.17.0 + github.com/fluxcd/pkg/apis/meta v1.18.0 k8s.io/apiextensions-apiserver v0.33.2 k8s.io/apimachinery v0.33.2 sigs.k8s.io/controller-runtime v0.21.0 diff --git a/api/go.sum b/api/go.sum index e8a6bb1..85ea5c6 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fluxcd/pkg/apis/kustomize v1.11.0 h1:0IzDgxZkc4v+5SDNCvgZhfwfkdkQLPXCner7TNaJFWE= github.com/fluxcd/pkg/apis/kustomize v1.11.0/go.mod h1:j302mJGDww8cn9qvMsRQ0LJ1HPAPs/IlX7CSsoJV7BI= -github.com/fluxcd/pkg/apis/meta v1.17.0 h1:KVMDyJQj1NYCsppsFUkbJGMnKxsqJVpnKBFolHf/q8E= -github.com/fluxcd/pkg/apis/meta v1.17.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8= +github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs= +github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8= github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md index 258e15c..8636506 100644 --- a/docs/spec/v2/helmreleases.md +++ b/docs/spec/v2/helmreleases.md @@ -458,6 +458,10 @@ a list). You can read more about the available formats and limitations in the For JSON strings, the [limitations are the same as while using `helm`](https://github.com/helm/helm/issues/5618) and require you to escape the full JSON string (including `=`, `[`, `,`, `.`). +To make a HelmRelease react immediately to changes in the referenced Secret +or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies) +section. + #### Inline values `.spec.values` is an optional field to inline values within a HelmRelease. When @@ -836,6 +840,10 @@ Two authentication alternatives are available: building a kubeconfig dynamically with parameters stored in a Kubernetes ConfigMap in the same namespace as the HelmRelease via workload identity. +To make a HelmRelease react immediately to changes in the referenced Secret +or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies) +section. + When both `.spec.kubeConfig` and [`.spec.serviceAccountName`](#service-account-reference) are specified, the controller will impersonate the ServiceAccount on the target cluster, @@ -1590,6 +1598,28 @@ with the values from the referenced ConfigMaps and/or Secrets. are referenced in the HelmRelease `.spec.valuesFrom` field, so exercise caution when using this command. +### Reacting immediately to configuration dependencies + +To trigger a Helm release upgrade when changes occur in referenced +Secrets or ConfigMaps, you can set the following label on the +Secret or ConfigMap: + +```yaml +metadata: + labels: + reconcile.fluxcd.io/watch: Enabled +``` + +An alternative to labeling every Secret or ConfigMap is +setting the `--watch-configs-label-selector=owner!=helm` +[flag](https://fluxcd.io/flux/components/helm/options/#flags) +in helm-controller, which allows watching all Secrets and +ConfigMaps except for Helm storage Secrets. + +**Note**: An upgrade will be triggered for an event on a referenced +Secret/ConfigMap even if it's marked as optional in the `.spec.valuesFrom` +field, including deletion events. + ## HelmRelease Status ### Events diff --git a/go.mod b/go.mod index 497b811..f661d09 100644 --- a/go.mod +++ b/go.mod @@ -21,11 +21,11 @@ require ( github.com/fluxcd/pkg/apis/acl v0.8.0 github.com/fluxcd/pkg/apis/event v0.18.0 github.com/fluxcd/pkg/apis/kustomize v1.11.0 - github.com/fluxcd/pkg/apis/meta v1.17.0 - github.com/fluxcd/pkg/auth v0.21.0 + github.com/fluxcd/pkg/apis/meta v1.18.0 + github.com/fluxcd/pkg/auth v0.22.0 github.com/fluxcd/pkg/cache v0.10.0 - github.com/fluxcd/pkg/chartutil v1.7.0 - github.com/fluxcd/pkg/runtime v0.69.0 + github.com/fluxcd/pkg/chartutil v1.8.0 + github.com/fluxcd/pkg/runtime v0.72.0 github.com/fluxcd/pkg/ssa v0.51.0 github.com/fluxcd/pkg/testserver v0.11.0 github.com/fluxcd/source-controller/api v1.6.0 diff --git a/go.sum b/go.sum index dc19549..01c89cd 100644 --- a/go.sum +++ b/go.sum @@ -148,16 +148,16 @@ github.com/fluxcd/pkg/apis/event v0.18.0 h1:PNbWk9gvX8gMIi6VsJapnuDO+giLEeY+6olL github.com/fluxcd/pkg/apis/event v0.18.0/go.mod h1:7S/DGboLolfbZ6stO6dcDhG1SfkPWQ9foCULvbiYpiA= github.com/fluxcd/pkg/apis/kustomize v1.11.0 h1:0IzDgxZkc4v+5SDNCvgZhfwfkdkQLPXCner7TNaJFWE= github.com/fluxcd/pkg/apis/kustomize v1.11.0/go.mod h1:j302mJGDww8cn9qvMsRQ0LJ1HPAPs/IlX7CSsoJV7BI= -github.com/fluxcd/pkg/apis/meta v1.17.0 h1:KVMDyJQj1NYCsppsFUkbJGMnKxsqJVpnKBFolHf/q8E= -github.com/fluxcd/pkg/apis/meta v1.17.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8= -github.com/fluxcd/pkg/auth v0.21.0 h1:ckAQqP12wuptXEkMY18SQKWEY09m9e6yI0mEMsDV15M= -github.com/fluxcd/pkg/auth v0.21.0/go.mod h1:MXmpsXT97c874HCw5hnfqFUP7TsG8/Ss1vFrk8JccfM= +github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs= +github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8= +github.com/fluxcd/pkg/auth v0.22.0 h1:h+tjYm4w/tC7Rvxph/A2wplOXAEohQCbh5u1TLMrEQE= +github.com/fluxcd/pkg/auth v0.22.0/go.mod h1:YEAHpBFuW5oLlH9ekuJaQdnJ2Q3A7Ny8kha3WY7QMnY= github.com/fluxcd/pkg/cache v0.10.0 h1:M+OGDM4da1cnz7q+sZSBtkBJHpiJsLnKVmR9OdMWxEY= github.com/fluxcd/pkg/cache v0.10.0/go.mod h1:pPXRzQUDQagsCniuOolqVhnAkbNgYOg8d2cTliPs7ME= -github.com/fluxcd/pkg/chartutil v1.7.0 h1:hzcqJ/60jj3WwYHDEHiFyrAfe2kZZ3GWZepd305ZpWo= -github.com/fluxcd/pkg/chartutil v1.7.0/go.mod h1:hfSQ5JPmT6oUEXUthuWb51oCqtQjbnt8i6GqBGYchfU= -github.com/fluxcd/pkg/runtime v0.69.0 h1:5gPY95NSFI34GlQTj0+NHjOFpirSwviCUb9bM09b5nA= -github.com/fluxcd/pkg/runtime v0.69.0/go.mod h1:ug+pat+I4wfOBuCy2E/pLmBNd3kOOo4cP2jxnxefPwY= +github.com/fluxcd/pkg/chartutil v1.8.0 h1:kDR87A1FyhUNgm48nIUrx2CKbEtix7tI8j7gESQdiTU= +github.com/fluxcd/pkg/chartutil v1.8.0/go.mod h1:yeZzhTNogNAbGlSge2RiaACOCQt5GwentCq3yFYgEUU= +github.com/fluxcd/pkg/runtime v0.72.0 h1:9JCto84iL2FziuTuuvDwvS+cfIzGhHOk25y8ulXpNOs= +github.com/fluxcd/pkg/runtime v0.72.0/go.mod h1:iGhdaEq+lMJQTJNAFEPOU4gUJ7kt3yeDcJPZy7O9IUw= github.com/fluxcd/pkg/ssa v0.51.0 h1:sFarxKZcS0J8sjq9qvs/r+1XiJqNgRodEiPjV75F8R4= github.com/fluxcd/pkg/ssa v0.51.0/go.mod h1:v+h9RC0JxWIqMTK2Eo+8Nh700AXyZChZ2TiLVj4tf3M= github.com/fluxcd/pkg/testserver v0.11.0 h1:a/kxpFqv7XQxZjwVPP3voooRmSd/3ipLVolK0xUIxXQ= diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 49ed23e..3c0a041 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -109,6 +109,7 @@ type HelmReleaseReconcilerOptions struct { HTTPRetry int DependencyRequeueInterval time.Duration RateLimiter workqueue.TypedRateLimiter[reconcile.Request] + WatchConfigsPredicate predicate.Predicate } var ( @@ -117,6 +118,11 @@ var ( ) func (r *HelmReleaseReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts HelmReleaseReconcilerOptions) error { + const ( + indexConfigMap = ".metadata.configMap" + indexSecret = ".metadata.secret" + ) + // Index the HelmRelease by the Source reference they point to. if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, v2.SourceIndexKey, func(o client.Object) []string { @@ -133,6 +139,46 @@ func (r *HelmReleaseReconciler) SetupWithManager(ctx context.Context, mgr ctrl.M return err } + // Index the HelmRelease by the ConfigMap references they point to. + if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, indexConfigMap, + func(o client.Object) []string { + obj := o.(*v2.HelmRelease) + namespace := obj.GetNamespace() + var keys []string + if kc := obj.Spec.KubeConfig; kc != nil && kc.ConfigMapRef != nil { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.ConfigMapRef.Name)) + } + for _, ref := range obj.Spec.ValuesFrom { + if ref.Kind == "ConfigMap" { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name)) + } + } + return keys + }, + ); err != nil { + return err + } + + // Index the HelmRelease by the Secret references they point to. + if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, indexSecret, + func(o client.Object) []string { + obj := o.(*v2.HelmRelease) + namespace := obj.GetNamespace() + var keys []string + if kc := obj.Spec.KubeConfig; kc != nil && kc.SecretRef != nil { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.SecretRef.Name)) + } + for _, ref := range obj.Spec.ValuesFrom { + if ref.Kind == "Secret" { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name)) + } + } + return keys + }, + ); err != nil { + return err + } + r.requeueDependency = opts.DependencyRequeueInterval r.artifactFetchRetries = opts.HTTPRetry @@ -150,6 +196,16 @@ func (r *HelmReleaseReconciler) SetupWithManager(ctx context.Context, mgr ctrl.M handler.EnqueueRequestsFromMapFunc(r.requestsForOCIRrepositoryChange), builder.WithPredicates(intpredicates.SourceRevisionChangePredicate{}), ). + WatchesMetadata( + &corev1.ConfigMap{}, + handler.EnqueueRequestsFromMapFunc(r.requestsForConfigDependency(indexConfigMap)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate), + ). + WatchesMetadata( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.requestsForConfigDependency(indexSecret)), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate), + ). WithOptions(controller.Options{ RateLimiter: opts.RateLimiter, }). @@ -877,6 +933,36 @@ func (r *HelmReleaseReconciler) requestsForOCIRrepositoryChange(ctx context.Cont return reqs } +// requestsForConfigDependency enqueues requests for watched ConfigMaps or Secrets +// according to the specified index. +func (r *HelmReleaseReconciler) requestsForConfigDependency( + index string) func(ctx context.Context, o client.Object) []reconcile.Request { + + return func(ctx context.Context, o client.Object) []reconcile.Request { + // List HelmReleases that have a dependency on the ConfigMap or Secret. + var list v2.HelmReleaseList + if err := r.List(ctx, &list, client.MatchingFields{ + index: client.ObjectKeyFromObject(o).String(), + }); err != nil { + ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmReleases for config dependency change", + "index", index, "objectRef", map[string]string{ + "name": o.GetName(), + "namespace": o.GetNamespace(), + }) + return nil + } + + // Enqueue requests for each HelmRelease in the list. + reqs := make([]reconcile.Request, 0, len(list.Items)) + for i := range list.Items { + reqs = append(reqs, reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(&list.Items[i]), + }) + } + return reqs + } +} + func isSourceReady(obj sourcev1.Source) (bool, string) { if o, ok := obj.(conditions.Getter); ok { return isReady(o, obj.GetArtifact()) diff --git a/main.go b/main.go index 0704bf2..9075904 100644 --- a/main.go +++ b/main.go @@ -181,6 +181,12 @@ func main() { os.Exit(1) } + watchConfigsPredicate, err := helper.GetWatchConfigsPredicate(watchOptions) + if err != nil { + setupLog.Error(err, "unable to configure watch configs label selector for controller") + os.Exit(1) + } + var disableCacheFor []ctrlclient.Object shouldCache, err := features.Enabled(features.CacheSecretsAndConfigMaps) if err != nil { @@ -317,6 +323,7 @@ func main() { DependencyRequeueInterval: requeueDependency, HTTPRetry: httpRetry, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), + WatchConfigsPredicate: watchConfigsPredicate, }); err != nil { setupLog.Error(err, "unable to create controller", "controller", v2.HelmReleaseKind) os.Exit(1)