From 1be534afa4a4075298d2ed76168e91f3f2a47187 Mon Sep 17 00:00:00 2001 From: Rajit Shah Date: Thu, 5 Mar 2026 15:39:54 -0600 Subject: [PATCH] helmchart: add ExternalArtifact as a valid source kind ExternalArtifact (source.toolkit.fluxcd.io/v1) was introduced in v1.7.0 but was never wired into the HelmChart controller. This commit adds the necessary plumbing so that a HelmChart can reference an ExternalArtifact as its source, enabling advanced source-composition patterns via the source-watcher controller. Changes: - Extend LocalHelmChartSourceReference.Kind enum to include ExternalArtifact - Register a Watch for ExternalArtifact in SetupWithManager - Add ExternalArtifactKind case to getSource() - Add *sourcev1.ExternalArtifact case to reconcileSource() - Add requestsForExternalArtifactChange() reconcile-trigger helper ExternalArtifact exposes a tarball artifact identical in structure to Bucket and GitRepository, so it reuses the existing buildFromTarballArtifact path with no new builder code required. Signed-off-by: Rajit Shah --- api/v1/helmchart_types.go | 4 +- internal/controller/helmchart_controller.go | 45 ++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/api/v1/helmchart_types.go b/api/v1/helmchart_types.go index 224d8533d..632172fb0 100644 --- a/api/v1/helmchart_types.go +++ b/api/v1/helmchart_types.go @@ -105,8 +105,8 @@ type LocalHelmChartSourceReference struct { APIVersion string `json:"apiVersion,omitempty"` // Kind of the referent, valid values are ('HelmRepository', 'GitRepository', - // 'Bucket'). - // +kubebuilder:validation:Enum=HelmRepository;GitRepository;Bucket + // 'Bucket', 'ExternalArtifact'). + // +kubebuilder:validation:Enum=HelmRepository;GitRepository;Bucket;ExternalArtifact // +required Kind string `json:"kind"` diff --git a/internal/controller/helmchart_controller.go b/internal/controller/helmchart_controller.go index 963d75dde..12732dbbe 100644 --- a/internal/controller/helmchart_controller.go +++ b/internal/controller/helmchart_controller.go @@ -201,6 +201,11 @@ func (r *HelmChartReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man handler.EnqueueRequestsFromMapFunc(r.requestsForBucketChange), builder.WithPredicates(SourceRevisionChangePredicate{}), ). + Watches( + &sourcev1.ExternalArtifact{}, + handler.EnqueueRequestsFromMapFunc(r.requestsForExternalArtifactChange), + builder.WithPredicates(SourceRevisionChangePredicate{}), + ). WithOptions(controller.Options{ RateLimiter: opts.RateLimiter, }). @@ -508,6 +513,8 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, sp *patch.Ser return r.buildFromHelmRepository(ctx, obj, typedSource, build) case *sourcev1.GitRepository, *sourcev1.Bucket: return r.buildFromTarballArtifact(ctx, obj, *typedSource.GetArtifact(), build) + case *sourcev1.ExternalArtifact: + return r.buildFromTarballArtifact(ctx, obj, *typedSource.GetArtifact(), build) default: // Ending up here should generally not be possible // as getSource already validates @@ -931,9 +938,15 @@ func (r *HelmChartReconciler) getSource(ctx context.Context, obj *sourcev1.HelmC return nil, err } s = &bucket + case sourcev1.ExternalArtifactKind: + var ea sourcev1.ExternalArtifact + if err := r.Client.Get(ctx, namespacedName, &ea); err != nil { + return nil, err + } + s = &ea default: return nil, fmt.Errorf("unsupported source kind '%s', must be one of: %v", obj.Spec.SourceRef.Kind, []string{ - sourcev1.HelmRepositoryKind, sourcev1.GitRepositoryKind, sourcev1.BucketKind}) + sourcev1.HelmRepositoryKind, sourcev1.GitRepositoryKind, sourcev1.BucketKind, sourcev1.ExternalArtifactKind}) } return s, nil } @@ -1202,6 +1215,36 @@ func (r *HelmChartReconciler) requestsForBucketChange(ctx context.Context, o cli return reqs } +func (r *HelmChartReconciler) requestsForExternalArtifactChange(ctx context.Context, o client.Object) []reconcile.Request { + ea, ok := o.(*sourcev1.ExternalArtifact) + if !ok { + ctrl.LoggerFrom(ctx).Error(fmt.Errorf("expected an ExternalArtifact, got %T", o), + "failed to get reconcile requests for ExternalArtifact change") + return nil + } + + // If we do not have an artifact, we have no requests to make + if ea.GetArtifact() == nil { + return nil + } + + var list sourcev1.HelmChartList + if err := r.List(ctx, &list, client.MatchingFields{ + indexKeyHelmChartSource: fmt.Sprintf("%s/%s", sourcev1.ExternalArtifactKind, ea.Name), + }); err != nil { + ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmCharts for ExternalArtifact change") + return nil + } + + var reqs []reconcile.Request + for i, v := range list.Items { + if !ea.GetArtifact().HasRevision(v.Status.ObservedSourceArtifactRevision) { + reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&list.Items[i])}) + } + } + return reqs +} + // eventLogf records events, and logs at the same time. // // This log is different from the debug log in the EventRecorder, in the sense