Skip to content

Commit 376ba3c

Browse files
committed
feat(api): allow dependencies on any Kubernetes resource
HelmReleases can now depend on any Kubernetes object, improving flexibility in release ordering and readiness behavior. Signed-off-by: Vincent Dely <vincent.dely@ik.me>
1 parent 0ec11c3 commit 376ba3c

7 files changed

Lines changed: 463 additions & 32 deletions

File tree

api/v2/helmrelease_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ type HelmReleaseSpec struct {
101101
StorageNamespace string `json:"storageNamespace,omitempty"`
102102

103103
// DependsOn may contain a DependencyReference slice with
104-
// references to HelmRelease resources that must be ready before this HelmRelease
104+
// references to Kubernetes resources that must be ready before this HelmRelease
105105
// can be reconciled.
106106
// +optional
107107
DependsOn []DependencyReference `json:"dependsOn,omitempty"`

api/v2/reference_types.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,32 @@ type CrossNamespaceSourceReference struct {
6969
Namespace string `json:"namespace,omitempty"`
7070
}
7171

72-
// DependencyReference defines a HelmRelease dependency on another HelmRelease resource.
72+
// DependencyReference defines a HelmRelease dependency on a Kubernetes resource.
73+
// When the dependency is a HelmRelease, defaults are applied during reconciliation.
7374
type DependencyReference struct {
74-
// Name of the referent.
75+
// APIVersion of the resource to depend on, defaults to the HelmRelease API
76+
// group version when the dependency is a HelmRelease.
77+
// +optional
78+
APIVersion string `json:"apiVersion,omitempty"`
79+
80+
// Kind of the resource to depend on, defaults to HelmRelease.
81+
// +optional
82+
Kind string `json:"kind,omitempty"`
83+
84+
// Name of the resource to depend on.
7585
// +required
7686
Name string `json:"name"`
7787

78-
// Namespace of the referent, defaults to the namespace of the HelmRelease
79-
// resource object that contains the reference.
88+
// Namespace of the resource to depend on, defaults to the namespace of the
89+
// HelmRelease resource object that contains the reference.
8090
// +optional
8191
Namespace string `json:"namespace,omitempty"`
8292

93+
// Ready checks if the resource Ready status condition is true, defaults to
94+
// true when the dependency is a HelmRelease.
95+
// +optional
96+
Ready *bool `json:"ready,omitempty"`
97+
8398
// ReadyExpr is a CEL expression that can be used to assess the readiness
8499
// of a dependency. When specified, the built-in readiness check
85100
// is replaced by the logic defined in the CEL expression.

api/v2/zz_generated.deepcopy.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,20 +241,35 @@ spec:
241241
dependsOn:
242242
description: |-
243243
DependsOn may contain a DependencyReference slice with
244-
references to HelmRelease resources that must be ready before this HelmRelease
244+
references to Kubernetes resources that must be ready before this HelmRelease
245245
can be reconciled.
246246
items:
247-
description: DependencyReference defines a HelmRelease dependency
248-
on another HelmRelease resource.
247+
description: |-
248+
DependencyReference defines a HelmRelease dependency on a Kubernetes resource.
249+
When the dependency is a HelmRelease, defaults are applied during reconciliation.
249250
properties:
251+
apiVersion:
252+
description: |-
253+
APIVersion of the resource to depend on, defaults to the HelmRelease API
254+
group version when the dependency is a HelmRelease.
255+
type: string
256+
kind:
257+
description: Kind of the resource to depend on, defaults to
258+
HelmRelease.
259+
type: string
250260
name:
251-
description: Name of the referent.
261+
description: Name of the resource to depend on.
252262
type: string
253263
namespace:
254264
description: |-
255-
Namespace of the referent, defaults to the namespace of the HelmRelease
256-
resource object that contains the reference.
265+
Namespace of the resource to depend on, defaults to the namespace of the
266+
HelmRelease resource object that contains the reference.
257267
type: string
268+
ready:
269+
description: |-
270+
Ready checks if the resource Ready status condition is true, defaults to
271+
true when the dependency is a HelmRelease.
272+
type: boolean
258273
readyExpr:
259274
description: |-
260275
ReadyExpr is a CEL expression that can be used to assess the readiness

docs/api/v2/helm.md

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ Defaults to the namespace of the HelmRelease.</p>
195195
<td>
196196
<em>(Optional)</em>
197197
<p>DependsOn may contain a DependencyReference slice with
198-
references to HelmRelease resources that must be ready before this HelmRelease
198+
references to Kubernetes resources that must be ready before this HelmRelease
199199
can be reconciled.</p>
200200
</td>
201201
</tr>
@@ -675,7 +675,8 @@ resource object that contains the reference.</p>
675675
(<em>Appears on:</em>
676676
<a href="#helm.toolkit.fluxcd.io/v2.HelmReleaseSpec">HelmReleaseSpec</a>)
677677
</p>
678-
<p>DependencyReference defines a HelmRelease dependency on another HelmRelease resource.</p>
678+
<p>DependencyReference defines a HelmRelease dependency on a Kubernetes resource.
679+
When the dependency is a HelmRelease, defaults are applied during reconciliation.</p>
679680
<div class="md-typeset__scrollwrap">
680681
<div class="md-typeset__table">
681682
<table>
@@ -688,13 +689,38 @@ resource object that contains the reference.</p>
688689
<tbody>
689690
<tr>
690691
<td>
692+
<code>apiVersion</code><br>
693+
<em>
694+
string
695+
</em>
696+
</td>
697+
<td>
698+
<em>(Optional)</em>
699+
<p>APIVersion of the resource to depend on, defaults to the HelmRelease API
700+
group version when the dependency is a HelmRelease.</p>
701+
</td>
702+
</tr>
703+
<tr>
704+
<td>
705+
<code>kind</code><br>
706+
<em>
707+
string
708+
</em>
709+
</td>
710+
<td>
711+
<em>(Optional)</em>
712+
<p>Kind of the resource to depend on, defaults to HelmRelease.</p>
713+
</td>
714+
</tr>
715+
<tr>
716+
<td>
691717
<code>name</code><br>
692718
<em>
693719
string
694720
</em>
695721
</td>
696722
<td>
697-
<p>Name of the referent.</p>
723+
<p>Name of the resource to depend on.</p>
698724
</td>
699725
</tr>
700726
<tr>
@@ -706,8 +732,21 @@ string
706732
</td>
707733
<td>
708734
<em>(Optional)</em>
709-
<p>Namespace of the referent, defaults to the namespace of the HelmRelease
710-
resource object that contains the reference.</p>
735+
<p>Namespace of the resource to depend on, defaults to the namespace of the
736+
HelmRelease resource object that contains the reference.</p>
737+
</td>
738+
</tr>
739+
<tr>
740+
<td>
741+
<code>ready</code><br>
742+
<em>
743+
bool
744+
</em>
745+
</td>
746+
<td>
747+
<em>(Optional)</em>
748+
<p>Ready checks if the resource Ready status condition is true, defaults to
749+
true when the dependency is a HelmRelease.</p>
711750
</td>
712751
</tr>
713752
<tr>
@@ -1381,7 +1420,7 @@ Defaults to the namespace of the HelmRelease.</p>
13811420
<td>
13821421
<em>(Optional)</em>
13831422
<p>DependsOn may contain a DependencyReference slice with
1384-
references to HelmRelease resources that must be ready before this HelmRelease
1423+
references to Kubernetes resources that must be ready before this HelmRelease
13851424
can be reconciled.</p>
13861425
</td>
13871426
</tr>

internal/controller/helmrelease_controller.go

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ import (
2727
"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
2828
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
2929
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
30+
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
3031
celtypes "github.com/google/cel-go/common/types"
3132
chart "helm.sh/helm/v4/pkg/chart/v2"
3233
corev1 "k8s.io/api/core/v1"
3334
apiequality "k8s.io/apimachinery/pkg/api/equality"
3435
apierrors "k8s.io/apimachinery/pkg/api/errors"
3536
apimeta "k8s.io/apimachinery/pkg/api/meta"
37+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3638
"k8s.io/apimachinery/pkg/runtime"
39+
"k8s.io/apimachinery/pkg/runtime/schema"
3740
"k8s.io/apimachinery/pkg/types"
3841
apierrutil "k8s.io/apimachinery/pkg/util/errors"
3942
"k8s.io/apimachinery/pkg/util/wait"
@@ -45,6 +48,7 @@ import (
4548
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
4649
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4750

51+
objectutils "github.com/fluxcd/cli-utils/pkg/object"
4852
aclv1 "github.com/fluxcd/pkg/apis/acl"
4953
"github.com/fluxcd/pkg/apis/meta"
5054
"github.com/fluxcd/pkg/auth"
@@ -61,6 +65,7 @@ import (
6165
"github.com/fluxcd/pkg/runtime/object"
6266
"github.com/fluxcd/pkg/runtime/patch"
6367
"github.com/fluxcd/pkg/ssa"
68+
ssautil "github.com/fluxcd/pkg/ssa/utils"
6469

6570
sourcev1 "github.com/fluxcd/source-controller/api/v1"
6671

@@ -617,29 +622,62 @@ func (r *HelmReleaseReconciler) checkDependencies(ctx context.Context, obj *v2.H
617622
}
618623

619624
for _, depRef := range obj.Spec.DependsOn {
620-
depName := types.NamespacedName{
621-
Namespace: depRef.Namespace,
625+
// Default the dependency Kind to HelmRelease if unset.
626+
if depRef.Kind == "" {
627+
depRef.Kind = v2.HelmReleaseKind
628+
}
629+
630+
// Apply HelmRelease defaults if the dependency is a HelmRelease.
631+
if depRef.Kind == v2.HelmReleaseKind {
632+
// Default APIVersion to HelmRelease if unset.
633+
if depRef.APIVersion == "" {
634+
depRef.APIVersion = v2.GroupVersion.String()
635+
}
636+
// Default namespace to the dependent's namespace if unset.
637+
if depRef.Namespace == "" {
638+
depRef.Namespace = obj.GetNamespace()
639+
}
640+
// Default readiness check to true if unset.
641+
if depRef.Ready == nil {
642+
depRef.Ready = new(true)
643+
}
644+
}
645+
646+
depMd := objectutils.ObjMetadata{
647+
GroupKind: schema.GroupKind{Kind: depRef.Kind},
622648
Name: depRef.Name,
649+
Namespace: depRef.Namespace,
623650
}
624-
if depName.Namespace == "" {
625-
depName.Namespace = obj.GetNamespace()
651+
depObj := &unstructured.Unstructured{
652+
Object: map[string]any{
653+
"apiVersion": depRef.APIVersion,
654+
"kind": depRef.Kind,
655+
"metadata": map[string]any{
656+
"name": depRef.Name,
657+
"namespace": depRef.Namespace,
658+
},
659+
},
626660
}
627661

628662
// Check if the dependency exists by querying
629663
// the API server bypassing the cache.
630-
dep := &v2.HelmRelease{}
631-
if err := r.APIReader.Get(ctx, depName, dep); err != nil {
632-
return fmt.Errorf("unable to get '%s' dependency: %w", depName, err)
664+
if err := r.APIReader.Get(ctx, client.ObjectKeyFromObject(depObj), depObj); err != nil {
665+
return fmt.Errorf("unable to get '%s/%s' dependency: %w", depRef.APIVersion, ssautil.FmtObjMetadata(depMd), err)
666+
}
667+
668+
// Skip all readiness checks if unset or set to false.
669+
if depRef.Ready == nil || !*depRef.Ready {
670+
continue
633671
}
634672

635673
// Evaluate the CEL expression (if specified) to determine if the dependency is ready.
636674
if depRef.ReadyExpr != "" {
637-
ready, err := r.evalReadyExpr(ctx, depRef.ReadyExpr, objMap, dep)
675+
ready, err := r.evalReadyExpr(ctx, depRef.ReadyExpr, objMap, depObj)
638676
if err != nil {
639677
return err
640678
}
641679
if !ready {
642-
return fmt.Errorf("dependency '%s' is not ready according to readyExpr eval", depName)
680+
return fmt.Errorf("dependency '%s/%s' is not ready according to readyExpr eval", depRef.APIVersion, ssautil.FmtObjMetadata(depMd))
643681
}
644682
}
645683

@@ -651,10 +689,30 @@ func (r *HelmReleaseReconciler) checkDependencies(ctx context.Context, obj *v2.H
651689

652690
// Check if the dependency observed generation is up to date
653691
// and if the dependency is in a ready state.
654-
if dep.Generation != dep.Status.ObservedGeneration || !conditions.IsTrue(dep, meta.ReadyCondition) {
655-
return fmt.Errorf("dependency '%s' is not ready", depName)
692+
stat, err := status.Compute(depObj)
693+
if err != nil {
694+
return fmt.Errorf("dependency '%s/%s' is not ready: %w", depRef.APIVersion, ssautil.FmtObjMetadata(depMd), err)
695+
}
696+
if stat.Status != status.CurrentStatus {
697+
return fmt.Errorf("dependency '%s/%s' is not ready: status %s", depRef.APIVersion, ssautil.FmtObjMetadata(depMd), stat.Status)
698+
}
699+
700+
// This check only applies to HelmRelease dependencies.
701+
// Additionally check HelmRelease dependencies for readiness.
702+
// kstatus.Compute() tolerates missing conditions, but HelmReleases are expected to have a Ready condition.
703+
if depRef.Kind != v2.HelmReleaseKind {
704+
continue
705+
}
706+
707+
var dep v2.HelmRelease
708+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(depObj.Object, &dep); err != nil {
709+
return fmt.Errorf("failed to convert unstructured to HelmRelease: %w", err)
710+
}
711+
if !apimeta.IsStatusConditionTrue(dep.Status.Conditions, meta.ReadyCondition) {
712+
return fmt.Errorf("dependency '%s/%s' is not ready", depRef.APIVersion, ssautil.FmtObjMetadata(depMd))
656713
}
657714
}
715+
658716
return nil
659717
}
660718

@@ -663,7 +721,7 @@ func (r *HelmReleaseReconciler) evalReadyExpr(
663721
ctx context.Context,
664722
expr string,
665723
selfMap map[string]any,
666-
dep *v2.HelmRelease,
724+
dep *unstructured.Unstructured,
667725
) (bool, error) {
668726
const (
669727
selfName = "self"
@@ -675,7 +733,7 @@ func (r *HelmReleaseReconciler) evalReadyExpr(
675733
cel.WithOutputType(celtypes.BoolType),
676734
cel.WithStructVariables(selfName, depName))
677735
if err != nil {
678-
return false, reconcile.TerminalError(fmt.Errorf("failed to evaluate dependency %s: %w", dep.Name, err))
736+
return false, reconcile.TerminalError(fmt.Errorf("failed to evaluate dependency %s: %w", dep.GetName(), err))
679737
}
680738

681739
depMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)

0 commit comments

Comments
 (0)