diff --git a/api/go.mod b/api/go.mod index 4b7f60cf..c7cf1ed8 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 e8a6bb12..85ea5c6d 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/v1/kustomizations.md b/docs/spec/v1/kustomizations.md index 712fc835..cf85650d 100644 --- a/docs/spec/v1/kustomizations.md +++ b/docs/spec/v1/kustomizations.md @@ -639,6 +639,10 @@ With `.spec.postBuild.substituteFrom` you can provide a list of ConfigMaps and Secrets from which the variables are loaded. The ConfigMap and Secret data keys are used as the variable names. +To make a Kustomization react immediately to changes in the referenced Secret +or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies) +section. + The `.spec.postBuild.substituteFrom.optional` field indicates how the controller should handle a referenced ConfigMap or Secret being absent at reconciliation time. The controller's default behavior ― with @@ -805,6 +809,10 @@ Two authentication alternatives are available: building a kubeconfig dynamically with parameters stored in a Kubernetes ConfigMap in the same namespace as the Kustomization via workload identity. +To make a Kustomization 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, @@ -958,6 +966,9 @@ The `.spec.decryption` field has the following subfields: - `.serviceAccountName`: The name of the service account used for secret-less authentication with KMS services from cloud providers. +To make a Kustomization react immediately to changes in the referenced Secret +see [this](#reacting-immediately-to-configuration-dependencies) section. + For a complete guide on how to set up authentication for KMS services from cloud providers, see the integration [docs](/flux/integrations/). @@ -1910,6 +1921,29 @@ the controller. The Flux CLI offer commands for filtering the logs for a specific Kustomization, e.g. `flux logs --level=error --kind=Kustomization --name=`. +### Reacting immediately to configuration dependencies + +To trigger a reconciliation 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/kustomize/options/#flags) +in kustomize-controller, which allows watching all Secrets and +ConfigMaps except for Helm storage Secrets. + +**Note**: A reconciliation will be triggered for an event on a +referenced Secret/ConfigMap even if it's marked as optional in +the `.spec.postBuild.substituteFrom` field, including deletion +events. + ## Kustomization Status ### Conditions diff --git a/go.mod b/go.mod index 01afe5e3..f86ee42d 100644 --- a/go.mod +++ b/go.mod @@ -23,12 +23,12 @@ 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/http/fetch v0.17.0 github.com/fluxcd/pkg/kustomize v1.19.0 - github.com/fluxcd/pkg/runtime v0.69.0 + github.com/fluxcd/pkg/runtime v0.72.0 github.com/fluxcd/pkg/ssa v0.51.0 github.com/fluxcd/pkg/tar v0.13.0 github.com/fluxcd/pkg/testserver v0.11.0 diff --git a/go.sum b/go.sum index a8448e83..a4ab526d 100644 --- a/go.sum +++ b/go.sum @@ -197,10 +197,10 @@ 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/envsubst v1.4.0 h1:pYsb6wrmXOSfHXuXQHaaBBMt3LumhgCb8SMdBNAwV/U= @@ -209,8 +209,8 @@ github.com/fluxcd/pkg/http/fetch v0.17.0 h1:U/Fuh+H1cRL2d/EOfdsjJPaPDPtL3pFanPSE github.com/fluxcd/pkg/http/fetch v0.17.0/go.mod h1:nMozZtiSKtPGwMrR5wGjIJoQmhvFqZ5P4UsM/Lqza2I= github.com/fluxcd/pkg/kustomize v1.19.0 h1:2eO8lMx0/H/Yyq35LMTAMhxEElOzMW0Yi9zUNZoimlU= github.com/fluxcd/pkg/kustomize v1.19.0/go.mod h1:OCCW9vU3lStDh3jyg9MM/a29MSdNAVk2wjl0lDos5Fs= -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/runtime v0.72.0 h1:9JCto84iL2FziuTuuvDwvS+cfIzGhHOk25y8ulXpNOs= +github.com/fluxcd/pkg/runtime v0.72.0/go.mod h1:iGhdaEq+lMJQTJNAFEPOU4gUJ7kt3yeDcJPZy7O9IUw= github.com/fluxcd/pkg/sourceignore v0.13.0 h1:ZvkzX2WsmyZK9cjlqOFFW1onHVzhPZIqDbCh96rPqbU= github.com/fluxcd/pkg/sourceignore v0.13.0/go.mod h1:Z9H1GoBx0ljOhptnzoV0PL6Nd/UzwKcSphP27lqb4xI= github.com/fluxcd/pkg/ssa v0.51.0 h1:sFarxKZcS0J8sjq9qvs/r+1XiJqNgRodEiPjV75F8R4= diff --git a/internal/controller/kustomization_controller.go b/internal/controller/kustomization_controller.go index bd733c38..cdba07e4 100644 --- a/internal/controller/kustomization_controller.go +++ b/internal/controller/kustomization_controller.go @@ -119,31 +119,81 @@ type KustomizationReconcilerOptions struct { HTTPRetry int DependencyRequeueInterval time.Duration RateLimiter workqueue.TypedRateLimiter[reconcile.Request] + WatchConfigsPredicate predicate.Predicate } func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts KustomizationReconcilerOptions) error { const ( - ociRepositoryIndexKey string = ".metadata.ociRepository" - gitRepositoryIndexKey string = ".metadata.gitRepository" - bucketIndexKey string = ".metadata.bucket" + indexOCIRepository = ".metadata.ociRepository" + indexGitRepository = ".metadata.gitRepository" + indexBucket = ".metadata.bucket" + indexConfigMap = ".metadata.configMap" + indexSecret = ".metadata.secret" ) // Index the Kustomizations by the OCIRepository references they (may) point at. - if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, ociRepositoryIndexKey, + if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexOCIRepository, r.indexBy(sourcev1.OCIRepositoryKind)); err != nil { - return fmt.Errorf("failed setting index fields: %w", err) + return fmt.Errorf("failed creating index %s: %w", indexOCIRepository, err) } // Index the Kustomizations by the GitRepository references they (may) point at. - if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, gitRepositoryIndexKey, + if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexGitRepository, r.indexBy(sourcev1.GitRepositoryKind)); err != nil { - return fmt.Errorf("failed setting index fields: %w", err) + return fmt.Errorf("failed creating index %s: %w", indexGitRepository, err) } // Index the Kustomizations by the Bucket references they (may) point at. - if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, bucketIndexKey, + if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexBucket, r.indexBy(sourcev1.BucketKind)); err != nil { - return fmt.Errorf("failed setting index fields: %w", err) + return fmt.Errorf("failed creating index %s: %w", indexBucket, err) + } + + // Index the Kustomization by the ConfigMap references they point to. + if err := mgr.GetFieldIndexer().IndexField(ctx, &kustomizev1.Kustomization{}, indexConfigMap, + func(o client.Object) []string { + obj := o.(*kustomizev1.Kustomization) + 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)) + } + if pb := obj.Spec.PostBuild; pb != nil { + for _, ref := range pb.SubstituteFrom { + if ref.Kind == "ConfigMap" { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name)) + } + } + } + return keys + }, + ); err != nil { + return fmt.Errorf("failed creating index %s: %w", indexConfigMap, err) + } + + // Index the Kustomization by the Secret references they point to. + if err := mgr.GetFieldIndexer().IndexField(ctx, &kustomizev1.Kustomization{}, indexSecret, + func(o client.Object) []string { + obj := o.(*kustomizev1.Kustomization) + namespace := obj.GetNamespace() + var keys []string + if dec := obj.Spec.Decryption; dec != nil && dec.SecretRef != nil { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, dec.SecretRef.Name)) + } + if kc := obj.Spec.KubeConfig; kc != nil && kc.SecretRef != nil { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.SecretRef.Name)) + } + if pb := obj.Spec.PostBuild; pb != nil { + for _, ref := range pb.SubstituteFrom { + if ref.Kind == "Secret" { + keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name)) + } + } + } + return keys + }, + ); err != nil { + return fmt.Errorf("failed creating index %s: %w", indexSecret, err) } r.requeueDependency = opts.DependencyRequeueInterval @@ -156,19 +206,29 @@ func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl )). Watches( &sourcev1.OCIRepository{}, - handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(ociRepositoryIndexKey)), + handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexOCIRepository)), builder.WithPredicates(SourceRevisionChangePredicate{}), ). Watches( &sourcev1.GitRepository{}, - handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(gitRepositoryIndexKey)), + handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexGitRepository)), builder.WithPredicates(SourceRevisionChangePredicate{}), ). Watches( &sourcev1.Bucket{}, - handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(bucketIndexKey)), + handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexBucket)), builder.WithPredicates(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, }). diff --git a/internal/controller/kustomization_indexers.go b/internal/controller/kustomization_indexers.go index e3c6ce27..61ab1b1b 100644 --- a/internal/controller/kustomization_indexers.go +++ b/internal/controller/kustomization_indexers.go @@ -64,16 +64,11 @@ func (r *KustomizationReconciler) requestsForRevisionChangeOf(indexKey string) h } dd = append(dd, d.DeepCopy()) } - sorted, err := dependency.Sort(dd) + reqs, err := sortAndEnqueue(dd) if err != nil { log.Error(err, "failed to sort dependencies for revision change") return nil } - reqs := make([]reconcile.Request, len(sorted)) - for i := range sorted { - reqs[i].NamespacedName.Name = sorted[i].Name - reqs[i].NamespacedName.Namespace = sorted[i].Namespace - } return reqs } } @@ -96,3 +91,54 @@ func (r *KustomizationReconciler) indexBy(kind string) func(o client.Object) []s return nil } } + +// requestsForConfigDependency enqueues requests for watched ConfigMaps or Secrets +// according to the specified index. +func (r *KustomizationReconciler) requestsForConfigDependency( + index string) func(ctx context.Context, o client.Object) []reconcile.Request { + + return func(ctx context.Context, o client.Object) []reconcile.Request { + log := ctrl.LoggerFrom(ctx).WithValues("index", index, "objectRef", map[string]string{ + "name": o.GetName(), + "namespace": o.GetNamespace(), + }) + + // List Kustomizations that have a dependency on the ConfigMap or Secret. + var list kustomizev1.KustomizationList + if err := r.List(ctx, &list, client.MatchingFields{ + index: client.ObjectKeyFromObject(o).String(), + }); err != nil { + log.Error(err, "failed to list Kustomizations for config dependency change") + return nil + } + + // Sort the Kustomizations by their dependencies to ensure + // that dependent Kustomizations are reconciled after their dependencies. + dd := make([]dependency.Dependent, 0, len(list.Items)) + for i := range list.Items { + dd = append(dd, &list.Items[i]) + } + + // Enqueue requests for each Kustomization in the list. + reqs, err := sortAndEnqueue(dd) + if err != nil { + log.Error(err, "failed to sort dependencies for config dependency change") + return nil + } + return reqs + } +} + +// sortAndEnqueue sorts the dependencies and returns a slice of reconcile.Requests. +func sortAndEnqueue(dd []dependency.Dependent) ([]reconcile.Request, error) { + sorted, err := dependency.Sort(dd) + if err != nil { + return nil, err + } + reqs := make([]reconcile.Request, len(sorted)) + for i := range sorted { + reqs[i].NamespacedName.Name = sorted[i].Name + reqs[i].NamespacedName.Namespace = sorted[i].Namespace + } + return reqs, nil +} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index ee724e99..7ae08583 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" controllerLog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/apis/meta" @@ -185,6 +186,7 @@ func TestMain(m *testing.M) { } if err := (reconciler).SetupWithManager(ctx, testEnv, KustomizationReconcilerOptions{ DependencyRequeueInterval: 2 * time.Second, + WatchConfigsPredicate: predicate.Not(predicate.Funcs{}), }); err != nil { panic(fmt.Sprintf("Failed to start KustomizationReconciler: %v", err)) } diff --git a/main.go b/main.go index f49c4c44..e5b55fea 100644 --- a/main.go +++ b/main.go @@ -162,6 +162,12 @@ func main() { os.Exit(1) } + watchConfigsPredicate, err := runtimeCtrl.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 { @@ -294,6 +300,7 @@ func main() { DependencyRequeueInterval: requeueDependency, HTTPRetry: httpRetry, RateLimiter: runtimeCtrl.GetRateLimiter(rateLimiterOptions), + WatchConfigsPredicate: watchConfigsPredicate, }); err != nil { setupLog.Error(err, "unable to create controller", "controller", controllerName) os.Exit(1)