Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/v2/helmrelease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ type HelmReleaseSpec struct {
StorageNamespace string `json:"storageNamespace,omitempty"`

// DependsOn may contain a DependencyReference slice with
// references to HelmRelease resources that must be ready before this HelmRelease
// references to Kubernetes resources that must be ready before this HelmRelease
// can be reconciled.
// +optional
DependsOn []DependencyReference `json:"dependsOn,omitempty"`
Expand Down
23 changes: 19 additions & 4 deletions api/v2/reference_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,32 @@ type CrossNamespaceSourceReference struct {
Namespace string `json:"namespace,omitempty"`
}

// DependencyReference defines a HelmRelease dependency on another HelmRelease resource.
// DependencyReference defines a HelmRelease dependency on a Kubernetes resource.
// When the dependency is a HelmRelease, defaults are applied during reconciliation.
type DependencyReference struct {
// Name of the referent.
// APIVersion of the resource to depend on, defaults to the HelmRelease API
// group version when the dependency is a HelmRelease.
// +optional
APIVersion string `json:"apiVersion,omitempty"`

// Kind of the resource to depend on, defaults to HelmRelease.
// +optional
Kind string `json:"kind,omitempty"`

// Name of the resource to depend on.
// +required
Name string `json:"name"`

// Namespace of the referent, defaults to the namespace of the HelmRelease
// resource object that contains the reference.
// Namespace of the resource to depend on, defaults to the namespace of the
// HelmRelease resource object that contains the reference.
// +optional
Namespace string `json:"namespace,omitempty"`

// Ready checks if the resource Ready status condition is true, defaults to
// true when the dependency is a HelmRelease.
// +optional
Ready *bool `json:"ready,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.
Expand Down
9 changes: 8 additions & 1 deletion api/v2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 21 additions & 6 deletions config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,35 @@ spec:
dependsOn:
description: |-
DependsOn may contain a DependencyReference slice with
references to HelmRelease resources that must be ready before this HelmRelease
references to Kubernetes resources that must be ready before this HelmRelease
can be reconciled.
items:
description: DependencyReference defines a HelmRelease dependency
on another HelmRelease resource.
description: |-
DependencyReference defines a HelmRelease dependency on a Kubernetes resource.
When the dependency is a HelmRelease, defaults are applied during reconciliation.
properties:
apiVersion:
description: |-
APIVersion of the resource to depend on, defaults to the HelmRelease API
group version when the dependency is a HelmRelease.
type: string
kind:
description: Kind of the resource to depend on, defaults to
HelmRelease.
type: string
name:
description: Name of the referent.
description: Name of the resource to depend on.
type: string
namespace:
description: |-
Namespace of the referent, defaults to the namespace of the HelmRelease
resource object that contains the reference.
Namespace of the resource to depend on, defaults to the namespace of the
HelmRelease resource object that contains the reference.
type: string
ready:
description: |-
Ready checks if the resource Ready status condition is true, defaults to
true when the dependency is a HelmRelease.
type: boolean
readyExpr:
description: |-
ReadyExpr is a CEL expression that can be used to assess the readiness
Expand Down
51 changes: 45 additions & 6 deletions docs/api/v2/helm.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Defaults to the namespace of the HelmRelease.</p>
<td>
<em>(Optional)</em>
<p>DependsOn may contain a DependencyReference slice with
references to HelmRelease resources that must be ready before this HelmRelease
references to Kubernetes resources that must be ready before this HelmRelease
can be reconciled.</p>
</td>
</tr>
Expand Down Expand Up @@ -675,7 +675,8 @@ resource object that contains the reference.</p>
(<em>Appears on:</em>
<a href="#helm.toolkit.fluxcd.io/v2.HelmReleaseSpec">HelmReleaseSpec</a>)
</p>
<p>DependencyReference defines a HelmRelease dependency on another HelmRelease resource.</p>
<p>DependencyReference defines a HelmRelease dependency on a Kubernetes resource.
When the dependency is a HelmRelease, defaults are applied during reconciliation.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
Expand All @@ -688,13 +689,38 @@ resource object that contains the reference.</p>
<tbody>
<tr>
<td>
<code>apiVersion</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>APIVersion of the resource to depend on, defaults to the HelmRelease API
group version when the dependency is a HelmRelease.</p>
</td>
</tr>
<tr>
<td>
<code>kind</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Kind of the resource to depend on, defaults to HelmRelease.</p>
</td>
</tr>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<p>Name of the referent.</p>
<p>Name of the resource to depend on.</p>
</td>
</tr>
<tr>
Expand All @@ -706,8 +732,21 @@ string
</td>
<td>
<em>(Optional)</em>
<p>Namespace of the referent, defaults to the namespace of the HelmRelease
resource object that contains the reference.</p>
<p>Namespace of the resource to depend on, defaults to the namespace of the
HelmRelease resource object that contains the reference.</p>
</td>
</tr>
<tr>
<td>
<code>ready</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>Ready checks if the resource Ready status condition is true, defaults to
true when the dependency is a HelmRelease.</p>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -1381,7 +1420,7 @@ Defaults to the namespace of the HelmRelease.</p>
<td>
<em>(Optional)</em>
<p>DependsOn may contain a DependencyReference slice with
references to HelmRelease resources that must be ready before this HelmRelease
references to Kubernetes resources that must be ready before this HelmRelease
can be reconciled.</p>
</td>
</tr>
Expand Down
84 changes: 71 additions & 13 deletions internal/controller/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ import (
"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
celtypes "github.com/google/cel-go/common/types"
chart "helm.sh/helm/v4/pkg/chart/v2"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
apierrutil "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
Expand All @@ -45,6 +48,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

objectutils "github.com/fluxcd/cli-utils/pkg/object"
aclv1 "github.com/fluxcd/pkg/apis/acl"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/auth"
Expand All @@ -61,6 +65,7 @@ import (
"github.com/fluxcd/pkg/runtime/object"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/ssa"
ssautil "github.com/fluxcd/pkg/ssa/utils"

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

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

for _, depRef := range obj.Spec.DependsOn {
depName := types.NamespacedName{
Namespace: depRef.Namespace,
// Default the dependency Kind to HelmRelease if unset.
if depRef.Kind == "" {
depRef.Kind = v2.HelmReleaseKind
}

// Apply HelmRelease defaults if the dependency is a HelmRelease.
if depRef.Kind == v2.HelmReleaseKind {
// Default APIVersion to HelmRelease if unset.
if depRef.APIVersion == "" {
depRef.APIVersion = v2.GroupVersion.String()
}
// Default namespace to the dependent's namespace if unset.
if depRef.Namespace == "" {
depRef.Namespace = obj.GetNamespace()
}
// Default readiness check to true if unset.
if depRef.Ready == nil {
depRef.Ready = new(true)
}
}

depMd := objectutils.ObjMetadata{
GroupKind: schema.GroupKind{Kind: depRef.Kind},
Name: depRef.Name,
Namespace: depRef.Namespace,
}
if depName.Namespace == "" {
depName.Namespace = obj.GetNamespace()
depObj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": depRef.APIVersion,
"kind": depRef.Kind,
"metadata": map[string]any{
"name": depRef.Name,
"namespace": depRef.Namespace,
},
},
}

// Check if the dependency exists by querying
// the API server bypassing the cache.
dep := &v2.HelmRelease{}
if err := r.APIReader.Get(ctx, depName, dep); err != nil {
return fmt.Errorf("unable to get '%s' dependency: %w", depName, err)
if err := r.APIReader.Get(ctx, client.ObjectKeyFromObject(depObj), depObj); err != nil {
return fmt.Errorf("unable to get '%s/%s' dependency: %w", depRef.APIVersion, ssautil.FmtObjMetadata(depMd), err)
}

// Skip all readiness checks if unset or set to false.
if depRef.Ready == nil || !*depRef.Ready {
continue
}

// 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)
ready, err := r.evalReadyExpr(ctx, depRef.ReadyExpr, objMap, depObj)
if err != nil {
return err
}
if !ready {
return fmt.Errorf("dependency '%s' is not ready according to readyExpr eval", depName)
return fmt.Errorf("dependency '%s/%s' is not ready according to readyExpr eval", depRef.APIVersion, ssautil.FmtObjMetadata(depMd))
}
}

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

// Check if the dependency observed generation is up to date
// and if the dependency is in a ready state.
if dep.Generation != dep.Status.ObservedGeneration || !conditions.IsTrue(dep, meta.ReadyCondition) {
return fmt.Errorf("dependency '%s' is not ready", depName)
stat, err := status.Compute(depObj)
if err != nil {
return fmt.Errorf("dependency '%s/%s' is not ready: %w", depRef.APIVersion, ssautil.FmtObjMetadata(depMd), err)
}
if stat.Status != status.CurrentStatus {
return fmt.Errorf("dependency '%s/%s' is not ready: status %s", depRef.APIVersion, ssautil.FmtObjMetadata(depMd), stat.Status)
}

// This check only applies to HelmRelease dependencies.
// Additionally check HelmRelease dependencies for readiness.
// kstatus.Compute() tolerates missing conditions, but HelmReleases are expected to have a Ready condition.
if depRef.Kind != v2.HelmReleaseKind {
continue
}

var dep v2.HelmRelease
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(depObj.Object, &dep); err != nil {
return fmt.Errorf("failed to convert unstructured to HelmRelease: %w", err)
}
if !apimeta.IsStatusConditionTrue(dep.Status.Conditions, meta.ReadyCondition) {
return fmt.Errorf("dependency '%s/%s' is not ready", depRef.APIVersion, ssautil.FmtObjMetadata(depMd))
}
}

return nil
}

Expand All @@ -663,7 +721,7 @@ func (r *HelmReleaseReconciler) evalReadyExpr(
ctx context.Context,
expr string,
selfMap map[string]any,
dep *v2.HelmRelease,
dep *unstructured.Unstructured,
) (bool, error) {
const (
selfName = "self"
Expand All @@ -675,7 +733,7 @@ func (r *HelmReleaseReconciler) evalReadyExpr(
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))
return false, reconcile.TerminalError(fmt.Errorf("failed to evaluate dependency %s: %w", dep.GetName(), err))
}

depMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dep)
Expand Down
Loading