@@ -2267,9 +2361,7 @@ Kubernetes meta/v1.Time
testHooks
-
-TestHookStatus
-
+map[string]*github.com/fluxcd/helm-controller/api/v2beta2.TestHookStatus
|
@@ -2283,7 +2375,7 @@ run by the controller.
+([]*github.com/fluxcd/helm-controller/api/v2beta2.Snapshot alias)
(Appears on:
HelmReleaseStatus)
@@ -2352,8 +2444,8 @@ actions in ‘Install.IgnoreTestFailures’ and ‘Upgrade.IgnoreTes
|
filters
-
-Filter
+
+[]github.com/fluxcd/helm-controller/api/v2beta2.Filter
|
@@ -2367,10 +2459,6 @@ Filter
-
-(Appears on:
-Snapshot)
-
TestHookStatus holds the status information for a test hook as observed
to be run by the controller.
diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go
index 1068ae84d..85f47f9c3 100644
--- a/internal/controller/helmrelease_controller.go
+++ b/internal/controller/helmrelease_controller.go
@@ -52,6 +52,7 @@ import (
"github.com/fluxcd/pkg/runtime/logger"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/predicates"
+ source "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
v2 "github.com/fluxcd/helm-controller/api/v2beta2"
@@ -108,15 +109,10 @@ func (r *HelmReleaseReconciler) SetupWithManager(ctx context.Context, mgr ctrl.M
if err := mgr.GetFieldIndexer().IndexField(ctx, &v2.HelmRelease{}, v2.SourceIndexKey,
func(o client.Object) []string {
obj := o.(*v2.HelmRelease)
- namespacedName := types.NamespacedName{}
- if obj.Spec.ChartRef != nil {
- namespacedName.Namespace = obj.Spec.ChartRef.Namespace
- namespacedName.Name = obj.Spec.ChartRef.Name
- } else {
- namespacedName.Namespace = obj.Spec.Chart.GetNamespace(obj.GetNamespace())
- namespacedName.Name = obj.GetHelmChartName()
+ namespacedName, err := getNamespacedName(obj)
+ if err != nil {
+ return nil
}
-
return []string{
namespacedName.String(),
}
@@ -158,6 +154,10 @@ func (r *HelmReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, client.IgnoreNotFound(err)
}
+ if !isValidChartRef(obj) {
+ return ctrl.Result{}, reconcile.TerminalError(fmt.Errorf("invalid HelmChart reference"))
+ }
+
// Initialize the patch helper with the current version of the object.
patchHelper := patch.NewSerialPatcher(obj, r.Client)
@@ -262,8 +262,8 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- // Get the HelmChart object for the release.
- hc, err := r.getHelmChart(ctx, obj)
+ // Get the source object containing the HelmChart.
+ source, err := r.getSource(ctx, obj)
if err != nil {
if acl.IsAccessDenied(err) {
conditions.MarkStalled(obj, aclv1.AccessDeniedReason, err.Error())
@@ -286,9 +286,8 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
}
- // Check if the HelmChart is ready.
- if ready, reason := isHelmChartReady(hc); !ready {
- msg := fmt.Sprintf("HelmChart '%s/%s' is not ready: %s", hc.GetNamespace(), hc.GetName(), reason)
+ // Check if the source is ready.
+ if ready, msg := isSourceReady(source); !ready {
log.Info(msg)
conditions.MarkFalse(obj, meta.ReadyCondition, "HelmChartNotReady", msg)
// Do not requeue immediately, when the artifact is created
@@ -313,7 +312,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
}
// Load chart from artifact.
- loadedChart, err := loader.SecureLoadChartFromURL(loader.NewRetryableHTTPClient(ctx, r.artifactFetchRetries), hc.GetArtifact().URL, hc.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("Chart not ready: artifact not found. Retrying in %s", r.requeueDependency.String())
@@ -668,11 +667,21 @@ func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, obj *
return kube.NewMemoryRESTClientGetter(cfg, opts...), nil
}
-// getHelmChart retrieves the v1beta2.HelmChart for the given v2beta2.HelmRelease
-// using the name that is advertised in the status object.
-// It returns the v1beta2.HelmChart, or an error.
-func (r *HelmReleaseReconciler) getHelmChart(ctx context.Context, obj *v2.HelmRelease) (*sourcev1.HelmChart, error) {
- namespace, name := obj.Status.GetHelmChart()
+// getSource returns the source object containing the HelmChart, either by
+// using the chartRef in the spec, or by looking up the HelmChart
+// referenced in the status object.
+// It returns the source object or an error.
+func (r *HelmReleaseReconciler) getSource(ctx context.Context, obj *v2.HelmRelease) (source.Source, error) {
+ var name, namespace string
+ if isChartRefPresent(obj) {
+ if obj.Spec.ChartRef.Kind == sourcev1.OCIRepositoryKind {
+ return r.getHelmChartFromOCIRef(ctx, obj)
+ }
+ name, namespace = obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
+ } else {
+ namespace, name = obj.Status.GetHelmChart()
+ }
+
chartRef := types.NamespacedName{Namespace: namespace, Name: name}
if err := intacl.AllowsAccessTo(obj, sourcev1.HelmChartKind, chartRef); err != nil {
@@ -686,6 +695,21 @@ func (r *HelmReleaseReconciler) getHelmChart(ctx context.Context, obj *v2.HelmRe
return &hc, nil
}
+func (r *HelmReleaseReconciler) getHelmChartFromOCIRef(ctx context.Context, obj *v2.HelmRelease) (source.Source, error) {
+ namespace, name := obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
+ ociRepoRef := types.NamespacedName{Namespace: namespace, Name: name}
+
+ if err := intacl.AllowsAccessTo(obj, sourcev1.OCIRepositoryKind, ociRepoRef); err != nil {
+ return nil, err
+ }
+
+ or := sourcev1.OCIRepository{}
+ if err := r.Client.Get(ctx, ociRepoRef, &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 v2beta2.HelmRelease.
@@ -736,6 +760,51 @@ func (r *HelmReleaseReconciler) requestsForHelmChartChange(ctx context.Context,
return reqs
}
+func (r *HelmReleaseReconciler) requestsForOCIRrepositoryChange(ctx context.Context, o client.Object) []reconcile.Request {
+ or, ok := o.(*sourcev1.OCIRepository)
+ if !ok {
+ err := fmt.Errorf("expected an OCIRepository, got %T", o)
+ ctrl.LoggerFrom(ctx).Error(err, "failed to get requests for OCIRepository change")
+ return nil
+ }
+ // If we do not have an artifact, we have no requests to make
+ if or.GetArtifact() == nil {
+ return nil
+ }
+
+ var list v2.HelmReleaseList
+ if err := r.List(ctx, &list, client.MatchingFields{
+ v2.SourceIndexKey: client.ObjectKeyFromObject(or).String(),
+ }); err != nil {
+ ctrl.LoggerFrom(ctx).Error(err, "failed to list HelmReleases for OCIRepository change")
+ return nil
+ }
+
+ var reqs []reconcile.Request
+ for i, hr := range list.Items {
+ // If the HelmRelease is ready and the revision of the artifact equals to the
+ // last attempted revision, we should not make a request for this HelmRelease
+ if conditions.IsReady(&list.Items[i]) && or.GetArtifact().HasRevision(hr.Status.LastAttemptedRevision) {
+ continue
+ }
+ reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&list.Items[i])})
+ }
+ return reqs
+}
+
+func isSourceReady(obj source.Source) (bool, string) {
+ if hc, ok := obj.(*sourcev1.HelmChart); ok {
+ return isHelmChartReady(hc)
+ }
+
+ or, ok := obj.(*sourcev1.OCIRepository)
+ if ok {
+ return isOCIRepositoryReady(or)
+ }
+
+ return false, "unknown source type"
+}
+
// isHelmChartReady returns true if the given HelmChart is ready, and a reason
// why it is not ready otherwise.
func isHelmChartReady(obj *sourcev1.HelmChart) (bool, string) {
@@ -752,12 +821,65 @@ func isHelmChartReady(obj *sourcev1.HelmChart) (bool, string) {
if conditions.IsFalse(obj, meta.ReadyCondition) {
msg = conditions.GetMessage(obj, meta.ReadyCondition)
}
- return false, msg
+ return false, fmt.Sprintf("HelmChart '%s/%s' is not ready: %s",
+ obj.GetNamespace(), obj.GetName(), msg)
case conditions.IsStalled(obj):
- return false, conditions.GetMessage(obj, meta.StalledCondition)
+ return false, fmt.Sprintf("HelmChart '%s/%s' is not ready: %s",
+ obj.GetNamespace(), obj.GetName(), conditions.GetMessage(obj, meta.StalledCondition))
case obj.Status.Artifact == nil:
- return false, "does not have an artifact"
+ return false, fmt.Sprintf("HelmChart '%s/%s' is not ready: %s",
+ obj.GetNamespace(), obj.GetName(), "does not have an artifact")
default:
return true, ""
}
}
+
+func isOCIRepositoryReady(obj *sourcev1.OCIRepository) (bool, string) {
+ switch {
+ case obj.Generation != obj.Status.ObservedGeneration:
+ msg := "latest generation of object has not been reconciled"
+
+ if conditions.IsFalse(obj, meta.ReadyCondition) {
+ msg = conditions.GetMessage(obj, meta.ReadyCondition)
+ }
+ return false, fmt.Sprintf("OCIRepository '%s/%s' is not ready: %s",
+ obj.GetNamespace(), obj.GetName(), msg)
+ case conditions.IsStalled(obj):
+ return false, fmt.Sprintf("OCIRepository '%s/%s' is not ready: %s",
+ obj.GetNamespace(), obj.GetName(), conditions.GetMessage(obj, meta.StalledCondition))
+ case obj.Status.Artifact == nil:
+ return false, fmt.Sprintf("OCIRepository '%s/%s' is not ready: %s",
+ obj.GetNamespace(), obj.GetName(), "does not have an artifact")
+ default:
+ return true, ""
+ }
+}
+
+func isChartRefPresent(obj *v2.HelmRelease) bool {
+ return obj.Spec.ChartRef != nil
+}
+
+func isChartTemplatePresent(obj *v2.HelmRelease) bool {
+ return obj.Spec.Chart.Spec.Chart != ""
+}
+
+func isValidChartRef(obj *v2.HelmRelease) bool {
+ return (isChartRefPresent(obj) && !isChartTemplatePresent(obj)) ||
+ (!isChartRefPresent(obj) && isChartTemplatePresent(obj))
+}
+
+func getNamespacedName(obj *v2.HelmRelease) (types.NamespacedName, error) {
+ namespacedName := types.NamespacedName{}
+ switch {
+ case isChartRefPresent(obj) && !isChartTemplatePresent(obj):
+ namespacedName.Namespace = obj.Spec.ChartRef.Namespace
+ namespacedName.Name = obj.Spec.ChartRef.Name
+ case !isChartRefPresent(obj) && isChartTemplatePresent(obj):
+ namespacedName.Namespace = obj.Spec.Chart.GetNamespace(obj.GetNamespace())
+ namespacedName.Name = obj.GetHelmChartName()
+ default:
+ return namespacedName, fmt.Errorf("one of chartRef or chart must be present")
+ }
+
+ return namespacedName, nil
+}
diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go
index 54166b445..0c8138d79 100644
--- a/internal/controller/helmrelease_controller_test.go
+++ b/internal/controller/helmrelease_controller_test.go
@@ -2034,14 +2034,16 @@ func TestHelmReleaseReconciler_getHelmChart(t *testing.T) {
intacl.AllowCrossNamespaceRef = !tt.disallowCrossNS
t.Cleanup(func() { intacl.AllowCrossNamespaceRef = !curAllow })
- got, err := r.getHelmChart(context.TODO(), tt.rel)
+ got, err := r.getSource(context.TODO(), tt.rel)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
- expect := g.Expect(got.ObjectMeta)
+ hc, ok := got.(*sourcev1b2.HelmChart)
+ g.Expect(ok).To(BeTrue())
+ expect := g.Expect(hc.ObjectMeta)
if tt.expectChart {
expect.To(BeEquivalentTo(tt.chart.ObjectMeta))
} else {
@@ -2330,6 +2332,8 @@ func TestValuesReferenceValidation(t *testing.T) {
func Test_isHelmChartReady(t *testing.T) {
mock := &sourcev1b2.HelmChart{
ObjectMeta: metav1.ObjectMeta{
+ Name: "mock",
+ Namespace: "default",
Generation: 2,
},
Status: sourcev1b2.HelmChartStatus{
@@ -2363,7 +2367,7 @@ func Test_isHelmChartReady(t *testing.T) {
return m
}(),
want: false,
- wantReason: "latest generation of object has not been reconciled",
+ wantReason: "HelmChart 'default/mock' is not ready: latest generation of object has not been reconciled",
},
{
name: "chart generation differs from observed generation while Ready=False",
@@ -2374,7 +2378,7 @@ func Test_isHelmChartReady(t *testing.T) {
return m
}(),
want: false,
- wantReason: "some reason",
+ wantReason: "HelmChart 'default/mock' is not ready: some reason",
},
{
name: "chart has Stalled=True",
@@ -2385,7 +2389,7 @@ func Test_isHelmChartReady(t *testing.T) {
return m
}(),
want: false,
- wantReason: "some stalled reason",
+ wantReason: "HelmChart 'default/mock' is not ready: some stalled reason",
},
{
name: "chart does not have an Artifact",
@@ -2395,7 +2399,7 @@ func Test_isHelmChartReady(t *testing.T) {
return m
}(),
want: false,
- wantReason: "does not have an artifact",
+ wantReason: "HelmChart 'default/mock' is not ready: does not have an artifact",
},
}
for _, tt := range tests {
diff --git a/internal/reconcile/helmchart_template.go b/internal/reconcile/helmchart_template.go
index 5bf5a9630..d41e9d4d6 100644
--- a/internal/reconcile/helmchart_template.go
+++ b/internal/reconcile/helmchart_template.go
@@ -76,6 +76,24 @@ func NewHelmChartTemplate(client client.Client, recorder record.EventRecorder, f
}
}
+func isChartRefPresent(obj *v2.HelmRelease) bool {
+ return obj.Spec.ChartRef != nil
+}
+
+func isChartTemplatePresent(obj *v2.HelmRelease) bool {
+ return obj.Spec.Chart.Spec.Chart != ""
+}
+
+func mustCleanDeployedChart(obj *v2.HelmRelease) bool {
+ if isChartRefPresent(obj) && !isChartTemplatePresent(obj) {
+ if obj.Status.HelmChart != "" {
+ return true
+ }
+ }
+
+ return false
+}
+
func (r *HelmChartTemplate) Reconcile(ctx context.Context, req *Request) error {
var (
obj = req.Object
@@ -95,6 +113,20 @@ func (r *HelmChartTemplate) Reconcile(ctx context.Context, req *Request) error {
}
}
+ if mustCleanDeployedChart(obj) {
+ // If the HelmRelease has a ChartRef and no Chart template, and the
+ // HelmChart is present, we need to clean it up.
+ if err := r.reconcileDelete(ctx, req.Object); err != nil {
+ return err
+ }
+ return nil
+ }
+
+ if isChartRefPresent(obj) {
+ // if a chartRef is present, we do not need to reconcile the HelmChart
+ return nil
+ }
+
// Confirm we are allowed to fetch the HelmChart.
if err := acl.AllowsAccessTo(req.Object, sourcev1.HelmChartKind, chartRef); err != nil {
return err