diff --git a/internal/reconcile/release.go b/internal/reconcile/release.go index a83ddb2e1..825d81291 100644 --- a/internal/reconcile/release.go +++ b/internal/reconcile/release.go @@ -104,6 +104,12 @@ func mutateOCIDigest(obj *v2.HelmRelease, obs release.Observation) release.Obser return obs } +func releaseToObservation(rls *helmrelease.Release, snapshot *v2.Snapshot) release.Observation { + obs := release.ObserveRelease(rls) + obs.OCIDigest = snapshot.OCIDigest + return obs +} + // observeRelease returns a storage.ObserveFunc that stores the observed // releases in the given observedReleases map. // It can be used for Helm actions that modify multiple releases in the diff --git a/internal/reconcile/rollback_remediation.go b/internal/reconcile/rollback_remediation.go index 2bb91e9bf..3fbad7099 100644 --- a/internal/reconcile/rollback_remediation.go +++ b/internal/reconcile/rollback_remediation.go @@ -182,7 +182,7 @@ func observeRollback(obj *v2.HelmRelease) storage.ObserveFunc { for i := range obj.Status.History { snap := obj.Status.History[i] if snap.Targets(rls.Name, rls.Namespace, rls.Version) { - newSnap := release.ObservedToSnapshot(release.ObserveRelease(rls)) + newSnap := release.ObservedToSnapshot(releaseToObservation(rls, snap)) newSnap.SetTestHooks(snap.GetTestHooks()) obj.Status.History[i] = newSnap return diff --git a/internal/reconcile/rollback_remediation_test.go b/internal/reconcile/rollback_remediation_test.go index 2300aefc3..71d0bb134 100644 --- a/internal/reconcile/rollback_remediation_test.go +++ b/internal/reconcile/rollback_remediation_test.go @@ -613,4 +613,47 @@ func Test_observeRollback(t *testing.T) { expect, })) }) + + t.Run("rollback with update to previous deployed with OCI Digest", func(t *testing.T) { + g := NewWithT(t) + + previous := &v2.Snapshot{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 2, + Status: helmrelease.StatusFailed.String(), + OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6", + } + latest := &v2.Snapshot{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 3, + Status: helmrelease.StatusDeployed.String(), + OCIDigest: "sha256:aedc2b0de1576a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6", + } + + obj := &v2.HelmRelease{ + Status: v2.HelmReleaseStatus{ + History: v2.Snapshots{ + latest, + previous, + }, + }, + } + rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{ + Name: previous.Name, + Namespace: previous.Namespace, + Version: previous.Version, + Status: helmrelease.StatusSuperseded, + }) + obs := release.ObserveRelease(rls) + obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6" + expect := release.ObservedToSnapshot(obs) + + observeRollback(obj)(rls) + g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{ + latest, + expect, + })) + }) } diff --git a/internal/reconcile/test.go b/internal/reconcile/test.go index e71f1ddcf..56e9882ef 100644 --- a/internal/reconcile/test.go +++ b/internal/reconcile/test.go @@ -200,7 +200,8 @@ func observeTest(obj *v2.HelmRelease) storage.ObserveFunc { } // Update the latest snapshot with the test result. - tested := release.ObservedToSnapshot(release.ObserveRelease(rls)) + latest := obj.Status.History.Latest() + tested := release.ObservedToSnapshot(releaseToObservation(rls, latest)) tested.SetTestHooks(release.TestHooksFromRelease(rls)) obj.Status.History[0] = tested } diff --git a/internal/reconcile/test_test.go b/internal/reconcile/test_test.go index d97dbe0c9..8ed0bdd8c 100644 --- a/internal/reconcile/test_test.go +++ b/internal/reconcile/test_test.go @@ -376,6 +376,38 @@ func Test_observeTest(t *testing.T) { })) }) + t.Run("test with current OCI Digest", func(t *testing.T) { + g := NewWithT(t) + + obj := &v2.HelmRelease{ + Status: v2.HelmReleaseStatus{ + History: v2.Snapshots{ + &v2.Snapshot{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 1, + OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6", + }, + }, + }, + } + rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 1, + }, testutil.ReleaseWithHooks(testHookFixtures)) + + obs := release.ObserveRelease(rls) + obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6" + expect := release.ObservedToSnapshot(obs) + expect.SetTestHooks(release.TestHooksFromRelease(rls)) + + observeTest(obj)(rls) + g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{ + expect, + })) + }) + t.Run("test targeting different version than latest", func(t *testing.T) { g := NewWithT(t) diff --git a/internal/reconcile/uninstall.go b/internal/reconcile/uninstall.go index 3c5f8a769..f92843499 100644 --- a/internal/reconcile/uninstall.go +++ b/internal/reconcile/uninstall.go @@ -224,7 +224,7 @@ func observeUninstall(obj *v2.HelmRelease) storage.ObserveFunc { for i := range obj.Status.History { snap := obj.Status.History[i] if snap.Targets(rls.Name, rls.Namespace, rls.Version) { - newSnap := release.ObservedToSnapshot(release.ObserveRelease(rls)) + newSnap := release.ObservedToSnapshot(releaseToObservation(rls, snap)) newSnap.SetTestHooks(snap.GetTestHooks()) obj.Status.History[i] = newSnap return diff --git a/internal/reconcile/uninstall_test.go b/internal/reconcile/uninstall_test.go index ec0a9e23a..65b118ccc 100644 --- a/internal/reconcile/uninstall_test.go +++ b/internal/reconcile/uninstall_test.go @@ -702,4 +702,36 @@ func Test_observeUninstall(t *testing.T) { current, })) }) + t.Run("uninstall of current with OCI Digest", func(t *testing.T) { + g := NewWithT(t) + + current := &v2.Snapshot{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 1, + Status: helmrelease.StatusDeployed.String(), + OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6", + } + obj := &v2.HelmRelease{ + Status: v2.HelmReleaseStatus{ + History: v2.Snapshots{ + current, + }, + }, + } + rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: current.Name, + Namespace: current.Namespace, + Version: current.Version, + Status: helmrelease.StatusUninstalled, + }) + obs := release.ObserveRelease(rls) + obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6" + expect := release.ObservedToSnapshot(obs) + + observeUninstall(obj)(rls) + g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{ + expect, + })) + }) } diff --git a/internal/reconcile/unlock.go b/internal/reconcile/unlock.go index e08bd558c..e6e4da5fa 100644 --- a/internal/reconcile/unlock.go +++ b/internal/reconcile/unlock.go @@ -77,7 +77,7 @@ func (r *Unlock) Reconcile(_ context.Context, req *Request) error { } // Ensure the release is in a pending state. - cur := release.ObservedToSnapshot(release.ObserveRelease(rls)) + cur := processCurrentSnaphot(req.Object, rls) if status := rls.Info.Status; status.IsPending() { // Update pending status to failed and persist. rls.SetStatus(helmrelease.StatusFailed, fmt.Sprintf("Release unlocked from stale '%s' state", status.String())) @@ -154,9 +154,23 @@ func observeUnlock(obj *v2.HelmRelease) storage.ObserveFunc { for i := range obj.Status.History { snap := obj.Status.History[i] if snap.Targets(rls.Name, rls.Namespace, rls.Version) { - obj.Status.History[i] = release.ObservedToSnapshot(release.ObserveRelease(rls)) + obj.Status.History[i] = release.ObservedToSnapshot(releaseToObservation(rls, snap)) return } } } } + +// processCurrentSnaphot processes the current snapshot based on a Helm release. +// It also looks for the OCIDigest in the corresponding v2.HelmRelease history and +// updates the current snapshot with the OCIDigest if found. +func processCurrentSnaphot(obj *v2.HelmRelease, rls *helmrelease.Release) *v2.Snapshot { + cur := release.ObservedToSnapshot(release.ObserveRelease(rls)) + for i := range obj.Status.History { + snap := obj.Status.History[i] + if snap.Targets(rls.Name, rls.Namespace, rls.Version) { + cur.OCIDigest = snap.OCIDigest + } + } + return cur +} diff --git a/internal/reconcile/unlock_test.go b/internal/reconcile/unlock_test.go index 6799fe198..7b13c8300 100644 --- a/internal/reconcile/unlock_test.go +++ b/internal/reconcile/unlock_test.go @@ -457,6 +457,95 @@ func TestUnlock_success(t *testing.T) { })) } +func TestUnlock_withOCIDigest(t *testing.T) { + g := NewWithT(t) + + namedNS, err := testEnv.CreateNamespace(context.TODO(), mockReleaseNamespace) + g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + _ = testEnv.Delete(context.TODO(), namedNS) + }) + releaseNamespace := namedNS.Name + + rls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: releaseNamespace, + Chart: testutil.BuildChart(), + Version: 4, + Status: helmrelease.StatusPendingInstall, + }) + + obs := release.ObserveRelease(rls) + obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6" + snap := release.ObservedToSnapshot(obs) + + obj := &v2.HelmRelease{ + Spec: v2.HelmReleaseSpec{ + ReleaseName: mockReleaseName, + TargetNamespace: releaseNamespace, + StorageNamespace: releaseNamespace, + Timeout: &metav1.Duration{Duration: 100 * time.Millisecond}, + }, + Status: v2.HelmReleaseStatus{ + History: v2.Snapshots{ + snap, + }, + }, + } + + getter, err := RESTClientGetterFromManager(testEnv.Manager, obj.GetReleaseNamespace()) + g.Expect(err).ToNot(HaveOccurred()) + + cfg, err := action.NewConfigFactory(getter, + action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()), + ) + g.Expect(err).ToNot(HaveOccurred()) + + store := helmstorage.Init(cfg.Driver) + g.Expect(store.Create(rls)).To(Succeed()) + + recorder := testutil.NewFakeRecorder(10, false) + got := NewUnlock(cfg, recorder).Reconcile(context.TODO(), &Request{ + Object: obj, + }) + + g.Expect(got).ToNot(HaveOccurred()) + + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions( + []metav1.Condition{ + *conditions.FalseCondition(meta.ReadyCondition, "PendingRelease", "Unlocked Helm release"), + *conditions.FalseCondition(v2.ReleasedCondition, "PendingRelease", "Unlocked Helm release"), + })) + + releases, _ := store.History(mockReleaseName) + helmreleaseutil.SortByRevision(releases) + expected := release.ObserveRelease(releases[0]) + expected.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6" + g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{ + release.ObservedToSnapshot(expected), + })) + + expectMsg := fmt.Sprintf(fmtUnlockSuccess, + fmt.Sprintf("%s/%s.v%d", rls.Namespace, snap.Name, snap.Version), + fmt.Sprintf("%s@%s", rls.Chart.Name(), rls.Chart.Metadata.Version), + rls.Info.Status.String()) + + g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{ + { + Type: corev1.EventTypeNormal, + Reason: "PendingRelease", + Message: expectMsg, + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + eventMetaGroupKey(metaOCIDigestKey): expected.OCIDigest, + eventMetaGroupKey(eventv1.MetaRevisionKey): rls.Chart.Metadata.Version, + eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, rls.Config).String(), + }, + }, + }, + })) +} + func Test_observeUnlock(t *testing.T) { t.Run("unlock", func(t *testing.T) { g := NewWithT(t) @@ -487,6 +576,38 @@ func Test_observeUnlock(t *testing.T) { })) }) + t.Run("unlock with OCI Digest", func(t *testing.T) { + g := NewWithT(t) + + obj := &v2.HelmRelease{ + Status: v2.HelmReleaseStatus{ + History: v2.Snapshots{ + { + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 1, + Status: helmrelease.StatusPendingRollback.String(), + OCIDigest: "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6", + }, + }, + }, + } + rls := helmrelease.Mock(&helmrelease.MockReleaseOptions{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + Version: 1, + Status: helmrelease.StatusFailed, + }) + obs := release.ObserveRelease(rls) + obs.OCIDigest = "sha256:fcdc2b0de1581a3633ada4afee3f918f6eaa5b5ab38c3fef03d5b48d3f85d9f6" + expect := release.ObservedToSnapshot(obs) + observeUnlock(obj)(rls) + + g.Expect(obj.Status.History).To(testutil.Equal(v2.Snapshots{ + expect, + })) + }) + t.Run("unlock without current", func(t *testing.T) { g := NewWithT(t)