Skip to content

Commit c48ffbe

Browse files
authored
Merge pull request #1486 from fluxcd/watch-label
Introduce label selector for watching ConfigMaps and Secrets
2 parents b4d23e7 + 2dcec19 commit c48ffbe

File tree

9 files changed

+179
-30
lines changed

9 files changed

+179
-30
lines changed

api/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.24.0
44

55
require (
66
github.com/fluxcd/pkg/apis/kustomize v1.11.0
7-
github.com/fluxcd/pkg/apis/meta v1.17.0
7+
github.com/fluxcd/pkg/apis/meta v1.18.0
88
k8s.io/apiextensions-apiserver v0.33.2
99
k8s.io/apimachinery v0.33.2
1010
sigs.k8s.io/controller-runtime v0.21.0

api/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/fluxcd/pkg/apis/kustomize v1.11.0 h1:0IzDgxZkc4v+5SDNCvgZhfwfkdkQLPXCner7TNaJFWE=
66
github.com/fluxcd/pkg/apis/kustomize v1.11.0/go.mod h1:j302mJGDww8cn9qvMsRQ0LJ1HPAPs/IlX7CSsoJV7BI=
7-
github.com/fluxcd/pkg/apis/meta v1.17.0 h1:KVMDyJQj1NYCsppsFUkbJGMnKxsqJVpnKBFolHf/q8E=
8-
github.com/fluxcd/pkg/apis/meta v1.17.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
7+
github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs=
8+
github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
99
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
1010
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
1111
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=

docs/spec/v1/kustomizations.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,10 @@ With `.spec.postBuild.substituteFrom` you can provide a list of ConfigMaps and
639639
Secrets from which the variables are loaded. The ConfigMap and Secret data keys
640640
are used as the variable names.
641641

642+
To make a Kustomization react immediately to changes in the referenced Secret
643+
or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies)
644+
section.
645+
642646
The `.spec.postBuild.substituteFrom.optional` field indicates how the
643647
controller should handle a referenced ConfigMap or Secret being absent
644648
at reconciliation time. The controller's default behavior ― with
@@ -805,6 +809,10 @@ Two authentication alternatives are available:
805809
building a kubeconfig dynamically with parameters stored in a Kubernetes
806810
ConfigMap in the same namespace as the Kustomization via workload identity.
807811

812+
To make a Kustomization react immediately to changes in the referenced Secret
813+
or ConfigMap see [this](#reacting-immediately-to-configuration-dependencies)
814+
section.
815+
808816
When both `.spec.kubeConfig` and
809817
[`.spec.serviceAccountName`](#service-account-reference) are specified,
810818
the controller will impersonate the ServiceAccount on the target cluster,
@@ -958,6 +966,9 @@ The `.spec.decryption` field has the following subfields:
958966
- `.serviceAccountName`: The name of the service account used for
959967
secret-less authentication with KMS services from cloud providers.
960968

969+
To make a Kustomization react immediately to changes in the referenced Secret
970+
see [this](#reacting-immediately-to-configuration-dependencies) section.
971+
961972
For a complete guide on how to set up authentication for KMS services from
962973
cloud providers, see the integration [docs](/flux/integrations/).
963974

@@ -1910,6 +1921,29 @@ the controller. The Flux CLI offer commands for filtering the logs for a
19101921
specific Kustomization, e.g.
19111922
`flux logs --level=error --kind=Kustomization --name=<kustomization-name>`.
19121923

1924+
### Reacting immediately to configuration dependencies
1925+
1926+
To trigger a reconciliation when changes occur in referenced
1927+
Secrets or ConfigMaps, you can set the following label on the
1928+
Secret or ConfigMap:
1929+
1930+
```yaml
1931+
metadata:
1932+
labels:
1933+
reconcile.fluxcd.io/watch: Enabled
1934+
```
1935+
1936+
An alternative to labeling every Secret or ConfigMap is
1937+
setting the `--watch-configs-label-selector=owner!=helm`
1938+
[flag](https://fluxcd.io/flux/components/kustomize/options/#flags)
1939+
in kustomize-controller, which allows watching all Secrets and
1940+
ConfigMaps except for Helm storage Secrets.
1941+
1942+
**Note**: A reconciliation will be triggered for an event on a
1943+
referenced Secret/ConfigMap even if it's marked as optional in
1944+
the `.spec.postBuild.substituteFrom` field, including deletion
1945+
events.
1946+
19131947
## Kustomization Status
19141948

19151949
### Conditions

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ require (
2323
github.com/fluxcd/pkg/apis/acl v0.8.0
2424
github.com/fluxcd/pkg/apis/event v0.18.0
2525
github.com/fluxcd/pkg/apis/kustomize v1.11.0
26-
github.com/fluxcd/pkg/apis/meta v1.17.0
27-
github.com/fluxcd/pkg/auth v0.21.0
26+
github.com/fluxcd/pkg/apis/meta v1.18.0
27+
github.com/fluxcd/pkg/auth v0.22.0
2828
github.com/fluxcd/pkg/cache v0.10.0
2929
github.com/fluxcd/pkg/http/fetch v0.17.0
3030
github.com/fluxcd/pkg/kustomize v1.19.0
31-
github.com/fluxcd/pkg/runtime v0.69.0
31+
github.com/fluxcd/pkg/runtime v0.72.0
3232
github.com/fluxcd/pkg/ssa v0.51.0
3333
github.com/fluxcd/pkg/tar v0.13.0
3434
github.com/fluxcd/pkg/testserver v0.11.0

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ github.com/fluxcd/pkg/apis/event v0.18.0 h1:PNbWk9gvX8gMIi6VsJapnuDO+giLEeY+6olL
197197
github.com/fluxcd/pkg/apis/event v0.18.0/go.mod h1:7S/DGboLolfbZ6stO6dcDhG1SfkPWQ9foCULvbiYpiA=
198198
github.com/fluxcd/pkg/apis/kustomize v1.11.0 h1:0IzDgxZkc4v+5SDNCvgZhfwfkdkQLPXCner7TNaJFWE=
199199
github.com/fluxcd/pkg/apis/kustomize v1.11.0/go.mod h1:j302mJGDww8cn9qvMsRQ0LJ1HPAPs/IlX7CSsoJV7BI=
200-
github.com/fluxcd/pkg/apis/meta v1.17.0 h1:KVMDyJQj1NYCsppsFUkbJGMnKxsqJVpnKBFolHf/q8E=
201-
github.com/fluxcd/pkg/apis/meta v1.17.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
202-
github.com/fluxcd/pkg/auth v0.21.0 h1:ckAQqP12wuptXEkMY18SQKWEY09m9e6yI0mEMsDV15M=
203-
github.com/fluxcd/pkg/auth v0.21.0/go.mod h1:MXmpsXT97c874HCw5hnfqFUP7TsG8/Ss1vFrk8JccfM=
200+
github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs=
201+
github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
202+
github.com/fluxcd/pkg/auth v0.22.0 h1:h+tjYm4w/tC7Rvxph/A2wplOXAEohQCbh5u1TLMrEQE=
203+
github.com/fluxcd/pkg/auth v0.22.0/go.mod h1:YEAHpBFuW5oLlH9ekuJaQdnJ2Q3A7Ny8kha3WY7QMnY=
204204
github.com/fluxcd/pkg/cache v0.10.0 h1:M+OGDM4da1cnz7q+sZSBtkBJHpiJsLnKVmR9OdMWxEY=
205205
github.com/fluxcd/pkg/cache v0.10.0/go.mod h1:pPXRzQUDQagsCniuOolqVhnAkbNgYOg8d2cTliPs7ME=
206206
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
209209
github.com/fluxcd/pkg/http/fetch v0.17.0/go.mod h1:nMozZtiSKtPGwMrR5wGjIJoQmhvFqZ5P4UsM/Lqza2I=
210210
github.com/fluxcd/pkg/kustomize v1.19.0 h1:2eO8lMx0/H/Yyq35LMTAMhxEElOzMW0Yi9zUNZoimlU=
211211
github.com/fluxcd/pkg/kustomize v1.19.0/go.mod h1:OCCW9vU3lStDh3jyg9MM/a29MSdNAVk2wjl0lDos5Fs=
212-
github.com/fluxcd/pkg/runtime v0.69.0 h1:5gPY95NSFI34GlQTj0+NHjOFpirSwviCUb9bM09b5nA=
213-
github.com/fluxcd/pkg/runtime v0.69.0/go.mod h1:ug+pat+I4wfOBuCy2E/pLmBNd3kOOo4cP2jxnxefPwY=
212+
github.com/fluxcd/pkg/runtime v0.72.0 h1:9JCto84iL2FziuTuuvDwvS+cfIzGhHOk25y8ulXpNOs=
213+
github.com/fluxcd/pkg/runtime v0.72.0/go.mod h1:iGhdaEq+lMJQTJNAFEPOU4gUJ7kt3yeDcJPZy7O9IUw=
214214
github.com/fluxcd/pkg/sourceignore v0.13.0 h1:ZvkzX2WsmyZK9cjlqOFFW1onHVzhPZIqDbCh96rPqbU=
215215
github.com/fluxcd/pkg/sourceignore v0.13.0/go.mod h1:Z9H1GoBx0ljOhptnzoV0PL6Nd/UzwKcSphP27lqb4xI=
216216
github.com/fluxcd/pkg/ssa v0.51.0 h1:sFarxKZcS0J8sjq9qvs/r+1XiJqNgRodEiPjV75F8R4=

internal/controller/kustomization_controller.go

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -119,31 +119,81 @@ type KustomizationReconcilerOptions struct {
119119
HTTPRetry int
120120
DependencyRequeueInterval time.Duration
121121
RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
122+
WatchConfigsPredicate predicate.Predicate
122123
}
123124

124125
func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts KustomizationReconcilerOptions) error {
125126
const (
126-
ociRepositoryIndexKey string = ".metadata.ociRepository"
127-
gitRepositoryIndexKey string = ".metadata.gitRepository"
128-
bucketIndexKey string = ".metadata.bucket"
127+
indexOCIRepository = ".metadata.ociRepository"
128+
indexGitRepository = ".metadata.gitRepository"
129+
indexBucket = ".metadata.bucket"
130+
indexConfigMap = ".metadata.configMap"
131+
indexSecret = ".metadata.secret"
129132
)
130133

131134
// Index the Kustomizations by the OCIRepository references they (may) point at.
132-
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, ociRepositoryIndexKey,
135+
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexOCIRepository,
133136
r.indexBy(sourcev1.OCIRepositoryKind)); err != nil {
134-
return fmt.Errorf("failed setting index fields: %w", err)
137+
return fmt.Errorf("failed creating index %s: %w", indexOCIRepository, err)
135138
}
136139

137140
// Index the Kustomizations by the GitRepository references they (may) point at.
138-
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, gitRepositoryIndexKey,
141+
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexGitRepository,
139142
r.indexBy(sourcev1.GitRepositoryKind)); err != nil {
140-
return fmt.Errorf("failed setting index fields: %w", err)
143+
return fmt.Errorf("failed creating index %s: %w", indexGitRepository, err)
141144
}
142145

143146
// Index the Kustomizations by the Bucket references they (may) point at.
144-
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, bucketIndexKey,
147+
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexBucket,
145148
r.indexBy(sourcev1.BucketKind)); err != nil {
146-
return fmt.Errorf("failed setting index fields: %w", err)
149+
return fmt.Errorf("failed creating index %s: %w", indexBucket, err)
150+
}
151+
152+
// Index the Kustomization by the ConfigMap references they point to.
153+
if err := mgr.GetFieldIndexer().IndexField(ctx, &kustomizev1.Kustomization{}, indexConfigMap,
154+
func(o client.Object) []string {
155+
obj := o.(*kustomizev1.Kustomization)
156+
namespace := obj.GetNamespace()
157+
var keys []string
158+
if kc := obj.Spec.KubeConfig; kc != nil && kc.ConfigMapRef != nil {
159+
keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.ConfigMapRef.Name))
160+
}
161+
if pb := obj.Spec.PostBuild; pb != nil {
162+
for _, ref := range pb.SubstituteFrom {
163+
if ref.Kind == "ConfigMap" {
164+
keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name))
165+
}
166+
}
167+
}
168+
return keys
169+
},
170+
); err != nil {
171+
return fmt.Errorf("failed creating index %s: %w", indexConfigMap, err)
172+
}
173+
174+
// Index the Kustomization by the Secret references they point to.
175+
if err := mgr.GetFieldIndexer().IndexField(ctx, &kustomizev1.Kustomization{}, indexSecret,
176+
func(o client.Object) []string {
177+
obj := o.(*kustomizev1.Kustomization)
178+
namespace := obj.GetNamespace()
179+
var keys []string
180+
if dec := obj.Spec.Decryption; dec != nil && dec.SecretRef != nil {
181+
keys = append(keys, fmt.Sprintf("%s/%s", namespace, dec.SecretRef.Name))
182+
}
183+
if kc := obj.Spec.KubeConfig; kc != nil && kc.SecretRef != nil {
184+
keys = append(keys, fmt.Sprintf("%s/%s", namespace, kc.SecretRef.Name))
185+
}
186+
if pb := obj.Spec.PostBuild; pb != nil {
187+
for _, ref := range pb.SubstituteFrom {
188+
if ref.Kind == "Secret" {
189+
keys = append(keys, fmt.Sprintf("%s/%s", namespace, ref.Name))
190+
}
191+
}
192+
}
193+
return keys
194+
},
195+
); err != nil {
196+
return fmt.Errorf("failed creating index %s: %w", indexSecret, err)
147197
}
148198

149199
r.requeueDependency = opts.DependencyRequeueInterval
@@ -156,19 +206,29 @@ func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl
156206
)).
157207
Watches(
158208
&sourcev1.OCIRepository{},
159-
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(ociRepositoryIndexKey)),
209+
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexOCIRepository)),
160210
builder.WithPredicates(SourceRevisionChangePredicate{}),
161211
).
162212
Watches(
163213
&sourcev1.GitRepository{},
164-
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(gitRepositoryIndexKey)),
214+
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexGitRepository)),
165215
builder.WithPredicates(SourceRevisionChangePredicate{}),
166216
).
167217
Watches(
168218
&sourcev1.Bucket{},
169-
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(bucketIndexKey)),
219+
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexBucket)),
170220
builder.WithPredicates(SourceRevisionChangePredicate{}),
171221
).
222+
WatchesMetadata(
223+
&corev1.ConfigMap{},
224+
handler.EnqueueRequestsFromMapFunc(r.requestsForConfigDependency(indexConfigMap)),
225+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate),
226+
).
227+
WatchesMetadata(
228+
&corev1.Secret{},
229+
handler.EnqueueRequestsFromMapFunc(r.requestsForConfigDependency(indexSecret)),
230+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate),
231+
).
172232
WithOptions(controller.Options{
173233
RateLimiter: opts.RateLimiter,
174234
}).

internal/controller/kustomization_indexers.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,11 @@ func (r *KustomizationReconciler) requestsForRevisionChangeOf(indexKey string) h
6464
}
6565
dd = append(dd, d.DeepCopy())
6666
}
67-
sorted, err := dependency.Sort(dd)
67+
reqs, err := sortAndEnqueue(dd)
6868
if err != nil {
6969
log.Error(err, "failed to sort dependencies for revision change")
7070
return nil
7171
}
72-
reqs := make([]reconcile.Request, len(sorted))
73-
for i := range sorted {
74-
reqs[i].NamespacedName.Name = sorted[i].Name
75-
reqs[i].NamespacedName.Namespace = sorted[i].Namespace
76-
}
7772
return reqs
7873
}
7974
}
@@ -96,3 +91,54 @@ func (r *KustomizationReconciler) indexBy(kind string) func(o client.Object) []s
9691
return nil
9792
}
9893
}
94+
95+
// requestsForConfigDependency enqueues requests for watched ConfigMaps or Secrets
96+
// according to the specified index.
97+
func (r *KustomizationReconciler) requestsForConfigDependency(
98+
index string) func(ctx context.Context, o client.Object) []reconcile.Request {
99+
100+
return func(ctx context.Context, o client.Object) []reconcile.Request {
101+
log := ctrl.LoggerFrom(ctx).WithValues("index", index, "objectRef", map[string]string{
102+
"name": o.GetName(),
103+
"namespace": o.GetNamespace(),
104+
})
105+
106+
// List Kustomizations that have a dependency on the ConfigMap or Secret.
107+
var list kustomizev1.KustomizationList
108+
if err := r.List(ctx, &list, client.MatchingFields{
109+
index: client.ObjectKeyFromObject(o).String(),
110+
}); err != nil {
111+
log.Error(err, "failed to list Kustomizations for config dependency change")
112+
return nil
113+
}
114+
115+
// Sort the Kustomizations by their dependencies to ensure
116+
// that dependent Kustomizations are reconciled after their dependencies.
117+
dd := make([]dependency.Dependent, 0, len(list.Items))
118+
for i := range list.Items {
119+
dd = append(dd, &list.Items[i])
120+
}
121+
122+
// Enqueue requests for each Kustomization in the list.
123+
reqs, err := sortAndEnqueue(dd)
124+
if err != nil {
125+
log.Error(err, "failed to sort dependencies for config dependency change")
126+
return nil
127+
}
128+
return reqs
129+
}
130+
}
131+
132+
// sortAndEnqueue sorts the dependencies and returns a slice of reconcile.Requests.
133+
func sortAndEnqueue(dd []dependency.Dependent) ([]reconcile.Request, error) {
134+
sorted, err := dependency.Sort(dd)
135+
if err != nil {
136+
return nil, err
137+
}
138+
reqs := make([]reconcile.Request, len(sorted))
139+
for i := range sorted {
140+
reqs[i].NamespacedName.Name = sorted[i].Name
141+
reqs[i].NamespacedName.Namespace = sorted[i].Namespace
142+
}
143+
return reqs, nil
144+
}

internal/controller/suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"sigs.k8s.io/controller-runtime/pkg/envtest"
3838
controllerLog "sigs.k8s.io/controller-runtime/pkg/log"
3939
"sigs.k8s.io/controller-runtime/pkg/log/zap"
40+
"sigs.k8s.io/controller-runtime/pkg/predicate"
4041
"sigs.k8s.io/yaml"
4142

4243
"github.com/fluxcd/pkg/apis/meta"
@@ -185,6 +186,7 @@ func TestMain(m *testing.M) {
185186
}
186187
if err := (reconciler).SetupWithManager(ctx, testEnv, KustomizationReconcilerOptions{
187188
DependencyRequeueInterval: 2 * time.Second,
189+
WatchConfigsPredicate: predicate.Not(predicate.Funcs{}),
188190
}); err != nil {
189191
panic(fmt.Sprintf("Failed to start KustomizationReconciler: %v", err))
190192
}

main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ func main() {
162162
os.Exit(1)
163163
}
164164

165+
watchConfigsPredicate, err := runtimeCtrl.GetWatchConfigsPredicate(watchOptions)
166+
if err != nil {
167+
setupLog.Error(err, "unable to configure watch configs label selector for controller")
168+
os.Exit(1)
169+
}
170+
165171
var disableCacheFor []ctrlclient.Object
166172
shouldCache, err := features.Enabled(features.CacheSecretsAndConfigMaps)
167173
if err != nil {
@@ -294,6 +300,7 @@ func main() {
294300
DependencyRequeueInterval: requeueDependency,
295301
HTTPRetry: httpRetry,
296302
RateLimiter: runtimeCtrl.GetRateLimiter(rateLimiterOptions),
303+
WatchConfigsPredicate: watchConfigsPredicate,
297304
}); err != nil {
298305
setupLog.Error(err, "unable to create controller", "controller", controllerName)
299306
os.Exit(1)

0 commit comments

Comments
 (0)