pull/32540/merge
Aleksei Sviridkin 2 days ago committed by GitHub
commit 32353fa159
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -28,6 +28,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
@ -571,8 +572,12 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
// Build allowed media types for chart pull
allowedMediaTypes := []string{
ocispec.MediaTypeImageIndex,
ocispec.MediaTypeImageManifest,
// A chart converted between registries comes back written to the Docker
// schema, which describes the same manifest. Copying it is what selecting it
// out of an index is for; leaving the type out drops the manifest the copy
// was pointed at and fails the pull on a blob it never stored.
dockerManifestMediaType,
ConfigMediaType,
}
if operation.withChart {
@ -582,10 +587,29 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType)
}
// An Image Index may hold several chart manifests. Name them, so selection can
// pick the requested one instead of whichever descriptor happens to arrive last.
// The chart name is the reference's last path segment; the version, when the
// reference carries a tag, breaks a tie between same-named charts.
var selectors map[string]string
if parsed, perr := newReference(ref); perr == nil {
if name := path.Base(parsed.Repository); name != "" && name != "." {
selectors = map[string]string{ocispec.AnnotationTitle: name}
if parsed.Tag != "" {
// newReference stores "+" as "_" in a tag; undo it to compare against
// the chart's own version annotation.
selectors[ocispec.AnnotationVersion] = strings.ReplaceAll(parsed.Tag, "_", "+")
}
}
}
// Use generic client for the pull operation
genericClient := c.Generic()
genericResult, err := genericClient.PullGeneric(ref, GenericPullOptions{
AllowedMediaTypes: allowedMediaTypes,
ArtifactType: ChartArtifactType,
Selectors: selectors,
ParseIdentity: parseChartIdentity,
})
if err != nil {
return nil, err
@ -910,6 +934,19 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (string, *ur
return "", u, err
}
// parseChartIdentity reads a chart's name and version out of its config blob,
// which is Chart.yaml serialised as JSON.
func parseChartIdentity(config []byte) (name, version string, err error) {
var meta struct {
Name string `json:"name"`
Version string `json:"version"`
}
if err := json.Unmarshal(config, &meta); err != nil {
return "", "", err
}
return meta.Name, meta.Version, nil
}
// tagManifest prepares and tags a manifest in memory storage
func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store,
configDescriptor ocispec.Descriptor, layers []ocispec.Descriptor,

@ -34,4 +34,11 @@ const (
// LegacyChartLayerMediaType is the legacy reserved media type for Helm chart package content.
LegacyChartLayerMediaType = "application/tar+gzip"
// ChartArtifactType identifies a Helm chart manifest inside an OCI Image Index.
// The OCI image spec defines an index descriptor's artifactType as the value of
// the referenced manifest's config mediaType, so the two are the same string by
// design rather than by coincidence.
// https://github.com/opencontainers/image-spec/blob/main/descriptor.md#properties
ChartArtifactType = ConfigMediaType
)

@ -18,10 +18,14 @@ package registry
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"slices"
"sort"
"strings"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@ -33,7 +37,9 @@ import (
"oras.land/oras-go/v2/registry/remote/credentials"
)
// GenericClient provides low-level OCI operations without artifact-specific assumptions
// GenericClient provides low-level OCI operations parameterised by artifact type
// rather than hardcoded to one artifact. Its diagnostics are not: index selection
// reports what it rejected in the vocabulary of Helm charts.
type GenericClient struct {
debug bool
enableCache bool
@ -56,6 +62,28 @@ type GenericPullOptions struct {
SkipMediaTypes []string
// Custom PreCopy function for filtering
PreCopy func(context.Context, ocispec.Descriptor) error
// ArtifactType to select from OCI Image Index (empty means no filtering).
// When pulling from an Image Index containing multiple manifests,
// this field is used to select the manifest with matching artifactType.
ArtifactType string
// Selectors disambiguate when more than one manifest in an Image Index matches
// ArtifactType. Two keys are honoured and the rest are ignored:
// org.opencontainers.image.title and org.opencontainers.image.version. Either
// annotation may be absent, and the missing one is then read out of the
// candidate's own config blob.
//
// Both choose between candidates rather than validate one: a reference that
// resolves to a single chart yields that chart whatever was asked for, matching
// what a reference to a plain manifest already does, and the version is consulted
// only once a name has left more than one candidate standing. Selectors are
// consulted for a single candidate in one case only, when entries were dropped
// and being the last one standing is therefore not a fact about the index.
Selectors map[string]string
// ParseIdentity reads a name and version out of an artifact's config blob. It is
// what lets selection fall back when an index descriptor carries no annotations,
// and it is the only place an artifact's own format is known here. Without it,
// selection matches on descriptor annotations alone.
ParseIdentity func(config []byte) (name, version string, err error)
}
// GenericPullResult contains the result of a generic pull operation
@ -83,7 +111,465 @@ func NewGenericClient(client *Client) *GenericClient {
}
}
// PullGeneric performs a generic OCI pull without artifact-specific assumptions
// maxIndexDepth is how many indexes deep the search follows, counting from the one
// the reference resolves to. A multi-arch image listed beside a chart is depth one,
// which is what tooling produces today, and an index aggregating such indexes is
// depth two. Three leaves a level beyond anything a publisher assembles. Past that
// the cost is one sequential request per level against a chain the registry is free
// to make up as it is walked.
const maxIndexDepth = 3
// nestedIndex is an index listed inside another one, carried with the depth it was
// found at, since that is what the limit above is applied to.
type nestedIndex struct {
desc ocispec.Descriptor
depth int
}
// dockerManifestListMediaType is what Docker Hub and older buildx write where the
// OCI spec writes an image index. It lists manifests the same way, so an entry
// carrying it holds artifacts the same way too.
const dockerManifestListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json"
// dockerManifestMediaType is the same story one level down: a manifest copied
// between registries by tooling that writes the Docker schema describes its config
// and layers exactly as an image manifest does.
const dockerManifestMediaType = "application/vnd.docker.distribution.manifest.v2+json"
// listsManifests reports whether an entry holds other entries rather than an
// artifact. Such an entry is never a candidate, since an artifact is identified by
// the config blob its manifest carries and a list has none, and it is searched
// rather than dropped, since it can hold the artifact that was asked for.
func listsManifests(mediaType string) bool {
return mediaType == ocispec.MediaTypeImageIndex || mediaType == dockerManifestListMediaType
}
// maxEntryBytes bounds what the pass below reads out of an entry it was not asked
// for. The size is the index's own claim about content the registry serves, and a
// manifest that large is not a manifest, so an entry declaring more is left unread
// instead of streamed. The figure is what oras allows a manifest to be. Reading a
// candidate's own identity is bounded by oras instead, which caps what it allocates
// ahead of the content and verifies size and digest against it.
const maxEntryBytes = 4 * 1024 * 1024
// entryBody is what an entry turns out to hold once read, which is what decides how
// it is treated: a config makes it an artifact and a list of manifests makes it a
// container of artifacts. An entry declared as a manifest that holds no config is
// read as neither; one declared as a list that holds a config is too, while a list
// that holds nothing is a list, and searching it finds nothing to find.
type entryBody struct {
Config ocispec.Descriptor `json:"config"`
Manifests []ocispec.Descriptor `json:"manifests"`
}
// resolveFromIndex selects one manifest from an OCI Image Index by artifactType.
// When nothing matches on artifactType it retries over the entries that declare
// none, matching their config mediaType instead, which is how indexes written
// before artifactType existed are still resolvable. More than one match is
// narrowed by the selectors; a choice they cannot settle is an error rather than
// a guess.
func resolveFromIndex(ctx context.Context, fetcher content.Fetcher, indexDesc ocispec.Descriptor, artifactType string, selectors map[string]string, parse func(config []byte) (name, version string, err error)) (ocispec.Descriptor, error) {
// Fetch the index manifest
indexData, err := content.FetchAll(ctx, fetcher, indexDesc)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("unable to fetch image index: %w", err)
}
var index ocispec.Index
if err := json.Unmarshal(indexData, &index); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("unable to parse image index: %w", err)
}
// First pass: entries that declare the artifact type. A declared type is taken
// at face value even on a descriptor that also carries a platform, since tooling
// that stamps a platform on every index entry would otherwise hide the artifact.
var candidates []ocispec.Descriptor
var availableTypes []string
var unmatched []ocispec.Descriptor
var nested []nestedIndex
var skipped []error
for _, manifest := range index.Manifests {
// Set aside to be searched, per listsManifests: taken as a candidate the pull
// fails on a manifest it never stored, dropped it takes the chart it holds with
// it, and the chart beside it answers instead.
if listsManifests(manifest.MediaType) {
nested = append(nested, nestedIndex{desc: manifest, depth: 1})
continue
}
if manifest.ArtifactType == artifactType {
candidates = append(candidates, manifest)
continue
}
if manifest.ArtifactType != "" {
availableTypes = append(availableTypes, manifest.ArtifactType)
}
unmatched = append(unmatched, manifest)
}
wantName := selectors[ocispec.AnnotationTitle]
wantVersion := selectors[ocispec.AnnotationVersion]
// Second pass: the indexes an entry points at, and every entry whose declaration
// did not match, read for the config mediaType its manifest carries. An index
// written before artifactType existed declares nothing, and a builder that sets
// the field wrongly declares something else; both leave the config as the only
// place the artifact says what it is, so both are read here rather than only the
// first. It runs when the first pass found nothing, and also when no single entry
// it found announces the whole requested name and version: narrowing the
// candidates before either is checked is how a mixed index answers with the wrong
// chart. The cost is one fetch per entry that is not already a candidate, and it
// is not confined to failures: an index builder may set artifactType without
// copying the manifest annotations onto the descriptor, and then the declared
// entries announce nothing, so pulls that go on to succeed pay it as well.
configCache := map[string]ocispec.Descriptor{}
// Counted rather than taken from the length of the queue: an index may list the
// same manifest twice and the repeats are not read again, so the queue says how
// many entries were queued and this says how many were looked at.
read := 0
if len(candidates) == 0 || !announcesChart(candidates, wantName, wantVersion) {
// An index an entry points at is searched the same way the top one is, so a
// chart keeps its place wherever the publisher nested it. Entries already seen
// are not read twice, which is also what stops an index that lists itself.
seen := map[string]bool{indexDesc.Digest.String(): true}
for i := 0; i < len(nested); i++ {
entry := nested[i]
if seen[entry.desc.Digest.String()] {
continue
}
seen[entry.desc.Digest.String()] = true
if entry.depth > maxIndexDepth {
skipped = append(skipped, fmt.Errorf(
"%s: nested more than %d indexes deep and was not searched", entry.desc.Digest, maxIndexDepth))
continue
}
if entry.desc.Size > maxEntryBytes {
skipped = append(skipped, fmt.Errorf(
"%s: entry declares %d bytes, more than a manifest is, and was not read", entry.desc.Digest, entry.desc.Size))
continue
}
indexData, err := content.FetchAll(ctx, fetcher, entry.desc)
if err != nil {
skipped = append(skipped, fmt.Errorf("%s: %w", entry.desc.Digest, err))
continue
}
var sub entryBody
if err := json.Unmarshal(indexData, &sub); err != nil {
skipped = append(skipped, fmt.Errorf("%s: %w", entry.desc.Digest, err))
continue
}
if len(sub.Manifests) == 0 && sub.Config.MediaType != "" {
skipped = append(skipped, fmt.Errorf(
"%s: entry declares a list of manifests and holds a config instead, and was searched as neither", entry.desc.Digest))
continue
}
for _, manifest := range sub.Manifests {
switch {
case listsManifests(manifest.MediaType):
nested = append(nested, nestedIndex{desc: manifest, depth: entry.depth + 1})
case manifest.ArtifactType == artifactType:
candidates = append(candidates, manifest)
default:
if manifest.ArtifactType != "" {
availableTypes = append(availableTypes, manifest.ArtifactType)
}
unmatched = append(unmatched, manifest)
}
}
}
for _, candidate := range unmatched {
if seen[candidate.Digest.String()] {
continue
}
seen[candidate.Digest.String()] = true
if candidate.Size > maxEntryBytes {
skipped = append(skipped, fmt.Errorf(
"%s: entry declares %d bytes, more than a manifest is, and was not read", candidate.Digest, candidate.Size))
continue
}
manifestData, err := content.FetchAll(ctx, fetcher, candidate)
if err != nil {
skipped = append(skipped, fmt.Errorf("%s: %w", candidate.Digest, err))
continue
}
var body entryBody
if err := json.Unmarshal(manifestData, &body); err != nil {
skipped = append(skipped, fmt.Errorf("%s: %w", candidate.Digest, err))
continue
}
if body.Config.MediaType == "" {
skipped = append(skipped, fmt.Errorf(
"%s: entry declares a manifest and holds no config, and was read as neither", candidate.Digest))
continue
}
read++
if body.Config.MediaType == artifactType {
candidates = append(candidates, candidate)
configCache[candidate.Digest.String()] = body.Config
}
}
}
// An index may legally list the same manifest twice. Left in, the repeats would
// be counted as separate candidates and one chart would be called ambiguous with
// itself, so they are collapsed before anything downstream counts them.
candidates = dedupeByDigest(candidates)
// Every answer below rests on nothing else in the index matching. Absence is a
// fact about the index only while every entry could be read; once entries have
// been dropped it is a fact about the pass instead. So an incomplete pass may
// not hand back a candidate that merely survived, and its negative answers have
// to admit what they could not see. Reading an identity can fail too, and those
// failures join the same record, which is why it is consulted where it is used
// rather than captured once.
// A multi-arch image repeats one artifactType per platform; listing it once is
// the whole content of the message.
slices.Sort(availableTypes)
availableTypes = slices.Compact(availableTypes)
switch len(candidates) {
case 0:
if len(skipped) > 0 {
return ocispec.Descriptor{}, fmt.Errorf(
"no manifest with artifactType %q found in image index; available types: %v; %d entries could not be read: %v",
artifactType, availableTypes, len(skipped), skipped)
}
readNote := fmt.Sprintf("the manifests of %d other entries were read and none declares a chart config", read)
if read == 1 {
readNote = "the manifest of the one other entry was read and it declares no chart config"
}
return ocispec.Descriptor{}, fmt.Errorf(
"no manifest with artifactType %q found in image index; available types: %v; %s",
artifactType, availableTypes, readNote)
case 1:
// One candidate is taken without checking the name against it. Requiring a
// match would break publishing a chart under a repository named differently
// from the chart, which is legal and common; there is also nothing to choose
// between, so a wrong name here means the reference itself was wrong.
//
// That reasoning holds only while the candidate set is complete. Once entries
// have been dropped, "nothing to choose between" describes what could be read
// rather than what the index holds, and the survivor has to earn the answer.
// With nothing skipped the set is complete and this is the only chart in the
// index, so it answers whatever name was asked for. Only an incomplete pass
// makes that reasoning unsound, and only then is the identity worth reading.
if len(skipped) > 0 && wantName != "" {
name, version, idErr := descriptorIdentity(ctx, fetcher, candidates[0], configCache, parse)
switch {
case idErr != nil:
return ocispec.Descriptor{}, fmt.Errorf(
"the identity of the only remaining chart in the image index could not be read: %w; %d other entries could not be read either: %v",
idErr, len(skipped), skipped)
case name != wantName || (wantVersion != "" && version != wantVersion):
return ocispec.Descriptor{}, fmt.Errorf(
"the only readable chart in the image index is %q version %q, and the reference asks for %q version %q; %d entries could not be read, so it cannot be told whether the requested one is among them: %v",
name, version, wantName, wantVersion, len(skipped), skipped)
}
}
return candidates[0], nil
}
// More than one chart in the index: the name is required to disambiguate and
// the version breaks a remaining tie. Every error below lists the candidates,
// because the caller cannot see the index and has no other way to find out
// what it would have to pick between.
resolved := make([]chartCandidate, 0, len(candidates))
for _, d := range candidates {
name, version, idErr := descriptorIdentity(ctx, fetcher, d, configCache, parse)
if idErr != nil {
// An identity that could not be read matches nothing below, so losing it
// silently would turn "this chart is not here" into a claim about the
// index that only describes the read.
skipped = append(skipped, idErr)
}
resolved = append(resolved, chartCandidate{desc: d, name: name, version: version, idErr: idErr})
}
if wantName == "" {
return ocispec.Descriptor{}, fmt.Errorf(
"image index holds %d charts and no chart name was given to disambiguate; candidates: %s",
len(resolved), describeCandidates(resolved))
}
var named []chartCandidate
for _, cand := range resolved {
if cand.name == wantName {
named = append(named, cand)
}
}
switch len(named) {
case 0:
if len(skipped) > 0 {
return ocispec.Descriptor{}, fmt.Errorf(
"no chart named %q among the %d found in the image index; %d entries could not be read: %v; candidates: %s",
wantName, len(resolved), len(skipped), skipped, describeCandidates(resolved))
}
return ocispec.Descriptor{}, fmt.Errorf(
"image index holds %d charts and none is named %q; candidates: %s",
len(resolved), wantName, describeCandidates(resolved))
case 1:
if len(skipped) > 0 && wantVersion != "" && named[0].version != wantVersion {
if named[0].idErr != nil {
return ocispec.Descriptor{}, fmt.Errorf(
"the version of the only chart named %q could not be read: %w; %d entries could not be read: %v",
wantName, named[0].idErr, len(skipped), skipped)
}
return ocispec.Descriptor{}, fmt.Errorf(
"the only readable chart named %q is version %q, not %q; %d entries could not be read: %v",
wantName, named[0].version, wantVersion, len(skipped), skipped)
}
return named[0].desc, nil
}
// Several charts share the requested name: break the tie by version.
if wantVersion != "" {
var versioned []chartCandidate
for _, cand := range named {
if cand.version == wantVersion {
versioned = append(versioned, cand)
}
}
switch len(versioned) {
case 1:
return versioned[0].desc, nil
case 0:
if len(skipped) > 0 {
return ocispec.Descriptor{}, fmt.Errorf(
"cannot choose between the %d charts named %q, and none is version %q; %d entries could not be read: %v; candidates: %s",
len(named), wantName, wantVersion, len(skipped), skipped, describeCandidates(named))
}
return ocispec.Descriptor{}, fmt.Errorf(
"cannot choose between the %d charts named %q, and none is version %q; candidates: %s",
len(named), wantName, wantVersion, describeCandidates(named))
default:
return ocispec.Descriptor{}, fmt.Errorf(
"image index holds %d entries for %s:%s; they can only be told apart by digest: %s",
len(versioned), wantName, wantVersion, describeDigests(versioned))
}
}
return ocispec.Descriptor{}, fmt.Errorf(
"image index is ambiguous: %d charts named %q; specify a version; candidates: %s",
len(named), wantName, describeCandidates(named))
}
// dedupeByDigest keeps the first entry for each digest, preserving index order.
func dedupeByDigest(descs []ocispec.Descriptor) []ocispec.Descriptor {
seen := make(map[string]bool, len(descs))
out := make([]ocispec.Descriptor, 0, len(descs))
for _, d := range descs {
key := d.Digest.String()
if seen[key] {
continue
}
seen[key] = true
out = append(out, d)
}
return out
}
// describeDigests renders candidates as digests, for the one case where their
// name and version are identical and nothing else tells them apart.
func describeDigests(candidates []chartCandidate) string {
parts := make([]string, 0, len(candidates))
for _, c := range candidates {
parts = append(parts, c.desc.Digest.String())
}
return strings.Join(parts, ", ")
}
// announcesChart reports whether any single descriptor in the index announces the
// whole requested identity. Asking per attribute instead would let one candidate supply
// the name and another the version, and the pair they form belongs to neither.
// It only reads what the index already states: an identity that is knowable but
// not announced answers false, which costs the second pass a fetch it did not
// strictly need and never costs a wrong answer.
func announcesChart(descs []ocispec.Descriptor, name, version string) bool {
return slices.ContainsFunc(descs, func(d ocispec.Descriptor) bool {
if name != "" && d.Annotations[ocispec.AnnotationTitle] != name {
return false
}
return version == "" || d.Annotations[ocispec.AnnotationVersion] == version
})
}
// chartCandidate pairs an index descriptor with the chart identity used to
// disambiguate it.
type chartCandidate struct {
desc ocispec.Descriptor
name string
version string
// idErr is set when the identity could not be read. Kept on the candidate so a
// message about it cannot claim a name or version that was never fetched.
idErr error
}
// descriptorIdentity returns a candidate's name and version, preferring the
// descriptor annotations and asking parse to read whichever of the two they omit
// out of the config blob. Both are needed: a name alone cannot break a tie between
// two candidates sharing it. Without a parser only the annotations are available.
func descriptorIdentity(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor, configCache map[string]ocispec.Descriptor, parse func(config []byte) (name, version string, err error)) (name, version string, err error) {
name = desc.Annotations[ocispec.AnnotationTitle]
version = desc.Annotations[ocispec.AnnotationVersion]
if (name != "" && version != "") || parse == nil {
return name, version, nil
}
// Resolve the config descriptor, reusing the one captured during the fallback
// pass when present so the manifest is not fetched a second time.
config, ok := configCache[desc.Digest.String()]
if !ok {
manifestData, ferr := content.FetchAll(ctx, fetcher, desc)
if ferr != nil {
return name, version, ferr
}
var manifest ocispec.Manifest
if uerr := json.Unmarshal(manifestData, &manifest); uerr != nil {
return name, version, fmt.Errorf("%s: %w", desc.Digest, uerr)
}
config = manifest.Config
}
configData, ferr := content.FetchAll(ctx, fetcher, config)
if ferr != nil {
return name, version, ferr
}
parsedName, parsedVersion, perr := parse(configData)
if perr != nil {
return name, version, fmt.Errorf("%s: %w", desc.Digest, perr)
}
if name == "" {
name = parsedName
}
if version == "" {
version = parsedVersion
}
return name, version, nil
}
// describeCandidates renders chart candidates as name:version, falling back to
// the digest, for disambiguation error messages. A candidate whose identity could
// not be read is marked as such: rendering it like one that simply carries no
// version would state as absent what was never looked at.
func describeCandidates(candidates []chartCandidate) string {
parts := make([]string, 0, len(candidates))
for _, c := range candidates {
switch {
case c.idErr != nil && c.name == "":
parts = append(parts, c.desc.Digest.String()+" (identity unreadable)")
case c.idErr != nil:
parts = append(parts, c.name+" (version unreadable)")
case c.name == "":
parts = append(parts, c.desc.Digest.String())
case c.version == "":
parts = append(parts, c.name)
default:
parts = append(parts, c.name+":"+c.version)
}
}
return strings.Join(parts, ", ")
}
// PullGeneric performs an OCI pull parameterised by artifact type.
func (c *GenericClient) PullGeneric(ref string, options GenericPullOptions) (*GenericPullResult, error) {
parsedRef, err := newReference(ref)
if err != nil {
@ -112,7 +598,7 @@ func (c *GenericClient) PullGeneric(ref string, options GenericPullOptions) (*Ge
}
var mu sync.Mutex
manifest, err := oras.Copy(ctx, repository, parsedRef.String(), memoryStore, "", oras.CopyOptions{
copyOptions := oras.CopyOptions{
CopyGraphOptions: oras.CopyGraphOptions{
PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
// Apply a custom PreCopy function if provided
@ -142,8 +628,43 @@ func (c *GenericClient) PullGeneric(ref string, options GenericPullOptions) (*Ge
return nil
},
},
})
}
// Select inside the copy rather than before it. oras resolves the root itself
// and hands it to MapRoot through a proxy that still serves the body it already
// read, so an index is recognised and narrowed without a request of our own.
if options.ArtifactType != "" {
copyOptions.MapRoot = func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) {
if root.MediaType != ocispec.MediaTypeImageIndex {
return root, nil
}
selected, err := resolveFromIndex(ctx, src, root, options.ArtifactType, options.Selectors, options.ParseIdentity)
if err != nil {
return ocispec.Descriptor{}, err
}
// The copy about to start filters on the same media types the caller
// allowed, and a root it will not store is dropped without an error of its
// own: the pull then fails on a blob nothing ever wrote, naming neither the
// entry nor why. Selection knows both, so it says so here instead.
if len(allowedMediaTypes) > 0 {
if i := sort.SearchStrings(allowedMediaTypes, selected.MediaType); i >= len(allowedMediaTypes) || allowedMediaTypes[i] != selected.MediaType {
return ocispec.Descriptor{}, fmt.Errorf(
"the chart selected from the image index is a %s, which this pull does not accept: %s",
selected.MediaType, selected.Digest)
}
}
return selected, nil
}
}
manifest, err := oras.Copy(ctx, repository, parsedRef.String(), memoryStore, "", copyOptions)
if err != nil {
// oras wraps a MapRoot failure as its own copy error. The selection message
// underneath is the part naming something the caller can act on.
var copyErr *oras.CopyError
if errors.As(err, &copyErr) && copyErr.Op == "MapRoot" {
return nil, copyErr.Err
}
return nil, err
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save