From 48e2f0a8c6084dd71a71fc7688dc329b8e9dd5c0 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Thu, 4 Apr 2024 11:30:59 +0200 Subject: [PATCH] Take into account the oci-digest This commit add the oci artifact digest into the release observed snapshot. This is used to later to add that value as an annotation. Signed-off-by: Soule BA --- api/v2beta2/helmrelease_types.go | 7 +- api/v2beta2/snapshot_types.go | 3 + .../helm.toolkit.fluxcd.io_helmreleases.yaml | 15 +- docs/api/v2beta2/helm.md | 27 ++- docs/spec/v2beta2/helmreleases.md | 16 -- internal/action/verify.go | 5 + internal/controller/helmrelease_controller.go | 23 ++- .../controller/helmrelease_controller_test.go | 14 +- internal/reconcile/correct_cluster_drift.go | 4 +- internal/reconcile/install.go | 7 +- internal/reconcile/install_test.go | 4 + internal/reconcile/release.go | 51 ++++- internal/reconcile/release_test.go | 178 ++++++++++++++++++ internal/reconcile/rollback_remediation.go | 4 +- internal/reconcile/test.go | 4 +- internal/reconcile/uninstall.go | 4 +- internal/reconcile/uninstall_remediation.go | 4 +- internal/reconcile/unlock.go | 4 +- internal/reconcile/upgrade.go | 7 +- internal/reconcile/upgrade_test.go | 4 + internal/release/digest_test.go | 2 +- internal/release/observation.go | 3 + 22 files changed, 338 insertions(+), 52 deletions(-) diff --git a/api/v2beta2/helmrelease_types.go b/api/v2beta2/helmrelease_types.go index 01a174653..9db38102f 100644 --- a/api/v2beta2/helmrelease_types.go +++ b/api/v2beta2/helmrelease_types.go @@ -1002,11 +1002,16 @@ type HelmReleaseStatus struct { LastAppliedRevision string `json:"lastAppliedRevision,omitempty"` // LastAttemptedRevision is the Source revision of the last reconciliation - // attempt. For OCIRegistry sources, the 12 first characters of the digest are + // attempt. For OCIRepository sources, the 12 first characters of the digest are // appended to the chart version e.g. "1.2.3+1234567890ab". // +optional LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"` + // LastAttemptedRevisionDigest is the digest of the last reconciliation attempt. + // This is only set for OCIRepository sources. + // +optional + LastAttemptedRevisionDigest string `json:"lastAttemptedRevisionDigest,omitempty"` + // LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last // reconciliation attempt. // Deprecated: Use LastAttemptedConfigDigest instead. diff --git a/api/v2beta2/snapshot_types.go b/api/v2beta2/snapshot_types.go index 587667665..6dcdb3457 100644 --- a/api/v2beta2/snapshot_types.go +++ b/api/v2beta2/snapshot_types.go @@ -156,6 +156,9 @@ type Snapshot struct { // run by the controller. // +optional TestHooks *map[string]*TestHookStatus `json:"testHooks,omitempty"` + // OciDigest is the digest of the OCI artifact associated with the release. + // +optional + OciDigest string `json:"ociDigest,omitempty"` } // FullReleaseName returns the full name of the release in the format diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index 429c43e3e..2430f0312 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -1100,6 +1100,10 @@ spec: description: Namespace is the namespace the release is deployed to. type: string + ociDigest: + description: OciDigest is the digest of the OCI artifact associated + with the release. + type: string status: description: Status is the current state of the release. type: string @@ -2374,6 +2378,10 @@ spec: description: Namespace is the namespace the release is deployed to. type: string + ociDigest: + description: OciDigest is the digest of the OCI artifact associated + with the release. + type: string status: description: Status is the current state of the release. type: string @@ -2452,9 +2460,14 @@ spec: lastAttemptedRevision: description: |- LastAttemptedRevision is the Source revision of the last reconciliation - attempt. For OCIRegistry sources, the 12 first characters of the digest are + attempt. For OCIRepository sources, the 12 first characters of the digest are appended to the chart version e.g. "1.2.3+1234567890ab". type: string + lastAttemptedRevisionDigest: + description: |- + LastAttemptedRevisionDigest is the digest of the last reconciliation attempt. + This is only set for OCIRepository sources. + type: string lastAttemptedValuesChecksum: description: |- LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last diff --git a/docs/api/v2beta2/helm.md b/docs/api/v2beta2/helm.md index 5decee575..4a00e1f6e 100644 --- a/docs/api/v2beta2/helm.md +++ b/docs/api/v2beta2/helm.md @@ -1584,12 +1584,25 @@ string (Optional)

LastAttemptedRevision is the Source revision of the last reconciliation -attempt. For OCIRegistry sources, the 12 first characters of the digest are +attempt. For OCIRepository sources, the 12 first characters of the digest are appended to the chart version e.g. “1.2.3+1234567890ab”.

+lastAttemptedRevisionDigest
+ +string + + + +(Optional) +

LastAttemptedRevisionDigest is the digest of the last reconciliation attempt. +This is only set for OCIRepository sources.

+ + + + lastAttemptedValuesChecksum
string @@ -2380,6 +2393,18 @@ TestHookStatus run by the controller.

+ + +ociDigest
+ +string + + + +(Optional) +

OciDigest is the digest of the OCI artifact associated with the release.

+ + diff --git a/docs/spec/v2beta2/helmreleases.md b/docs/spec/v2beta2/helmreleases.md index 159d37054..a8efba435 100644 --- a/docs/spec/v2beta2/helmreleases.md +++ b/docs/spec/v2beta2/helmreleases.md @@ -242,26 +242,10 @@ metadata: namespace: default spec: interval: 10m - timeout: 5m chartRef: kind: OCIRepository name: podinfo namespace: default - releaseName: podinfo - install: - remediation: - retries: 3 - upgrade: - remediation: - retries: 3 - test: - enable: true - driftDetection: - mode: enabled - ignore: - - paths: ["/spec/replicas"] - target: - kind: Deployment values: replicaCount: 2 ``` diff --git a/internal/action/verify.go b/internal/action/verify.go index 3c6852260..84d1a1978 100644 --- a/internal/action/verify.go +++ b/internal/action/verify.go @@ -126,6 +126,11 @@ func VerifyReleaseObject(snapshot *v2.Snapshot, rls *helmrelease.Release) error verifier := relDig.Verifier() obs := release.ObserveRelease(rls) + + // unfortunately we have to pass in the OciDigest as is, because helmrelease.Release + // does not have a field for it. + obs.OciDigest = snapshot.OciDigest + if err = obs.Encode(verifier); err != nil { // We are expected to be able to encode valid JSON, error out without a // typed error assuming malfunction to signal to e.g. retry. diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 0615bb9d4..e3cf00fc9 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -335,7 +335,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress") } - err = mutateChartWithSourceRevision(loadedChart, source) + ociDigest, err := mutateChartWithSourceRevision(loadedChart, source) if err != nil { conditions.MarkFalse(obj, meta.ReadyCondition, "ChartMutateError", err.Error()) return ctrl.Result{}, err @@ -388,6 +388,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe // Set last attempt values. obj.Status.LastAttemptedGeneration = obj.Generation obj.Status.LastAttemptedRevision = loadedChart.Metadata.Version + obj.Status.LastAttemptedRevisionDigest = ociDigest obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, values).String() obj.Status.LastAttemptedValuesChecksum = "" obj.Status.LastReleaseRevision = 0 @@ -865,24 +866,28 @@ func getNamespacedName(obj *v2.HelmRelease) (types.NamespacedName, error) { return namespacedName, nil } -func mutateChartWithSourceRevision(chart *chart.Chart, source source.Source) error { +func mutateChartWithSourceRevision(chart *chart.Chart, source source.Source) (string, error) { // If the source is an OCIRepository, we can try to mutate the chart version // with the artifact revision. The revision is either a @ or // just a digest. obj, ok := source.(*sourcev1.OCIRepository) if !ok { - return nil + // if not make sure to return an empty string to delete the digest of the + // last attempted revision + return "", nil } ver, err := semver.NewVersion(chart.Metadata.Version) if err != nil { - return err + return "", err } + + var ociDigest string revision := obj.GetArtifact().Revision switch { case strings.Contains(revision, "@"): tagD := strings.Split(revision, "@") if len(tagD) != 2 || tagD[0] != chart.Metadata.Version { - return fmt.Errorf("artifact revision %s does not match chart version %s", tagD[0], chart.Metadata.Version) + return "", fmt.Errorf("artifact revision %s does not match chart version %s", tagD[0], chart.Metadata.Version) } // algotithm are sha256, sha384, sha512 with the canonical being sha256 // So every digest starts with a sha algorithm and a colon @@ -890,17 +895,19 @@ func mutateChartWithSourceRevision(chart *chart.Chart, source source.Source) err // add the digest to the chart version to make sure mutable tags are detected *ver, err = ver.SetMetadata(sha[0:12]) if err != nil { - return err + return "", err } + ociDigest = sha default: // default to the digest sha := strings.Split(revision, ":")[1] *ver, err = ver.SetMetadata(sha[0:12]) if err != nil { - return err + return "", err } + ociDigest = sha } chart.Metadata.Version = ver.String() - return nil + return ociDigest, nil } diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go index f6e4c398a..e54b495fc 100644 --- a/internal/controller/helmrelease_controller_test.go +++ b/internal/controller/helmrelease_controller_test.go @@ -186,6 +186,10 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) { g := NewWithT(t) chart := &sourcev1b2.HelmChart{ + TypeMeta: metav1.TypeMeta{ + APIVersion: sourcev1b2.GroupVersion.String(), + Kind: sourcev1b2.HelmChartKind, + }, ObjectMeta: metav1.ObjectMeta{ Name: "chart", Namespace: "mock", @@ -238,6 +242,10 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) { g := NewWithT(t) chart := &sourcev1b2.HelmChart{ + TypeMeta: metav1.TypeMeta{ + APIVersion: sourcev1b2.GroupVersion.String(), + Kind: sourcev1b2.HelmChartKind, + }, ObjectMeta: metav1.ObjectMeta{ Name: "chart", Namespace: "mock", @@ -960,6 +968,10 @@ func TestHelmReleaseReconciler_reconcileReleaseFromOCIRepositorySource(t *testin g := NewWithT(t) ocirepo := &sourcev1b2.OCIRepository{ + TypeMeta: metav1.TypeMeta{ + APIVersion: sourcev1b2.GroupVersion.String(), + Kind: sourcev1b2.OCIRepositoryKind, + }, ObjectMeta: metav1.ObjectMeta{ Name: "ocirepo", Namespace: "mock", @@ -2972,7 +2984,7 @@ func Test_TryMutateChartWithSourceRevision(t *testing.T) { }, } - err := mutateChartWithSourceRevision(c, s) + _, err := mutateChartWithSourceRevision(c, s) if tt.wantErr { g.Expect(err).To(HaveOccurred()) } else { diff --git a/internal/reconcile/correct_cluster_drift.go b/internal/reconcile/correct_cluster_drift.go index bbd6f7a31..d46f0172d 100644 --- a/internal/reconcile/correct_cluster_drift.go +++ b/internal/reconcile/correct_cluster_drift.go @@ -99,10 +99,10 @@ func (r *CorrectClusterDrift) report(obj *v2.HelmRelease, changeSet *ssa.ChangeS sb.WriteString(changeSet.String()) } - r.eventRecorder.AnnotatedEventf(obj, eventMeta(cur.ChartVersion, cur.ConfigDigest), corev1.EventTypeWarning, + r.eventRecorder.AnnotatedEventf(obj, eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeWarning, "DriftCorrectionFailed", sb.String()) case changeSet != nil && len(changeSet.Entries) > 0: - r.eventRecorder.AnnotatedEventf(obj, eventMeta(cur.ChartVersion, cur.ConfigDigest), corev1.EventTypeNormal, + r.eventRecorder.AnnotatedEventf(obj, eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, "DriftCorrected", "Cluster state of release %s has been corrected:\n%s", obj.Status.History.Latest().FullReleaseName(), changeSet.String()) } diff --git a/internal/reconcile/install.go b/internal/reconcile/install.go index 0567b549d..367beb7ab 100644 --- a/internal/reconcile/install.go +++ b/internal/reconcile/install.go @@ -92,7 +92,7 @@ func (r *Install) Reconcile(ctx context.Context, req *Request) error { _, err := action.Install(ctx, cfg, req.Object, req.Chart, req.Values) // Record the history of releases observed during the install. - obsReleases.recordOnObject(req.Object) + obsReleases.recordOnObject(req.Object, mutateOciDigest) if err != nil { r.failure(req, logBuf, err) @@ -154,7 +154,8 @@ func (r *Install) failure(req *Request, buffer *action.LogBuffer, err error) { // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(req.Chart.Metadata.Version, chartutil.DigestValues(digest.Canonical, req.Values).String()), + eventMeta(req.Chart.Metadata.Version, chartutil.DigestValues(digest.Canonical, req.Values).String(), + addOciDigest(req.Object.Status.LastAttemptedRevisionDigest)), corev1.EventTypeWarning, v2.InstallFailedReason, eventMessageWithLog(msg, buffer), @@ -181,7 +182,7 @@ func (r *Install) success(req *Request) { // Record event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, v2.InstallSucceededReason, msg, diff --git a/internal/reconcile/install_test.go b/internal/reconcile/install_test.go index d156e45ca..4b4fe3efe 100644 --- a/internal/reconcile/install_test.go +++ b/internal/reconcile/install_test.go @@ -307,6 +307,9 @@ func TestInstall_failure(t *testing.T) { ReleaseName: mockReleaseName, TargetNamespace: mockReleaseNamespace, }, + Status: v2.HelmReleaseStatus{ + LastAttemptedRevisionDigest: "sha256:1234567890", + }, } chrt = testutil.BuildChart() err = errors.New("installation error") @@ -337,6 +340,7 @@ func TestInstall_failure(t *testing.T) { Message: expectMsg, ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ + eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest, eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version, eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(), }, diff --git a/internal/reconcile/release.go b/internal/reconcile/release.go index ce0a74d50..89239c419 100644 --- a/internal/reconcile/release.go +++ b/internal/reconcile/release.go @@ -43,6 +43,10 @@ var ( ErrReleaseMismatch = errors.New("release mismatch") ) +// mutateObservedRelease is a function that mutates the Observation with the +// given HelmRelease object. +type mutateObservedRelease func(*v2.HelmRelease, release.Observation) release.Observation + // observedReleases is a map of Helm releases as observed to be written to the // Helm storage. The key is the version of the release. type observedReleases map[int]release.Observation @@ -58,7 +62,7 @@ func (r observedReleases) sortedVersions() (versions []int) { } // recordOnObject records the observed releases on the HelmRelease object. -func (r observedReleases) recordOnObject(obj *v2.HelmRelease) { +func (r observedReleases) recordOnObject(obj *v2.HelmRelease, mutators ...mutateObservedRelease) { switch len(r) { case 0: return @@ -67,17 +71,27 @@ func (r observedReleases) recordOnObject(obj *v2.HelmRelease) { for _, o := range r { obs = o } + for _, mut := range mutators { + obs = mut(obj, obs) + } obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...) default: versions := r.sortedVersions() - - obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(r[versions[0]])}, obj.Status.History...) + obs := r[versions[0]] + for _, mut := range mutators { + obs = mut(obj, obs) + } + obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...) for _, ver := range versions[1:] { for i := range obj.Status.History { snap := obj.Status.History[i] if snap.Targets(r[ver].Name, r[ver].Namespace, r[ver].Version) { - newSnap := release.ObservedToSnapshot(r[ver]) + obs := r[ver] + for _, mut := range mutators { + obs = mut(obj, obs) + } + newSnap := release.ObservedToSnapshot(obs) newSnap.SetTestHooks(snap.GetTestHooks()) obj.Status.History[i] = newSnap return @@ -87,6 +101,11 @@ func (r observedReleases) recordOnObject(obj *v2.HelmRelease) { } } +func mutateOciDigest(obj *v2.HelmRelease, obs release.Observation) release.Observation { + obs.OciDigest = obj.Status.LastAttemptedRevisionDigest + 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 @@ -174,9 +193,15 @@ func eventMessageWithLog(msg string, log *action.LogBuffer) string { return msg } +// addMeta is a function that adds metadata to an event map. +type addMeta func(map[string]string) + +// metaOCIDigestKey is the key for the OCI digest metadata. +const metaOCIDigestKey = "oci-digest" + // eventMeta returns the event (annotation) metadata based on the given // parameters. -func eventMeta(revision, token string) map[string]string { +func eventMeta(revision, token string, metas ...addMeta) map[string]string { var metadata map[string]string if revision != "" || token != "" { metadata = make(map[string]string) @@ -187,9 +212,25 @@ func eventMeta(revision, token string) map[string]string { metadata[eventMetaGroupKey(eventv1.MetaTokenKey)] = token } } + + for _, add := range metas { + add(metadata) + } + return metadata } +func addOciDigest(digest string) addMeta { + return func(m map[string]string) { + if digest != "" { + if m == nil { + m = make(map[string]string) + } + m[eventMetaGroupKey(metaOCIDigestKey)] = digest + } + } +} + // eventMetaGroupKey returns the event (annotation) metadata key prefixed with // the group. func eventMetaGroupKey(key string) string { diff --git a/internal/reconcile/release_test.go b/internal/reconcile/release_test.go index 167b8df72..d6ebaa7c5 100644 --- a/internal/reconcile/release_test.go +++ b/internal/reconcile/release_test.go @@ -17,10 +17,12 @@ limitations under the License. package reconcile import ( + "fmt" "testing" "github.com/go-logr/logr" . "github.com/onsi/gomega" + "helm.sh/helm/v3/pkg/chart" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/fluxcd/pkg/apis/meta" @@ -454,3 +456,179 @@ func mockLogBuffer(size int, lines int) *action.LogBuffer { } return log } + +func Test_RecordOnObject(t *testing.T) { + tests := []struct { + name string + obj *v2.HelmRelease + r observedReleases + mutate bool + testFunc func(*v2.HelmRelease) error + }{ + { + name: "record observed releases", + obj: &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + }, + }, + r: observedReleases{ + 1: { + Name: mockReleaseName, + Version: 1, + ChartMetadata: chart.Metadata{ + Name: mockReleaseName, + Version: "1.0.0", + }, + }, + }, + testFunc: func(obj *v2.HelmRelease) error { + if len(obj.Status.History) != 1 { + return fmt.Errorf("history length is not 1") + } + if obj.Status.History[0].Name != mockReleaseName { + return fmt.Errorf("release name is not %s", mockReleaseName) + } + return nil + }, + }, + { + name: "record observed releases with multiple versions", + obj: &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + }, + }, + r: observedReleases{ + 1: { + Name: mockReleaseName, + Version: 1, + ChartMetadata: chart.Metadata{ + Name: mockReleaseName, + Version: "1.0.0", + }, + }, + 2: { + Name: mockReleaseName, + Version: 2, + ChartMetadata: chart.Metadata{ + Name: mockReleaseName, + Version: "2.0.0", + }, + }, + }, + testFunc: func(obj *v2.HelmRelease) error { + if len(obj.Status.History) != 1 { + return fmt.Errorf("want history length 1, got %d", len(obj.Status.History)) + } + if obj.Status.History[0].Name != mockReleaseName { + return fmt.Errorf("release name is not %s", mockReleaseName) + } + if obj.Status.History[0].ChartVersion != "2.0.0" { + return fmt.Errorf("want chart version %s, got %s", "2.0.0", obj.Status.History[0].ChartVersion) + } + return nil + }, + }, + { + name: "record observed releases with status digest", + obj: &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + }, + Status: v2.HelmReleaseStatus{ + LastAttemptedRevisionDigest: "sha256:123456", + }, + }, + r: observedReleases{ + 1: { + Name: mockReleaseName, + Version: 1, + ChartMetadata: chart.Metadata{ + Name: mockReleaseName, + Version: "1.0.0", + }, + }, + }, + mutate: true, + testFunc: func(obj *v2.HelmRelease) error { + h := obj.Status.History.Latest() + if h.Name != mockReleaseName { + return fmt.Errorf("release name is not %s", mockReleaseName) + } + if h.ChartVersion != "1.0.0" { + return fmt.Errorf("want chart version %s, got %s", "1.0.0", h.ChartVersion) + } + if h.OciDigest != obj.Status.LastAttemptedRevisionDigest { + return fmt.Errorf("want digest %s, got %s", obj.Status.LastAttemptedRevisionDigest, h.OciDigest) + } + return nil + }, + }, + { + name: "record observed releases with multiple versions and status digest", + obj: &v2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: mockReleaseName, + Namespace: mockReleaseNamespace, + }, + Status: v2.HelmReleaseStatus{ + LastAttemptedRevisionDigest: "sha256:123456", + }, + }, + r: observedReleases{ + 1: { + Name: mockReleaseName, + Version: 1, + ChartMetadata: chart.Metadata{ + Name: mockReleaseName, + Version: "1.0.0", + }, + }, + 2: { + Name: mockReleaseName, + Version: 2, + ChartMetadata: chart.Metadata{ + Name: mockReleaseName, + Version: "2.0.0", + }, + }, + }, + mutate: true, + testFunc: func(obj *v2.HelmRelease) error { + if len(obj.Status.History) != 1 { + return fmt.Errorf("want history length 1, got %d", len(obj.Status.History)) + } + h := obj.Status.History.Latest() + if h.Name != mockReleaseName { + return fmt.Errorf("release name is not %s", mockReleaseName) + } + if h.ChartVersion != "2.0.0" { + return fmt.Errorf("want chart version %s, got %s", "2.0.0", h.ChartVersion) + } + if h.OciDigest != obj.Status.LastAttemptedRevisionDigest { + return fmt.Errorf("want digest %s, got %s", obj.Status.LastAttemptedRevisionDigest, h.OciDigest) + } + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + if tt.mutate { + tt.r.recordOnObject(tt.obj, mutateOciDigest) + } else { + tt.r.recordOnObject(tt.obj) + } + err := tt.testFunc(tt.obj) + g.Expect(err).ToNot(HaveOccurred()) + }) + } + +} diff --git a/internal/reconcile/rollback_remediation.go b/internal/reconcile/rollback_remediation.go index e614f5a30..d0073a8ef 100644 --- a/internal/reconcile/rollback_remediation.go +++ b/internal/reconcile/rollback_remediation.go @@ -143,7 +143,7 @@ func (r *RollbackRemediation) failure(req *Request, prev *v2.Snapshot, buffer *a // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(prev.ChartVersion, chartutil.DigestValues(digest.Canonical, req.Values).String()), + eventMeta(prev.ChartVersion, chartutil.DigestValues(digest.Canonical, req.Values).String(), addOciDigest(prev.OciDigest)), corev1.EventTypeWarning, v2.RollbackFailedReason, eventMessageWithLog(msg, buffer), @@ -162,7 +162,7 @@ func (r *RollbackRemediation) success(req *Request, prev *v2.Snapshot) { // Record event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(prev.ChartVersion, chartutil.DigestValues(digest.Canonical, req.Values).String()), + eventMeta(prev.ChartVersion, chartutil.DigestValues(digest.Canonical, req.Values).String(), addOciDigest(prev.OciDigest)), corev1.EventTypeNormal, v2.RollbackSucceededReason, msg, diff --git a/internal/reconcile/test.go b/internal/reconcile/test.go index f40a3d4cf..49a5d91cc 100644 --- a/internal/reconcile/test.go +++ b/internal/reconcile/test.go @@ -145,7 +145,7 @@ func (r *Test) failure(req *Request, err error) { // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeWarning, v2.TestFailedReason, msg, @@ -181,7 +181,7 @@ func (r *Test) success(req *Request) { // Record event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, v2.TestSucceededReason, msg, diff --git a/internal/reconcile/uninstall.go b/internal/reconcile/uninstall.go index 8c8158745..f8721b832 100644 --- a/internal/reconcile/uninstall.go +++ b/internal/reconcile/uninstall.go @@ -180,7 +180,7 @@ func (r *Uninstall) failure(req *Request, buffer *action.LogBuffer, err error) { // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeWarning, v2.UninstallFailedReason, eventMessageWithLog(msg, buffer), ) @@ -201,7 +201,7 @@ func (r *Uninstall) success(req *Request) { // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, v2.UninstallSucceededReason, msg, diff --git a/internal/reconcile/uninstall_remediation.go b/internal/reconcile/uninstall_remediation.go index 4e244cdc0..34798cff0 100644 --- a/internal/reconcile/uninstall_remediation.go +++ b/internal/reconcile/uninstall_remediation.go @@ -154,7 +154,7 @@ func (r *UninstallRemediation) failure(req *Request, buffer *action.LogBuffer, e // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeWarning, v2.UninstallFailedReason, eventMessageWithLog(msg, buffer), @@ -175,7 +175,7 @@ func (r *UninstallRemediation) success(req *Request) { // Record event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, v2.UninstallSucceededReason, msg, diff --git a/internal/reconcile/unlock.go b/internal/reconcile/unlock.go index 7d045856c..a1dc5fded 100644 --- a/internal/reconcile/unlock.go +++ b/internal/reconcile/unlock.go @@ -119,7 +119,7 @@ func (r *Unlock) failure(req *Request, cur *v2.Snapshot, status helmrelease.Stat // Record warning event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeWarning, "PendingRelease", msg, @@ -138,7 +138,7 @@ func (r *Unlock) success(req *Request, cur *v2.Snapshot, status helmrelease.Stat // Record event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, "PendingRelease", msg, diff --git a/internal/reconcile/upgrade.go b/internal/reconcile/upgrade.go index 8cdbb0828..6a2a01ff1 100644 --- a/internal/reconcile/upgrade.go +++ b/internal/reconcile/upgrade.go @@ -83,7 +83,7 @@ func (r *Upgrade) Reconcile(ctx context.Context, req *Request) error { _, err := action.Upgrade(ctx, cfg, req.Object, req.Chart, req.Values) // Record the history of releases observed during the upgrade. - obsReleases.recordOnObject(req.Object) + obsReleases.recordOnObject(req.Object, mutateOciDigest) if err != nil { r.failure(req, logBuf, err) @@ -144,7 +144,8 @@ func (r *Upgrade) failure(req *Request, buffer *action.LogBuffer, err error) { // Condition summary. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(req.Chart.Metadata.Version, chartutil.DigestValues(digest.Canonical, req.Values).String()), + eventMeta(req.Chart.Metadata.Version, chartutil.DigestValues(digest.Canonical, req.Values).String(), + addOciDigest(req.Object.Status.LastAttemptedRevisionDigest)), corev1.EventTypeWarning, v2.UpgradeFailedReason, eventMessageWithLog(msg, buffer), @@ -171,7 +172,7 @@ func (r *Upgrade) success(req *Request) { // Record event. r.eventRecorder.AnnotatedEventf( req.Object, - eventMeta(cur.ChartVersion, cur.ConfigDigest), + eventMeta(cur.ChartVersion, cur.ConfigDigest, addOciDigest(cur.OciDigest)), corev1.EventTypeNormal, v2.UpgradeSucceededReason, msg, diff --git a/internal/reconcile/upgrade_test.go b/internal/reconcile/upgrade_test.go index dbfb85b7c..08bc98632 100644 --- a/internal/reconcile/upgrade_test.go +++ b/internal/reconcile/upgrade_test.go @@ -438,6 +438,9 @@ func TestUpgrade_failure(t *testing.T) { ReleaseName: mockReleaseName, TargetNamespace: mockReleaseNamespace, }, + Status: v2.HelmReleaseStatus{ + LastAttemptedRevisionDigest: "sha256:1234567890", + }, } chrt = testutil.BuildChart() err = errors.New("upgrade error") @@ -468,6 +471,7 @@ func TestUpgrade_failure(t *testing.T) { Message: expectMsg, ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ + eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest, eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version, eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(), }, diff --git a/internal/release/digest_test.go b/internal/release/digest_test.go index c340ca965..3aca5b8eb 100644 --- a/internal/release/digest_test.go +++ b/internal/release/digest_test.go @@ -37,7 +37,7 @@ func TestDigest(t *testing.T) { rel: Observation{ Name: "foo", }, - exp: "sha256:91b6773f7696d3eb405708a07e2daedc6e69664dabac8e10af7d570d09f947d5", + exp: "sha256:ca8901e499a79368647134cc4f1c2118f8e7ec64c8a4703b281d17fb01acfbed", }, } for _, tt := range tests { diff --git a/internal/release/observation.go b/internal/release/observation.go index 35c0a4340..9fe5b9ead 100644 --- a/internal/release/observation.go +++ b/internal/release/observation.go @@ -79,6 +79,8 @@ type Observation struct { Hooks []helmrelease.Hook `json:"hooks"` // Namespace is the Kubernetes namespace of the release. Namespace string `json:"namespace"` + // OciDigest is the digest of the OCI artifact that was used to + OciDigest string `json:"ociDigest"` } // Targets returns if the release matches the given name, namespace and @@ -166,6 +168,7 @@ func ObservedToSnapshot(rls Observation) *v2.Snapshot { LastDeployed: metav1.NewTime(rls.Info.LastDeployed.Time), Deleted: metav1.NewTime(rls.Info.Deleted.Time), Status: rls.Info.Status.String(), + OciDigest: rls.OciDigest, } }