Skip to content

Commit

Permalink
Ignore 'v' version prefix in OCI artifact and Helm chart
Browse files Browse the repository at this point in the history
Tools such as Bitnami's charts-syncer strip the `v` prefix from the
chart version so that the OCI artifact version differs from the
version defined in the chart's metadata. This leads to an error
similar to this returned from h-c:

```
artifact revision 1.14.5 does not match chart version v1.14.5
```

This commit makes h-c ignore a leading `v` prefix in either the chart
version of the OCI artifact tag.

Signed-off-by: Max Jonas Werner <[email protected]>
  • Loading branch information
makkes committed May 26, 2024
1 parent bd7e561 commit 98ecb33
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 1 deletion.
6 changes: 5 additions & 1 deletion internal/controller/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,11 @@ func mutateChartWithSourceRevision(chart *chart.Chart, source sourcev1.Source) (
switch {
case strings.Contains(revision, "@"):
tagD := strings.Split(revision, "@")
if len(tagD) != 2 || tagD[0] != chart.Metadata.Version {
tagVer, err := semver.NewVersion(tagD[0])
if err != nil {
return "", fmt.Errorf("failed parsing artifact revision %s", tagD[0])
}
if len(tagD) != 2 || !tagVer.Equal(ver) {
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
Expand Down
154 changes: 154 additions & 0 deletions internal/controller/helmrelease_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,160 @@ func TestHelmReleaseReconciler_reconcileReleaseFromOCIRepositorySource(t *testin
g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
})

t.Run("ignore 'v' prefix in OCIRepository tag", func(t *testing.T) {
g := NewWithT(t)

// Create HelmChart mock.
chartMock := testutil.BuildChart()
chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
g.Expect(err).ToNot(HaveOccurred())
// copy the artifact to mutate the revision
ociArtifact := chartArtifact.DeepCopy()
ociArtifact.Revision = "v" + ociArtifact.Revision
ociArtifact.Revision += "@" + chartArtifact.Digest

ns, err := testEnv.CreateNamespace(context.TODO(), "mock")
g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(func() {
_ = testEnv.Delete(context.TODO(), ns)
})

// ocirepo is the chartRef object to switch to.
ocirepo := &sourcev1beta2.OCIRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "ocirepo",
Namespace: ns.Name,
Generation: 1,
},
Spec: sourcev1beta2.OCIRepositorySpec{
URL: "oci://test-example.com",
Interval: metav1.Duration{Duration: 1 * time.Second},
},
Status: sourcev1beta2.OCIRepositoryStatus{
ObservedGeneration: 1,
Artifact: ociArtifact,
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
},
},
},
}

obj := &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "release",
Namespace: ns.Name,
},
Spec: v2.HelmReleaseSpec{
ChartRef: &v2.CrossNamespaceSourceReference{
Kind: "OCIRepository",
Name: "ocirepo",
},
},
}

c := fake.NewClientBuilder().
WithScheme(NewTestScheme()).
WithStatusSubresource(&v2.HelmRelease{}).
WithObjects(ocirepo, obj).
Build()

r := &HelmReleaseReconciler{
Client: c,
GetClusterConfig: GetTestClusterConfig,
EventRecorder: record.NewFakeRecorder(32),
}

_, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj)
g.Expect(err).ToNot(HaveOccurred())

// Verify attempted values are set.
g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
dig := strings.Split(ociArtifact.Revision, ":")[1][0:12]
g.Expect(obj.Status.LastAttemptedRevision).To(Equal(chartMock.Metadata.Version + "+" + dig))
g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
})

t.Run("ignore 'v' prefix in chart version", func(t *testing.T) {
g := NewWithT(t)

// Create HelmChart mock.
chartMock := testutil.BuildChart(testutil.ChartWithVersion("v0.1.0"))
chartArtifact, err := testutil.SaveChartAsArtifact(chartMock, digest.SHA256, testServer.URL(), testServer.Root())
g.Expect(err).ToNot(HaveOccurred())
// copy the artifact to mutate the revision
ociArtifact := chartArtifact.DeepCopy()
ociArtifact.Revision = "0.1.0"
ociArtifact.Revision += "@" + chartArtifact.Digest

ns, err := testEnv.CreateNamespace(context.TODO(), "mock")
g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(func() {
_ = testEnv.Delete(context.TODO(), ns)
})

// ocirepo is the chartRef object to switch to.
ocirepo := &sourcev1beta2.OCIRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "ocirepo",
Namespace: ns.Name,
Generation: 1,
},
Spec: sourcev1beta2.OCIRepositorySpec{
URL: "oci://test-example.com",
Interval: metav1.Duration{Duration: 1 * time.Second},
},
Status: sourcev1beta2.OCIRepositoryStatus{
ObservedGeneration: 1,
Artifact: ociArtifact,
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
},
},
},
}

obj := &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "release",
Namespace: ns.Name,
},
Spec: v2.HelmReleaseSpec{
ChartRef: &v2.CrossNamespaceSourceReference{
Kind: "OCIRepository",
Name: "ocirepo",
},
},
}

c := fake.NewClientBuilder().
WithScheme(NewTestScheme()).
WithStatusSubresource(&v2.HelmRelease{}).
WithObjects(ocirepo, obj).
Build()

r := &HelmReleaseReconciler{
Client: c,
GetClusterConfig: GetTestClusterConfig,
EventRecorder: record.NewFakeRecorder(32),
}

_, err = r.reconcileRelease(context.TODO(), patch.NewSerialPatcher(obj, r.Client), obj)
g.Expect(err).ToNot(HaveOccurred())

// Verify attempted values are set.
g.Expect(obj.Status.LastAttemptedGeneration).To(Equal(obj.Generation))
dig := strings.Split(ociArtifact.Revision, ":")[1][0:12]
g.Expect(obj.Status.LastAttemptedRevision).To(Equal(strings.Split(ociArtifact.Revision, "@")[0] + "+" + dig))
g.Expect(obj.Status.LastAttemptedConfigDigest).To(Equal("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
g.Expect(obj.Status.LastAttemptedValuesChecksum).To(BeEmpty())
})

t.Run("upgrade by switching from existing HelmChat to chartRef", func(t *testing.T) {
g := NewWithT(t)

Expand Down

0 comments on commit 98ecb33

Please sign in to comment.