From ad9fb68fa3911979ccaba1e230a8043971b84042 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 20 Sep 2024 09:47:39 -0600 Subject: [PATCH 1/3] feat: allow installation by OCI digest Signed-off-by: Terry Howe --- cmd/helm/pull_test.go | 19 ++++-- pkg/downloader/chart_downloader.go | 36 +++++++++- pkg/downloader/chart_downloader_test.go | 6 ++ pkg/getter/ocigetter.go | 4 ++ pkg/registry/client.go | 40 +++++++++--- pkg/registry/reference.go | 71 ++++++++++++++++++++ pkg/registry/reference_test.go | 87 +++++++++++++++++++++++++ pkg/registry/util.go | 26 -------- 8 files changed, 244 insertions(+), 45 deletions(-) create mode 100644 pkg/registry/reference.go create mode 100644 pkg/registry/reference_test.go diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go index ae70595f9..fe6a5a68e 100644 --- a/cmd/helm/pull_test.go +++ b/cmd/helm/pull_test.go @@ -183,15 +183,20 @@ func TestPullCmd(t *testing.T) { wantError: true, }, { - name: "Fail fetching OCI chart without version specified", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL), - wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", - wantError: true, + name: "Fetching OCI chart without version option specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", }, { - name: "Fail fetching OCI chart without version specified", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL), - wantError: true, + name: "Fetching OCI chart with version specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "Fail fetching OCI chart with version mismatch", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.2.0 --version 0.1.0", ociSrv.RegistryURL), + wantErrorMsg: "Error: chart reference and version mismatch: 0.2.0 is not 0.1.0", + wantError: true, }, } diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index a95894e00..2d2e22f68 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -143,7 +143,39 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, error) { var tag string - var err error + + registryReference, err := registry.NewReference(u.Path) + if err != nil { + return nil, err + } + + if version == "" { + // Use OCI URI tag as default + version = registryReference.Tag + } else { + if registryReference.Tag != "" && registryReference.Tag != version { + return nil, errors.Errorf("chart reference and version mismatch: %s is not %s", version, registryReference.Tag) + } + } + + if registryReference.Digest != "" { + if registryReference.Tag == "" { + // Install by digest only + return u, nil + } + + // Validate the tag if it was specified + path := registryReference.Registry + "/" + registryReference.Repository + ":" + registryReference.Tag + desc, err := c.RegistryClient.Resolve(path) + if err != nil { + // The resource does not have to be tagged when digest is specified + return u, nil + } + if desc != nil && desc.Digest.String() != registryReference.Digest { + return nil, errors.Errorf("chart reference digest mismatch: %s is not %s", desc.Digest.String(), registryReference.Digest) + } + return u, nil + } // Evaluate whether an explicit version has been provided. Otherwise, determine version to use _, errSemVer := semver.NewVersion(version) @@ -169,7 +201,7 @@ func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, } } - u.Path = fmt.Sprintf("%s:%s", u.Path, tag) + u.Path = fmt.Sprintf("%s/%s:%s", registryReference.Registry, registryReference.Repository, tag) return u, err } diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 131e21306..1a741bc1b 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -53,6 +53,12 @@ func TestResolveChartRef(t *testing.T) { {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, {name: "invalid", ref: "invalid-1.2.3", fail: true}, {name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true}, + {name: "ref with tag", ref: "oci://example.com/helm-charts/nginx:15.4.2", expect: "oci://example.com/helm-charts/nginx:15.4.2"}, + {name: "no repository", ref: "oci://", fail: true}, + {name: "oci ref", ref: "oci://example.com/helm-charts/nginx", version: "15.4.2", expect: "oci://example.com/helm-charts/nginx:15.4.2"}, + {name: "oci ref with sha256", ref: "oci://example.com/install/by/sha@sha256:d234555386402a5867ef0169fefe5486858b6d8d209eaf32fd26d29b16807fd6", version: "0.1.1", expect: "oci://example.com/install/by/sha@sha256:d234555386402a5867ef0169fefe5486858b6d8d209eaf32fd26d29b16807fd6"}, + {name: "oci ref with sha256 and version", ref: "oci://example.com/install/by/sha:0.1.1@sha256:d234555386402a5867ef0169fefe5486858b6d8d209eaf32fd26d29b16807fd6", version: "0.1.1", expect: "oci://example.com/install/by/sha:0.1.1@sha256:d234555386402a5867ef0169fefe5486858b6d8d209eaf32fd26d29b16807fd6"}, + {name: "oci ref with sha256 and version mismatch", ref: "oci://example.com/install/by/sha:0.1.1@sha256:d234555386402a5867ef0169fefe5486858b6d8d209eaf32fd26d29b16807fd6", version: "0.1.2", fail: true}, } c := ChartDownloader{ diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go index 0547cdcbb..5b0522395 100644 --- a/pkg/getter/ocigetter.go +++ b/pkg/getter/ocigetter.go @@ -20,6 +20,7 @@ import ( "fmt" "net" "net/http" + "path" "strings" "sync" "time" @@ -58,6 +59,9 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)) + if version := g.opts.version; version != "" && !strings.Contains(path.Base(ref), ":") { + ref = fmt.Sprintf("%s:%s", ref, version) + } var pullOpts []registry.PullOption requestingProv := strings.HasSuffix(ref, ".prov") if requestingProv { diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 42f736816..32d2773a3 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -319,7 +319,7 @@ type ( // Pull downloads a chart from a registry func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { - parsedRef, err := parseReference(ref) + parsedRef, err := NewReference(ref) if err != nil { return nil, err } @@ -351,13 +351,13 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } var descriptors, layers []ocispec.Descriptor - remotesResolver, err := c.resolver(parsedRef) + remotesResolver, err := c.resolver(parsedRef.OrasReference) if err != nil { return nil, err } registryStore := content.Registry{Resolver: remotesResolver} - manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "", + manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.OrasReference.String(), memoryStore, "", oras.WithPullEmptyNameAllowed(), oras.WithAllowedMediaTypes(allowedMediaTypes), oras.WithLayerDescriptors(func(l []ocispec.Descriptor) { @@ -419,7 +419,7 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { }, Chart: &DescriptorPullSummaryWithMeta{}, Prov: &DescriptorPullSummary{}, - Ref: parsedRef.String(), + Ref: parsedRef.OrasReference.String(), } var getManifestErr error if _, manifestData, ok := memoryStore.Get(manifest); !ok { @@ -535,7 +535,7 @@ type ( // Push uploads a chart to a registry. func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) { - parsedRef, err := parseReference(ref) + parsedRef, err := NewReference(ref) if err != nil { return nil, err } @@ -590,16 +590,16 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu return nil, err } - if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil { + if err := memoryStore.StoreManifest(parsedRef.OrasReference.String(), manifest, manifestData); err != nil { return nil, err } - remotesResolver, err := c.resolver(parsedRef) + remotesResolver, err := c.resolver(parsedRef.OrasReference) if err != nil { return nil, err } registryStore := content.Registry{Resolver: remotesResolver} - _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "", + _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.OrasReference.String(), registryStore, "", oras.WithNameValidation(nil)) if err != nil { return nil, err @@ -620,7 +620,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu }, Chart: chartSummary, Prov: &descriptorPushSummary{}, // prevent nil references - Ref: parsedRef.String(), + Ref: parsedRef.OrasReference.String(), } if operation.provData != nil { result.Prov = &descriptorPushSummary{ @@ -630,7 +630,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu } fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref) fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) - if strings.Contains(parsedRef.Reference, "_") { + if strings.Contains(parsedRef.OrasReference.Reference, "_") { fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) fmt.Fprint(c.out, registryUnderscoreMessage+"\n") } @@ -701,3 +701,23 @@ func (c *Client) Tags(ref string) ([]string, error) { return tags, nil } + +// Resolve a reference to a descriptor. +func (c *Client) Resolve(ref string) (*ocispec.Descriptor, error) { + ctx := context.Background() + parsedRef, err := NewReference(ref) + if err != nil { + return nil, err + } + if parsedRef.Registry == "" { + return nil, nil + } + + remotesResolver, err := c.resolver(parsedRef.OrasReference) + if err != nil { + return nil, err + } + + _, desc, err := remotesResolver.Resolve(ctx, ref) + return &desc, err +} diff --git a/pkg/registry/reference.go b/pkg/registry/reference.go new file mode 100644 index 000000000..09b99588b --- /dev/null +++ b/pkg/registry/reference.go @@ -0,0 +1,71 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "strings" + + orasregistry "oras.land/oras-go/pkg/registry" +) + +type Reference struct { + OrasReference orasregistry.Reference + Registry string + Repository string + Tag string + Digest string +} + +// NewReference will parse and validate the reference, and clean tags when +// applicable tags are only cleaned when plus (+) signs are present, and are +// converted to underscores (_) before pushing +// See https://github.com/helm/helm/issues/10166 +func NewReference(raw string) (result Reference, err error) { + // Remove oci:// prefix if it is there + raw = strings.TrimPrefix(raw, OCIScheme+"://") + + // The sole possible reference modification is replacing plus (+) signs + // present in tags with underscores (_). To do this properly, we first + // need to identify a tag, and then pass it on to the reference parser + // NOTE: Passing immediately to the reference parser will fail since (+) + // signs are an invalid tag character, and simply replacing all plus (+) + // occurrences could invalidate other portions of the URI + lastIndex := strings.LastIndex(raw, "@") + if lastIndex >= 0 { + result.Digest = raw[(lastIndex + 1):] + raw = raw[:lastIndex] + } + parts := strings.Split(raw, ":") + if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { + tag := parts[len(parts)-1] + + if tag != "" { + // Replace any plus (+) signs with known underscore (_) conversion + newTag := strings.ReplaceAll(tag, "+", "_") + raw = strings.ReplaceAll(raw, tag, newTag) + } + } + + result.OrasReference, err = orasregistry.ParseReference(raw) + if err != nil { + return result, err + } + result.Registry = result.OrasReference.Registry + result.Repository = result.OrasReference.Repository + result.Tag = result.OrasReference.Reference + return result, nil +} diff --git a/pkg/registry/reference_test.go b/pkg/registry/reference_test.go new file mode 100644 index 000000000..986c10edd --- /dev/null +++ b/pkg/registry/reference_test.go @@ -0,0 +1,87 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import "testing" + +func verify(t *testing.T, actual Reference, registry, repository, tag, digest string) { + if registry != actual.OrasReference.Registry { + t.Errorf("Oras Reference registry expected %v actual %v", registry, actual.Registry) + } + if repository != actual.OrasReference.Repository { + t.Errorf("Oras Reference repository expected %v actual %v", repository, actual.Repository) + } + if tag != actual.OrasReference.Reference { + t.Errorf("Oras Reference reference expected %v actual %v", tag, actual.Tag) + } + if registry != actual.Registry { + t.Errorf("Registry expected %v actual %v", registry, actual.Registry) + } + if repository != actual.Repository { + t.Errorf("Repository expected %v actual %v", repository, actual.Repository) + } + if tag != actual.Tag { + t.Errorf("Tag expected %v actual %v", tag, actual.Tag) + } + if digest != actual.Digest { + t.Errorf("Digest expected %v actual %v", digest, actual.Digest) + } +} + +func TestNewReference(t *testing.T) { + actual, err := NewReference("registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + + actual, err = NewReference("oci://registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + + actual, err = NewReference("a/b:1@c") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "a", "b", "1", "c") + + actual, err = NewReference("a/b:@") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "a", "b", "", "") + + actual, err = NewReference("registry.example.com/repository:1.0+001") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "repository", "1.0_001", "") + + actual, err = NewReference("thing:1.0") + if err == nil { + t.Errorf("Expect error error %v", err) + } + verify(t, actual, "", "", "", "") + + actual, err = NewReference("registry.example.com/the/repository@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "the/repository", "", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") +} diff --git a/pkg/registry/util.go b/pkg/registry/util.go index 727cdae03..44519de41 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/util.go @@ -32,7 +32,6 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" orascontext "oras.land/oras-go/pkg/context" - "oras.land/oras-go/pkg/registry" "helm.sh/helm/v3/internal/tlsutil" "helm.sh/helm/v3/pkg/chart" @@ -115,31 +114,6 @@ func ctx(out io.Writer, debug bool) context.Context { return ctx } -// parseReference will parse and validate the reference, and clean tags when -// applicable tags are only cleaned when plus (+) signs are present, and are -// converted to underscores (_) before pushing -// See https://github.com/helm/helm/issues/10166 -func parseReference(raw string) (registry.Reference, error) { - // The sole possible reference modification is replacing plus (+) signs - // present in tags with underscores (_). To do this properly, we first - // need to identify a tag, and then pass it on to the reference parser - // NOTE: Passing immediately to the reference parser will fail since (+) - // signs are an invalid tag character, and simply replacing all plus (+) - // occurrences could invalidate other portions of the URI - parts := strings.Split(raw, ":") - if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { - tag := parts[len(parts)-1] - - if tag != "" { - // Replace any plus (+) signs with known underscore (_) conversion - newTag := strings.ReplaceAll(tag, "+", "_") - raw = strings.ReplaceAll(raw, tag, newTag) - } - } - - return registry.ParseReference(raw) -} - // NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled. func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) { tlsConf, err := tlsutil.NewClientTLS(certFile, keyFile, caFile, insecureSkipTLSverify) From aca7e8d775a3674b41d989ac5be263236273be7b Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 20 Sep 2024 11:55:34 -0600 Subject: [PATCH 2/3] fix: issue with helm template and oci chart Signed-off-by: Terry Howe --- pkg/registry/client.go | 8 ++++---- pkg/registry/reference.go | 7 +++++++ pkg/registry/reference_test.go | 12 ++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 32d2773a3..4291fe568 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -357,7 +357,7 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } registryStore := content.Registry{Resolver: remotesResolver} - manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.OrasReference.String(), memoryStore, "", + manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "", oras.WithPullEmptyNameAllowed(), oras.WithAllowedMediaTypes(allowedMediaTypes), oras.WithLayerDescriptors(func(l []ocispec.Descriptor) { @@ -419,7 +419,7 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { }, Chart: &DescriptorPullSummaryWithMeta{}, Prov: &DescriptorPullSummary{}, - Ref: parsedRef.OrasReference.String(), + Ref: parsedRef.String(), } var getManifestErr error if _, manifestData, ok := memoryStore.Get(manifest); !ok { @@ -590,7 +590,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu return nil, err } - if err := memoryStore.StoreManifest(parsedRef.OrasReference.String(), manifest, manifestData); err != nil { + if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil { return nil, err } @@ -620,7 +620,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu }, Chart: chartSummary, Prov: &descriptorPushSummary{}, // prevent nil references - Ref: parsedRef.OrasReference.String(), + Ref: parsedRef.String(), } if operation.provData != nil { result.Prov = &descriptorPushSummary{ diff --git a/pkg/registry/reference.go b/pkg/registry/reference.go index 09b99588b..2ba0266a9 100644 --- a/pkg/registry/reference.go +++ b/pkg/registry/reference.go @@ -69,3 +69,10 @@ func NewReference(raw string) (result Reference, err error) { result.Tag = result.OrasReference.Reference return result, nil } + +func (r *Reference) String() string { + if r.Tag == "" { + return r.OrasReference.String() + "@" + r.Digest + } + return r.OrasReference.String() +} diff --git a/pkg/registry/reference_test.go b/pkg/registry/reference_test.go index 986c10edd..d62a62eb4 100644 --- a/pkg/registry/reference_test.go +++ b/pkg/registry/reference_test.go @@ -40,6 +40,18 @@ func verify(t *testing.T, actual Reference, registry, repository, tag, digest st if digest != actual.Digest { t.Errorf("Digest expected %v actual %v", digest, actual.Digest) } + expectedString := registry + if repository != "" { + expectedString = expectedString + "/" + repository + } + if tag != "" { + expectedString = expectedString + ":" + tag + } else { + expectedString = expectedString + "@" + digest + } + if actual.String() != expectedString { + t.Errorf("String expected %s actual %s", expectedString, actual.String()) + } } func TestNewReference(t *testing.T) { From d2b94f62004e79864ec530989109cf0effd4aaae Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 24 Oct 2024 07:38:41 -0600 Subject: [PATCH 3/3] fix: make ORAS reference private Signed-off-by: Terry Howe --- pkg/downloader/chart_downloader.go | 68 +----------------------- pkg/registry/client.go | 83 +++++++++++++++++++++++++++--- pkg/registry/reference.go | 22 ++++---- pkg/registry/reference_test.go | 28 +++++----- 4 files changed, 101 insertions(+), 100 deletions(-) diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 2d2e22f68..4dc8328c0 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -23,7 +23,6 @@ import ( "path/filepath" "strings" - "github.com/Masterminds/semver/v3" "github.com/pkg/errors" "helm.sh/helm/v3/internal/fileutil" @@ -141,71 +140,6 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return destfile, ver, nil } -func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, error) { - var tag string - - registryReference, err := registry.NewReference(u.Path) - if err != nil { - return nil, err - } - - if version == "" { - // Use OCI URI tag as default - version = registryReference.Tag - } else { - if registryReference.Tag != "" && registryReference.Tag != version { - return nil, errors.Errorf("chart reference and version mismatch: %s is not %s", version, registryReference.Tag) - } - } - - if registryReference.Digest != "" { - if registryReference.Tag == "" { - // Install by digest only - return u, nil - } - - // Validate the tag if it was specified - path := registryReference.Registry + "/" + registryReference.Repository + ":" + registryReference.Tag - desc, err := c.RegistryClient.Resolve(path) - if err != nil { - // The resource does not have to be tagged when digest is specified - return u, nil - } - if desc != nil && desc.Digest.String() != registryReference.Digest { - return nil, errors.Errorf("chart reference digest mismatch: %s is not %s", desc.Digest.String(), registryReference.Digest) - } - return u, nil - } - - // Evaluate whether an explicit version has been provided. Otherwise, determine version to use - _, errSemVer := semver.NewVersion(version) - if errSemVer == nil { - tag = version - } else { - // Retrieve list of repository tags - tags, err := c.RegistryClient.Tags(strings.TrimPrefix(ref, fmt.Sprintf("%s://", registry.OCIScheme))) - if err != nil { - return nil, err - } - if len(tags) == 0 { - return nil, errors.Errorf("Unable to locate any tags in provided repository: %s", ref) - } - - // Determine if version provided - // If empty, try to get the highest available tag - // If exact version, try to find it - // If semver constraint string, try to find a match - tag, err = registry.GetTagMatchingVersionOrConstraint(tags, version) - if err != nil { - return nil, err - } - } - - u.Path = fmt.Sprintf("%s/%s:%s", registryReference.Registry, registryReference.Repository, tag) - - return u, err -} - // ResolveChartVersion resolves a chart reference to a URL. // // It returns the URL and sets the ChartDownloader's Options that can fetch @@ -228,7 +162,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er } if registry.IsOCI(u.String()) { - return c.getOciURI(ref, version, u) + return c.RegistryClient.ValidateReference(ref, version, u) } rf, err := loadRepoConfig(c.RepositoryConfig) diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 4291fe568..744f528ef 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net/http" + "net/url" "sort" "strings" @@ -319,7 +320,7 @@ type ( // Pull downloads a chart from a registry func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { - parsedRef, err := NewReference(ref) + parsedRef, err := newReference(ref) if err != nil { return nil, err } @@ -351,7 +352,7 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } var descriptors, layers []ocispec.Descriptor - remotesResolver, err := c.resolver(parsedRef.OrasReference) + remotesResolver, err := c.resolver(parsedRef.orasReference) if err != nil { return nil, err } @@ -535,7 +536,7 @@ type ( // Push uploads a chart to a registry. func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) { - parsedRef, err := NewReference(ref) + parsedRef, err := newReference(ref) if err != nil { return nil, err } @@ -594,12 +595,12 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu return nil, err } - remotesResolver, err := c.resolver(parsedRef.OrasReference) + remotesResolver, err := c.resolver(parsedRef.orasReference) if err != nil { return nil, err } registryStore := content.Registry{Resolver: remotesResolver} - _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.OrasReference.String(), registryStore, "", + _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.orasReference.String(), registryStore, "", oras.WithNameValidation(nil)) if err != nil { return nil, err @@ -630,7 +631,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu } fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref) fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) - if strings.Contains(parsedRef.OrasReference.Reference, "_") { + if strings.Contains(parsedRef.orasReference.Reference, "_") { fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) fmt.Fprint(c.out, registryUnderscoreMessage+"\n") } @@ -705,7 +706,7 @@ func (c *Client) Tags(ref string) ([]string, error) { // Resolve a reference to a descriptor. func (c *Client) Resolve(ref string) (*ocispec.Descriptor, error) { ctx := context.Background() - parsedRef, err := NewReference(ref) + parsedRef, err := newReference(ref) if err != nil { return nil, err } @@ -713,7 +714,7 @@ func (c *Client) Resolve(ref string) (*ocispec.Descriptor, error) { return nil, nil } - remotesResolver, err := c.resolver(parsedRef.OrasReference) + remotesResolver, err := c.resolver(parsedRef.orasReference) if err != nil { return nil, err } @@ -721,3 +722,69 @@ func (c *Client) Resolve(ref string) (*ocispec.Descriptor, error) { _, desc, err := remotesResolver.Resolve(ctx, ref) return &desc, err } + +// ValidateReference for path and version +func (c *Client) ValidateReference(ref, version string, u *url.URL) (*url.URL, error) { + var tag string + + registryReference, err := newReference(u.Path) + if err != nil { + return nil, err + } + + if version == "" { + // Use OCI URI tag as default + version = registryReference.Tag + } else { + if registryReference.Tag != "" && registryReference.Tag != version { + return nil, errors.Errorf("chart reference and version mismatch: %s is not %s", version, registryReference.Tag) + } + } + + if registryReference.Digest != "" { + if registryReference.Tag == "" { + // Install by digest only + return u, nil + } + + // Validate the tag if it was specified + path := registryReference.Registry + "/" + registryReference.Repository + ":" + registryReference.Tag + desc, err := c.Resolve(path) + if err != nil { + // The resource does not have to be tagged when digest is specified + return u, nil + } + if desc != nil && desc.Digest.String() != registryReference.Digest { + return nil, errors.Errorf("chart reference digest mismatch: %s is not %s", desc.Digest.String(), registryReference.Digest) + } + return u, nil + } + + // Evaluate whether an explicit version has been provided. Otherwise, determine version to use + _, errSemVer := semver.NewVersion(version) + if errSemVer == nil { + tag = version + } else { + // Retrieve list of repository tags + tags, err := c.Tags(strings.TrimPrefix(ref, fmt.Sprintf("%s://", OCIScheme))) + if err != nil { + return nil, err + } + if len(tags) == 0 { + return nil, errors.Errorf("Unable to locate any tags in provided repository: %s", ref) + } + + // Determine if version provided + // If empty, try to get the highest available tag + // If exact version, try to find it + // If semver constraint string, try to find a match + tag, err = GetTagMatchingVersionOrConstraint(tags, version) + if err != nil { + return nil, err + } + } + + u.Path = fmt.Sprintf("%s/%s:%s", registryReference.Registry, registryReference.Repository, tag) + + return u, err +} diff --git a/pkg/registry/reference.go b/pkg/registry/reference.go index 2ba0266a9..9b99d73bf 100644 --- a/pkg/registry/reference.go +++ b/pkg/registry/reference.go @@ -22,19 +22,19 @@ import ( orasregistry "oras.land/oras-go/pkg/registry" ) -type Reference struct { - OrasReference orasregistry.Reference +type reference struct { + orasReference orasregistry.Reference Registry string Repository string Tag string Digest string } -// NewReference will parse and validate the reference, and clean tags when +// newReference will parse and validate the reference, and clean tags when // applicable tags are only cleaned when plus (+) signs are present, and are // converted to underscores (_) before pushing // See https://github.com/helm/helm/issues/10166 -func NewReference(raw string) (result Reference, err error) { +func newReference(raw string) (result reference, err error) { // Remove oci:// prefix if it is there raw = strings.TrimPrefix(raw, OCIScheme+"://") @@ -60,19 +60,19 @@ func NewReference(raw string) (result Reference, err error) { } } - result.OrasReference, err = orasregistry.ParseReference(raw) + result.orasReference, err = orasregistry.ParseReference(raw) if err != nil { return result, err } - result.Registry = result.OrasReference.Registry - result.Repository = result.OrasReference.Repository - result.Tag = result.OrasReference.Reference + result.Registry = result.orasReference.Registry + result.Repository = result.orasReference.Repository + result.Tag = result.orasReference.Reference return result, nil } -func (r *Reference) String() string { +func (r *reference) String() string { if r.Tag == "" { - return r.OrasReference.String() + "@" + r.Digest + return r.orasReference.String() + "@" + r.Digest } - return r.OrasReference.String() + return r.orasReference.String() } diff --git a/pkg/registry/reference_test.go b/pkg/registry/reference_test.go index d62a62eb4..31317d18f 100644 --- a/pkg/registry/reference_test.go +++ b/pkg/registry/reference_test.go @@ -18,15 +18,15 @@ package registry import "testing" -func verify(t *testing.T, actual Reference, registry, repository, tag, digest string) { - if registry != actual.OrasReference.Registry { - t.Errorf("Oras Reference registry expected %v actual %v", registry, actual.Registry) +func verify(t *testing.T, actual reference, registry, repository, tag, digest string) { + if registry != actual.orasReference.Registry { + t.Errorf("Oras reference registry expected %v actual %v", registry, actual.Registry) } - if repository != actual.OrasReference.Repository { - t.Errorf("Oras Reference repository expected %v actual %v", repository, actual.Repository) + if repository != actual.orasReference.Repository { + t.Errorf("Oras reference repository expected %v actual %v", repository, actual.Repository) } - if tag != actual.OrasReference.Reference { - t.Errorf("Oras Reference reference expected %v actual %v", tag, actual.Tag) + if tag != actual.orasReference.Reference { + t.Errorf("Oras reference reference expected %v actual %v", tag, actual.Tag) } if registry != actual.Registry { t.Errorf("Registry expected %v actual %v", registry, actual.Registry) @@ -55,43 +55,43 @@ func verify(t *testing.T, actual Reference, registry, repository, tag, digest st } func TestNewReference(t *testing.T) { - actual, err := NewReference("registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + actual, err := newReference("registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") if err != nil { t.Errorf("Unexpected error %v", err) } verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") - actual, err = NewReference("oci://registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + actual, err = newReference("oci://registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") if err != nil { t.Errorf("Unexpected error %v", err) } verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") - actual, err = NewReference("a/b:1@c") + actual, err = newReference("a/b:1@c") if err != nil { t.Errorf("Unexpected error %v", err) } verify(t, actual, "a", "b", "1", "c") - actual, err = NewReference("a/b:@") + actual, err = newReference("a/b:@") if err != nil { t.Errorf("Unexpected error %v", err) } verify(t, actual, "a", "b", "", "") - actual, err = NewReference("registry.example.com/repository:1.0+001") + actual, err = newReference("registry.example.com/repository:1.0+001") if err != nil { t.Errorf("Unexpected error %v", err) } verify(t, actual, "registry.example.com", "repository", "1.0_001", "") - actual, err = NewReference("thing:1.0") + actual, err = newReference("thing:1.0") if err == nil { t.Errorf("Expect error error %v", err) } verify(t, actual, "", "", "", "") - actual, err = NewReference("registry.example.com/the/repository@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + actual, err = newReference("registry.example.com/the/repository@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") if err != nil { t.Errorf("Unexpected error %v", err) }