From 5edcf5b394d21364d0892a47b693dad7268b0772 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 22 Jul 2025 12:57:08 +0300 Subject: [PATCH 1/5] api: Add the `readyExpr` field to `dependsOn` Extend the readiness evaluation of dependencies with CEL expressions Signed-off-by: Stefan Prodan --- api/v1/kustomization_types.go | 18 ++++- api/v1/reference_types.go | 26 ++++++- api/v1/zz_generated.deepcopy.go | 17 ++++- ...mize.toolkit.fluxcd.io_kustomizations.yaml | 20 +++-- docs/api/v1/kustomize.md | 73 +++++++++++++++++-- 5 files changed, 136 insertions(+), 18 deletions(-) diff --git a/api/v1/kustomization_types.go b/api/v1/kustomization_types.go index 4854aaa92..0852d7deb 100644 --- a/api/v1/kustomization_types.go +++ b/api/v1/kustomization_types.go @@ -49,11 +49,11 @@ type KustomizationSpec struct { // +optional CommonMetadata *CommonMetadata `json:"commonMetadata,omitempty"` - // DependsOn may contain a meta.NamespacedObjectReference slice + // DependsOn may contain a DependencyReference slice // with references to Kustomization resources that must be ready before this // Kustomization can be reconciled. // +optional - DependsOn []meta.NamespacedObjectReference `json:"dependsOn,omitempty"` + DependsOn []DependencyReference `json:"dependsOn,omitempty"` // Decrypt Kubernetes secrets before applying them on the cluster. // +optional @@ -333,9 +333,19 @@ func (in Kustomization) GetDeletionPolicy() string { return in.Spec.DeletionPolicy } -// GetDependsOn returns the list of dependencies across-namespaces. +// GetDependsOn returns the dependencies as a list of meta.NamespacedObjectReference. +// +// This function makes the Kustomization type conformant with the meta.ObjectWithDependencies interface +// and allows the controller-runtime to index Kustomizations by their dependencies. func (in Kustomization) GetDependsOn() []meta.NamespacedObjectReference { - return in.Spec.DependsOn + deps := make([]meta.NamespacedObjectReference, len(in.Spec.DependsOn)) + for i := range in.Spec.DependsOn { + deps[i] = meta.NamespacedObjectReference{ + Name: in.Spec.DependsOn[i].Name, + Namespace: in.Spec.DependsOn[i].Namespace, + } + } + return deps } // GetConditions returns the status conditions of the object. diff --git a/api/v1/reference_types.go b/api/v1/reference_types.go index cf0d9abd2..3a2412d71 100644 --- a/api/v1/reference_types.go +++ b/api/v1/reference_types.go @@ -16,7 +16,9 @@ limitations under the License. package v1 -import "fmt" +import ( + "fmt" +) // CrossNamespaceSourceReference contains enough information to let you locate the // typed Kubernetes resource object at cluster level. @@ -40,9 +42,31 @@ type CrossNamespaceSourceReference struct { Namespace string `json:"namespace,omitempty"` } +// String returns a string representation of the CrossNamespaceSourceReference +// in the format "Kind/Name" or "Kind/Namespace/Name" if Namespace is set. func (s *CrossNamespaceSourceReference) String() string { if s.Namespace != "" { return fmt.Sprintf("%s/%s/%s", s.Kind, s.Namespace, s.Name) } return fmt.Sprintf("%s/%s", s.Kind, s.Name) } + +// DependencyReference defines a Kustomization dependency on another Kustomization resource. +type DependencyReference struct { + // Name of the referent. + // +required + Name string `json:"name"` + + // Namespace of the referent, defaults to the namespace of the Kustomization + // resource object that contains the reference. + // +optional + Namespace string `json:"namespace,omitempty"` + + // ReadyExpr is a CEL expression that can be used to assess the readiness + // of a dependency. When specified, the built-in readiness check + // is replaced by the logic defined in the CEL expression. + // To make the CEL expression additive to the built-in readiness check, + // the feature gate `AdditiveCELDependencyCheck` must be set to `true`. + // +optional + ReadyExpr string `json:"readyExpr,omitempty"` +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 8164afba3..fe4bbf878 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -91,6 +91,21 @@ func (in *Decryption) DeepCopy() *Decryption { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DependencyReference) DeepCopyInto(out *DependencyReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyReference. +func (in *DependencyReference) DeepCopy() *DependencyReference { + if in == nil { + return nil + } + out := new(DependencyReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Kustomization) DeepCopyInto(out *Kustomization) { *out = *in @@ -160,7 +175,7 @@ func (in *KustomizationSpec) DeepCopyInto(out *KustomizationSpec) { } if in.DependsOn != nil { in, out := &in.DependsOn, &out.DependsOn - *out = make([]meta.NamespacedObjectReference, len(*in)) + *out = make([]DependencyReference, len(*in)) copy(*out, *in) } if in.Decryption != nil { diff --git a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml index e38c8f5c1..850627fdb 100644 --- a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml +++ b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml @@ -123,20 +123,28 @@ spec: type: string dependsOn: description: |- - DependsOn may contain a meta.NamespacedObjectReference slice + DependsOn may contain a DependencyReference slice with references to Kustomization resources that must be ready before this Kustomization can be reconciled. items: - description: |- - NamespacedObjectReference contains enough information to locate the referenced Kubernetes resource object in any - namespace. + description: DependencyReference defines a Kustomization dependency + on another Kustomization resource. properties: name: description: Name of the referent. type: string namespace: - description: Namespace of the referent, when not specified it - acts as LocalObjectReference. + description: |- + Namespace of the referent, defaults to the namespace of the Kustomization + resource object that contains the reference. + type: string + readyExpr: + description: |- + ReadyExpr is a CEL expression that can be used to assess the readiness + of a dependency. When specified, the built-in readiness check + is replaced by the logic defined in the CEL expression. + To make the CEL expression additive to the built-in readiness check, + the feature gate `AdditiveCELDependencyCheck` must be set to `true`. type: string required: - name diff --git a/docs/api/v1/kustomize.md b/docs/api/v1/kustomize.md index a109c61e5..70ae86119 100644 --- a/docs/api/v1/kustomize.md +++ b/docs/api/v1/kustomize.md @@ -89,14 +89,14 @@ overridden if its key matches a common one.

dependsOn
- -[]github.com/fluxcd/pkg/apis/meta.NamespacedObjectReference + +[]DependencyReference (Optional) -

DependsOn may contain a meta.NamespacedObjectReference slice +

DependsOn may contain a DependencyReference slice with references to Kustomization resources that must be ready before this Kustomization can be reconciled.

@@ -609,6 +609,67 @@ field.

+

DependencyReference +

+

+(Appears on: +KustomizationSpec) +

+

DependencyReference defines a Kustomization dependency on another Kustomization resource.

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the referent.

+
+namespace
+ +string + +
+(Optional) +

Namespace of the referent, defaults to the namespace of the Kustomization +resource object that contains the reference.

+
+readyExpr
+ +string + +
+(Optional) +

ReadyExpr is a CEL expression that can be used to assess the readiness +of a dependency. When specified, the built-in readiness check +is replaced by the logic defined in the CEL expression. +To make the CEL expression additive to the built-in readiness check, +the feature gate AdditiveCELDependencyCheck must be set to true.

+
+
+

KustomizationSpec

@@ -647,14 +708,14 @@ overridden if its key matches a common one.

dependsOn
- -[]github.com/fluxcd/pkg/apis/meta.NamespacedObjectReference + +[]DependencyReference (Optional) -

DependsOn may contain a meta.NamespacedObjectReference slice +

DependsOn may contain a DependencyReference slice with references to Kustomization resources that must be ready before this Kustomization can be reconciled.

From c2754dd5dedae6470befd44bfd99158746ce65ba Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 22 Jul 2025 12:57:52 +0300 Subject: [PATCH 2/5] controller: Add `AdditiveCELDependencyCheck` feature gate Signed-off-by: Stefan Prodan --- internal/features/features.go | 12 ++++++++-- main.go | 43 ++++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/internal/features/features.go b/internal/features/features.go index b70ce1ccf..c8adfcd19 100644 --- a/internal/features/features.go +++ b/internal/features/features.go @@ -48,9 +48,14 @@ const ( // but is missing from the input vars. StrictPostBuildSubstitutions = "StrictPostBuildSubstitutions" - // GroupChangelog controls groups kubernetes objects names on log output - // reduces cardinality of logs when logging to elasticsearch + // GroupChangeLog controls whether to group Kubernetes objects names in log output + // to reduce cardinality of logs. GroupChangeLog = "GroupChangeLog" + + // AdditiveCELDependencyCheck controls whether the CEL dependency check + // should be additive, meaning that the built-in readiness check will + // be added to the user-defined CEL expressions. + AdditiveCELDependencyCheck = "AdditiveCELDependencyCheck" ) var features = map[string]bool{ @@ -69,6 +74,9 @@ var features = map[string]bool{ // GroupChangeLog // opt-in from v1.5 GroupChangeLog: false, + // AdditiveCELDependencyCheck + // opt-in from v1.7 + AdditiveCELDependencyCheck: false, } func init() { diff --git a/main.go b/main.go index e5b55fea5..0c56d9e9f 100644 --- a/main.go +++ b/main.go @@ -264,6 +264,12 @@ func main() { os.Exit(1) } + additiveCELDependencyCheck, err := features.Enabled(features.AdditiveCELDependencyCheck) + if err != nil { + setupLog.Error(err, "unable to check feature gate "+features.AdditiveCELDependencyCheck) + os.Exit(1) + } + var tokenCache *pkgcache.TokenCache if tokenCacheOptions.MaxSize > 0 { var err error @@ -278,24 +284,25 @@ func main() { } if err = (&controller.KustomizationReconciler{ - ControllerName: controllerName, - DefaultServiceAccount: defaultServiceAccount, - SOPSAgeSecret: sopsAgeSecret, - Client: mgr.GetClient(), - Mapper: restMapper, - APIReader: mgr.GetAPIReader(), - Metrics: metricsH, - EventRecorder: eventRecorder, - NoCrossNamespaceRefs: aclOptions.NoCrossNamespaceRefs, - NoRemoteBases: noRemoteBases, - FailFast: failFast, - ConcurrentSSA: concurrentSSA, - KubeConfigOpts: kubeConfigOpts, - ClusterReader: clusterReader, - DisallowedFieldManagers: disallowedFieldManagers, - StrictSubstitutions: strictSubstitutions, - GroupChangeLog: groupChangeLog, - TokenCache: tokenCache, + ControllerName: controllerName, + DefaultServiceAccount: defaultServiceAccount, + SOPSAgeSecret: sopsAgeSecret, + Client: mgr.GetClient(), + Mapper: restMapper, + APIReader: mgr.GetAPIReader(), + Metrics: metricsH, + EventRecorder: eventRecorder, + NoCrossNamespaceRefs: aclOptions.NoCrossNamespaceRefs, + NoRemoteBases: noRemoteBases, + FailFast: failFast, + ConcurrentSSA: concurrentSSA, + KubeConfigOpts: kubeConfigOpts, + ClusterReader: clusterReader, + DisallowedFieldManagers: disallowedFieldManagers, + StrictSubstitutions: strictSubstitutions, + GroupChangeLog: groupChangeLog, + AdditiveCELDependencyCheck: additiveCELDependencyCheck, + TokenCache: tokenCache, }).SetupWithManager(ctx, mgr, controller.KustomizationReconcilerOptions{ DependencyRequeueInterval: requeueDependency, HTTPRetry: httpRetry, From e0e6e22272b6859ff00285324e01007fb6671d2e Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 22 Jul 2025 12:59:16 +0300 Subject: [PATCH 3/5] controller: Implement CEL evaluation for dependency checks Signed-off-by: Stefan Prodan --- go.mod | 4 +- go.sum | 4 +- internal/controller/constants.go | 5 +- .../controller/kustomization_controller.go | 168 ++++++++++---- .../kustomization_dependson_test.go | 215 +++++++++++++++++- 5 files changed, 337 insertions(+), 59 deletions(-) diff --git a/go.mod b/go.mod index ebd0e776a..e706705ae 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/fluxcd/pkg/testserver v0.11.0 github.com/fluxcd/source-controller/api v1.6.0 github.com/getsops/sops/v3 v3.10.2 + github.com/google/cel-go v0.23.2 github.com/hashicorp/vault/api v1.20.0 github.com/onsi/gomega v1.37.0 github.com/opencontainers/go-digest v1.0.0 @@ -147,11 +148,10 @@ require ( github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.23.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-containerregistry v0.20.6 // indirect diff --git a/go.sum b/go.sum index 4b3f4d5c3..250939bd0 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= diff --git a/internal/controller/constants.go b/internal/controller/constants.go index ed9a9866f..16041c983 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -16,4 +16,7 @@ limitations under the License. package controller -const OCIArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision" +const ( + OCIArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision" + TerminalErrorMessage = "Reconciliation failed terminally due to configuration error" +) diff --git a/internal/controller/kustomization_controller.go b/internal/controller/kustomization_controller.go index cdba07e45..5f56f048c 100644 --- a/internal/controller/kustomization_controller.go +++ b/internal/controller/kustomization_controller.go @@ -26,6 +26,7 @@ import ( "time" securejoin "github.com/cyphar/filepath-securejoin" + celtypes "github.com/google/cel-go/common/types" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -96,22 +97,23 @@ type KustomizationReconciler struct { artifactFetchRetries int requeueDependency time.Duration - Mapper apimeta.RESTMapper - APIReader client.Reader - ClusterReader engine.ClusterReaderFactory - ControllerName string - statusManager string - NoCrossNamespaceRefs bool - NoRemoteBases bool - FailFast bool - DefaultServiceAccount string - SOPSAgeSecret string - KubeConfigOpts runtimeClient.KubeConfigOptions - ConcurrentSSA int - DisallowedFieldManagers []string - StrictSubstitutions bool - GroupChangeLog bool - TokenCache *cache.TokenCache + Mapper apimeta.RESTMapper + APIReader client.Reader + ClusterReader engine.ClusterReaderFactory + ControllerName string + statusManager string + NoCrossNamespaceRefs bool + NoRemoteBases bool + FailFast bool + DefaultServiceAccount string + SOPSAgeSecret string + KubeConfigOpts runtimeClient.KubeConfigOptions + ConcurrentSSA int + DisallowedFieldManagers []string + StrictSubstitutions bool + GroupChangeLog bool + AdditiveCELDependencyCheck bool + TokenCache *cache.TokenCache } // KustomizationReconcilerOptions contains options for the KustomizationReconciler. @@ -298,14 +300,12 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Configure custom health checks. statusReaders, err := cel.PollerWithCustomHealthChecks(ctx, obj.Spec.HealthCheckExprs) if err != nil { - const msg = "Reconciliation failed terminally due to configuration error" - errMsg := fmt.Sprintf("%s: %v", msg, err) + errMsg := fmt.Sprintf("%s: %v", TerminalErrorMessage, err) conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidCELExpressionReason, "%s", errMsg) conditions.MarkStalled(obj, meta.InvalidCELExpressionReason, "%s", errMsg) obj.Status.ObservedGeneration = obj.Generation - log.Error(err, msg) r.event(obj, "", "", eventv1.EventSeverityError, errMsg, nil) - return ctrl.Result{}, nil + return ctrl.Result{}, reconcile.TerminalError(err) } // Check object-level workload identity feature gate and decryption with service account. @@ -355,6 +355,17 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Check dependencies and requeue the reconciliation if the check fails. if len(obj.Spec.DependsOn) > 0 { if err := r.checkDependencies(ctx, obj, artifactSource); err != nil { + // Check if this is a terminal error that should not trigger retries + if errors.Is(err, reconcile.TerminalError(nil)) { + errMsg := fmt.Sprintf("%s: %v", TerminalErrorMessage, err) + conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidCELExpressionReason, "%s", errMsg) + conditions.MarkStalled(obj, meta.InvalidCELExpressionReason, "%s", errMsg) + obj.Status.ObservedGeneration = obj.Generation + r.event(obj, revision, originRevision, eventv1.EventSeverityError, errMsg, nil) + return ctrl.Result{}, err + } + + // Retry on transient errors. conditions.MarkFalse(obj, meta.ReadyCondition, meta.DependencyNotReadyReason, "%s", err) msg := fmt.Sprintf("Dependencies do not meet ready condition, retrying in %s", r.requeueDependency.String()) log.Info(msg) @@ -595,51 +606,120 @@ func (r *KustomizationReconciler) reconcile( return nil } +// checkDependencies checks if the dependencies of the current Kustomization are ready. +// To be considered ready, a dependencies must meet the following criteria: +// - The dependency exists in the API server. +// - The CEL expression (if provided) must evaluate to true. +// - The dependency observed generation must match the current generation. +// - The dependency Ready condition must be true. +// - The dependency last applied revision must match the current source artifact revision. func (r *KustomizationReconciler) checkDependencies(ctx context.Context, obj *kustomizev1.Kustomization, source sourcev1.Source) error { - for _, d := range obj.Spec.DependsOn { - if d.Namespace == "" { - d.Namespace = obj.GetNamespace() + + // Convert the Kustomization object to Unstructured for CEL evaluation. + objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return fmt.Errorf("failed to convert Kustomization to unstructured: %w", err) + } + + for _, depRef := range obj.Spec.DependsOn { + // Check if the dependency exists by querying + // the API server bypassing the cache. + if depRef.Namespace == "" { + depRef.Namespace = obj.GetNamespace() } - dName := types.NamespacedName{ - Namespace: d.Namespace, - Name: d.Name, + depName := types.NamespacedName{ + Namespace: depRef.Namespace, + Name: depRef.Name, } - var k kustomizev1.Kustomization - err := r.APIReader.Get(ctx, dName, &k) + var dep kustomizev1.Kustomization + err := r.APIReader.Get(ctx, depName, &dep) if err != nil { - return fmt.Errorf("dependency '%s' not found: %w", dName, err) + return fmt.Errorf("dependency '%s' not found: %w", depName, err) } - if len(k.Status.Conditions) == 0 || k.Generation != k.Status.ObservedGeneration { - return fmt.Errorf("dependency '%s' is not ready", dName) + // Evaluate the CEL expression (if specified) to determine if the dependency is ready. + if depRef.ReadyExpr != "" { + ready, err := r.evalReadyExpr(ctx, depRef.ReadyExpr, objMap, &dep) + if err != nil { + return err + } + if !ready { + return fmt.Errorf("dependency '%s' is not ready according to readyExpr eval", depName) + } } - if !apimeta.IsStatusConditionTrue(k.Status.Conditions, meta.ReadyCondition) { - return fmt.Errorf("dependency '%s' is not ready", dName) + // Skip the built-in readiness check if the CEL expression is provided + // and the AdditiveCELDependencyCheck feature gate is not enabled. + if depRef.ReadyExpr != "" && !r.AdditiveCELDependencyCheck { + continue } - srcNamespace := k.Spec.SourceRef.Namespace - if srcNamespace == "" { - srcNamespace = k.GetNamespace() + // Check if the dependency observed generation is up to date + // and if the dependency is in a ready state. + if len(dep.Status.Conditions) == 0 || dep.Generation != dep.Status.ObservedGeneration { + return fmt.Errorf("dependency '%s' is not ready", depName) } - dSrcNamespace := obj.Spec.SourceRef.Namespace - if dSrcNamespace == "" { - dSrcNamespace = obj.GetNamespace() + if !apimeta.IsStatusConditionTrue(dep.Status.Conditions, meta.ReadyCondition) { + return fmt.Errorf("dependency '%s' is not ready", depName) } - if k.Spec.SourceRef.Name == obj.Spec.SourceRef.Name && - srcNamespace == dSrcNamespace && - k.Spec.SourceRef.Kind == obj.Spec.SourceRef.Kind && - !source.GetArtifact().HasRevision(k.Status.LastAppliedRevision) { - return fmt.Errorf("dependency '%s' revision is not up to date", dName) + // Check if the dependency source matches the current source + // and if so, verify that the last applied revision of the dependency + // matches the current source artifact revision. + srcNamespace := dep.Spec.SourceRef.Namespace + if srcNamespace == "" { + srcNamespace = dep.GetNamespace() + } + depSrcNamespace := obj.Spec.SourceRef.Namespace + if depSrcNamespace == "" { + depSrcNamespace = obj.GetNamespace() + } + if dep.Spec.SourceRef.Name == obj.Spec.SourceRef.Name && + srcNamespace == depSrcNamespace && + dep.Spec.SourceRef.Kind == obj.Spec.SourceRef.Kind && + !source.GetArtifact().HasRevision(dep.Status.LastAppliedRevision) { + return fmt.Errorf("dependency '%s' revision is not up to date", depName) } } return nil } +// evalReadyExpr evaluates the CEL expression for the dependency readiness check. +func (r *KustomizationReconciler) evalReadyExpr( + ctx context.Context, + expr string, + selfMap map[string]any, + dep *kustomizev1.Kustomization, +) (bool, error) { + const ( + selfName = "self" + depName = "dep" + ) + + celExpr, err := cel.NewExpression(expr, + cel.WithCompile(), + cel.WithOutputType(celtypes.BoolType), + cel.WithStructVariables(selfName, depName)) + if err != nil { + return false, reconcile.TerminalError(fmt.Errorf("failed to evaluate dependency %s: %w", dep.Name, err)) + } + + depMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep) + if err != nil { + return false, fmt.Errorf("failed to convert %s object to map: %w", depName, err) + } + + vars := map[string]any{ + selfName: selfMap, + depName: depMap, + } + + return celExpr.EvaluateBoolean(ctx, vars) +} + func (r *KustomizationReconciler) getSource(ctx context.Context, obj *kustomizev1.Kustomization) (sourcev1.Source, error) { var src sourcev1.Source diff --git a/internal/controller/kustomization_dependson_test.go b/internal/controller/kustomization_dependson_test.go index aceb8a576..14eac8032 100644 --- a/internal/controller/kustomization_dependson_test.go +++ b/internal/controller/kustomization_dependson_test.go @@ -19,14 +19,16 @@ package controller import ( "context" "fmt" + "strings" "testing" "time" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/testserver" sourcev1 "github.com/fluxcd/source-controller/api/v1" . "github.com/onsi/gomega" - apimeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -117,7 +119,7 @@ spec: Namespace: kustomizationKey.Namespace, }, Spec: kustomizev1.KustomizationSpec{ - Interval: metav1.Duration{Duration: reconciliationInterval}, + Interval: metav1.Duration{Duration: time.Hour}, Path: "./", KubeConfig: &meta.KubeConfigReference{ SecretRef: &meta.SecretKeyReference{ @@ -140,15 +142,14 @@ spec: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) - return apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) != nil + return conditions.Has(resultK, meta.ReadyCondition) }, timeout, time.Second).Should(BeTrue()) t.Run("fails due to source not found", func(t *testing.T) { g := NewWithT(t) g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) - ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return ready.Reason == meta.ArtifactFailedReason + return conditions.HasAnyReason(resultK, meta.ReadyCondition, meta.ArtifactFailedReason) }, timeout, time.Second).Should(BeTrue()) }) @@ -159,8 +160,7 @@ spec: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) - ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return ready.Reason == meta.ReconciliationSucceededReason + return conditions.IsReady(resultK) }, timeout, time.Second).Should(BeTrue()) }) @@ -168,7 +168,7 @@ spec: g := NewWithT(t) g.Eventually(func() error { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) - resultK.Spec.DependsOn = []meta.NamespacedObjectReference{ + resultK.Spec.DependsOn = []kustomizev1.DependencyReference{ { Namespace: id, Name: "root", @@ -179,8 +179,203 @@ spec: g.Eventually(func() bool { _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) - ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition) - return ready.Reason == meta.DependencyNotReadyReason + return conditions.HasAnyReason(resultK, meta.ReadyCondition, meta.DependencyNotReadyReason) + }, timeout, time.Second).Should(BeTrue()) + }) +} + +func TestKustomizationReconciler_DependsOn_CEL(t *testing.T) { + g := NewWithT(t) + id := "dep-cel" + randStringRunes(5) + depID := "test-dep-" + randStringRunes(5) + revision := "v1.0.0" + + err := createNamespace(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + err = createKubeConfigSecret(id) + g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret") + + manifests := func(name string, data string) []testserver.File { + return []testserver.File{ + { + Name: "config.yaml", + Body: fmt.Sprintf(`--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: %[1]s +data: + key: "%[2]s" +`, name, data), + }, + } + } + + artifact, err := testServer.ArtifactFromFiles(manifests(id, id)) + g.Expect(err).NotTo(HaveOccurred()) + + repositoryName := types.NamespacedName{ + Name: fmt.Sprintf("dep-%s", randStringRunes(5)), + Namespace: id, + } + err = applyGitRepository(repositoryName, artifact, revision) + g.Expect(err).NotTo(HaveOccurred()) + + kustomizationKey := types.NamespacedName{ + Name: fmt.Sprintf("dep-%s", randStringRunes(5)), + Namespace: id, + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: kustomizationKey.Name, + Namespace: kustomizationKey.Namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: time.Hour}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: &meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + TargetNamespace: id, + Prune: true, + }, + } + + g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed()) + + resultK := &kustomizev1.Kustomization{} + + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.Has(resultK, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + + t.Run("succeeds with readyExpr dependency check", func(t *testing.T) { + g := NewWithT(t) + + // Create a dependency Kustomization with matching annotations + dependency := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: depID, + Namespace: id, + Annotations: map[string]string{ + "app/version": "v1.2.3", + }, + }, + Spec: kustomizev1.KustomizationSpec{ + Suspend: true, // Suspended dependency should work with readyExpr and AdditiveCELDependencyCheck disabled + Interval: metav1.Duration{Duration: reconciliationInterval}, + Path: "./", + KubeConfig: &meta.KubeConfigReference{ + SecretRef: &meta.SecretKeyReference{ + Name: "kubeconfig", + }, + }, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Name: repositoryName.Name, + Namespace: repositoryName.Namespace, + Kind: sourcev1.GitRepositoryKind, + }, + Prune: true, + }, + } + + g.Expect(k8sClient.Create(context.Background(), dependency)).To(Succeed()) + + // Update the main Kustomization with matching annotations and readyExpr + g.Eventually(func() error { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + resultK.ObjectMeta.Annotations = map[string]string{ + "app/version": "v1.2.3", + } + resultK.Spec.DependsOn = []kustomizev1.DependencyReference{ + { + Name: dependency.Name, + ReadyExpr: `self.metadata.annotations['app/version'] == dep.metadata.annotations['app/version']`, + }, + } + return k8sClient.Update(context.Background(), resultK) + }, timeout, time.Second).Should(BeNil()) + + // Should succeed because CEL expression evaluates to true + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.IsReady(resultK) + }, timeout, time.Second).Should(BeTrue()) + }) + + t.Run("fails with readyExpr when condition not met", func(t *testing.T) { + g := NewWithT(t) + + // Update the main kustomization with mismatched annotations + g.Eventually(func() error { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + resultK.ObjectMeta.Annotations = map[string]string{ + "app/version": "v1.2.4", + } + resultK.Spec.DependsOn = []kustomizev1.DependencyReference{ + { + Namespace: id, + Name: depID, + ReadyExpr: `self.metadata.annotations['app/version'] == dep.metadata.annotations['app/version']`, + }, + } + return k8sClient.Update(context.Background(), resultK) + }, timeout, time.Second).Should(BeNil()) + + // Should fail because CEL expression evaluates to false + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + ready := conditions.Get(resultK, meta.ReadyCondition) + return ready.Reason == meta.DependencyNotReadyReason && + strings.Contains(ready.Message, "not ready according to readyExpr") + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.IsStalled(resultK)).Should(BeFalse()) + }) + + t.Run("fails terminally with invalid readyExpr", func(t *testing.T) { + g := NewWithT(t) + + // Update the main kustomization with invalid CEL expression + g.Eventually(func() error { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + resultK.Spec.DependsOn = []kustomizev1.DependencyReference{ + { + Name: depID, + ReadyExpr: `self.generation == deps.generation`, // Invalid vars + }, + } + return k8sClient.Update(context.Background(), resultK) + }, timeout, time.Second).Should(BeNil()) + + // Should be marked as stalled because CEL expression is invalid + g.Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return conditions.IsStalled(resultK) + }, timeout, time.Second).Should(BeTrue()) + + g.Expect(conditions.IsReady(resultK)).Should(BeFalse()) + g.Expect(conditions.GetReason(resultK, meta.ReadyCondition)).Should(BeIdenticalTo(meta.InvalidCELExpressionReason)) + g.Expect(conditions.GetMessage(resultK, meta.ReadyCondition)).Should(ContainSubstring("failed to parse")) + }) + + t.Run("GC works with failing dependency", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed()) + + g.Eventually(func() bool { + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK) + return errors.IsNotFound(err) }, timeout, time.Second).Should(BeTrue()) }) } From d17e5d25144d16caf59d4c7b74f5174e3bbaa9ca Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 22 Jul 2025 13:00:53 +0300 Subject: [PATCH 4/5] docs: Add dependency ready expression to API docs Signed-off-by: Stefan Prodan --- docs/spec/v1/kustomizations.md | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/spec/v1/kustomizations.md b/docs/spec/v1/kustomizations.md index af3da7826..28eee557c 100644 --- a/docs/spec/v1/kustomizations.md +++ b/docs/spec/v1/kustomizations.md @@ -487,6 +487,51 @@ is running before deploying applications inside the mesh. **Note:** Circular dependencies between Kustomizations must be avoided, otherwise the interdependent Kustomizations will never be applied on the cluster. +#### Dependency Ready Expression + +`.spec.dependsOn[].readyExpr` is an optional field that can be used to define a CEL expression +to determine the readiness of a Kustomization dependency. + +This is helpful for when custom logic is needed to determine if a dependency is ready. +For example, when performing a lockstep upgrade, the `readyExpr` can be used to +verify that a dependency has a matching version label before proceeding with the +reconciliation of the dependent Kustomization. + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app-backend + namespace: apps + labels: + app/version: v1.2.3 +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: app-frontend + namespace: apps + labels: + app/version: v1.2.3 +spec: + dependsOn: + - name: app-backend + readyExpr: > + dep.metadata.labels['app/version'] == self.metadata.labels['app/version'] && + dep.status.conditions.filter(e, e.type == 'Ready').all(e, e.status == 'True') && + dep.metadata.generation == dep.status.observedGeneration +``` + +The CEL expression contains the following variables: + +- `dep`: The dependency Kustomization object being evaluated. +- `self`: The Kustomization object being reconciled. + +**Note:** When `readyExpr` is specified, the built-in readiness check is replaced by the logic +defined in the CEL expression. You can configure the controller to run both the CEL expression +evaluation and the built-in readiness check, with the `AdditiveCELDependencyCheck` +[feature gate](https://fluxcd.io/flux/components/kustomize/options/#feature-gates). + ### Service Account reference `.spec.serviceAccountName` is an optional field used to specify the From fd63b520d584a07651aa2ce8e827a3d361860a03 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 22 Jul 2025 20:43:14 +0300 Subject: [PATCH 5/5] controller: Move manager to a dedicated file Signed-off-by: Stefan Prodan --- .../controller/kustomization_controller.go | 127 -------------- internal/controller/kustomization_manager.go | 161 ++++++++++++++++++ 2 files changed, 161 insertions(+), 127 deletions(-) create mode 100644 internal/controller/kustomization_manager.go diff --git a/internal/controller/kustomization_controller.go b/internal/controller/kustomization_controller.go index 5f56f048c..1c95c4b25 100644 --- a/internal/controller/kustomization_controller.go +++ b/internal/controller/kustomization_controller.go @@ -36,14 +36,9 @@ import ( "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" kuberecorder "k8s.io/client-go/tools/record" - "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/fluxcd/cli-utils/pkg/kstatus/polling" @@ -64,7 +59,6 @@ import ( runtimeCtrl "github.com/fluxcd/pkg/runtime/controller" "github.com/fluxcd/pkg/runtime/jitter" "github.com/fluxcd/pkg/runtime/patch" - "github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/statusreaders" "github.com/fluxcd/pkg/ssa" "github.com/fluxcd/pkg/ssa/normalize" @@ -116,127 +110,6 @@ type KustomizationReconciler struct { TokenCache *cache.TokenCache } -// KustomizationReconcilerOptions contains options for the KustomizationReconciler. -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 ( - 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{}, indexOCIRepository, - r.indexBy(sourcev1.OCIRepositoryKind)); err != nil { - 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{}, indexGitRepository, - r.indexBy(sourcev1.GitRepositoryKind)); err != nil { - 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{}, indexBucket, - r.indexBy(sourcev1.BucketKind)); err != nil { - 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 - r.statusManager = fmt.Sprintf("gotk-%s", r.ControllerName) - r.artifactFetchRetries = opts.HTTPRetry - - return ctrl.NewControllerManagedBy(mgr). - For(&kustomizev1.Kustomization{}, builder.WithPredicates( - predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}), - )). - Watches( - &sourcev1.OCIRepository{}, - handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexOCIRepository)), - builder.WithPredicates(SourceRevisionChangePredicate{}), - ). - Watches( - &sourcev1.GitRepository{}, - handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexGitRepository)), - builder.WithPredicates(SourceRevisionChangePredicate{}), - ). - Watches( - &sourcev1.Bucket{}, - 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, - }). - Complete(r) -} - func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { log := ctrl.LoggerFrom(ctx) reconcileStart := time.Now() diff --git a/internal/controller/kustomization_manager.go b/internal/controller/kustomization_manager.go new file mode 100644 index 000000000..278f11dd9 --- /dev/null +++ b/internal/controller/kustomization_manager.go @@ -0,0 +1,161 @@ +/* +Copyright 2025 The Flux 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 controller + +import ( + "context" + "fmt" + "time" + + "github.com/fluxcd/pkg/runtime/predicates" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" +) + +// KustomizationReconcilerOptions contains options for the KustomizationReconciler. +type KustomizationReconcilerOptions struct { + HTTPRetry int + DependencyRequeueInterval time.Duration + RateLimiter workqueue.TypedRateLimiter[reconcile.Request] + WatchConfigsPredicate predicate.Predicate +} + +// SetupWithManager sets up the controller with the Manager. +// It indexes the Kustomizations by the source references, and sets up watches for +// changes in those sources, as well as for ConfigMaps and Secrets that the Kustomizations depend on. +func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts KustomizationReconcilerOptions) error { + const ( + 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{}, indexOCIRepository, + r.indexBy(sourcev1.OCIRepositoryKind)); err != nil { + 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{}, indexGitRepository, + r.indexBy(sourcev1.GitRepositoryKind)); err != nil { + 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{}, indexBucket, + r.indexBy(sourcev1.BucketKind)); err != nil { + 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 + r.statusManager = fmt.Sprintf("gotk-%s", r.ControllerName) + r.artifactFetchRetries = opts.HTTPRetry + + return ctrl.NewControllerManagedBy(mgr). + For(&kustomizev1.Kustomization{}, builder.WithPredicates( + predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}), + )). + Watches( + &sourcev1.OCIRepository{}, + handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexOCIRepository)), + builder.WithPredicates(SourceRevisionChangePredicate{}), + ). + Watches( + &sourcev1.GitRepository{}, + handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexGitRepository)), + builder.WithPredicates(SourceRevisionChangePredicate{}), + ). + Watches( + &sourcev1.Bucket{}, + 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, + }). + Complete(r) +}