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 cca9894
Show file tree
Hide file tree
Showing 22 changed files with 338 additions and 52 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
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
16 changes: 0 additions & 16 deletions docs/spec/v2beta2/helmreleases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
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 = tagD[1]
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 = revision
}

chart.Metadata.Version = ver.String()
return nil
return ociDigest, nil
}
14 changes: 13 additions & 1 deletion internal/controller/helmrelease_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
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
Loading

0 comments on commit cca9894

Please sign in to comment.