Skip to content
Merged
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
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ SOURCE_CRD_VER = $(CRD_DEP_ROOT)/.src-crd-$(SOURCE_VER)

# HelmChart source CRD.
HELMCHART_SOURCE_CRD ?= $(CRD_DEP_ROOT)/source.toolkit.fluxcd.io_helmcharts.yaml
OCIREPO_CRD ?= $(CRD_DEP_ROOT)/source.toolkit.fluxcd.io_ocirepositories.yaml
EA_CRD ?= $(CRD_DEP_ROOT)/source.toolkit.fluxcd.io_externalartifacts.yaml

# API (doc) generation utilities
CONTROLLER_GEN_VERSION ?= v0.19.0
Expand Down Expand Up @@ -135,8 +137,14 @@ $(SOURCE_CRD_VER):
$(HELMCHART_SOURCE_CRD):
curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml > $(HELMCHART_SOURCE_CRD)

$(OCIREPO_CRD):
curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml -o $(OCIREPO_CRD)

$(EA_CRD):
curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${SOURCE_VER}/config/crd/bases/source.toolkit.fluxcd.io_externalartifacts.yaml -o $(EA_CRD)

# Download the CRDs the controller depends on
download-crd-deps: $(SOURCE_CRD_VER) $(HELMCHART_SOURCE_CRD)
download-crd-deps: $(SOURCE_CRD_VER) $(HELMCHART_SOURCE_CRD) $(OCIREPO_CRD) $(EA_CRD)

# Delete the downloaded CRD dependencies.
cleanup-crd-deps:
Expand Down
2 changes: 1 addition & 1 deletion api/v2/reference_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type CrossNamespaceSourceReference struct {
APIVersion string `json:"apiVersion,omitempty"`

// Kind of the referent.
// +kubebuilder:validation:Enum=OCIRepository;HelmChart
// +kubebuilder:validation:Enum=OCIRepository;HelmChart;ExternalArtifact
// +required
Kind string `json:"kind"`

Expand Down
1 change: 1 addition & 0 deletions config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ spec:
enum:
- OCIRepository
- HelmChart
- ExternalArtifact
type: string
name:
description: Name of the referent.
Expand Down
4 changes: 2 additions & 2 deletions config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: helm-system
resources:
- https://github.com/fluxcd/source-controller/releases/download/v1.6.0/source-controller.crds.yaml
- https://github.com/fluxcd/source-controller/releases/download/v1.6.0/source-controller.deployment.yaml
- https://github.com/fluxcd/source-controller/releases/download/v1.7.0-rc.3/source-controller.crds.yaml
- https://github.com/fluxcd/source-controller/releases/download/v1.7.0-rc.3/source-controller.deployment.yaml
- ../crd
- ../rbac
- ../manager
Expand Down
13 changes: 9 additions & 4 deletions docs/spec/v2/helmreleases.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,16 @@ HelmRelease object.

### Chart reference

`.spec.chartRef` is an optional field used to refer to an [OCIRepository resource](https://fluxcd.io/flux/components/source/ocirepositories/) or a [HelmChart resource](https://fluxcd.io/flux/components/source/helmcharts/)
from which to fetch the Helm chart. The chart is fetched by the controller with the
information provided by `.status.artifact` of the referenced resource.
`.spec.chartRef` is an optional field used to refer to the Source object which has an
Artifact containing the Helm chart. It has two required fields:

For a referenced resource of `kind OCIRepository`, the chart version of the last
- `kind`: The Kind of the referred Source object. Supported Source types:
+ [OCIRepository](https://fluxcd.io/flux/components/source/ocirepositories/)
+ [HelmChart](https://fluxcd.io/flux/components/source/helmcharts/)
+ [ExternalArtifact](https://fluxcd.io/flux/components/source/externalartifacts/) (requires `--feature-gates=ExternalArtifact=true` flag)
- `name`: The Name of the referred Source object.

For a referenced resource of kind `OCIRepository`, the chart version of the last
release attempt is reported in `.status.lastAttemptedRevision`. The version is in
the format `<version>+<digest[0:12]>`. The digest of the OCI artifact is appended
to the version to ensure that a change in the artifact content triggers a new release.
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ require (
github.com/fluxcd/pkg/apis/acl v0.9.0
github.com/fluxcd/pkg/apis/event v0.19.0
github.com/fluxcd/pkg/apis/kustomize v1.12.0
github.com/fluxcd/pkg/apis/meta v1.20.0
github.com/fluxcd/pkg/auth v0.29.0
github.com/fluxcd/pkg/apis/meta v1.21.0
github.com/fluxcd/pkg/auth v0.30.0
github.com/fluxcd/pkg/cache v0.11.0
github.com/fluxcd/pkg/chartutil v1.11.0
github.com/fluxcd/pkg/runtime v0.82.0
github.com/fluxcd/pkg/chartutil v1.12.0
github.com/fluxcd/pkg/runtime v0.83.0
github.com/fluxcd/pkg/ssa v0.53.0
github.com/fluxcd/pkg/testserver v0.13.0
github.com/fluxcd/source-controller/api v1.6.0
github.com/fluxcd/source-controller/api v1.7.0-rc.3
github.com/go-logr/logr v1.4.3
github.com/google/cel-go v0.26.1
github.com/google/go-cmp v0.7.0
Expand All @@ -39,6 +39,7 @@ require (
github.com/opencontainers/go-digest/blake3 v0.0.0-20250116041648-1e56c6daea3b
github.com/spf13/pflag v1.0.7
github.com/wI2L/jsondiff v0.7.0
go.uber.org/zap v1.27.0
golang.org/x/text v0.28.0
helm.sh/helm/v3 v3.18.6
k8s.io/api v0.34.0
Expand Down Expand Up @@ -100,7 +101,7 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/docker/cli v28.3.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
Expand Down Expand Up @@ -201,7 +202,6 @@ require (
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect
Expand Down
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
Expand All @@ -152,22 +152,22 @@ github.com/fluxcd/pkg/apis/event v0.19.0 h1:ZJU2voontkzp5rNYA4JMOu40S4tRcrWi4Do5
github.com/fluxcd/pkg/apis/event v0.19.0/go.mod h1:deuIyUb6lh+Z1Ccvwwxhm1wNM3kpSo+vF1IgRnpaZfQ=
github.com/fluxcd/pkg/apis/kustomize v1.12.0 h1:KvZN6xwgP/dNSeckL4a/Uv715XqiN1C3xS+jGcPejtE=
github.com/fluxcd/pkg/apis/kustomize v1.12.0/go.mod h1:OojLxIdKm1JAAdh3sL4j4F+vfrLKb7kq1vr8bpyEKgg=
github.com/fluxcd/pkg/apis/meta v1.20.0 h1:l9h0kWoDZTcYV0WJkFMgDXq6Q4tSojrJ+bHpFJSsaW0=
github.com/fluxcd/pkg/apis/meta v1.20.0/go.mod h1:XUAEUgT4gkWDAEN79E141tmL+v4SV50tVZ/Ojpc/ueg=
github.com/fluxcd/pkg/auth v0.29.0 h1:lLc63zjodqIqg5ydlU/Kp3Qa+wvh6G2khjop5MHALvk=
github.com/fluxcd/pkg/auth v0.29.0/go.mod h1:bjZ+6RMSGgsQQK+aPfVP8HWuBbb+FLlFxMiqd8ywzik=
github.com/fluxcd/pkg/apis/meta v1.21.0 h1:R+bN02chcs0HUmyVDQhqe/FHmYLjipVDMLnyYfNX850=
github.com/fluxcd/pkg/apis/meta v1.21.0/go.mod h1:XUAEUgT4gkWDAEN79E141tmL+v4SV50tVZ/Ojpc/ueg=
github.com/fluxcd/pkg/auth v0.30.0 h1:7JMnY1ClArvOsadt6hOxceu8Q2hLsYHFMt0DV3BQl4Q=
github.com/fluxcd/pkg/auth v0.30.0/go.mod h1:me38o1nDfSLw6YvnkT9Ce/zqJZICZSA7j5pNMR3JUbc=
github.com/fluxcd/pkg/cache v0.11.0 h1:fsE8S+una21fSNw4MDXGUIf0Gf1J+pqa4RbsVKf2aTI=
github.com/fluxcd/pkg/cache v0.11.0/go.mod h1:2RTIU6PsJniHmfnllQWFEo7fa5V8KQlnMgn4o0sme40=
github.com/fluxcd/pkg/chartutil v1.11.0 h1:mtMqxsIIHLHSZS+D/9OfFS5ykABIOD2EOQbo3RyvjZY=
github.com/fluxcd/pkg/chartutil v1.11.0/go.mod h1:zQEbp6pHdRnhZN+RBjpQorRf9HKhqiO0bTk4vaOxbNU=
github.com/fluxcd/pkg/runtime v0.82.0 h1:VdPPRJtj8/rcBdqY7GZSffoxe5elFHt+ymwQHNbPOlc=
github.com/fluxcd/pkg/runtime v0.82.0/go.mod h1:rIDynMhU5upbn8ce3bXQhH5L6vtDw5MELycvtJG/+og=
github.com/fluxcd/pkg/chartutil v1.12.0 h1:AsOXWYQIUpZMCtlyQeaPmlWBYqSfHZTQ6A6eTGAhBLc=
github.com/fluxcd/pkg/chartutil v1.12.0/go.mod h1:W1VxE52pIaimf9upYQW/fvonU3BDs2zxM2zdg/48vEE=
github.com/fluxcd/pkg/runtime v0.83.0 h1:XzpwKzo7GqfBE/BKpxG5B4U7cUnojnB407S9Dpp6oLU=
github.com/fluxcd/pkg/runtime v0.83.0/go.mod h1:r8KLvXRguKtpLAa66fA19rIbwPViXm8az038IUabYvw=
github.com/fluxcd/pkg/ssa v0.53.0 h1:EtKFAYWXohIGkzPhIrv8NbV5zYr4iZHY6DaNxMR9+bU=
github.com/fluxcd/pkg/ssa v0.53.0/go.mod h1:0IZxnnH8XkDkFzWjoMJsFEwPIWPOk3Gc/WR5+gT/KgI=
github.com/fluxcd/pkg/testserver v0.13.0 h1:xEpBcEYtD7bwvZ+i0ZmChxKkDo/wfQEV3xmnzVybSSg=
github.com/fluxcd/pkg/testserver v0.13.0/go.mod h1:akRYv3FLQUsme15na9ihECRG6hBuqni4XEY9W8kzs8E=
github.com/fluxcd/source-controller/api v1.6.0 h1:IxfjUczJ2pzbXIef6iQ0RHEH4AYA9anJfTGK8dzwODM=
github.com/fluxcd/source-controller/api v1.6.0/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw=
github.com/fluxcd/source-controller/api v1.7.0-rc.3 h1:+9cd//77LAgp0XRe97CXUaPnu78jsRNWTXq95GHGhgc=
github.com/fluxcd/source-controller/api v1.7.0-rc.3/go.mod h1:sbJibK4Ik+2AuTRRLXPA+n2u6nLUIGaxC07ava+RqeM=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
Expand Down
80 changes: 61 additions & 19 deletions internal/controller/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,26 @@ type HelmReleaseReconciler struct {
kuberecorder.EventRecorder
helper.Metrics

GetClusterConfig func() (*rest.Config, error)
ClientOpts runtimeClient.Options
KubeConfigOpts runtimeClient.KubeConfigOptions
APIReader client.Reader
TokenCache *cache.TokenCache

FieldManager string
DefaultServiceAccount string
DisableChartDigestTracking bool
AdditiveCELDependencyCheck bool
// Kubernetes configuration

FieldManager string
DefaultServiceAccount string
GetClusterConfig func() (*rest.Config, error)
ClientOpts runtimeClient.Options
KubeConfigOpts runtimeClient.KubeConfigOptions
APIReader client.Reader
TokenCache *cache.TokenCache

// Retry and requeue configuration

requeueDependency time.Duration
artifactFetchRetries int
DependencyRequeueInterval time.Duration
ArtifactFetchRetries int

// Feature gates

AdditiveCELDependencyCheck bool
AllowExternalArtifact bool
DisableChartDigestTracking bool
}

var (
Expand Down Expand Up @@ -221,14 +228,14 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe

// Retry on transient errors.
msg := fmt.Sprintf("dependencies do not meet ready condition (%s): retrying in %s",
err.Error(), r.requeueDependency.String())
err.Error(), r.DependencyRequeueInterval.String())
conditions.MarkFalse(obj, meta.ReadyCondition, v2.DependencyNotReadyReason, "%s", err)
r.Eventf(obj, corev1.EventTypeNormal, v2.DependencyNotReadyReason, err.Error())
log.Info(msg)

// Exponential backoff would cause execution to be prolonged too much,
// instead we requeue on a fixed interval.
return ctrl.Result{RequeueAfter: r.requeueDependency}, errWaitForDependency
return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, errWaitForDependency
}

log.Info("all dependencies are ready")
Expand Down Expand Up @@ -268,7 +275,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
conditions.MarkFalse(obj, meta.ReadyCondition, "SourceNotReady", "%s", msg)
// Do not requeue immediately, when the artifact is created
// the watcher should trigger a reconciliation.
return jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: r.requeueDependency}), errWaitForChart
return jitter.JitteredRequeueInterval(ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}), errWaitForChart
}
// Remove any stale corresponding Ready=False condition with Unknown.
if conditions.HasAnyReason(obj, meta.ReadyCondition, "SourceNotReady") {
Expand All @@ -293,13 +300,13 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
}

// Load chart from artifact.
loadedChart, err := loader.SecureLoadChartFromURL(loader.NewRetryableHTTPClient(ctx, r.artifactFetchRetries), source.GetArtifact().URL, source.GetArtifact().Digest)
loadedChart, err := loader.SecureLoadChartFromURL(loader.NewRetryableHTTPClient(ctx, r.ArtifactFetchRetries), source.GetArtifact().URL, source.GetArtifact().Digest)
if err != nil {
if errors.Is(err, loader.ErrFileNotFound) {
msg := fmt.Sprintf("Source not ready: artifact not found. Retrying in %s", r.requeueDependency.String())
msg := fmt.Sprintf("Source not ready: artifact not found. Retrying in %s", r.DependencyRequeueInterval.String())
conditions.MarkFalse(obj, meta.ReadyCondition, v2.ArtifactFailedReason, "%s", msg)
log.Info(msg)
return ctrl.Result{RequeueAfter: r.requeueDependency}, errWaitForDependency
return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, errWaitForDependency
}

conditions.MarkFalse(obj, meta.ReadyCondition, v2.ArtifactFailedReason, "Could not load chart: %s", err)
Expand Down Expand Up @@ -774,6 +781,9 @@ func (r *HelmReleaseReconciler) getSource(ctx context.Context, obj *v2.HelmRelea
if obj.Spec.ChartRef.Kind == sourcev1.OCIRepositoryKind {
return r.getSourceFromOCIRef(ctx, obj)
}
if obj.Spec.ChartRef.Kind == sourcev1.ExternalArtifactKind {
return r.getSourceFromExternalArtifact(ctx, obj)
}
name, namespace = obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
if namespace == "" {
namespace = obj.GetNamespace()
Expand Down Expand Up @@ -813,6 +823,31 @@ func (r *HelmReleaseReconciler) getSourceFromOCIRef(ctx context.Context, obj *v2
return &or, nil
}

func (r *HelmReleaseReconciler) getSourceFromExternalArtifact(ctx context.Context, obj *v2.HelmRelease) (sourcev1.Source, error) {
name, namespace := obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
if namespace == "" {
namespace = obj.GetNamespace()
}
sourceRef := types.NamespacedName{Namespace: namespace, Name: name}

if err := intacl.AllowsAccessTo(obj, sourcev1.ExternalArtifactKind, sourceRef); err != nil {
return nil, err
}

// Check if ExternalArtifact kind is allowed.
if obj.Spec.ChartRef.Kind == sourcev1.ExternalArtifactKind && !r.AllowExternalArtifact {
return nil, acl.AccessDeniedError(
fmt.Sprintf("can't access '%s/%s/%s', %s feature gate is disabled",
obj.Spec.ChartRef.Kind, namespace, name, features.ExternalArtifact))
}

or := sourcev1.ExternalArtifact{}
if err := r.Client.Get(ctx, sourceRef, &or); err != nil {
return nil, err
}
return &or, nil
}

// waitForHistoryCacheSync returns a function that can be used to wait for the
// cache backing the Kubernetes client to be in sync with the current state of
// the v2.HelmRelease.
Expand All @@ -832,13 +867,20 @@ func (r *HelmReleaseReconciler) waitForHistoryCacheSync(obj *v2.HelmRelease) wai
}

func isSourceReady(obj sourcev1.Source) (bool, string) {
if o, ok := obj.(*sourcev1.ExternalArtifact); ok {
if obj.GetArtifact() == nil {
Comment thread
stefanprodan marked this conversation as resolved.
return false, fmt.Sprintf("ExternalArtifact '%s/%s' is not ready: does not have an artifact",
o.GetNamespace(), o.GetName())
}
return true, ""
}
if o, ok := obj.(conditions.Getter); ok {
return isReady(o, obj.GetArtifact())
}
return false, fmt.Sprintf("unknown sourcev1 type: %T", obj)
}

func isReady(obj conditions.Getter, artifact *sourcev1.Artifact) (bool, string) {
func isReady(obj conditions.Getter, artifact *meta.Artifact) (bool, string) {
observedGen, err := object.GetStatusObservedGeneration(obj)
if err != nil {
return false, err.Error()
Expand Down
Loading