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 <bah.soule@gmail.com>
This commit is contained in:
Soule BA 2024-04-04 11:30:59 +02:00
parent 49b47d4c44
commit edec322a3d
No known key found for this signature in database
GPG Key ID: 4D40965192802994
22 changed files with 336 additions and 52 deletions

View File

@ -1002,11 +1002,16 @@ type HelmReleaseStatus struct {
LastAppliedRevision string `json:"lastAppliedRevision,omitempty"` LastAppliedRevision string `json:"lastAppliedRevision,omitempty"`
// LastAttemptedRevision is the Source revision of the last reconciliation // 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". // appended to the chart version e.g. "1.2.3+1234567890ab".
// +optional // +optional
LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"` 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 // LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last
// reconciliation attempt. // reconciliation attempt.
// Deprecated: Use LastAttemptedConfigDigest instead. // Deprecated: Use LastAttemptedConfigDigest instead.

View File

@ -156,6 +156,9 @@ type Snapshot struct {
// run by the controller. // run by the controller.
// +optional // +optional
TestHooks *map[string]*TestHookStatus `json:"testHooks,omitempty"` 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 // FullReleaseName returns the full name of the release in the format

View File

@ -1100,6 +1100,10 @@ spec:
description: Namespace is the namespace the release is deployed description: Namespace is the namespace the release is deployed
to. to.
type: string type: string
ociDigest:
description: OCIDigest is the digest of the OCI artifact associated
with the release.
type: string
status: status:
description: Status is the current state of the release. description: Status is the current state of the release.
type: string type: string
@ -2374,6 +2378,10 @@ spec:
description: Namespace is the namespace the release is deployed description: Namespace is the namespace the release is deployed
to. to.
type: string type: string
ociDigest:
description: OCIDigest is the digest of the OCI artifact associated
with the release.
type: string
status: status:
description: Status is the current state of the release. description: Status is the current state of the release.
type: string type: string
@ -2452,9 +2460,14 @@ spec:
lastAttemptedRevision: lastAttemptedRevision:
description: |- description: |-
LastAttemptedRevision is the Source revision of the last reconciliation 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". appended to the chart version e.g. "1.2.3+1234567890ab".
type: string type: string
lastAttemptedRevisionDigest:
description: |-
LastAttemptedRevisionDigest is the digest of the last reconciliation attempt.
This is only set for OCIRepository sources.
type: string
lastAttemptedValuesChecksum: lastAttemptedValuesChecksum:
description: |- description: |-
LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last

View File

@ -1584,12 +1584,25 @@ string
<td> <td>
<em>(Optional)</em> <em>(Optional)</em>
<p>LastAttemptedRevision is the Source revision of the last reconciliation <p>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. &ldquo;1.2.3+1234567890ab&rdquo;.</p> appended to the chart version e.g. &ldquo;1.2.3+1234567890ab&rdquo;.</p>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<code>lastAttemptedRevisionDigest</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>LastAttemptedRevisionDigest is the digest of the last reconciliation attempt.
This is only set for OCIRepository sources.</p>
</td>
</tr>
<tr>
<td>
<code>lastAttemptedValuesChecksum</code><br> <code>lastAttemptedValuesChecksum</code><br>
<em> <em>
string string
@ -2380,6 +2393,18 @@ TestHookStatus
run by the controller.</p> run by the controller.</p>
</td> </td>
</tr> </tr>
<tr>
<td>
<code>ociDigest</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>OCIDigest is the digest of the OCI artifact associated with the release.</p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -242,26 +242,10 @@ metadata:
namespace: default namespace: default
spec: spec:
interval: 10m interval: 10m
timeout: 5m
chartRef: chartRef:
kind: OCIRepository kind: OCIRepository
name: podinfo name: podinfo
namespace: default 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: values:
replicaCount: 2 replicaCount: 2
``` ```

View File

@ -126,6 +126,11 @@ func VerifyReleaseObject(snapshot *v2.Snapshot, rls *helmrelease.Release) error
verifier := relDig.Verifier() verifier := relDig.Verifier()
obs := release.ObserveRelease(rls) 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 { if err = obs.Encode(verifier); err != nil {
// We are expected to be able to encode valid JSON, error out without a // We are expected to be able to encode valid JSON, error out without a
// typed error assuming malfunction to signal to e.g. retry. // typed error assuming malfunction to signal to e.g. retry.

View File

@ -335,7 +335,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress") conditions.MarkUnknown(obj, meta.ReadyCondition, meta.ProgressingReason, "reconciliation in progress")
} }
err = mutateChartWithSourceRevision(loadedChart, source) ociDigest, err := mutateChartWithSourceRevision(loadedChart, source)
if err != nil { if err != nil {
conditions.MarkFalse(obj, meta.ReadyCondition, "ChartMutateError", err.Error()) conditions.MarkFalse(obj, meta.ReadyCondition, "ChartMutateError", err.Error())
return ctrl.Result{}, err return ctrl.Result{}, err
@ -388,6 +388,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, patchHelpe
// Set last attempt values. // Set last attempt values.
obj.Status.LastAttemptedGeneration = obj.Generation obj.Status.LastAttemptedGeneration = obj.Generation
obj.Status.LastAttemptedRevision = loadedChart.Metadata.Version obj.Status.LastAttemptedRevision = loadedChart.Metadata.Version
obj.Status.LastAttemptedRevisionDigest = ociDigest
obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, values).String() obj.Status.LastAttemptedConfigDigest = chartutil.DigestValues(digest.Canonical, values).String()
obj.Status.LastAttemptedValuesChecksum = "" obj.Status.LastAttemptedValuesChecksum = ""
obj.Status.LastReleaseRevision = 0 obj.Status.LastReleaseRevision = 0
@ -865,24 +866,28 @@ func getNamespacedName(obj *v2.HelmRelease) (types.NamespacedName, error) {
return namespacedName, nil 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 // If the source is an OCIRepository, we can try to mutate the chart version
// with the artifact revision. The revision is either a <tag>@<digest> or // with the artifact revision. The revision is either a <tag>@<digest> or
// just a digest. // just a digest.
obj, ok := source.(*sourcev1.OCIRepository) obj, ok := source.(*sourcev1.OCIRepository)
if !ok { 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) ver, err := semver.NewVersion(chart.Metadata.Version)
if err != nil { if err != nil {
return err return "", err
} }
var ociDigest string
revision := obj.GetArtifact().Revision revision := obj.GetArtifact().Revision
switch { switch {
case strings.Contains(revision, "@"): case strings.Contains(revision, "@"):
tagD := strings.Split(revision, "@") tagD := strings.Split(revision, "@")
if len(tagD) != 2 || tagD[0] != chart.Metadata.Version { 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 // algotithm are sha256, sha384, sha512 with the canonical being sha256
// So every digest starts with a sha algorithm and a colon // 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 // add the digest to the chart version to make sure mutable tags are detected
*ver, err = ver.SetMetadata(sha[0:12]) *ver, err = ver.SetMetadata(sha[0:12])
if err != nil { if err != nil {
return err return "", err
} }
ociDigest = tagD[1]
default: default:
// default to the digest // default to the digest
sha := strings.Split(revision, ":")[1] sha := strings.Split(revision, ":")[1]
*ver, err = ver.SetMetadata(sha[0:12]) *ver, err = ver.SetMetadata(sha[0:12])
if err != nil { if err != nil {
return err return "", err
} }
ociDigest = revision
} }
chart.Metadata.Version = ver.String() chart.Metadata.Version = ver.String()
return nil return ociDigest, nil
} }

View File

@ -186,6 +186,10 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
chart := &sourcev1b2.HelmChart{ chart := &sourcev1b2.HelmChart{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1b2.GroupVersion.String(),
Kind: sourcev1b2.HelmChartKind,
},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "chart", Name: "chart",
Namespace: "mock", Namespace: "mock",
@ -238,6 +242,10 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
chart := &sourcev1b2.HelmChart{ chart := &sourcev1b2.HelmChart{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1b2.GroupVersion.String(),
Kind: sourcev1b2.HelmChartKind,
},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "chart", Name: "chart",
Namespace: "mock", Namespace: "mock",
@ -960,6 +968,10 @@ func TestHelmReleaseReconciler_reconcileReleaseFromOCIRepositorySource(t *testin
g := NewWithT(t) g := NewWithT(t)
ocirepo := &sourcev1b2.OCIRepository{ ocirepo := &sourcev1b2.OCIRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1b2.GroupVersion.String(),
Kind: sourcev1b2.OCIRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "ocirepo", Name: "ocirepo",
Namespace: "mock", Namespace: "mock",
@ -2972,7 +2984,7 @@ func Test_TryMutateChartWithSourceRevision(t *testing.T) {
}, },
} }
err := mutateChartWithSourceRevision(c, s) _, err := mutateChartWithSourceRevision(c, s)
if tt.wantErr { if tt.wantErr {
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
} else { } else {

View File

@ -99,10 +99,10 @@ func (r *CorrectClusterDrift) report(obj *v2.HelmRelease, changeSet *ssa.ChangeS
sb.WriteString(changeSet.String()) 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()) "DriftCorrectionFailed", sb.String())
case changeSet != nil && len(changeSet.Entries) > 0: 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", "DriftCorrected", "Cluster state of release %s has been corrected:\n%s",
obj.Status.History.Latest().FullReleaseName(), changeSet.String()) obj.Status.History.Latest().FullReleaseName(), changeSet.String())
} }

View File

@ -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) _, err := action.Install(ctx, cfg, req.Object, req.Chart, req.Values)
// Record the history of releases observed during the install. // Record the history of releases observed during the install.
obsReleases.recordOnObject(req.Object) obsReleases.recordOnObject(req.Object, mutateOCIDigest)
if err != nil { if err != nil {
r.failure(req, logBuf, err) r.failure(req, logBuf, err)
@ -154,7 +154,8 @@ func (r *Install) failure(req *Request, buffer *action.LogBuffer, err error) {
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, 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, corev1.EventTypeWarning,
v2.InstallFailedReason, v2.InstallFailedReason,
eventMessageWithLog(msg, buffer), eventMessageWithLog(msg, buffer),
@ -181,7 +182,7 @@ func (r *Install) success(req *Request) {
// Record event. // Record event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeNormal, corev1.EventTypeNormal,
v2.InstallSucceededReason, v2.InstallSucceededReason,
msg, msg,

View File

@ -307,6 +307,9 @@ func TestInstall_failure(t *testing.T) {
ReleaseName: mockReleaseName, ReleaseName: mockReleaseName,
TargetNamespace: mockReleaseNamespace, TargetNamespace: mockReleaseNamespace,
}, },
Status: v2.HelmReleaseStatus{
LastAttemptedRevisionDigest: "sha256:1234567890",
},
} }
chrt = testutil.BuildChart() chrt = testutil.BuildChart()
err = errors.New("installation error") err = errors.New("installation error")
@ -337,6 +340,7 @@ func TestInstall_failure(t *testing.T) {
Message: expectMsg, Message: expectMsg,
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{ Annotations: map[string]string{
eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest,
eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version, eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version,
eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(), eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
}, },

View File

@ -43,6 +43,10 @@ var (
ErrReleaseMismatch = errors.New("release mismatch") 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 // observedReleases is a map of Helm releases as observed to be written to the
// Helm storage. The key is the version of the release. // Helm storage. The key is the version of the release.
type observedReleases map[int]release.Observation 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. // 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) { switch len(r) {
case 0: case 0:
return return
@ -67,17 +71,25 @@ func (r observedReleases) recordOnObject(obj *v2.HelmRelease) {
for _, o := range r { for _, o := range r {
obs = o obs = o
} }
for _, mut := range mutators {
obs = mut(obj, obs)
}
obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...) obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(obs)}, obj.Status.History...)
default: default:
versions := r.sortedVersions() versions := r.sortedVersions()
obs := r[versions[0]]
obj.Status.History = append(v2.Snapshots{release.ObservedToSnapshot(r[versions[0]])}, obj.Status.History...) 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 _, ver := range versions[1:] {
for i := range obj.Status.History { for i := range obj.Status.History {
snap := obj.Status.History[i] snap := obj.Status.History[i]
if snap.Targets(r[ver].Name, r[ver].Namespace, r[ver].Version) { if snap.Targets(r[ver].Name, r[ver].Namespace, r[ver].Version) {
newSnap := release.ObservedToSnapshot(r[ver]) obs := r[ver]
obs.OCIDigest = snap.OCIDigest
newSnap := release.ObservedToSnapshot(obs)
newSnap.SetTestHooks(snap.GetTestHooks()) newSnap.SetTestHooks(snap.GetTestHooks())
obj.Status.History[i] = newSnap obj.Status.History[i] = newSnap
return return
@ -87,6 +99,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 // observeRelease returns a storage.ObserveFunc that stores the observed
// releases in the given observedReleases map. // releases in the given observedReleases map.
// It can be used for Helm actions that modify multiple releases in the // It can be used for Helm actions that modify multiple releases in the
@ -174,9 +191,15 @@ func eventMessageWithLog(msg string, log *action.LogBuffer) string {
return msg 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 // eventMeta returns the event (annotation) metadata based on the given
// parameters. // parameters.
func eventMeta(revision, token string) map[string]string { func eventMeta(revision, token string, metas ...addMeta) map[string]string {
var metadata map[string]string var metadata map[string]string
if revision != "" || token != "" { if revision != "" || token != "" {
metadata = make(map[string]string) metadata = make(map[string]string)
@ -187,9 +210,25 @@ func eventMeta(revision, token string) map[string]string {
metadata[eventMetaGroupKey(eventv1.MetaTokenKey)] = token metadata[eventMetaGroupKey(eventv1.MetaTokenKey)] = token
} }
} }
for _, add := range metas {
add(metadata)
}
return 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 // eventMetaGroupKey returns the event (annotation) metadata key prefixed with
// the group. // the group.
func eventMetaGroupKey(key string) string { func eventMetaGroupKey(key string) string {

View File

@ -17,10 +17,12 @@ limitations under the License.
package reconcile package reconcile
import ( import (
"fmt"
"testing" "testing"
"github.com/go-logr/logr" "github.com/go-logr/logr"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"helm.sh/helm/v3/pkg/chart"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
@ -454,3 +456,179 @@ func mockLogBuffer(size int, lines int) *action.LogBuffer {
} }
return log 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())
})
}
}

View File

@ -143,7 +143,7 @@ func (r *RollbackRemediation) failure(req *Request, prev *v2.Snapshot, buffer *a
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, 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, corev1.EventTypeWarning,
v2.RollbackFailedReason, v2.RollbackFailedReason,
eventMessageWithLog(msg, buffer), eventMessageWithLog(msg, buffer),
@ -162,7 +162,7 @@ func (r *RollbackRemediation) success(req *Request, prev *v2.Snapshot) {
// Record event. // Record event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, 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, corev1.EventTypeNormal,
v2.RollbackSucceededReason, v2.RollbackSucceededReason,
msg, msg,

View File

@ -145,7 +145,7 @@ func (r *Test) failure(req *Request, err error) {
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeWarning, corev1.EventTypeWarning,
v2.TestFailedReason, v2.TestFailedReason,
msg, msg,
@ -181,7 +181,7 @@ func (r *Test) success(req *Request) {
// Record event. // Record event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeNormal, corev1.EventTypeNormal,
v2.TestSucceededReason, v2.TestSucceededReason,
msg, msg,

View File

@ -180,7 +180,7 @@ func (r *Uninstall) failure(req *Request, buffer *action.LogBuffer, err error) {
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeWarning, v2.UninstallFailedReason, corev1.EventTypeWarning, v2.UninstallFailedReason,
eventMessageWithLog(msg, buffer), eventMessageWithLog(msg, buffer),
) )
@ -201,7 +201,7 @@ func (r *Uninstall) success(req *Request) {
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeNormal, corev1.EventTypeNormal,
v2.UninstallSucceededReason, v2.UninstallSucceededReason,
msg, msg,

View File

@ -154,7 +154,7 @@ func (r *UninstallRemediation) failure(req *Request, buffer *action.LogBuffer, e
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeWarning, corev1.EventTypeWarning,
v2.UninstallFailedReason, v2.UninstallFailedReason,
eventMessageWithLog(msg, buffer), eventMessageWithLog(msg, buffer),
@ -175,7 +175,7 @@ func (r *UninstallRemediation) success(req *Request) {
// Record event. // Record event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeNormal, corev1.EventTypeNormal,
v2.UninstallSucceededReason, v2.UninstallSucceededReason,
msg, msg,

View File

@ -119,7 +119,7 @@ func (r *Unlock) failure(req *Request, cur *v2.Snapshot, status helmrelease.Stat
// Record warning event. // Record warning event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeWarning, corev1.EventTypeWarning,
"PendingRelease", "PendingRelease",
msg, msg,
@ -138,7 +138,7 @@ func (r *Unlock) success(req *Request, cur *v2.Snapshot, status helmrelease.Stat
// Record event. // Record event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeNormal, corev1.EventTypeNormal,
"PendingRelease", "PendingRelease",
msg, msg,

View File

@ -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) _, err := action.Upgrade(ctx, cfg, req.Object, req.Chart, req.Values)
// Record the history of releases observed during the upgrade. // Record the history of releases observed during the upgrade.
obsReleases.recordOnObject(req.Object) obsReleases.recordOnObject(req.Object, mutateOCIDigest)
if err != nil { if err != nil {
r.failure(req, logBuf, err) r.failure(req, logBuf, err)
@ -144,7 +144,8 @@ func (r *Upgrade) failure(req *Request, buffer *action.LogBuffer, err error) {
// Condition summary. // Condition summary.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, 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, corev1.EventTypeWarning,
v2.UpgradeFailedReason, v2.UpgradeFailedReason,
eventMessageWithLog(msg, buffer), eventMessageWithLog(msg, buffer),
@ -171,7 +172,7 @@ func (r *Upgrade) success(req *Request) {
// Record event. // Record event.
r.eventRecorder.AnnotatedEventf( r.eventRecorder.AnnotatedEventf(
req.Object, req.Object,
eventMeta(cur.ChartVersion, cur.ConfigDigest), eventMeta(cur.ChartVersion, cur.ConfigDigest, addOCIDigest(cur.OCIDigest)),
corev1.EventTypeNormal, corev1.EventTypeNormal,
v2.UpgradeSucceededReason, v2.UpgradeSucceededReason,
msg, msg,

View File

@ -438,6 +438,9 @@ func TestUpgrade_failure(t *testing.T) {
ReleaseName: mockReleaseName, ReleaseName: mockReleaseName,
TargetNamespace: mockReleaseNamespace, TargetNamespace: mockReleaseNamespace,
}, },
Status: v2.HelmReleaseStatus{
LastAttemptedRevisionDigest: "sha256:1234567890",
},
} }
chrt = testutil.BuildChart() chrt = testutil.BuildChart()
err = errors.New("upgrade error") err = errors.New("upgrade error")
@ -468,6 +471,7 @@ func TestUpgrade_failure(t *testing.T) {
Message: expectMsg, Message: expectMsg,
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{ Annotations: map[string]string{
eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest,
eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version, eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version,
eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(), eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
}, },

View File

@ -37,7 +37,7 @@ func TestDigest(t *testing.T) {
rel: Observation{ rel: Observation{
Name: "foo", Name: "foo",
}, },
exp: "sha256:91b6773f7696d3eb405708a07e2daedc6e69664dabac8e10af7d570d09f947d5", exp: "sha256:ca8901e499a79368647134cc4f1c2118f8e7ec64c8a4703b281d17fb01acfbed",
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -79,6 +79,8 @@ type Observation struct {
Hooks []helmrelease.Hook `json:"hooks"` Hooks []helmrelease.Hook `json:"hooks"`
// Namespace is the Kubernetes namespace of the release. // Namespace is the Kubernetes namespace of the release.
Namespace string `json:"namespace"` 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 // 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), LastDeployed: metav1.NewTime(rls.Info.LastDeployed.Time),
Deleted: metav1.NewTime(rls.Info.Deleted.Time), Deleted: metav1.NewTime(rls.Info.Deleted.Time),
Status: rls.Info.Status.String(), Status: rls.Info.Status.String(),
OCIDigest: rls.OCIDigest,
} }
} }