diff --git a/go.mod b/go.mod index 8cea95188b..8b09b8d00c 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( google.golang.org/grpc v1.64.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 - oras.land/oras-go/v2 v2.3.1 + oras.land/oras-go/v2 v2.5.0 sigs.k8s.io/yaml v1.4.0 ) diff --git a/go.sum b/go.sum index fc02080b47..e30f3ed8ff 100644 --- a/go.sum +++ b/go.sum @@ -429,7 +429,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -oras.land/oras-go/v2 v2.3.1 h1:lUC6q8RkeRReANEERLfH86iwGn55lbSWP20egdFHVec= -oras.land/oras-go/v2 v2.3.1/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/vendor/modules.txt b/vendor/modules.txt index 0f57a4fdec..b2df1d79c0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -568,8 +568,8 @@ gopkg.in/yaml.v2 # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 -# oras.land/oras-go/v2 v2.3.1 -## explicit; go 1.20 +# oras.land/oras-go/v2 v2.5.0 +## explicit; go 1.21 oras.land/oras-go/v2 oras.land/oras-go/v2/content oras.land/oras-go/v2/content/oci diff --git a/vendor/oras.land/oras-go/v2/README.md b/vendor/oras.land/oras-go/v2/README.md index 70662555da..7c3013c7a2 100644 --- a/vendor/oras.land/oras-go/v2/README.md +++ b/vendor/oras.land/oras-go/v2/README.md @@ -20,7 +20,7 @@ The ORAS Go library follows [Semantic Versioning](https://semver.org/), where br The version `2` is actively developed in the [`main`](https://github.com/oras-project/oras-go/tree/main) branch with all new features. > [!Note] -> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.20` and `1.21`). +> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.21` and `1.22`). Examples for common use cases can be found below: diff --git a/vendor/oras.land/oras-go/v2/content.go b/vendor/oras.land/oras-go/v2/content.go index 53eb6c75ac..b8bf26386f 100644 --- a/vendor/oras.land/oras-go/v2/content.go +++ b/vendor/oras.land/oras-go/v2/content.go @@ -29,7 +29,6 @@ import ( "oras.land/oras-go/v2/internal/docker" "oras.land/oras-go/v2/internal/interfaces" "oras.land/oras-go/v2/internal/platform" - "oras.land/oras-go/v2/internal/registryutil" "oras.land/oras-go/v2/internal/syncutil" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote/auth" @@ -91,7 +90,7 @@ func TagN(ctx context.Context, target Target, srcReference string, dstReferences if err != nil { return ocispec.Descriptor{}, err } - ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull, auth.ActionPush) + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush) } desc, contentBytes, err := FetchBytes(ctx, target, srcReference, FetchBytesOptions{ @@ -149,7 +148,7 @@ func Tag(ctx context.Context, target Target, src, dst string) (ocispec.Descripto if err != nil { return ocispec.Descriptor{}, err } - ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull, auth.ActionPush) + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush) } desc, rc, err := refFetcher.FetchReference(ctx, src) if err != nil { diff --git a/vendor/oras.land/oras-go/v2/content/oci/oci.go b/vendor/oras.land/oras-go/v2/content/oci/oci.go index 27afde16e1..748aeecc6f 100644 --- a/vendor/oras.land/oras-go/v2/content/oci/oci.go +++ b/vendor/oras.land/oras-go/v2/content/oci/oci.go @@ -14,7 +14,7 @@ limitations under the License. */ // Package oci provides access to an OCI content store. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md package oci import ( @@ -23,7 +23,9 @@ import ( "errors" "fmt" "io" + "maps" "os" + "path" "path/filepath" "sync" @@ -35,29 +37,47 @@ import ( "oras.land/oras-go/v2/internal/container/set" "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/internal/graph" + "oras.land/oras-go/v2/internal/manifestutil" "oras.land/oras-go/v2/internal/resolver" + "oras.land/oras-go/v2/registry" ) // Store implements `oras.Target`, and represents a content store // based on file system with the OCI-Image layout. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md type Store struct { // AutoSaveIndex controls if the OCI store will automatically save the index - // file on each Tag() call. - // - If AutoSaveIndex is set to true, the OCI store will automatically call - // this method on each Tag() call. + // file when needed. + // - If AutoSaveIndex is set to true, the OCI store will automatically save + // the changes to `index.json` when + // 1. pushing a manifest + // 2. calling Tag() or Delete() // - If AutoSaveIndex is set to false, it's the caller's responsibility // to manually call SaveIndex() when needed. // - Default value: true. AutoSaveIndex bool - root string - indexPath string - index *ocispec.Index - indexLock sync.Mutex - storage content.Storage + // AutoGC controls if the OCI store will automatically clean dangling + // (unreferenced) blobs created by the Delete() operation. This includes the + // referrers and the unreferenced successor blobs of the deleted content. + // Tagged manifests will not be deleted. + // - Default value: true. + AutoGC bool + + root string + indexPath string + index *ocispec.Index + storage *Storage tagResolver *resolver.Memory graph *graph.Memory + + // sync ensures that most operations can be done concurrently, while Delete + // has the exclusive access to Store if a delete operation is underway. + // Operations such as Fetch, Push use sync.RLock(), while Delete uses + // sync.Lock(). + sync sync.RWMutex + // indexLock ensures that only one go-routine is writing to the index. + indexLock sync.Mutex } // New creates a new OCI store with context.Background(). @@ -78,6 +98,7 @@ func NewWithContext(ctx context.Context, root string) (*Store, error) { store := &Store{ AutoSaveIndex: true, + AutoGC: true, root: rootAbs, indexPath: filepath.Join(rootAbs, ocispec.ImageIndexFile), storage: storage, @@ -98,13 +119,21 @@ func NewWithContext(ctx context.Context, root string) (*Store, error) { return store, nil } -// Fetch fetches the content identified by the descriptor. +// Fetch fetches the content identified by the descriptor. It returns an io.ReadCloser. +// It's recommended to close the io.ReadCloser before a Delete operation, otherwise +// Delete may fail (for example on NTFS file systems). func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + s.sync.RLock() + defer s.sync.RUnlock() + return s.storage.Fetch(ctx, target) } // Push pushes the content, matching the expected descriptor. func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error { + s.sync.RLock() + defer s.sync.RUnlock() + if err := s.storage.Push(ctx, expected, reader); err != nil { return err } @@ -120,13 +149,84 @@ func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io // Exists returns true if the described content exists. func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + s.sync.RLock() + defer s.sync.RUnlock() + return s.storage.Exists(ctx, target) } +// Delete deletes the content matching the descriptor from the store. Delete may +// fail on certain systems (i.e. NTFS), if there is a process (i.e. an unclosed +// Reader) using target. If s.AutoGC is set to true, Delete will recursively +// remove the dangling blobs caused by the current delete. If s.AutoDeleteReferrers +// is set to true, Delete will recursively remove the referrers of the manifests +// being deleted. +func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error { + s.sync.Lock() + defer s.sync.Unlock() + + deleteQueue := []ocispec.Descriptor{target} + for len(deleteQueue) > 0 { + head := deleteQueue[0] + deleteQueue = deleteQueue[1:] + + // get referrers if applicable + if s.AutoGC && descriptor.IsManifest(head) { + referrers, err := registry.Referrers(ctx, &unsafeStore{s}, head, "") + if err != nil { + return err + } + deleteQueue = append(deleteQueue, referrers...) + } + + // delete the head of queue + danglings, err := s.delete(ctx, head) + if err != nil { + return err + } + if s.AutoGC { + for _, d := range danglings { + // do not delete existing tagged manifests + if !s.isTagged(d) { + deleteQueue = append(deleteQueue, d) + } + } + } + } + + return nil +} + +// delete deletes one node and returns the dangling nodes caused by the delete. +func (s *Store) delete(ctx context.Context, target ocispec.Descriptor) ([]ocispec.Descriptor, error) { + resolvers := s.tagResolver.Map() + untagged := false + for reference, desc := range resolvers { + if content.Equal(desc, target) { + s.tagResolver.Untag(reference) + untagged = true + } + } + danglings := s.graph.Remove(target) + if untagged && s.AutoSaveIndex { + err := s.saveIndex() + if err != nil { + return nil, err + } + } + if err := s.storage.Delete(ctx, target); err != nil { + return nil, err + } + return danglings, nil +} + // Tag tags a descriptor with a reference string. // reference should be a valid tag (e.g. "latest"). -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md#indexjson-file +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md#indexjson-file func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { + s.sync.RLock() + defer s.sync.RUnlock() + if err := validateReference(reference); err != nil { return err } @@ -155,7 +255,7 @@ func (s *Store) tag(ctx context.Context, desc ocispec.Descriptor, reference stri return err } if s.AutoSaveIndex { - return s.SaveIndex() + return s.saveIndex() } return nil } @@ -166,6 +266,9 @@ func (s *Store) tag(ctx context.Context, desc ocispec.Descriptor, reference stri // digest the returned descriptor will be a plain descriptor (containing only // the digest, media type and size). func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + s.sync.RLock() + defer s.sync.RUnlock() + if reference == "" { return ocispec.Descriptor{}, errdef.ErrMissingReference } @@ -187,10 +290,36 @@ func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descript return desc, nil } +func (s *Store) Untag(ctx context.Context, reference string) error { + if reference == "" { + return errdef.ErrMissingReference + } + + s.sync.RLock() + defer s.sync.RUnlock() + + desc, err := s.tagResolver.Resolve(ctx, reference) + if err != nil { + return fmt.Errorf("resolving reference %q: %w", reference, err) + } + if reference == desc.Digest.String() { + return fmt.Errorf("reference %q is a digest and not a tag: %w", reference, errdef.ErrInvalidReference) + } + + s.tagResolver.Untag(reference) + if s.AutoSaveIndex { + return s.saveIndex() + } + return nil +} + // Predecessors returns the nodes directly pointing to the current node. // Predecessors returns nil without error if the node does not exists in the // store. func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + s.sync.RLock() + defer s.sync.RUnlock() + return s.graph.Predecessors(ctx, node) } @@ -202,7 +331,10 @@ func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]oc // // See also `Tags()` in the package `registry`. func (s *Store) Tags(ctx context.Context, last string, fn func(tags []string) error) error { - return listTags(ctx, s.tagResolver, last, fn) + s.sync.RLock() + defer s.sync.RUnlock() + + return listTags(s.tagResolver, last, fn) } // ensureOCILayoutFile ensures the `oci-layout` file. @@ -263,10 +395,18 @@ func (s *Store) loadIndexFile(ctx context.Context) error { // SaveIndex writes the `index.json` file to the file system. // - If AutoSaveIndex is set to true (default value), -// the OCI store will automatically call this method on each Tag() call. +// the OCI store will automatically save the changes to `index.json` +// on Tag() and Delete() calls, and when pushing a manifest. // - If AutoSaveIndex is set to false, it's the caller's responsibility // to manually call this method when needed. func (s *Store) SaveIndex() error { + s.sync.RLock() + defer s.sync.RUnlock() + + return s.saveIndex() +} + +func (s *Store) saveIndex() error { s.indexLock.Lock() defer s.indexLock.Unlock() @@ -279,9 +419,7 @@ func (s *Store) SaveIndex() error { for ref, desc := range refMap { if ref != desc.Digest.String() { annotations := make(map[string]string, len(desc.Annotations)+1) - for k, v := range desc.Annotations { - annotations[k] = v - } + maps.Copy(annotations, desc.Annotations) annotations[ocispec.AnnotationRefName] = ref desc.Annotations = annotations manifests = append(manifests, desc) @@ -310,6 +448,155 @@ func (s *Store) writeIndexFile() error { return os.WriteFile(s.indexPath, indexJSON, 0666) } +// GC removes garbage from Store. Unsaved index will be lost. To prevent unexpected +// loss, call SaveIndex() before GC or set AutoSaveIndex to true. +// The garbage to be cleaned are: +// - unreferenced (dangling) blobs in Store which have no predecessors +// - garbage blobs in the storage whose metadata is not stored in Store +func (s *Store) GC(ctx context.Context) error { + s.sync.Lock() + defer s.sync.Unlock() + + // get reachable nodes by reloading the index + err := s.gcIndex(ctx) + if err != nil { + return fmt.Errorf("unable to reload index: %w", err) + } + reachableNodes := s.graph.DigestSet() + + // clean up garbage blobs in the storage + rootpath := filepath.Join(s.root, ocispec.ImageBlobsDir) + algDirs, err := os.ReadDir(rootpath) + if err != nil { + return err + } + for _, algDir := range algDirs { + if !algDir.IsDir() { + continue + } + alg := algDir.Name() + // skip unsupported directories + if !isKnownAlgorithm(alg) { + continue + } + algPath := path.Join(rootpath, alg) + digestEntries, err := os.ReadDir(algPath) + if err != nil { + return err + } + for _, digestEntry := range digestEntries { + if err := isContextDone(ctx); err != nil { + return err + } + dgst := digestEntry.Name() + blobDigest := digest.NewDigestFromEncoded(digest.Algorithm(alg), dgst) + if err := blobDigest.Validate(); err != nil { + // skip irrelevant content + continue + } + if !reachableNodes.Contains(blobDigest) { + // remove the blob from storage if it does not exist in Store + err = os.Remove(path.Join(algPath, dgst)) + if err != nil { + return err + } + } + } + } + return nil +} + +// gcIndex reloads the index and updates metadata. Information of untagged blobs +// are cleaned and only tagged blobs remain. +func (s *Store) gcIndex(ctx context.Context) error { + tagResolver := resolver.NewMemory() + graph := graph.NewMemory() + tagged := set.New[digest.Digest]() + + // index tagged manifests + refMap := s.tagResolver.Map() + for ref, desc := range refMap { + if ref == desc.Digest.String() { + continue + } + if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil { + return err + } + if err := tagResolver.Tag(ctx, desc, ref); err != nil { + return err + } + plain := descriptor.Plain(desc) + if err := graph.IndexAll(ctx, s.storage, plain); err != nil { + return err + } + tagged.Add(desc.Digest) + } + + // index referrer manifests + for ref, desc := range refMap { + if ref != desc.Digest.String() || tagged.Contains(desc.Digest) { + continue + } + // check if the referrers manifest can traverse to the existing graph + subject := &desc + for { + subject, err := manifestutil.Subject(ctx, s.storage, *subject) + if err != nil { + return err + } + if subject == nil { + break + } + if graph.Exists(*subject) { + if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil { + return err + } + plain := descriptor.Plain(desc) + if err := graph.IndexAll(ctx, s.storage, plain); err != nil { + return err + } + break + } + } + } + s.tagResolver = tagResolver + s.graph = graph + return nil +} + +// isTagged checks if the blob given by the descriptor is tagged. +func (s *Store) isTagged(desc ocispec.Descriptor) bool { + tagSet := s.tagResolver.TagSet(desc) + if tagSet.Contains(string(desc.Digest)) { + return len(tagSet) > 1 + } + return len(tagSet) > 0 +} + +// unsafeStore is used to bypass lock restrictions in Delete. +type unsafeStore struct { + *Store +} + +func (s *unsafeStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + return s.storage.Fetch(ctx, target) +} + +func (s *unsafeStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + return s.graph.Predecessors(ctx, node) +} + +// isContextDone returns an error if the context is done. +// Reference: https://pkg.go.dev/context#Context +func isContextDone(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } +} + // validateReference validates ref. func validateReference(ref string) error { if ref == "" { @@ -319,3 +606,13 @@ func validateReference(ref string) error { // TODO: may enforce more strict validation if needed. return nil } + +// isKnownAlgorithm checks is a string is a supported hash algorithm +func isKnownAlgorithm(alg string) bool { + switch digest.Algorithm(alg) { + case digest.SHA256, digest.SHA512, digest.SHA384: + return true + default: + return false + } +} diff --git a/vendor/oras.land/oras-go/v2/content/oci/readonlyoci.go b/vendor/oras.land/oras-go/v2/content/oci/readonlyoci.go index 66ca54c922..3f1ee4ee0f 100644 --- a/vendor/oras.land/oras-go/v2/content/oci/readonlyoci.go +++ b/vendor/oras.land/oras-go/v2/content/oci/readonlyoci.go @@ -22,7 +22,7 @@ import ( "fmt" "io" "io/fs" - "sort" + "slices" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -36,7 +36,7 @@ import ( // ReadOnlyStore implements `oras.ReadonlyTarget`, and represents a read-only // content store based on file system with the OCI-Image layout. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md type ReadOnlyStore struct { fsys fs.FS storage content.ReadOnlyStorage @@ -125,7 +125,7 @@ func (s *ReadOnlyStore) Predecessors(ctx context.Context, node ocispec.Descripto // // See also `Tags()` in the package `registry`. func (s *ReadOnlyStore) Tags(ctx context.Context, last string, fn func(tags []string) error) error { - return listTags(ctx, s.tagResolver, last, fn) + return listTags(s.tagResolver, last, fn) } // validateOCILayoutFile validates the `oci-layout` file. @@ -216,7 +216,7 @@ func resolveBlob(fsys fs.FS, dgst string) (ocispec.Descriptor, error) { // list. // // See also `Tags()` in the package `registry`. -func listTags(ctx context.Context, tagResolver *resolver.Memory, last string, fn func(tags []string) error) error { +func listTags(tagResolver *resolver.Memory, last string, fn func(tags []string) error) error { var tags []string tagMap := tagResolver.Map() @@ -229,7 +229,7 @@ func listTags(ctx context.Context, tagResolver *resolver.Memory, last string, fn } tags = append(tags, tag) } - sort.Strings(tags) + slices.Sort(tags) return fn(tags) } diff --git a/vendor/oras.land/oras-go/v2/content/oci/readonlystorage.go b/vendor/oras.land/oras-go/v2/content/oci/readonlystorage.go index 6e319a649b..50106d1d76 100644 --- a/vendor/oras.land/oras-go/v2/content/oci/readonlystorage.go +++ b/vendor/oras.land/oras-go/v2/content/oci/readonlystorage.go @@ -31,7 +31,7 @@ import ( // ReadOnlyStorage is a read-only CAS based on file system with the OCI-Image // layout. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md type ReadOnlyStorage struct { fsys fs.FS } diff --git a/vendor/oras.land/oras-go/v2/content/oci/storage.go b/vendor/oras.land/oras-go/v2/content/oci/storage.go index 9d4d03b3c5..0a3dec3757 100644 --- a/vendor/oras.land/oras-go/v2/content/oci/storage.go +++ b/vendor/oras.land/oras-go/v2/content/oci/storage.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "sync" @@ -42,7 +43,7 @@ var bufPool = sync.Pool{ } // Storage is a CAS based on file system with the OCI-Image layout. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md type Storage struct { *ReadOnlyStorage // root is the root directory of the OCI layout. @@ -106,6 +107,23 @@ func (s *Storage) Push(_ context.Context, expected ocispec.Descriptor, content i return nil } +// Delete removes the target from the system. +func (s *Storage) Delete(ctx context.Context, target ocispec.Descriptor) error { + path, err := blobPath(target.Digest) + if err != nil { + return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest) + } + targetPath := filepath.Join(s.root, path) + err = os.Remove(targetPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound) + } + return err + } + return nil +} + // ingest write the content into a temporary ingest file. func (s *Storage) ingest(expected ocispec.Descriptor, content io.Reader) (path string, ingestErr error) { if err := ensureDir(s.ingestRoot); err != nil { diff --git a/vendor/oras.land/oras-go/v2/content/resolver.go b/vendor/oras.land/oras-go/v2/content/resolver.go index b536b5ddca..bc0fd8df9c 100644 --- a/vendor/oras.land/oras-go/v2/content/resolver.go +++ b/vendor/oras.land/oras-go/v2/content/resolver.go @@ -39,3 +39,9 @@ type TagResolver interface { Tagger Resolver } + +// Untagger untags reference tags. +type Untagger interface { + // Untag untags the given reference string. + Untag(ctx context.Context, reference string) error +} diff --git a/vendor/oras.land/oras-go/v2/copy.go b/vendor/oras.land/oras-go/v2/copy.go index ca0f5d6f55..2f131a8c6a 100644 --- a/vendor/oras.land/oras-go/v2/copy.go +++ b/vendor/oras.land/oras-go/v2/copy.go @@ -37,8 +37,9 @@ import ( // defaultConcurrency is the default value of CopyGraphOptions.Concurrency. const defaultConcurrency int = 3 // This value is consistent with dockerd and containerd. -// errSkipDesc signals copyNode() to stop processing a descriptor. -var errSkipDesc = errors.New("skip descriptor") +// SkipNode signals to stop copying a node. When returned from PreCopy the blob must exist in the target. +// This can be used to signal that a blob has been made available in the target repository by "Mount()" or some other technique. +var SkipNode = errors.New("skip node") // DefaultCopyOptions provides the default CopyOptions. var DefaultCopyOptions CopyOptions = CopyOptions{ @@ -95,13 +96,21 @@ type CopyGraphOptions struct { // cached in the memory. // If less than or equal to 0, a default (currently 4 MiB) is used. MaxMetadataBytes int64 - // PreCopy handles the current descriptor before copying it. + // PreCopy handles the current descriptor before it is copied. PreCopy can + // return a SkipNode to signal that desc should be skipped when it already + // exists in the target. PreCopy func(ctx context.Context, desc ocispec.Descriptor) error - // PostCopy handles the current descriptor after copying it. + // PostCopy handles the current descriptor after it is copied. PostCopy func(ctx context.Context, desc ocispec.Descriptor) error // OnCopySkipped will be called when the sub-DAG rooted by the current node // is skipped. OnCopySkipped func(ctx context.Context, desc ocispec.Descriptor) error + // MountFrom returns the candidate repositories that desc may be mounted from. + // The OCI references will be tried in turn. If mounting fails on all of them, + // then it falls back to a copy. + MountFrom func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) + // OnMounted will be invoked when desc is mounted. + OnMounted func(ctx context.Context, desc ocispec.Descriptor) error // FindSuccessors finds the successors of the current node. // fetcher provides cached access to the source storage, and is suitable // for fetching non-leaf nodes like manifests. Since anything fetched from @@ -256,12 +265,85 @@ func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Sto if exists { return copyNode(ctx, proxy.Cache, dst, desc, opts) } - return copyNode(ctx, src, dst, desc, opts) + return mountOrCopyNode(ctx, src, dst, desc, opts) } return syncutil.Go(ctx, limiter, fn, root) } +// mountOrCopyNode tries to mount the node, if not falls back to copying. +func mountOrCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor, opts CopyGraphOptions) error { + // Need MountFrom and it must be a blob + if opts.MountFrom == nil || descriptor.IsManifest(desc) { + return copyNode(ctx, src, dst, desc, opts) + } + + mounter, ok := dst.(registry.Mounter) + if !ok { + // mounting is not supported by the destination + return copyNode(ctx, src, dst, desc, opts) + } + + sourceRepositories, err := opts.MountFrom(ctx, desc) + if err != nil { + // Technically this error is not fatal, we can still attempt to copy the node + // But for consistency with the other callbacks we bail out. + return err + } + + if len(sourceRepositories) == 0 { + return copyNode(ctx, src, dst, desc, opts) + } + + skipSource := errors.New("skip source") + for i, sourceRepository := range sourceRepositories { + // try mounting this source repository + var mountFailed bool + getContent := func() (io.ReadCloser, error) { + // the invocation of getContent indicates that mounting has failed + mountFailed = true + + if i < len(sourceRepositories)-1 { + // If this is not the last one, skip this source and try next one + // We want to return an error that we will test for from mounter.Mount() + return nil, skipSource + } + // this is the last iteration so we need to actually get the content and do the copy + // but first call the PreCopy function + if opts.PreCopy != nil { + if err := opts.PreCopy(ctx, desc); err != nil { + return nil, err + } + } + return src.Fetch(ctx, desc) + } + + // Mount or copy + if err := mounter.Mount(ctx, desc, sourceRepository, getContent); err != nil && !errors.Is(err, skipSource) { + return err + } + + if !mountFailed { + // mounted, success + if opts.OnMounted != nil { + if err := opts.OnMounted(ctx, desc); err != nil { + return err + } + } + return nil + } + } + + // we copied it + if opts.PostCopy != nil { + if err := opts.PostCopy(ctx, desc); err != nil { + return err + } + } + + return nil +} + // doCopyNode copies a single content from the source CAS to the destination CAS. func doCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor) error { rc, err := src.Fetch(ctx, desc) @@ -281,7 +363,7 @@ func doCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.St func copyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor, opts CopyGraphOptions) error { if opts.PreCopy != nil { if err := opts.PreCopy(ctx, desc); err != nil { - if err == errSkipDesc { + if err == SkipNode { return nil } return err @@ -373,7 +455,7 @@ func prepareCopy(ctx context.Context, dst Target, dstRef string, proxy *cas.Prox } } // skip the regular copy workflow - return errSkipDesc + return SkipNode } } else { postCopy := opts.PostCopy diff --git a/vendor/oras.land/oras-go/v2/internal/container/set/set.go b/vendor/oras.land/oras-go/v2/internal/container/set/set.go index a084e28899..07c96d47d1 100644 --- a/vendor/oras.land/oras-go/v2/internal/container/set/set.go +++ b/vendor/oras.land/oras-go/v2/internal/container/set/set.go @@ -33,3 +33,8 @@ func (s Set[T]) Contains(item T) bool { _, ok := s[item] return ok } + +// Delete deletes an item from the set. +func (s Set[T]) Delete(item T) { + delete(s, item) +} diff --git a/vendor/oras.land/oras-go/v2/internal/graph/memory.go b/vendor/oras.land/oras-go/v2/internal/graph/memory.go index 0aa25aee84..016e5f96a8 100644 --- a/vendor/oras.land/oras-go/v2/internal/graph/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/graph/memory.go @@ -20,9 +20,11 @@ import ( "errors" "sync" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/container/set" "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/internal/status" "oras.land/oras-go/v2/internal/syncutil" @@ -30,35 +32,49 @@ import ( // Memory is a memory based PredecessorFinder. type Memory struct { - predecessors sync.Map // map[descriptor.Descriptor]map[descriptor.Descriptor]ocispec.Descriptor - indexed sync.Map // map[descriptor.Descriptor]any + // nodes has the following properties and behaviors: + // 1. a node exists in Memory.nodes if and only if it exists in the memory + // 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by + // the other fields. + nodes map[descriptor.Descriptor]ocispec.Descriptor + + // predecessors has the following properties and behaviors: + // 1. a node exists in Memory.predecessors if it has at least one predecessor + // in the memory, regardless of whether or not the node itself exists in + // the memory. + // 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors + // in the memory. + predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + + // successors has the following properties and behaviors: + // 1. a node exists in Memory.successors if and only if it exists in the memory. + // 2. a node's entry in Memory.successors is always consistent with the actual + // content of the node, regardless of whether or not each successor exists + // in the memory. + successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + + lock sync.RWMutex } // NewMemory creates a new memory PredecessorFinder. func NewMemory() *Memory { - return &Memory{} + return &Memory{ + nodes: make(map[descriptor.Descriptor]ocispec.Descriptor), + predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + } } // Index indexes predecessors for each direct successor of the given node. -// There is no data consistency issue as long as deletion is not implemented -// for the underlying storage. func (m *Memory) Index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error { - successors, err := content.Successors(ctx, fetcher, node) - if err != nil { - return err - } - - m.index(ctx, node, successors) - return nil + _, err := m.index(ctx, fetcher, node) + return err } // Index indexes predecessors for all the successors of the given node. -// There is no data consistency issue as long as deletion is not implemented -// for the underlying storage. func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error { // track content status tracker := status.NewTracker() - var fn syncutil.GoFunc[ocispec.Descriptor] fn = func(ctx context.Context, region *syncutil.LimitedRegion, desc ocispec.Descriptor) error { // skip the node if other go routine is working on it @@ -66,15 +82,7 @@ func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node oci if !committed { return nil } - - // skip the node if it has been indexed - key := descriptor.FromOCI(desc) - _, exists := m.indexed.Load(key) - if exists { - return nil - } - - successors, err := content.Successors(ctx, fetcher, desc) + successors, err := m.index(ctx, fetcher, desc) if err != nil { if errors.Is(err, errdef.ErrNotFound) { // skip the node if it does not exist @@ -82,9 +90,6 @@ func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node oci } return err } - m.index(ctx, desc, successors) - m.indexed.Store(key, nil) - if len(successors) > 0 { // traverse and index successors return syncutil.Go(ctx, nil, fn, successors...) @@ -96,39 +101,101 @@ func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node oci // Predecessors returns the nodes directly pointing to the current node. // Predecessors returns nil without error if the node does not exists in the -// store. -// Like other operations, calling Predecessors() is go-routine safe. However, -// it does not necessarily correspond to any consistent snapshot of the stored -// contents. +// store. Like other operations, calling Predecessors() is go-routine safe. +// However, it does not necessarily correspond to any consistent snapshot of +// the stored contents. func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + m.lock.RLock() + defer m.lock.RUnlock() + key := descriptor.FromOCI(node) - value, exists := m.predecessors.Load(key) + set, exists := m.predecessors[key] if !exists { return nil, nil } - predecessors := value.(*sync.Map) - var res []ocispec.Descriptor - predecessors.Range(func(key, value interface{}) bool { - res = append(res, value.(ocispec.Descriptor)) - return true - }) + for k := range set { + res = append(res, m.nodes[k]) + } return res, nil } +// Remove removes the node from its predecessors and successors, and returns the +// dangling root nodes caused by the deletion. +func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor { + m.lock.Lock() + defer m.lock.Unlock() + + nodeKey := descriptor.FromOCI(node) + var danglings []ocispec.Descriptor + // remove the node from its successors' predecessor list + for successorKey := range m.successors[nodeKey] { + predecessorEntry := m.predecessors[successorKey] + predecessorEntry.Delete(nodeKey) + + // if none of the predecessors of the node still exists, we remove the + // predecessors entry and return it as a dangling node. Otherwise, we do + // not remove the entry. + if len(predecessorEntry) == 0 { + delete(m.predecessors, successorKey) + if _, exists := m.nodes[successorKey]; exists { + danglings = append(danglings, m.nodes[successorKey]) + } + } + } + delete(m.successors, nodeKey) + delete(m.nodes, nodeKey) + return danglings +} + +// DigestSet returns the set of node digest in memory. +func (m *Memory) DigestSet() set.Set[digest.Digest] { + m.lock.RLock() + defer m.lock.RUnlock() + + s := set.New[digest.Digest]() + for desc := range m.nodes { + s.Add(desc.Digest) + } + return s +} + // index indexes predecessors for each direct successor of the given node. -// There is no data consistency issue as long as deletion is not implemented -// for the underlying storage. -func (m *Memory) index(ctx context.Context, node ocispec.Descriptor, successors []ocispec.Descriptor) { - if len(successors) == 0 { - return +func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + successors, err := content.Successors(ctx, fetcher, node) + if err != nil { + return nil, err } + m.lock.Lock() + defer m.lock.Unlock() - predecessorKey := descriptor.FromOCI(node) + // index the node + nodeKey := descriptor.FromOCI(node) + m.nodes[nodeKey] = node + + // for each successor, put it into the node's successors list, and + // put node into the succeesor's predecessors list + successorSet := set.New[descriptor.Descriptor]() + m.successors[nodeKey] = successorSet for _, successor := range successors { successorKey := descriptor.FromOCI(successor) - value, _ := m.predecessors.LoadOrStore(successorKey, &sync.Map{}) - predecessors := value.(*sync.Map) - predecessors.Store(predecessorKey, node) + successorSet.Add(successorKey) + predecessorSet, exists := m.predecessors[successorKey] + if !exists { + predecessorSet = set.New[descriptor.Descriptor]() + m.predecessors[successorKey] = predecessorSet + } + predecessorSet.Add(nodeKey) } + return successors, nil +} + +// Exists checks if the node exists in the graph +func (m *Memory) Exists(node ocispec.Descriptor) bool { + m.lock.RLock() + defer m.lock.RUnlock() + + nodeKey := descriptor.FromOCI(node) + _, exists := m.nodes[nodeKey] + return exists } diff --git a/vendor/oras.land/oras-go/v2/internal/ioutil/io.go b/vendor/oras.land/oras-go/v2/internal/ioutil/io.go index 888d26f895..de41bda909 100644 --- a/vendor/oras.land/oras-go/v2/internal/ioutil/io.go +++ b/vendor/oras.land/oras-go/v2/internal/ioutil/io.go @@ -44,15 +44,23 @@ func CopyBuffer(dst io.Writer, src io.Reader, buf []byte, desc ocispec.Descripto return vr.Verify() } -// nopCloserType is the type of `io.NopCloser()`. -var nopCloserType = reflect.TypeOf(io.NopCloser(nil)) +// Types returned by `io.NopCloser()`. +var ( + nopCloserType = reflect.TypeOf(io.NopCloser(nil)) + nopCloserWriterToType = reflect.TypeOf(io.NopCloser(struct { + io.Reader + io.WriterTo + }{})) +) // UnwrapNopCloser unwraps the reader wrapped by `io.NopCloser()`. // Similar implementation can be found in the built-in package `net/http`. -// Reference: https://github.com/golang/go/blob/go1.17.6/src/net/http/transfer.go#L423-L425 -func UnwrapNopCloser(rc io.Reader) io.Reader { - if reflect.TypeOf(rc) == nopCloserType { - return reflect.ValueOf(rc).Field(0).Interface().(io.Reader) +// Reference: https://github.com/golang/go/blob/go1.22.1/src/net/http/transfer.go#L1090-L1105 +func UnwrapNopCloser(r io.Reader) io.Reader { + switch reflect.TypeOf(r) { + case nopCloserType, nopCloserWriterToType: + return reflect.ValueOf(r).Field(0).Interface().(io.Reader) + default: + return r } - return rc } diff --git a/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go b/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go index c904dc690e..89d556b8fe 100644 --- a/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go +++ b/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go @@ -22,6 +22,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" ) // Config returns the config of desc, if present. @@ -61,3 +62,23 @@ func Manifests(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descri return nil, nil } } + +// Subject returns the subject of desc, if present. +func Subject(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) { + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex, spec.MediaTypeArtifactManifest: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + var manifest struct { + Subject *ocispec.Descriptor `json:"subject,omitempty"` + } + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + return manifest.Subject, nil + default: + return nil, nil + } +} diff --git a/vendor/oras.land/oras-go/v2/internal/platform/platform.go b/vendor/oras.land/oras-go/v2/internal/platform/platform.go index e903fe3d5a..3aea3a1b10 100644 --- a/vendor/oras.land/oras-go/v2/internal/platform/platform.go +++ b/vendor/oras.land/oras-go/v2/internal/platform/platform.go @@ -38,6 +38,14 @@ import ( // Note: Variant, OSVersion and OSFeatures are optional fields, will skip // the comparison if the target platform does not provide specific value. func Match(got *ocispec.Platform, want *ocispec.Platform) bool { + if got == nil && want == nil { + return true + } + + if got == nil || want == nil { + return false + } + if got.Architecture != want.Architecture || got.OS != want.OS { return false } diff --git a/vendor/oras.land/oras-go/v2/internal/registryutil/auth.go b/vendor/oras.land/oras-go/v2/internal/registryutil/auth.go deleted file mode 100644 index 4a601f0c6b..0000000000 --- a/vendor/oras.land/oras-go/v2/internal/registryutil/auth.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright The ORAS 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 registryutil - -import ( - "context" - - "oras.land/oras-go/v2/registry" - "oras.land/oras-go/v2/registry/remote/auth" -) - -// WithScopeHint adds a hinted scope to the context. -func WithScopeHint(ctx context.Context, ref registry.Reference, actions ...string) context.Context { - scope := auth.ScopeRepository(ref.Repository, actions...) - return auth.AppendScopes(ctx, scope) -} diff --git a/vendor/oras.land/oras-go/v2/internal/resolver/memory.go b/vendor/oras.land/oras-go/v2/internal/resolver/memory.go index 6fac5e2d08..092a29e97b 100644 --- a/vendor/oras.land/oras-go/v2/internal/resolver/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/resolver/memory.go @@ -17,45 +17,88 @@ package resolver import ( "context" + "maps" "sync" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/container/set" ) // Memory is a memory based resolver. type Memory struct { - index sync.Map // map[string]ocispec.Descriptor + lock sync.RWMutex + index map[string]ocispec.Descriptor + tags map[digest.Digest]set.Set[string] } // NewMemory creates a new Memory resolver. func NewMemory() *Memory { - return &Memory{} + return &Memory{ + index: make(map[string]ocispec.Descriptor), + tags: make(map[digest.Digest]set.Set[string]), + } } // Resolve resolves a reference to a descriptor. func (m *Memory) Resolve(_ context.Context, reference string) (ocispec.Descriptor, error) { - desc, ok := m.index.Load(reference) + m.lock.RLock() + defer m.lock.RUnlock() + + desc, ok := m.index[reference] if !ok { return ocispec.Descriptor{}, errdef.ErrNotFound } - return desc.(ocispec.Descriptor), nil + return desc, nil } // Tag tags a descriptor with a reference string. func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference string) error { - m.index.Store(reference, desc) + m.lock.Lock() + defer m.lock.Unlock() + + m.index[reference] = desc + tagSet, ok := m.tags[desc.Digest] + if !ok { + tagSet = set.New[string]() + m.tags[desc.Digest] = tagSet + } + tagSet.Add(reference) return nil } +// Untag removes a reference from index map. +func (m *Memory) Untag(reference string) { + m.lock.Lock() + defer m.lock.Unlock() + + desc, ok := m.index[reference] + if !ok { + return + } + delete(m.index, reference) + tagSet := m.tags[desc.Digest] + tagSet.Delete(reference) + if len(tagSet) == 0 { + delete(m.tags, desc.Digest) + } +} + // Map dumps the memory into a built-in map structure. -// Like other operations, calling Map() is go-routine safe. However, it does not -// necessarily correspond to any consistent snapshot of the storage contents. +// Like other operations, calling Map() is go-routine safe. func (m *Memory) Map() map[string]ocispec.Descriptor { - res := make(map[string]ocispec.Descriptor) - m.index.Range(func(key, value interface{}) bool { - res[key.(string)] = value.(ocispec.Descriptor) - return true - }) - return res + m.lock.RLock() + defer m.lock.RUnlock() + + return maps.Clone(m.index) +} + +// TagSet returns the set of tags of the descriptor. +func (m *Memory) TagSet(desc ocispec.Descriptor) set.Set[string] { + m.lock.RLock() + defer m.lock.RUnlock() + + tagSet := m.tags[desc.Digest] + return maps.Clone(tagSet) } diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go index 1d55719800..e449705304 100644 --- a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go @@ -15,10 +15,14 @@ limitations under the License. package syncutil -import "context" +import ( + "context" + "sync" + "sync/atomic" +) // Once is an object that will perform exactly one action. -// Unlike sync.Once, this Once allowes the action to have return values. +// Unlike sync.Once, this Once allows the action to have return values. type Once struct { result interface{} err error @@ -68,3 +72,31 @@ func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, int } } } + +// OnceOrRetry is an object that will perform exactly one success action. +type OnceOrRetry struct { + done atomic.Bool + lock sync.Mutex +} + +// OnceOrRetry calls the function f if and only if Do is being called for the +// first time for this instance of Once or all previous calls to Do are failed. +func (o *OnceOrRetry) Do(f func() error) error { + // fast path + if o.done.Load() { + return nil + } + + // slow path + o.lock.Lock() + defer o.lock.Unlock() + + if o.done.Load() { + return nil + } + if err := f(); err != nil { + return err + } + o.done.Store(true) + return nil +} diff --git a/vendor/oras.land/oras-go/v2/pack.go b/vendor/oras.land/oras-go/v2/pack.go index 08e14e1954..1b99561290 100644 --- a/vendor/oras.land/oras-go/v2/pack.go +++ b/vendor/oras.land/oras-go/v2/pack.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "regexp" "time" @@ -53,7 +54,7 @@ var ( ErrInvalidDateTimeFormat = errors.New("invalid date and time format") // ErrMissingArtifactType is returned by [PackManifest] when - // packManifestVersion is PackManifestVersion1_1_RC4 and artifactType is + // packManifestVersion is PackManifestVersion1_1 and artifactType is // empty and the config media type is set to // "application/vnd.oci.empty.v1+json". ErrMissingArtifactType = errors.New("missing artifact type") @@ -71,7 +72,15 @@ const ( // PackManifestVersion1_1_RC4 represents the OCI Image Manifest defined // in image-spec v1.1.0-rc4. // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md - PackManifestVersion1_1_RC4 PackManifestVersion = 2 + // + // Deprecated: This constant is deprecated and not recommended for future use. + // Use [PackManifestVersion1_1] instead. + PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1 + + // PackManifestVersion1_1 represents the OCI Image Manifest defined in + // image-spec v1.1.0. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md + PackManifestVersion1_1 PackManifestVersion = 2 ) // PackManifestOptions contains optional parameters for [PackManifest]. @@ -98,16 +107,16 @@ type PackManifestOptions struct { // mediaTypeRegexp checks the format of media types. // References: -// - https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/schema/defs-descriptor.json#L7 +// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 // - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`) // PackManifest generates an OCI Image Manifest based on the given parameters // and pushes the packed manifest to a content storage using pusher. The version // of the manifest to be packed is determined by packManifestVersion -// (Recommended value: PackManifestVersion1_1_RC4). +// (Recommended value: PackManifestVersion1_1). // -// - If packManifestVersion is [PackManifestVersion1_1_RC4]: +// - If packManifestVersion is [PackManifestVersion1_1]: // artifactType MUST NOT be empty unless opts.ConfigDescriptor is specified. // - If packManifestVersion is [PackManifestVersion1_0]: // if opts.ConfigDescriptor is nil, artifactType will be used as the @@ -122,8 +131,8 @@ func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersio switch packManifestVersion { case PackManifestVersion1_0: return packManifestV1_0(ctx, pusher, artifactType, opts) - case PackManifestVersion1_1_RC4: - return packManifestV1_1_RC4(ctx, pusher, artifactType, opts) + case PackManifestVersion1_1: + return packManifestV1_1(ctx, pusher, artifactType, opts) default: return ocispec.Descriptor{}, fmt.Errorf("PackManifestVersion(%v): %w", packManifestVersion, errdef.ErrUnsupported) } @@ -283,9 +292,9 @@ func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMedi return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) } -// packManifestV1_1_RC4 packs an image manifest defined in image-spec v1.1.0-rc4. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md#guidelines-for-artifact-usage -func packManifestV1_1_RC4(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { +// packManifestV1_1 packs an image manifest defined in image-spec v1.1.0. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidelines-for-artifact-usage +func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) { // artifactType MUST be set when config.mediaType is set to the empty value return ocispec.Descriptor{}, ErrMissingArtifactType @@ -412,9 +421,8 @@ func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey // copy the original annotation map copied := make(map[string]string, len(annotations)+1) - for k, v := range annotations { - copied[k] = v - } + maps.Copy(copied, annotations) + // set creation time in RFC 3339 format // reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys now := time.Now().UTC() diff --git a/vendor/oras.land/oras-go/v2/registry/reference.go b/vendor/oras.land/oras-go/v2/registry/reference.go index 7661a162bd..fc3e95e559 100644 --- a/vendor/oras.land/oras-go/v2/registry/reference.go +++ b/vendor/oras.land/oras-go/v2/registry/reference.go @@ -34,13 +34,13 @@ var ( // // References: // - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53 - // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#pulling-manifests + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests repositoryRegexp = regexp.MustCompile(`^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$`) // tagRegexp checks the tag name. // The docker and OCI spec have the same regular expression. // - // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#pulling-manifests + // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) ) @@ -64,6 +64,9 @@ type Reference struct { } // ParseReference parses a string (artifact) into an `artifact reference`. +// Corresponding cryptographic hash implementations are required to be imported +// as specified by https://pkg.go.dev/github.com/opencontainers/go-digest#readme-usage +// if the string contains a digest. // // Note: An "image" is an "artifact", however, an "artifact" is not necessarily // an "image". @@ -117,7 +120,7 @@ func ParseReference(artifact string) (Reference, error) { parts := strings.SplitN(artifact, "/", 2) if len(parts) == 1 { // Invalid Form - return Reference{}, fmt.Errorf("%w: missing repository", errdef.ErrInvalidReference) + return Reference{}, fmt.Errorf("%w: missing registry or repository", errdef.ErrInvalidReference) } registry, path := parts[0], parts[1] @@ -189,8 +192,8 @@ func (r Reference) Validate() error { // ValidateRegistry validates the registry. func (r Reference) ValidateRegistry() error { - if uri, err := url.ParseRequestURI("dummy://" + r.Registry); err != nil || uri.Host != r.Registry { - return fmt.Errorf("%w: invalid registry", errdef.ErrInvalidReference) + if uri, err := url.ParseRequestURI("dummy://" + r.Registry); err != nil || uri.Host == "" || uri.Host != r.Registry { + return fmt.Errorf("%w: invalid registry %q", errdef.ErrInvalidReference, r.Registry) } return nil } @@ -198,7 +201,7 @@ func (r Reference) ValidateRegistry() error { // ValidateRepository validates the repository. func (r Reference) ValidateRepository() error { if !repositoryRegexp.MatchString(r.Repository) { - return fmt.Errorf("%w: invalid repository", errdef.ErrInvalidReference) + return fmt.Errorf("%w: invalid repository %q", errdef.ErrInvalidReference, r.Repository) } return nil } @@ -206,7 +209,7 @@ func (r Reference) ValidateRepository() error { // ValidateReferenceAsTag validates the reference as a tag. func (r Reference) ValidateReferenceAsTag() error { if !tagRegexp.MatchString(r.Reference) { - return fmt.Errorf("%w: invalid tag", errdef.ErrInvalidReference) + return fmt.Errorf("%w: invalid tag %q", errdef.ErrInvalidReference, r.Reference) } return nil } @@ -214,7 +217,7 @@ func (r Reference) ValidateReferenceAsTag() error { // ValidateReferenceAsDigest validates the reference as a digest. func (r Reference) ValidateReferenceAsDigest() error { if _, err := r.Digest(); err != nil { - return fmt.Errorf("%w: invalid digest; %v", errdef.ErrInvalidReference, err) + return fmt.Errorf("%w: invalid digest %q: %v", errdef.ErrInvalidReference, r.Reference, err) } return nil } @@ -250,6 +253,8 @@ func (r Reference) ReferenceOrDefault() string { } // Digest returns the reference as a digest. +// Corresponding cryptographic hash implementations are required to be imported +// as specified by https://pkg.go.dev/github.com/opencontainers/go-digest#readme-usage func (r Reference) Digest() (digest.Digest, error) { return digest.Parse(r.Reference) } diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go index 91a52e1c16..d11c092bf5 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go @@ -157,3 +157,76 @@ func (noCache) GetToken(ctx context.Context, registry string, scheme Scheme, key func (noCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { return fetch(ctx) } + +// hostCache is an auth cache that ignores scopes. Uses only the registry's hostname to find a token. +type hostCache struct { + Cache +} + +// GetToken implements Cache. +func (c *hostCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { + return c.Cache.GetToken(ctx, registry, scheme, "") +} + +// Set implements Cache. +func (c *hostCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { + return c.Cache.Set(ctx, registry, scheme, "", fetch) +} + +// fallbackCache tries the primary cache then falls back to the secondary cache. +type fallbackCache struct { + primary Cache + secondary Cache +} + +// GetScheme implements Cache. +func (fc *fallbackCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { + scheme, err := fc.primary.GetScheme(ctx, registry) + if err == nil { + return scheme, nil + } + + // fallback + return fc.secondary.GetScheme(ctx, registry) +} + +// GetToken implements Cache. +func (fc *fallbackCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { + token, err := fc.primary.GetToken(ctx, registry, scheme, key) + if err == nil { + return token, nil + } + + // fallback + return fc.secondary.GetToken(ctx, registry, scheme, key) +} + +// Set implements Cache. +func (fc *fallbackCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { + token, err := fc.primary.Set(ctx, registry, scheme, key, fetch) + if err != nil { + return "", err + } + + return fc.secondary.Set(ctx, registry, scheme, key, func(ctx context.Context) (string, error) { + return token, nil + }) +} + +// NewSingleContextCache creates a host-based cache for optimizing the auth flow for non-compliant registries. +// It is intended to be used in a single context, such as pulling from a single repository. +// This cache should not be shared. +// +// Note: [NewCache] should be used for compliant registries as it can be shared +// across context and will generally make less re-authentication requests. +func NewSingleContextCache() Cache { + cache := NewCache() + return &fallbackCache{ + primary: cache, + // We can re-use the came concurrentCache here because the key space is different + // (keys are always empty for the hostCache) so there is no collision. + // Even if there is a collision it is not an issue. + // Re-using saves a little memory. + secondary: &hostCache{cache}, + } +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go index 37eb65ed25..8d9685a213 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go @@ -31,6 +31,10 @@ import ( "oras.land/oras-go/v2/registry/remote/retry" ) +// ErrBasicCredentialNotFound is returned when the credential is not found for +// basic auth. +var ErrBasicCredentialNotFound = errors.New("basic credential not found") + // DefaultClient is the default auth-decorated client. var DefaultClient = &Client{ Client: retry.DefaultClient, @@ -54,16 +58,23 @@ var maxResponseBytes int64 = 128 * 1024 // 128 KiB // See also ClientID. var defaultClientID = "oras-go" +// CredentialFunc represents a function that resolves the credential for the +// given registry (i.e. host:port). +// +// [EmptyCredential] is a valid return value and should not be considered as +// an error. +type CredentialFunc func(ctx context.Context, hostport string) (Credential, error) + // StaticCredential specifies static credentials for the given host. -func StaticCredential(registry string, cred Credential) func(context.Context, string) (Credential, error) { +func StaticCredential(registry string, cred Credential) CredentialFunc { if registry == "docker.io" { // it is expected that traffic targeting "docker.io" will be redirected // to "registry-1.docker.io" // reference: https://github.com/moby/moby/blob/v24.0.0-beta.2/registry/config.go#L25-L48 registry = "registry-1.docker.io" } - return func(_ context.Context, target string) (Credential, error) { - if target == registry { + return func(_ context.Context, hostport string) (Credential, error) { + if hostport == registry { return cred, nil } return EmptyCredential, nil @@ -88,10 +99,10 @@ type Client struct { // Credential specifies the function for resolving the credential for the // given registry (i.e. host:port). - // `EmptyCredential` is a valid return value and should not be considered as + // EmptyCredential is a valid return value and should not be considered as // an error. - // If nil, the credential is always resolved to `EmptyCredential`. - Credential func(context.Context, string) (Credential, error) + // If nil, the credential is always resolved to EmptyCredential. + Credential CredentialFunc // Cache caches credentials for direct accessing the remote registry. // If nil, no cache is used. @@ -170,19 +181,19 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { // attempt cached auth token var attemptedKey string cache := c.cache() - registry := originalReq.Host - scheme, err := cache.GetScheme(ctx, registry) + host := originalReq.Host + scheme, err := cache.GetScheme(ctx, host) if err == nil { switch scheme { case SchemeBasic: - token, err := cache.GetToken(ctx, registry, SchemeBasic, "") + token, err := cache.GetToken(ctx, host, SchemeBasic, "") if err == nil { req.Header.Set("Authorization", "Basic "+token) } case SchemeBearer: - scopes := GetScopes(ctx) + scopes := GetAllScopesForHost(ctx, host) attemptedKey = strings.Join(scopes, " ") - token, err := cache.GetToken(ctx, registry, SchemeBearer, attemptedKey) + token, err := cache.GetToken(ctx, host, SchemeBearer, attemptedKey) if err == nil { req.Header.Set("Authorization", "Bearer "+token) } @@ -204,8 +215,8 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { case SchemeBasic: resp.Body.Close() - token, err := cache.Set(ctx, registry, SchemeBasic, "", func(ctx context.Context) (string, error) { - return c.fetchBasicAuth(ctx, registry) + token, err := cache.Set(ctx, host, SchemeBasic, "", func(ctx context.Context) (string, error) { + return c.fetchBasicAuth(ctx, host) }) if err != nil { return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) @@ -216,17 +227,17 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { case SchemeBearer: resp.Body.Close() - // merge hinted scopes with challenged scopes - scopes := GetScopes(ctx) - if scope := params["scope"]; scope != "" { - scopes = append(scopes, strings.Split(scope, " ")...) + scopes := GetAllScopesForHost(ctx, host) + if paramScope := params["scope"]; paramScope != "" { + // merge hinted scopes with challenged scopes + scopes = append(scopes, strings.Split(paramScope, " ")...) scopes = CleanScopes(scopes) } key := strings.Join(scopes, " ") // attempt the cache again if there is a scope change if key != attemptedKey { - if token, err := cache.GetToken(ctx, registry, SchemeBearer, key); err == nil { + if token, err := cache.GetToken(ctx, host, SchemeBearer, key); err == nil { req = originalReq.Clone(ctx) req.Header.Set("Authorization", "Bearer "+token) if err := rewindRequestBody(req); err != nil { @@ -247,8 +258,8 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { // attempt with credentials realm := params["realm"] service := params["service"] - token, err := cache.Set(ctx, registry, SchemeBearer, key, func(ctx context.Context) (string, error) { - return c.fetchBearerToken(ctx, registry, realm, service, scopes) + token, err := cache.Set(ctx, host, SchemeBearer, key, func(ctx context.Context) (string, error) { + return c.fetchBearerToken(ctx, host, realm, service, scopes) }) if err != nil { return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) @@ -273,7 +284,7 @@ func (c *Client) fetchBasicAuth(ctx context.Context, registry string) (string, e return "", fmt.Errorf("failed to resolve credential: %w", err) } if cred == EmptyCredential { - return "", errors.New("credential required for basic auth") + return "", ErrBasicCredentialNotFound } if cred.Username == "" || cred.Password == "" { return "", errors.New("missing username or password for basic auth") diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go index 24a0f89837..d81cc0d4f5 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go @@ -17,8 +17,10 @@ package auth import ( "context" - "sort" + "slices" "strings" + + "oras.land/oras-go/v2/registry" ) // Actions used in scopes. @@ -54,6 +56,28 @@ func ScopeRepository(repository string, actions ...string) string { }, ":") } +// AppendRepositoryScope returns a new context containing scope hints for the +// auth client to fetch bearer tokens with the given actions on the repository. +// If called multiple times, the new scopes will be appended to the existing +// scopes. The resulted scopes are de-duplicated. +// +// For example, uploading blob to the repository "hello-world" does HEAD request +// first then POST and PUT. The HEAD request will return a challenge for scope +// `repository:hello-world:pull`, and the auth client will fetch a token for +// that challenge. Later, the POST request will return a challenge for scope +// `repository:hello-world:push`, and the auth client will fetch a token for +// that challenge again. By invoking AppendRepositoryScope with the actions +// [ActionPull] and [ActionPush] for the repository `hello-world`, +// the auth client with cache is hinted to fetch a token via a single token +// fetch request for all the HEAD, POST, PUT requests. +func AppendRepositoryScope(ctx context.Context, ref registry.Reference, actions ...string) context.Context { + if len(actions) == 0 { + return ctx + } + scope := ScopeRepository(ref.Repository, actions...) + return AppendScopesForHost(ctx, ref.Host(), scope) +} + // scopesContextKey is the context key for scopes. type scopesContextKey struct{} @@ -66,7 +90,7 @@ type scopesContextKey struct{} // `repository:hello-world:pull`, and the auth client will fetch a token for // that challenge. Later, the POST request will return a challenge for scope // `repository:hello-world:push`, and the auth client will fetch a token for -// that challenge again. By invoking `WithScopes()` with the scope +// that challenge again. By invoking WithScopes with the scope // `repository:hello-world:pull,push`, the auth client with cache is hinted to // fetch a token via a single token fetch request for all the HEAD, POST, PUT // requests. @@ -93,11 +117,76 @@ func AppendScopes(ctx context.Context, scopes ...string) context.Context { // GetScopes returns the scopes in the context. func GetScopes(ctx context.Context) []string { if scopes, ok := ctx.Value(scopesContextKey{}).([]string); ok { - return append([]string(nil), scopes...) + return slices.Clone(scopes) + } + return nil +} + +// scopesForHostContextKey is the context key for per-host scopes. +type scopesForHostContextKey string + +// WithScopesForHost returns a context with per-host scopes added. +// Scopes are de-duplicated. +// Scopes are used as hints for the auth client to fetch bearer tokens with +// larger scopes. +// +// For example, uploading blob to the repository "hello-world" does HEAD request +// first then POST and PUT. The HEAD request will return a challenge for scope +// `repository:hello-world:pull`, and the auth client will fetch a token for +// that challenge. Later, the POST request will return a challenge for scope +// `repository:hello-world:push`, and the auth client will fetch a token for +// that challenge again. By invoking WithScopesForHost with the scope +// `repository:hello-world:pull,push`, the auth client with cache is hinted to +// fetch a token via a single token fetch request for all the HEAD, POST, PUT +// requests. +// +// Passing an empty list of scopes will virtually remove the scope hints in the +// context for the given host. +// +// Reference: https://docs.docker.com/registry/spec/auth/scope/ +func WithScopesForHost(ctx context.Context, host string, scopes ...string) context.Context { + scopes = CleanScopes(scopes) + return context.WithValue(ctx, scopesForHostContextKey(host), scopes) +} + +// AppendScopesForHost appends additional scopes to the existing scopes +// in the context for the given host and returns a new context. +// The resulted scopes are de-duplicated. +// The append operation does modify the existing scope in the context passed in. +func AppendScopesForHost(ctx context.Context, host string, scopes ...string) context.Context { + if len(scopes) == 0 { + return ctx + } + oldScopes := GetScopesForHost(ctx, host) + return WithScopesForHost(ctx, host, append(oldScopes, scopes...)...) +} + +// GetScopesForHost returns the scopes in the context for the given host, +// excluding global scopes added by [WithScopes] and [AppendScopes]. +func GetScopesForHost(ctx context.Context, host string) []string { + if scopes, ok := ctx.Value(scopesForHostContextKey(host)).([]string); ok { + return slices.Clone(scopes) } return nil } +// GetAllScopesForHost returns the scopes in the context for the given host, +// including global scopes added by [WithScopes] and [AppendScopes]. +func GetAllScopesForHost(ctx context.Context, host string) []string { + scopes := GetScopesForHost(ctx, host) + globalScopes := GetScopes(ctx) + + if len(scopes) == 0 { + return globalScopes + } + if len(globalScopes) == 0 { + return scopes + } + // re-clean the scopes + allScopes := append(scopes, globalScopes...) + return CleanScopes(allScopes) +} + // CleanScopes merges and sort the actions in ascending order if the scopes have // the same resource type and name. The final scopes are sorted in ascending // order. In other words, the scopes passed in are de-duplicated and sorted. @@ -186,14 +275,14 @@ func CleanScopes(scopes []string) []string { } actions = append(actions, action) } - sort.Strings(actions) + slices.Sort(actions) scope := resourceType + ":" + resourceName + ":" + strings.Join(actions, ",") result = append(result, scope) } } // sort and return - sort.Strings(result) + slices.Sort(result) return result } @@ -212,7 +301,7 @@ func cleanActions(actions []string) []string { } // slow path - sort.Strings(actions) + slices.Sort(actions) n := 0 for i := 0; i < len(actions); i++ { if actions[i] == "*" { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go b/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go index fb192aa8a3..9f87d86d17 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go @@ -24,7 +24,7 @@ import ( ) // References: -// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#error-codes +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes // - https://docs.docker.com/registry/spec/api/#errors-2 const ( ErrorCodeBlobUnknown = "BLOB_UNKNOWN" @@ -45,7 +45,7 @@ const ( // Error represents a response inner error returned by the remote // registry. // References: -// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#error-codes +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes // - https://docs.docker.com/registry/spec/api/#errors-2 type Error struct { Code string `json:"code"` @@ -73,7 +73,7 @@ func (e Error) Error() string { // Errors represents a list of response inner errors returned by the remote // server. // References: -// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#error-codes +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes // - https://docs.docker.com/registry/spec/api/#errors-2 type Errors []Error diff --git a/vendor/oras.land/oras-go/v2/registry/repository.go b/vendor/oras.land/oras-go/v2/registry/repository.go index b75b7b8ea4..84a50e2af2 100644 --- a/vendor/oras.land/oras-go/v2/registry/repository.go +++ b/vendor/oras.land/oras-go/v2/registry/repository.go @@ -17,10 +17,15 @@ package registry import ( "context" + "encoding/json" + "fmt" "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/descriptor" + "oras.land/oras-go/v2/internal/spec" ) // Repository is an ORAS target and an union of the blob and the manifest CASs. @@ -82,7 +87,7 @@ type ReferenceFetcher interface { } // ReferrerLister provides the Referrers API. -// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#listing-referrers +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers type ReferrerLister interface { Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error } @@ -104,7 +109,7 @@ type TagLister interface { // specification. // // References: - // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#content-discovery + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#content-discovery // - https://docs.docker.com/registry/spec/api/#tags // See also `Tags()` in this package. Tags(ctx context.Context, last string, fn func(tags []string) error) error @@ -134,3 +139,88 @@ func Tags(ctx context.Context, repo TagLister) ([]string, error) { } return res, nil } + +// Referrers lists the descriptors of image or artifact manifests directly +// referencing the given manifest descriptor. +// +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +func Referrers(ctx context.Context, store content.ReadOnlyGraphStorage, desc ocispec.Descriptor, artifactType string) ([]ocispec.Descriptor, error) { + if !descriptor.IsManifest(desc) { + return nil, fmt.Errorf("the descriptor %v is not a manifest: %w", desc, errdef.ErrUnsupported) + } + + var results []ocispec.Descriptor + + // use the Referrer API if it is available + if rf, ok := store.(ReferrerLister); ok { + if err := rf.Referrers(ctx, desc, artifactType, func(referrers []ocispec.Descriptor) error { + results = append(results, referrers...) + return nil + }); err != nil { + return nil, err + } + return results, nil + } + + predecessors, err := store.Predecessors(ctx, desc) + if err != nil { + return nil, err + } + for _, node := range predecessors { + switch node.MediaType { + case ocispec.MediaTypeImageManifest: + fetched, err := content.FetchAll(ctx, store, node) + if err != nil { + return nil, err + } + var manifest ocispec.Manifest + if err := json.Unmarshal(fetched, &manifest); err != nil { + return nil, err + } + if manifest.Subject == nil || !content.Equal(*manifest.Subject, desc) { + continue + } + node.ArtifactType = manifest.ArtifactType + if node.ArtifactType == "" { + node.ArtifactType = manifest.Config.MediaType + } + node.Annotations = manifest.Annotations + case ocispec.MediaTypeImageIndex: + fetched, err := content.FetchAll(ctx, store, node) + if err != nil { + return nil, err + } + var index ocispec.Index + if err := json.Unmarshal(fetched, &index); err != nil { + return nil, err + } + if index.Subject == nil || !content.Equal(*index.Subject, desc) { + continue + } + node.ArtifactType = index.ArtifactType + node.Annotations = index.Annotations + case spec.MediaTypeArtifactManifest: + fetched, err := content.FetchAll(ctx, store, node) + if err != nil { + return nil, err + } + var artifact spec.Artifact + if err := json.Unmarshal(fetched, &artifact); err != nil { + return nil, err + } + if artifact.Subject == nil || !content.Equal(*artifact.Subject, desc) { + continue + } + node.ArtifactType = artifact.ArtifactType + node.Annotations = artifact.Annotations + default: + continue + } + if artifactType == "" || artifactType == node.ArtifactType { + // the field artifactType in referrers descriptor is allowed to be empty + // https://github.com/opencontainers/distribution-spec/issues/458 + results = append(results, node) + } + } + return results, nil +}