@@ -26,6 +26,7 @@ import (
2626 "time"
2727
2828 securejoin "github.com/cyphar/filepath-securejoin"
29+ celtypes "github.com/google/cel-go/common/types"
2930 corev1 "k8s.io/api/core/v1"
3031 apierrors "k8s.io/apimachinery/pkg/api/errors"
3132 apimeta "k8s.io/apimachinery/pkg/api/meta"
@@ -96,22 +97,23 @@ type KustomizationReconciler struct {
9697 artifactFetchRetries int
9798 requeueDependency time.Duration
9899
99- Mapper apimeta.RESTMapper
100- APIReader client.Reader
101- ClusterReader engine.ClusterReaderFactory
102- ControllerName string
103- statusManager string
104- NoCrossNamespaceRefs bool
105- NoRemoteBases bool
106- FailFast bool
107- DefaultServiceAccount string
108- SOPSAgeSecret string
109- KubeConfigOpts runtimeClient.KubeConfigOptions
110- ConcurrentSSA int
111- DisallowedFieldManagers []string
112- StrictSubstitutions bool
113- GroupChangeLog bool
114- TokenCache * cache.TokenCache
100+ Mapper apimeta.RESTMapper
101+ APIReader client.Reader
102+ ClusterReader engine.ClusterReaderFactory
103+ ControllerName string
104+ statusManager string
105+ NoCrossNamespaceRefs bool
106+ NoRemoteBases bool
107+ FailFast bool
108+ DefaultServiceAccount string
109+ SOPSAgeSecret string
110+ KubeConfigOpts runtimeClient.KubeConfigOptions
111+ ConcurrentSSA int
112+ DisallowedFieldManagers []string
113+ StrictSubstitutions bool
114+ GroupChangeLog bool
115+ AdditiveCELDependencyCheck bool
116+ TokenCache * cache.TokenCache
115117}
116118
117119// KustomizationReconcilerOptions contains options for the KustomizationReconciler.
@@ -298,14 +300,12 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
298300 // Configure custom health checks.
299301 statusReaders , err := cel .PollerWithCustomHealthChecks (ctx , obj .Spec .HealthCheckExprs )
300302 if err != nil {
301- const msg = "Reconciliation failed terminally due to configuration error"
302- errMsg := fmt .Sprintf ("%s: %v" , msg , err )
303+ errMsg := fmt .Sprintf ("%s: %v" , TerminalErrorMessage , err )
303304 conditions .MarkFalse (obj , meta .ReadyCondition , meta .InvalidCELExpressionReason , "%s" , errMsg )
304305 conditions .MarkStalled (obj , meta .InvalidCELExpressionReason , "%s" , errMsg )
305306 obj .Status .ObservedGeneration = obj .Generation
306- log .Error (err , msg )
307307 r .event (obj , "" , "" , eventv1 .EventSeverityError , errMsg , nil )
308- return ctrl.Result {}, nil
308+ return ctrl.Result {}, reconcile . TerminalError ( err )
309309 }
310310
311311 // 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
355355 // Check dependencies and requeue the reconciliation if the check fails.
356356 if len (obj .Spec .DependsOn ) > 0 {
357357 if err := r .checkDependencies (ctx , obj , artifactSource ); err != nil {
358+ // Check if this is a terminal error that should not trigger retries
359+ if errors .Is (err , reconcile .TerminalError (nil )) {
360+ errMsg := fmt .Sprintf ("%s: %v" , TerminalErrorMessage , err )
361+ conditions .MarkFalse (obj , meta .ReadyCondition , meta .InvalidCELExpressionReason , "%s" , errMsg )
362+ conditions .MarkStalled (obj , meta .InvalidCELExpressionReason , "%s" , errMsg )
363+ obj .Status .ObservedGeneration = obj .Generation
364+ r .event (obj , revision , originRevision , eventv1 .EventSeverityError , errMsg , nil )
365+ return ctrl.Result {}, err
366+ }
367+
368+ // Retry on transient errors.
358369 conditions .MarkFalse (obj , meta .ReadyCondition , meta .DependencyNotReadyReason , "%s" , err )
359370 msg := fmt .Sprintf ("Dependencies do not meet ready condition, retrying in %s" , r .requeueDependency .String ())
360371 log .Info (msg )
@@ -595,51 +606,120 @@ func (r *KustomizationReconciler) reconcile(
595606 return nil
596607}
597608
609+ // checkDependencies checks if the dependencies of the current Kustomization are ready.
610+ // To be considered ready, a dependencies must meet the following criteria:
611+ // - The dependency exists in the API server.
612+ // - The CEL expression (if provided) must evaluate to true.
613+ // - The dependency observed generation must match the current generation.
614+ // - The dependency Ready condition must be true.
615+ // - The dependency last applied revision must match the current source artifact revision.
598616func (r * KustomizationReconciler ) checkDependencies (ctx context.Context ,
599617 obj * kustomizev1.Kustomization ,
600618 source sourcev1.Source ) error {
601- for _ , d := range obj .Spec .DependsOn {
602- if d .Namespace == "" {
603- d .Namespace = obj .GetNamespace ()
619+
620+ // Convert the Kustomization object to Unstructured for CEL evaluation.
621+ objMap , err := runtime .DefaultUnstructuredConverter .ToUnstructured (obj )
622+ if err != nil {
623+ return fmt .Errorf ("failed to convert Kustomization to unstructured: %w" , err )
624+ }
625+
626+ for _ , depRef := range obj .Spec .DependsOn {
627+ // Check if the dependency exists by querying
628+ // the API server bypassing the cache.
629+ if depRef .Namespace == "" {
630+ depRef .Namespace = obj .GetNamespace ()
604631 }
605- dName := types.NamespacedName {
606- Namespace : d .Namespace ,
607- Name : d .Name ,
632+ depName := types.NamespacedName {
633+ Namespace : depRef .Namespace ,
634+ Name : depRef .Name ,
608635 }
609- var k kustomizev1.Kustomization
610- err := r .APIReader .Get (ctx , dName , & k )
636+ var dep kustomizev1.Kustomization
637+ err := r .APIReader .Get (ctx , depName , & dep )
611638 if err != nil {
612- return fmt .Errorf ("dependency '%s' not found: %w" , dName , err )
639+ return fmt .Errorf ("dependency '%s' not found: %w" , depName , err )
613640 }
614641
615- if len (k .Status .Conditions ) == 0 || k .Generation != k .Status .ObservedGeneration {
616- return fmt .Errorf ("dependency '%s' is not ready" , dName )
642+ // Evaluate the CEL expression (if specified) to determine if the dependency is ready.
643+ if depRef .ReadyExpr != "" {
644+ ready , err := r .evalReadyExpr (ctx , depRef .ReadyExpr , objMap , & dep )
645+ if err != nil {
646+ return err
647+ }
648+ if ! ready {
649+ return fmt .Errorf ("dependency '%s' is not ready according to readyExpr eval" , depName )
650+ }
617651 }
618652
619- if ! apimeta .IsStatusConditionTrue (k .Status .Conditions , meta .ReadyCondition ) {
620- return fmt .Errorf ("dependency '%s' is not ready" , dName )
653+ // Skip the built-in readiness check if the CEL expression is provided
654+ // and the AdditiveCELDependencyCheck feature gate is not enabled.
655+ if depRef .ReadyExpr != "" && ! r .AdditiveCELDependencyCheck {
656+ continue
621657 }
622658
623- srcNamespace := k .Spec .SourceRef .Namespace
624- if srcNamespace == "" {
625- srcNamespace = k .GetNamespace ()
659+ // Check if the dependency observed generation is up to date
660+ // and if the dependency is in a ready state.
661+ if len (dep .Status .Conditions ) == 0 || dep .Generation != dep .Status .ObservedGeneration {
662+ return fmt .Errorf ("dependency '%s' is not ready" , depName )
626663 }
627- dSrcNamespace := obj .Spec .SourceRef .Namespace
628- if dSrcNamespace == "" {
629- dSrcNamespace = obj .GetNamespace ()
664+ if ! apimeta .IsStatusConditionTrue (dep .Status .Conditions , meta .ReadyCondition ) {
665+ return fmt .Errorf ("dependency '%s' is not ready" , depName )
630666 }
631667
632- if k .Spec .SourceRef .Name == obj .Spec .SourceRef .Name &&
633- srcNamespace == dSrcNamespace &&
634- k .Spec .SourceRef .Kind == obj .Spec .SourceRef .Kind &&
635- ! source .GetArtifact ().HasRevision (k .Status .LastAppliedRevision ) {
636- return fmt .Errorf ("dependency '%s' revision is not up to date" , dName )
668+ // Check if the dependency source matches the current source
669+ // and if so, verify that the last applied revision of the dependency
670+ // matches the current source artifact revision.
671+ srcNamespace := dep .Spec .SourceRef .Namespace
672+ if srcNamespace == "" {
673+ srcNamespace = dep .GetNamespace ()
674+ }
675+ depSrcNamespace := obj .Spec .SourceRef .Namespace
676+ if depSrcNamespace == "" {
677+ depSrcNamespace = obj .GetNamespace ()
678+ }
679+ if dep .Spec .SourceRef .Name == obj .Spec .SourceRef .Name &&
680+ srcNamespace == depSrcNamespace &&
681+ dep .Spec .SourceRef .Kind == obj .Spec .SourceRef .Kind &&
682+ ! source .GetArtifact ().HasRevision (dep .Status .LastAppliedRevision ) {
683+ return fmt .Errorf ("dependency '%s' revision is not up to date" , depName )
637684 }
638685 }
639686
640687 return nil
641688}
642689
690+ // evalReadyExpr evaluates the CEL expression for the dependency readiness check.
691+ func (r * KustomizationReconciler ) evalReadyExpr (
692+ ctx context.Context ,
693+ expr string ,
694+ selfMap map [string ]any ,
695+ dep * kustomizev1.Kustomization ,
696+ ) (bool , error ) {
697+ const (
698+ selfName = "self"
699+ depName = "dep"
700+ )
701+
702+ celExpr , err := cel .NewExpression (expr ,
703+ cel .WithCompile (),
704+ cel .WithOutputType (celtypes .BoolType ),
705+ cel .WithStructVariables (selfName , depName ))
706+ if err != nil {
707+ return false , reconcile .TerminalError (fmt .Errorf ("failed to evaluate dependency %s: %w" , dep .Name , err ))
708+ }
709+
710+ depMap , err := runtime .DefaultUnstructuredConverter .ToUnstructured (dep )
711+ if err != nil {
712+ return false , fmt .Errorf ("failed to convert %s object to map: %w" , depName , err )
713+ }
714+
715+ vars := map [string ]any {
716+ selfName : selfMap ,
717+ depName : depMap ,
718+ }
719+
720+ return celExpr .EvaluateBoolean (ctx , vars )
721+ }
722+
643723func (r * KustomizationReconciler ) getSource (ctx context.Context ,
644724 obj * kustomizev1.Kustomization ) (sourcev1.Source , error ) {
645725 var src sourcev1.Source
0 commit comments