Skip to content

Commit fa9f9ba

Browse files
fix: allow spec compliant, but unusually ordered HELM Chart OCI Artifacts in HELM OCI Artifact downloader (#1470)
<!-- markdownlint-disable MD041 --> #### What this PR does / why we need it This allows arbitrarily ordered, but spec (https://github.com/helm/community/blob/main/hips/hip-0006.md) compliant HELM Charts in the Downloader when interpreting HELM Chart Artifact Types. #### Which issue(s) this PR fixes <!-- Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> This will especially fix resource downloads which previously assumed layer 0, which is incorrect. Instead we have to find the first and only occurrence of `application/vnd.cncf.helm.chart.content.v1.tar+gzip`. Similarly, finding provenance data should not be hardcoded to layer 1, but to the first and only occurrence of `application/vnd.cncf.helm.chart.provenance.v1.prov` Fix #1469 --------- Co-authored-by: Frederic Wilhelm <frederic.wilhelm@sap.com>
1 parent 742586b commit fa9f9ba

3 files changed

Lines changed: 73 additions & 15 deletions

File tree

api/ocm/extensions/download/handlers/helm/download_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
. "github.com/mandelsoft/goutils/testutils"
55
. "github.com/onsi/ginkgo/v2"
66
. "github.com/onsi/gomega"
7+
78
. "ocm.software/ocm/api/helper/builder"
89

910
"github.com/mandelsoft/filepath/pkg/filepath"
@@ -31,6 +32,7 @@ const (
3132

3233
ArtifactType = "NotHelmChart"
3334
SpecialOCIResource = "specialhelm"
35+
UnusualOCIResource = "unusualhelm"
3436
)
3537

3638
var _ = Describe("upload", func() {
@@ -48,6 +50,9 @@ var _ = Describe("upload", func() {
4850
env.Resource(SpecialOCIResource, Version, ArtifactType, v1.LocalRelation, func() {
4951
env.BlobFromFile(artifactset.MediaType(artdesc.MediaTypeImageManifest), filepath.Join("/testdata/test-chart-oci-artifact.tgz"))
5052
})
53+
env.Resource(UnusualOCIResource, Version, resourcetypes.HELM_CHART, v1.LocalRelation, func() {
54+
env.BlobFromFile(artifactset.MediaType(artdesc.MediaTypeImageManifest), filepath.Join("/testdata/unusual-ordered-helm-chart.tgz"))
55+
})
5156
})
5257
})
5358
})
@@ -73,4 +78,14 @@ var _ = Describe("upload", func() {
7378
MustBeSuccessful(tarutils.ExtractArchiveToFs(env.FileSystem(), path, env.FileSystem()))
7479
Expect(Must(vfs.DirExists(env.FileSystem(), "/test-chart"))).To(BeTrue())
7580
})
81+
82+
It("successfully download unusual artifacts with non-defacto helm chart order", func() {
83+
MustBeSuccessful(download.RegisterHandlerByName(env, helm.PATH, nil, download.ForArtifactType(ArtifactType)))
84+
85+
repo := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, CTFPath, 0o777, env))
86+
cv := Must(repo.LookupComponentVersion(Component, Version))
87+
path := Must(download.DownloadResource(env.OCMContext(), Must(cv.SelectResources(selectors.Identity(v1.Identity{"name": UnusualOCIResource})))[0], "/resource", download.WithFileSystem(env.FileSystem())))
88+
MustBeSuccessful(tarutils.ExtractArchiveToFs(env.FileSystem(), path, env.FileSystem()))
89+
Expect(Must(vfs.FileExists(env.FileSystem(), "/postgresql/Chart.yaml"))).To(BeTrue())
90+
})
7691
})

api/ocm/extensions/download/handlers/helm/handler.go

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package helm
22

33
import (
4+
"errors"
5+
"fmt"
46
"io"
57
"strings"
68

7-
"github.com/mandelsoft/goutils/errors"
89
"github.com/mandelsoft/goutils/finalizer"
910
"github.com/mandelsoft/vfs/pkg/vfs"
11+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1012
helmregistry "helm.sh/helm/v3/pkg/registry"
1113

1214
"ocm.software/ocm/api/oci"
@@ -24,6 +26,16 @@ import (
2426

2527
const TYPE = resourcetypes.HELM_CHART
2628

29+
var (
30+
// ErrNoMatchingLayer denotes that no layer matching the requested media type was found,,
31+
// which is invalid as per HELM OCI specification for [helmregistry.ChartLayerMediaType]
32+
// but valid for [helmregistry.ProvLayerMediaType].
33+
ErrNoMatchingLayer = errors.New("no matching layer found")
34+
// ErrMultipleMatchingLayers denotes that multiple layers matching the requested media type were found,
35+
// which is invalid as per HELM OCI specification.
36+
ErrMultipleMatchingLayers = errors.New("multiple matching layers found")
37+
)
38+
2739
type Handler struct{}
2840

2941
func init() {
@@ -101,41 +113,72 @@ func (h Handler) Download(p common.Printer, racc cpi.ResourceAccess, path string
101113
return h.fromOCIArtifact(p, meth, path, fs)
102114
}
103115

116+
// download downloads the chart and optional provenance file from an oci.ArtifactAccess.
117+
// the format of the artifact is expected to match the official HELM Reference Specification
118+
//
119+
// see https://github.com/helm/community/blob/dd5fe7878e293c573cc22db5d36558709c7b8a43/hips/hip-0006.md
104120
func download(p common.Printer, art oci.ArtifactAccess, path string, fs vfs.FileSystem) (chart, prov string, err error) {
105121
var finalize finalizer.Finalizer
106122
defer finalize.FinalizeWithErrorPropagation(&err)
107123

108124
m := art.ManifestAccess()
109125
if m == nil {
110-
return "", "", errors.Newf("artifact is no image manifest")
126+
return "", "", errors.New("artifact is no image manifest")
111127
}
112-
if len(m.GetDescriptor().Layers) < 1 {
113-
return "", "", errors.Newf("no layers found")
128+
desc := m.GetDescriptor()
129+
130+
chartDesc, err := findLayer(desc.Layers, helmregistry.ChartLayerMediaType)
131+
if err != nil {
132+
return "", "", fmt.Errorf("no valid chart layer found: %w", err)
114133
}
115134

116-
blob, err := m.GetBlob(m.GetDescriptor().Layers[0].Digest)
135+
chartBlob, err := m.GetBlob(chartDesc.Digest)
117136
if err != nil {
118-
return "", "", err
137+
return "", "", fmt.Errorf("no valid chart blob found: %w", err)
119138
}
120-
finalize.Close(blob)
139+
finalize.Close(chartBlob)
121140

122141
chart = AssureArchiveSuffix(path)
123-
err = write(p, blob, chart, fs)
124-
if err != nil {
142+
if err := write(p, chartBlob, chart, fs); err != nil {
125143
return "", "", err
126144
}
127-
if len(m.GetDescriptor().Layers) > 1 {
128-
prov = chart[:len(chart)-3] + "prov"
129-
blob, err := m.GetBlob(m.GetDescriptor().Layers[1].Digest)
145+
146+
// Optional provenance layer, if present, add it separately
147+
if provDesc, err := findLayer(desc.Layers, helmregistry.ProvLayerMediaType); err == nil {
148+
provBlob, err := m.GetBlob(provDesc.Digest)
130149
if err != nil {
131150
return "", "", err
132151
}
133-
err = write(p, blob, path, fs)
134-
if err != nil {
152+
finalize.Close(provBlob)
153+
154+
prov = chart[:len(chart)-3] + "prov"
155+
if err := write(p, provBlob, path, fs); err != nil {
135156
return "", "", err
136157
}
158+
} else if !errors.Is(err, ErrNoMatchingLayer) { // Ignore if no provenance layer is found, because its optional.
159+
return "", "", err
160+
}
161+
162+
return chart, prov, nil
163+
}
164+
165+
func findLayer(layers []ocispec.Descriptor, mediaType string) (*ocispec.Descriptor, error) {
166+
var candidates []*ocispec.Descriptor
167+
168+
for _, l := range layers {
169+
if mime.BaseType(l.MediaType) == mime.BaseType(mediaType) {
170+
candidates = append(candidates, &l)
171+
}
172+
}
173+
174+
switch {
175+
case len(candidates) > 1:
176+
return nil, fmt.Errorf("%w: %s", ErrMultipleMatchingLayers, mediaType)
177+
case len(candidates) == 0:
178+
return nil, fmt.Errorf("%w: %s", ErrNoMatchingLayer, mediaType)
179+
default:
180+
return candidates[0], nil
137181
}
138-
return chart, prov, err
139182
}
140183

141184
func write(p common.Printer, blob blobaccess.DataReader, path string, fs vfs.FileSystem) (err error) {
Binary file not shown.

0 commit comments

Comments
 (0)