Skip to content

Commit 6e3f22d

Browse files
committed
controller: Implement CEL evaluation for dependency checks
Signed-off-by: Stefan Prodan <[email protected]>
1 parent c2754dd commit 6e3f22d

File tree

4 files changed

+334
-56
lines changed

4 files changed

+334
-56
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ require (
3434
github.com/fluxcd/pkg/testserver v0.11.0
3535
github.com/fluxcd/source-controller/api v1.6.0
3636
github.com/getsops/sops/v3 v3.10.2
37+
github.com/google/cel-go v0.23.2
3738
github.com/hashicorp/vault/api v1.20.0
3839
github.com/onsi/gomega v1.37.0
3940
github.com/opencontainers/go-digest v1.0.0
@@ -151,7 +152,6 @@ require (
151152
github.com/gogo/protobuf v1.3.2 // indirect
152153
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
153154
github.com/google/btree v1.1.3 // indirect
154-
github.com/google/cel-go v0.23.2 // indirect
155155
github.com/google/gnostic-models v0.7.0 // indirect
156156
github.com/google/go-cmp v0.7.0 // indirect
157157
github.com/google/go-containerregistry v0.20.6 // indirect

internal/controller/constants.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ limitations under the License.
1616

1717
package controller
1818

19-
const OCIArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision"
19+
const (
20+
OCIArtifactOriginRevisionAnnotation = "org.opencontainers.image.revision"
21+
TerminalErrorMessage = "Reconciliation failed terminally due to configuration error"
22+
)

internal/controller/kustomization_controller.go

Lines changed: 124 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
598616
func (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+
643723
func (r *KustomizationReconciler) getSource(ctx context.Context,
644724
obj *kustomizev1.Kustomization) (sourcev1.Source, error) {
645725
var src sourcev1.Source

0 commit comments

Comments
 (0)