Skip to content

Commit

Permalink
Take into account the oci-digest
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
souleb committed Apr 4, 2024
1 parent 3ff9cc8 commit 4bdc4c3
Show file tree
Hide file tree
Showing 22 changed files with 329 additions and 39 deletions.
7 changes: 6 additions & 1 deletion api/v2beta2/helmrelease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions api/v2beta2/snapshot_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ kind: Kustomization
resources:
- deployment.yaml
images:
- name: fluxcd/helm-controller
newName: fluxcd/helm-controller
newTag: v0.37.4
- name: fluxcd/helm-controller
newName: fluxcd/helm-controller
newTag: latest
27 changes: 26 additions & 1 deletion docs/api/v2beta2/helm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1584,12 +1584,25 @@ string
<td>
<em>(Optional)</em>
<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>
</td>
</tr>
<tr>
<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>
<em>
string
Expand Down Expand Up @@ -2380,6 +2393,18 @@ TestHookStatus
run by the controller.</p>
</td>
</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>
</table>
</div>
Expand Down
5 changes: 5 additions & 0 deletions internal/action/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 15 additions & 8 deletions internal/controller/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -865,42 +866,48 @@ 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 <tag>@<digest> 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
sha := strings.Split(tagD[1], ":")[1]
// 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
}
2 changes: 1 addition & 1 deletion internal/controller/helmrelease_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2972,7 +2972,7 @@ func Test_TryMutateChartWithSourceRevision(t *testing.T) {
},
}

err := mutateChartWithSourceRevision(c, s)
_, err := mutateChartWithSourceRevision(c, s)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
} else {
Expand Down
4 changes: 2 additions & 2 deletions internal/reconcile/correct_cluster_drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
7 changes: 4 additions & 3 deletions internal/reconcile/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions internal/reconcile/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(),
},
Expand Down
51 changes: 46 additions & 5 deletions internal/reconcile/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 4bdc4c3

Please sign in to comment.