feat: load model from oci image
This commit is contained in:
269
vendor/oras.land/oras-go/v2/registry/reference.go
vendored
Normal file
269
vendor/oras.land/oras-go/v2/registry/reference.go
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
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 registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
)
|
||||
|
||||
// regular expressions for components.
|
||||
var (
|
||||
// repositoryRegexp is adapted from the distribution implementation. The
|
||||
// repository name set under OCI distribution spec is a subset of the docker
|
||||
// spec. For maximum compatability, the docker spec is verified client-side.
|
||||
// Further checks are left to the server-side.
|
||||
// References:
|
||||
// - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53
|
||||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/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-rc1/spec.md#pulling-manifests
|
||||
tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`)
|
||||
)
|
||||
|
||||
// Reference references either a resource descriptor (where Reference.Reference
|
||||
// is a tag or a digest), or a resource repository (where Reference.Reference
|
||||
// is the empty string).
|
||||
type Reference struct {
|
||||
// Registry is the name of the registry. It is usually the domain name of
|
||||
// the registry optionally with a port.
|
||||
Registry string
|
||||
|
||||
// Repository is the name of the repository.
|
||||
Repository string
|
||||
|
||||
// Reference is the reference of the object in the repository. This field
|
||||
// can take any one of the four valid forms (see ParseReference). In the
|
||||
// case where it's the empty string, it necessarily implies valid form D,
|
||||
// and where it is non-empty, then it is either a tag, or a digest
|
||||
// (implying one of valid forms A, B, or C).
|
||||
Reference string
|
||||
}
|
||||
|
||||
// ParseReference parses a string (artifact) into an `artifact reference`.
|
||||
//
|
||||
// Note: An "image" is an "artifact", however, an "artifact" is not necessarily
|
||||
// an "image".
|
||||
//
|
||||
// The token `artifact` is composed of other tokens, and those in turn are
|
||||
// composed of others. This definition recursivity requires a notation capable
|
||||
// of recursion, thus the following two forms have been adopted:
|
||||
//
|
||||
// 1. Backus–Naur Form (BNF) has been adopted to address the recursive nature
|
||||
// of the definition.
|
||||
// 2. Token opacity is revealed via its label letter-casing. That is, "opaque"
|
||||
// tokens (i.e., tokens that are not final, and must therefore be further
|
||||
// broken down into their constituents) are denoted in *lowercase*, while
|
||||
// final tokens (i.e., leaf-node tokens that are final) are denoted in
|
||||
// *uppercase*.
|
||||
//
|
||||
// Finally, note that a number of the opaque tokens are polymorphic in nature;
|
||||
// that is, they can take on one of numerous forms, not restricted to a single
|
||||
// defining form.
|
||||
//
|
||||
// The top-level token, `artifact`, is composed of two (opaque) tokens; namely
|
||||
// `socketaddr` and `path`:
|
||||
//
|
||||
// <artifact> ::= <socketaddr> "/" <path>
|
||||
//
|
||||
// The former is described as follows:
|
||||
//
|
||||
// <socketaddr> ::= <host> | <host> ":" <PORT>
|
||||
// <host> ::= <ip> | <FQDN>
|
||||
// <ip> ::= <IPV4-ADDR> | <IPV6-ADDR>
|
||||
//
|
||||
// The latter, which is of greater interest here, is described as follows:
|
||||
//
|
||||
// <path> ::= <REPOSITORY> | <REPOSITORY> <reference>
|
||||
// <reference> ::= "@" <digest> | ":" <TAG> "@" <DIGEST> | ":" <TAG>
|
||||
// <digest> ::= <ALGO> ":" <HASH>
|
||||
//
|
||||
// This second token--`path`--can take on exactly four forms, each of which will
|
||||
// now be illustrated:
|
||||
//
|
||||
// <--- path --------------------------------------------> | - Decode `path`
|
||||
// <=== REPOSITORY ===> <--- reference ------------------> | - Decode `reference`
|
||||
// <=== REPOSITORY ===> @ <=================== digest ===> | - Valid Form A
|
||||
// <=== REPOSITORY ===> : <!!! TAG !!!> @ <=== digest ===> | - Valid Form B (tag is dropped)
|
||||
// <=== REPOSITORY ===> : <=== TAG ======================> | - Valid Form C
|
||||
// <=== REPOSITORY ======================================> | - Valid Form D
|
||||
//
|
||||
// Note: In the case of Valid Form B, TAG is dropped without any validation or
|
||||
// further consideration.
|
||||
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)
|
||||
}
|
||||
registry, path := parts[0], parts[1]
|
||||
|
||||
var isTag bool
|
||||
var repository string
|
||||
var reference string
|
||||
if index := strings.Index(path, "@"); index != -1 {
|
||||
// `digest` found; Valid Form A (if not B)
|
||||
isTag = false
|
||||
repository = path[:index]
|
||||
reference = path[index+1:]
|
||||
|
||||
if index = strings.Index(repository, ":"); index != -1 {
|
||||
// `tag` found (and now dropped without validation) since `the
|
||||
// `digest` already present; Valid Form B
|
||||
repository = repository[:index]
|
||||
}
|
||||
} else if index = strings.Index(path, ":"); index != -1 {
|
||||
// `tag` found; Valid Form C
|
||||
isTag = true
|
||||
repository = path[:index]
|
||||
reference = path[index+1:]
|
||||
} else {
|
||||
// empty `reference`; Valid Form D
|
||||
repository = path
|
||||
}
|
||||
ref := Reference{
|
||||
Registry: registry,
|
||||
Repository: repository,
|
||||
Reference: reference,
|
||||
}
|
||||
|
||||
if err := ref.ValidateRegistry(); err != nil {
|
||||
return Reference{}, err
|
||||
}
|
||||
|
||||
if err := ref.ValidateRepository(); err != nil {
|
||||
return Reference{}, err
|
||||
}
|
||||
|
||||
if len(ref.Reference) == 0 {
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
validator := ref.ValidateReferenceAsDigest
|
||||
if isTag {
|
||||
validator = ref.ValidateReferenceAsTag
|
||||
}
|
||||
if err := validator(); err != nil {
|
||||
return Reference{}, err
|
||||
}
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
// Validate the entire reference object; the registry, the repository, and the
|
||||
// reference.
|
||||
func (r Reference) Validate() error {
|
||||
if err := r.ValidateRegistry(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.ValidateRepository(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.ValidateReference()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRepository validates the repository.
|
||||
func (r Reference) ValidateRepository() error {
|
||||
if !repositoryRegexp.MatchString(r.Repository) {
|
||||
return fmt.Errorf("%w: invalid repository", errdef.ErrInvalidReference)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 nil
|
||||
}
|
||||
|
||||
// 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 nil
|
||||
}
|
||||
|
||||
// ValidateReference where the reference is first tried as an ampty string, then
|
||||
// as a digest, and if that fails, as a tag.
|
||||
func (r Reference) ValidateReference() error {
|
||||
if len(r.Reference) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if index := strings.IndexByte(r.Reference, ':'); index != -1 {
|
||||
return r.ValidateReferenceAsDigest()
|
||||
}
|
||||
|
||||
return r.ValidateReferenceAsTag()
|
||||
}
|
||||
|
||||
// Host returns the host name of the registry.
|
||||
func (r Reference) Host() string {
|
||||
if r.Registry == "docker.io" {
|
||||
return "registry-1.docker.io"
|
||||
}
|
||||
return r.Registry
|
||||
}
|
||||
|
||||
// ReferenceOrDefault returns the reference or the default reference if empty.
|
||||
func (r Reference) ReferenceOrDefault() string {
|
||||
if r.Reference == "" {
|
||||
return "latest"
|
||||
}
|
||||
return r.Reference
|
||||
}
|
||||
|
||||
// Digest returns the reference as a digest.
|
||||
func (r Reference) Digest() (digest.Digest, error) {
|
||||
return digest.Parse(r.Reference)
|
||||
}
|
||||
|
||||
// String implements `fmt.Stringer` and returns the reference string.
|
||||
// The resulted string is meaningful only if the reference is valid.
|
||||
func (r Reference) String() string {
|
||||
if r.Repository == "" {
|
||||
return r.Registry
|
||||
}
|
||||
ref := r.Registry + "/" + r.Repository
|
||||
if r.Reference == "" {
|
||||
return ref
|
||||
}
|
||||
if d, err := r.Digest(); err == nil {
|
||||
return ref + "@" + d.String()
|
||||
}
|
||||
return ref + ":" + r.Reference
|
||||
}
|
52
vendor/oras.land/oras-go/v2/registry/registry.go
vendored
Normal file
52
vendor/oras.land/oras-go/v2/registry/registry.go
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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 registry provides high-level operations to manage registries.
|
||||
package registry
|
||||
|
||||
import "context"
|
||||
|
||||
// Registry represents a collection of repositories.
|
||||
type Registry interface {
|
||||
// Repositories lists the name of repositories available in the registry.
|
||||
// Since the returned repositories may be paginated by the underlying
|
||||
// implementation, a function should be passed in to process the paginated
|
||||
// repository list.
|
||||
// `last` argument is the `last` parameter when invoking the catalog API.
|
||||
// If `last` is NOT empty, the entries in the response start after the
|
||||
// repo specified by `last`. Otherwise, the response starts from the top
|
||||
// of the Repositories list.
|
||||
// Note: When implemented by a remote registry, the catalog API is called.
|
||||
// However, not all registries supports pagination or conforms the
|
||||
// specification.
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#catalog
|
||||
// See also `Repositories()` in this package.
|
||||
Repositories(ctx context.Context, last string, fn func(repos []string) error) error
|
||||
|
||||
// Repository returns a repository reference by the given name.
|
||||
Repository(ctx context.Context, name string) (Repository, error)
|
||||
}
|
||||
|
||||
// Repositories lists the name of repositories available in the registry.
|
||||
func Repositories(ctx context.Context, reg Registry) ([]string, error) {
|
||||
var res []string
|
||||
if err := reg.Repositories(ctx, "", func(repos []string) error {
|
||||
res = append(res, repos...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
159
vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go
vendored
Normal file
159
vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/internal/syncutil"
|
||||
)
|
||||
|
||||
// DefaultCache is the sharable cache used by DefaultClient.
|
||||
var DefaultCache Cache = NewCache()
|
||||
|
||||
// Cache caches the auth-scheme and auth-token for the "Authorization" header in
|
||||
// accessing the remote registry.
|
||||
// Precisely, the header is `Authorization: auth-scheme auth-token`.
|
||||
// The `auth-token` is a generic term as `token68` in RFC 7235 section 2.1.
|
||||
type Cache interface {
|
||||
// GetScheme returns the auth-scheme part cached for the given registry.
|
||||
// A single registry is assumed to have a consistent scheme.
|
||||
// If a registry has different schemes per path, the auth client is still
|
||||
// workable. However, the cache may not be effective as the cache cannot
|
||||
// correctly guess the scheme.
|
||||
GetScheme(ctx context.Context, registry string) (Scheme, error)
|
||||
|
||||
// GetToken returns the auth-token part cached for the given registry of a
|
||||
// given scheme.
|
||||
// The underlying implementation MAY cache the token for all schemes for the
|
||||
// given registry.
|
||||
GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error)
|
||||
|
||||
// Set fetches the token using the given fetch function and caches the token
|
||||
// for the given scheme with the given key for the given registry.
|
||||
// The return values of the fetch function is returned by this function.
|
||||
// The underlying implementation MAY combine the fetch operation if the Set
|
||||
// function is invoked multiple times at the same time.
|
||||
Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error)
|
||||
}
|
||||
|
||||
// cacheEntry is a cache entry for a single registry.
|
||||
type cacheEntry struct {
|
||||
scheme Scheme
|
||||
tokens sync.Map // map[string]string
|
||||
}
|
||||
|
||||
// concurrentCache is a cache suitable for concurrent invocation.
|
||||
type concurrentCache struct {
|
||||
status sync.Map // map[string]*syncutil.Once
|
||||
cache sync.Map // map[string]*cacheEntry
|
||||
}
|
||||
|
||||
// NewCache creates a new go-routine safe cache instance.
|
||||
func NewCache() Cache {
|
||||
return &concurrentCache{}
|
||||
}
|
||||
|
||||
// GetScheme returns the auth-scheme part cached for the given registry.
|
||||
func (cc *concurrentCache) GetScheme(ctx context.Context, registry string) (Scheme, error) {
|
||||
entry, ok := cc.cache.Load(registry)
|
||||
if !ok {
|
||||
return SchemeUnknown, errdef.ErrNotFound
|
||||
}
|
||||
return entry.(*cacheEntry).scheme, nil
|
||||
}
|
||||
|
||||
// GetToken returns the auth-token part cached for the given registry of a given
|
||||
// scheme.
|
||||
func (cc *concurrentCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) {
|
||||
entryValue, ok := cc.cache.Load(registry)
|
||||
if !ok {
|
||||
return "", errdef.ErrNotFound
|
||||
}
|
||||
entry := entryValue.(*cacheEntry)
|
||||
if entry.scheme != scheme {
|
||||
return "", errdef.ErrNotFound
|
||||
}
|
||||
if token, ok := entry.tokens.Load(key); ok {
|
||||
return token.(string), nil
|
||||
}
|
||||
return "", errdef.ErrNotFound
|
||||
}
|
||||
|
||||
// Set fetches the token using the given fetch function and caches the token
|
||||
// for the given scheme with the given key for the given registry.
|
||||
// Set combines the fetch operation if the Set is invoked multiple times at the
|
||||
// same time.
|
||||
func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) {
|
||||
// fetch token
|
||||
statusKey := strings.Join([]string{
|
||||
registry,
|
||||
scheme.String(),
|
||||
key,
|
||||
}, " ")
|
||||
statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce())
|
||||
fetchOnce := statusValue.(*syncutil.Once)
|
||||
fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) {
|
||||
return fetch(ctx)
|
||||
})
|
||||
if fetchedFirst {
|
||||
cc.status.Delete(statusKey)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := result.(string)
|
||||
if !fetchedFirst {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// cache token
|
||||
newEntry := &cacheEntry{
|
||||
scheme: scheme,
|
||||
}
|
||||
entryValue, exists := cc.cache.LoadOrStore(registry, newEntry)
|
||||
entry := entryValue.(*cacheEntry)
|
||||
if exists && entry.scheme != scheme {
|
||||
// there is a scheme change, which is not expected in most scenarios.
|
||||
// force invalidating all previous cache.
|
||||
entry = newEntry
|
||||
cc.cache.Store(registry, entry)
|
||||
}
|
||||
entry.tokens.Store(key, token)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// noCache is a cache implementation that does not do cache at all.
|
||||
type noCache struct{}
|
||||
|
||||
// GetScheme always returns not found error as it has no cache.
|
||||
func (noCache) GetScheme(ctx context.Context, registry string) (Scheme, error) {
|
||||
return SchemeUnknown, errdef.ErrNotFound
|
||||
}
|
||||
|
||||
// GetToken always returns not found error as it has no cache.
|
||||
func (noCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) {
|
||||
return "", errdef.ErrNotFound
|
||||
}
|
||||
|
||||
// Set calls fetch directly without caching.
|
||||
func (noCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) {
|
||||
return fetch(ctx)
|
||||
}
|
167
vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go
vendored
Normal file
167
vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Scheme define the authentication method.
|
||||
type Scheme byte
|
||||
|
||||
const (
|
||||
// SchemeUnknown represents unknown or unsupported schemes
|
||||
SchemeUnknown Scheme = iota
|
||||
|
||||
// SchemeBasic represents the "Basic" HTTP authentication scheme.
|
||||
// Reference: https://tools.ietf.org/html/rfc7617
|
||||
SchemeBasic
|
||||
|
||||
// SchemeBearer represents the Bearer token in OAuth 2.0.
|
||||
// Reference: https://tools.ietf.org/html/rfc6750
|
||||
SchemeBearer
|
||||
)
|
||||
|
||||
// parseScheme parse the authentication scheme from the given string
|
||||
// case-insensitively.
|
||||
func parseScheme(scheme string) Scheme {
|
||||
switch {
|
||||
case strings.EqualFold(scheme, "basic"):
|
||||
return SchemeBasic
|
||||
case strings.EqualFold(scheme, "bearer"):
|
||||
return SchemeBearer
|
||||
}
|
||||
return SchemeUnknown
|
||||
}
|
||||
|
||||
// String return the string for the scheme.
|
||||
func (s Scheme) String() string {
|
||||
switch s {
|
||||
case SchemeBasic:
|
||||
return "Basic"
|
||||
case SchemeBearer:
|
||||
return "Bearer"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// parseChallenge parses the "WWW-Authenticate" header returned by the remote
|
||||
// registry, and extracts parameters if scheme is Bearer.
|
||||
// References:
|
||||
// - https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate
|
||||
// - https://tools.ietf.org/html/rfc7235#section-2.1
|
||||
func parseChallenge(header string) (scheme Scheme, params map[string]string) {
|
||||
// as defined in RFC 7235 section 2.1, we have
|
||||
// challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
|
||||
// auth-scheme = token
|
||||
// auth-param = token BWS "=" BWS ( token / quoted-string )
|
||||
//
|
||||
// since we focus parameters only on Bearer, we have
|
||||
// challenge = auth-scheme [ 1*SP #auth-param ]
|
||||
schemeString, rest := parseToken(header)
|
||||
scheme = parseScheme(schemeString)
|
||||
|
||||
// fast path for non bearer challenge
|
||||
if scheme != SchemeBearer {
|
||||
return
|
||||
}
|
||||
|
||||
// parse params for bearer auth.
|
||||
// combining RFC 7235 section 2.1 with RFC 7230 section 7, we have
|
||||
// #auth-param => auth-param *( OWS "," OWS auth-param )
|
||||
var key, value string
|
||||
for {
|
||||
key, rest = parseToken(skipSpace(rest))
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
|
||||
rest = skipSpace(rest)
|
||||
if rest == "" || rest[0] != '=' {
|
||||
return
|
||||
}
|
||||
rest = skipSpace(rest[1:])
|
||||
if rest == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if rest[0] == '"' {
|
||||
prefix, err := strconv.QuotedPrefix(rest)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value, err = strconv.Unquote(prefix)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rest = rest[len(prefix):]
|
||||
} else {
|
||||
value, rest = parseToken(rest)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
if params == nil {
|
||||
params = map[string]string{
|
||||
key: value,
|
||||
}
|
||||
} else {
|
||||
params[key] = value
|
||||
}
|
||||
|
||||
rest = skipSpace(rest)
|
||||
if rest == "" || rest[0] != ',' {
|
||||
return
|
||||
}
|
||||
rest = rest[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// isNotTokenChar reports whether rune is not a `tchar` defined in RFC 7230
|
||||
// section 3.2.6.
|
||||
func isNotTokenChar(r rune) bool {
|
||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
||||
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||
// / DIGIT / ALPHA
|
||||
// ; any VCHAR, except delimiters
|
||||
return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') &&
|
||||
(r < '0' || r > '9') && !strings.ContainsRune("!#$%&'*+-.^_`|~", r)
|
||||
}
|
||||
|
||||
// parseToken finds the next token from the given string. If no token found,
|
||||
// an empty token is returned and the whole of the input is returned in rest.
|
||||
// Note: Since token = 1*tchar, empty string is not a valid token.
|
||||
func parseToken(s string) (token, rest string) {
|
||||
if i := strings.IndexFunc(s, isNotTokenChar); i != -1 {
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
return s, ""
|
||||
}
|
||||
|
||||
// skipSpace skips "bad" whitespace (BWS) defined in RFC 7230 section 3.2.3.
|
||||
func skipSpace(s string) string {
|
||||
// OWS = *( SP / HTAB )
|
||||
// ; optional whitespace
|
||||
// BWS = OWS
|
||||
// ; "bad" whitespace
|
||||
if i := strings.IndexFunc(s, func(r rune) bool {
|
||||
return r != ' ' && r != '\t'
|
||||
}); i != -1 {
|
||||
return s[i:]
|
||||
}
|
||||
return s
|
||||
}
|
413
vendor/oras.land/oras-go/v2/registry/remote/auth/client.go
vendored
Normal file
413
vendor/oras.land/oras-go/v2/registry/remote/auth/client.go
vendored
Normal file
@@ -0,0 +1,413 @@
|
||||
/*
|
||||
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 auth provides authentication for a client to a remote registry.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote/internal/errutil"
|
||||
"oras.land/oras-go/v2/registry/remote/retry"
|
||||
)
|
||||
|
||||
// DefaultClient is the default auth-decorated client.
|
||||
var DefaultClient = &Client{
|
||||
Client: retry.DefaultClient,
|
||||
Header: http.Header{
|
||||
"User-Agent": {"oras-go"},
|
||||
},
|
||||
Cache: DefaultCache,
|
||||
}
|
||||
|
||||
// maxResponseBytes specifies the default limit on how many response bytes are
|
||||
// allowed in the server's response from authorization service servers.
|
||||
// A typical response message from authorization service servers is around 1 to
|
||||
// 4 KiB. Since the size of a token must be smaller than the HTTP header size
|
||||
// limit, which is usually 16 KiB. As specified by the distribution, the
|
||||
// response may contain 2 identical tokens, that is, 16 x 2 = 32 KiB.
|
||||
// Hence, 128 KiB should be sufficient.
|
||||
// References: https://docs.docker.com/registry/spec/auth/token/
|
||||
var maxResponseBytes int64 = 128 * 1024 // 128 KiB
|
||||
|
||||
// defaultClientID specifies the default client ID used in OAuth2.
|
||||
// See also ClientID.
|
||||
var defaultClientID = "oras-go"
|
||||
|
||||
// StaticCredential specifies static credentials for the given host.
|
||||
func StaticCredential(registry string, cred Credential) func(context.Context, string) (Credential, error) {
|
||||
return func(_ context.Context, target string) (Credential, error) {
|
||||
if target == registry {
|
||||
return cred, nil
|
||||
}
|
||||
return EmptyCredential, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Client is an auth-decorated HTTP client.
|
||||
// Its zero value is a usable client that uses http.DefaultClient with no cache.
|
||||
type Client struct {
|
||||
// Client is the underlying HTTP client used to access the remote
|
||||
// server.
|
||||
// If nil, http.DefaultClient is used.
|
||||
// It is possible to use the default retry client from the package
|
||||
// `oras.land/oras-go/v2/registry/remote/retry`. That client is already available
|
||||
// in the DefaultClient.
|
||||
// It is also possible to use a custom client. For example, github.com/hashicorp/go-retryablehttp
|
||||
// is a popular HTTP client that supports retries.
|
||||
Client *http.Client
|
||||
|
||||
// Header contains the custom headers to be added to each request.
|
||||
Header http.Header
|
||||
|
||||
// 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
|
||||
// an error.
|
||||
// If nil, the credential is always resolved to `EmptyCredential`.
|
||||
Credential func(context.Context, string) (Credential, error)
|
||||
|
||||
// Cache caches credentials for direct accessing the remote registry.
|
||||
// If nil, no cache is used.
|
||||
Cache Cache
|
||||
|
||||
// ClientID used in fetching OAuth2 token as a required field.
|
||||
// If empty, a default client ID is used.
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/oauth/#getting-a-token
|
||||
ClientID string
|
||||
|
||||
// ForceAttemptOAuth2 controls whether to follow OAuth2 with password grant
|
||||
// instead the distribution spec when authenticating using username and
|
||||
// password.
|
||||
// References:
|
||||
// - https://docs.docker.com/registry/spec/auth/jwt/
|
||||
// - https://docs.docker.com/registry/spec/auth/oauth/
|
||||
ForceAttemptOAuth2 bool
|
||||
}
|
||||
|
||||
// client returns an HTTP client used to access the remote registry.
|
||||
// http.DefaultClient is return if the client is not configured.
|
||||
func (c *Client) client() *http.Client {
|
||||
if c.Client == nil {
|
||||
return http.DefaultClient
|
||||
}
|
||||
return c.Client
|
||||
}
|
||||
|
||||
// send adds headers to the request and sends the request to the remote server.
|
||||
func (c *Client) send(req *http.Request) (*http.Response, error) {
|
||||
for key, values := range c.Header {
|
||||
req.Header[key] = append(req.Header[key], values...)
|
||||
}
|
||||
return c.client().Do(req)
|
||||
}
|
||||
|
||||
// credential resolves the credential for the given registry.
|
||||
func (c *Client) credential(ctx context.Context, reg string) (Credential, error) {
|
||||
if c.Credential == nil {
|
||||
return EmptyCredential, nil
|
||||
}
|
||||
return c.Credential(ctx, reg)
|
||||
}
|
||||
|
||||
// cache resolves the cache.
|
||||
// noCache is return if the cache is not configured.
|
||||
func (c *Client) cache() Cache {
|
||||
if c.Cache == nil {
|
||||
return noCache{}
|
||||
}
|
||||
return c.Cache
|
||||
}
|
||||
|
||||
// SetUserAgent sets the user agent for all out-going requests.
|
||||
func (c *Client) SetUserAgent(userAgent string) {
|
||||
if c.Header == nil {
|
||||
c.Header = http.Header{}
|
||||
}
|
||||
c.Header.Set("User-Agent", userAgent)
|
||||
}
|
||||
|
||||
// Do sends the request to the remote server, attempting to resolve
|
||||
// authentication if 'Authorization' header is not set.
|
||||
//
|
||||
// On authentication failure due to bad credential,
|
||||
// - Do returns error if it fails to fetch token for bearer auth.
|
||||
// - Do returns the registry response without error for basic auth.
|
||||
func (c *Client) Do(originalReq *http.Request) (*http.Response, error) {
|
||||
if auth := originalReq.Header.Get("Authorization"); auth != "" {
|
||||
return c.send(originalReq)
|
||||
}
|
||||
|
||||
ctx := originalReq.Context()
|
||||
req := originalReq.Clone(ctx)
|
||||
|
||||
// attempt cached auth token
|
||||
var attemptedKey string
|
||||
cache := c.cache()
|
||||
registry := originalReq.Host
|
||||
scheme, err := cache.GetScheme(ctx, registry)
|
||||
if err == nil {
|
||||
switch scheme {
|
||||
case SchemeBasic:
|
||||
token, err := cache.GetToken(ctx, registry, SchemeBasic, "")
|
||||
if err == nil {
|
||||
req.Header.Set("Authorization", "Basic "+token)
|
||||
}
|
||||
case SchemeBearer:
|
||||
scopes := GetScopes(ctx)
|
||||
attemptedKey = strings.Join(scopes, " ")
|
||||
token, err := cache.GetToken(ctx, registry, SchemeBearer, attemptedKey)
|
||||
if err == nil {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := c.send(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// attempt again with credentials for recognized schemes
|
||||
challenge := resp.Header.Get("Www-Authenticate")
|
||||
scheme, params := parseChallenge(challenge)
|
||||
switch scheme {
|
||||
case SchemeBasic:
|
||||
resp.Body.Close()
|
||||
|
||||
token, err := cache.Set(ctx, registry, SchemeBasic, "", func(ctx context.Context) (string, error) {
|
||||
return c.fetchBasicAuth(ctx, registry)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err)
|
||||
}
|
||||
|
||||
req = originalReq.Clone(ctx)
|
||||
req.Header.Set("Authorization", "Basic "+token)
|
||||
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 = 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 {
|
||||
req = originalReq.Clone(ctx)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if err := rewindRequestBody(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.send(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
return resp, nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err)
|
||||
}
|
||||
|
||||
req = originalReq.Clone(ctx)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
default:
|
||||
return resp, nil
|
||||
}
|
||||
if err := rewindRequestBody(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.send(req)
|
||||
}
|
||||
|
||||
// fetchBasicAuth fetches a basic auth token for the basic challenge.
|
||||
func (c *Client) fetchBasicAuth(ctx context.Context, registry string) (string, error) {
|
||||
cred, err := c.credential(ctx, registry)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve credential: %w", err)
|
||||
}
|
||||
if cred == EmptyCredential {
|
||||
return "", errors.New("credential required for basic auth")
|
||||
}
|
||||
if cred.Username == "" || cred.Password == "" {
|
||||
return "", errors.New("missing username or password for basic auth")
|
||||
}
|
||||
auth := cred.Username + ":" + cred.Password
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth)), nil
|
||||
}
|
||||
|
||||
// fetchBearerToken fetches an access token for the bearer challenge.
|
||||
func (c *Client) fetchBearerToken(ctx context.Context, registry, realm, service string, scopes []string) (string, error) {
|
||||
cred, err := c.credential(ctx, registry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if cred.AccessToken != "" {
|
||||
return cred.AccessToken, nil
|
||||
}
|
||||
if cred == EmptyCredential || (cred.RefreshToken == "" && !c.ForceAttemptOAuth2) {
|
||||
return c.fetchDistributionToken(ctx, realm, service, scopes, cred.Username, cred.Password)
|
||||
}
|
||||
return c.fetchOAuth2Token(ctx, realm, service, scopes, cred)
|
||||
}
|
||||
|
||||
// fetchDistributionToken fetches an access token as defined by the distribution
|
||||
// specification.
|
||||
// It fetches anonymous tokens if no credential is provided.
|
||||
// References:
|
||||
// - https://docs.docker.com/registry/spec/auth/jwt/
|
||||
// - https://docs.docker.com/registry/spec/auth/token/
|
||||
func (c *Client) fetchDistributionToken(ctx context.Context, realm, service string, scopes []string, username, password string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, realm, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if username != "" || password != "" {
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
q := req.URL.Query()
|
||||
if service != "" {
|
||||
q.Set("service", service)
|
||||
}
|
||||
for _, scope := range scopes {
|
||||
q.Add("scope", scope)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := c.send(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errutil.ParseErrorResponse(resp)
|
||||
}
|
||||
|
||||
// As specified in https://docs.docker.com/registry/spec/auth/token/ section
|
||||
// "Token Response Fields", the token is either in `token` or
|
||||
// `access_token`. If both present, they are identical.
|
||||
var result struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
lr := io.LimitReader(resp.Body, maxResponseBytes)
|
||||
if err := json.NewDecoder(lr).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
|
||||
}
|
||||
if result.AccessToken != "" {
|
||||
return result.AccessToken, nil
|
||||
}
|
||||
if result.Token != "" {
|
||||
return result.Token, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s %q: empty token returned", resp.Request.Method, resp.Request.URL)
|
||||
}
|
||||
|
||||
// fetchOAuth2Token fetches an OAuth2 access token.
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/oauth/
|
||||
func (c *Client) fetchOAuth2Token(ctx context.Context, realm, service string, scopes []string, cred Credential) (string, error) {
|
||||
form := url.Values{}
|
||||
if cred.RefreshToken != "" {
|
||||
form.Set("grant_type", "refresh_token")
|
||||
form.Set("refresh_token", cred.RefreshToken)
|
||||
} else if cred.Username != "" && cred.Password != "" {
|
||||
form.Set("grant_type", "password")
|
||||
form.Set("username", cred.Username)
|
||||
form.Set("password", cred.Password)
|
||||
} else {
|
||||
return "", errors.New("missing username or password for bearer auth")
|
||||
}
|
||||
form.Set("service", service)
|
||||
clientID := c.ClientID
|
||||
if clientID == "" {
|
||||
clientID = defaultClientID
|
||||
}
|
||||
form.Set("client_id", clientID)
|
||||
if len(scopes) != 0 {
|
||||
form.Set("scope", strings.Join(scopes, " "))
|
||||
}
|
||||
body := strings.NewReader(form.Encode())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, realm, body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := c.send(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errutil.ParseErrorResponse(resp)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
lr := io.LimitReader(resp.Body, maxResponseBytes)
|
||||
if err := json.NewDecoder(lr).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
|
||||
}
|
||||
if result.AccessToken != "" {
|
||||
return result.AccessToken, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s %q: empty token returned", resp.Request.Method, resp.Request.URL)
|
||||
}
|
||||
|
||||
// rewindRequestBody tries to rewind the request body if exists.
|
||||
func rewindRequestBody(req *http.Request) error {
|
||||
if req.Body == nil || req.Body == http.NoBody {
|
||||
return nil
|
||||
}
|
||||
if req.GetBody == nil {
|
||||
return fmt.Errorf("%s %q: request body is not rewindable", req.Method, req.URL)
|
||||
}
|
||||
body, err := req.GetBody()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %q: failed to get request body: %w", req.Method, req.URL, err)
|
||||
}
|
||||
req.Body = body
|
||||
return nil
|
||||
}
|
40
vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go
vendored
Normal file
40
vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
// EmptyCredential represents an empty credential.
|
||||
var EmptyCredential Credential
|
||||
|
||||
// Credential contains authentication credentials used to access remote
|
||||
// registries.
|
||||
type Credential struct {
|
||||
// Username is the name of the user for the remote registry.
|
||||
Username string
|
||||
|
||||
// Password is the secret associated with the username.
|
||||
Password string
|
||||
|
||||
// RefreshToken is a bearer token to be sent to the authorization service
|
||||
// for fetching access tokens.
|
||||
// A refresh token is often referred as an identity token.
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/oauth/
|
||||
RefreshToken string
|
||||
|
||||
// AccessToken is a bearer token to be sent to the registry.
|
||||
// An access token is often referred as a registry token.
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/token/
|
||||
AccessToken string
|
||||
}
|
236
vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go
vendored
Normal file
236
vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Actions used in scopes.
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/scope/
|
||||
const (
|
||||
// ActionPull represents generic read access for resources of the repository
|
||||
// type.
|
||||
ActionPull = "pull"
|
||||
|
||||
// ActionPush represents generic write access for resources of the
|
||||
// repository type.
|
||||
ActionPush = "push"
|
||||
|
||||
// ActionDelete represents the delete permission for resources of the
|
||||
// repository type.
|
||||
ActionDelete = "delete"
|
||||
)
|
||||
|
||||
// ScopeRegistryCatalog is the scope for registry catalog access.
|
||||
const ScopeRegistryCatalog = "registry:catalog:*"
|
||||
|
||||
// ScopeRepository returns a repository scope with given actions.
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/scope/
|
||||
func ScopeRepository(repository string, actions ...string) string {
|
||||
actions = cleanActions(actions)
|
||||
if repository == "" || len(actions) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"repository",
|
||||
repository,
|
||||
strings.Join(actions, ","),
|
||||
}, ":")
|
||||
}
|
||||
|
||||
// scopesContextKey is the context key for scopes.
|
||||
type scopesContextKey struct{}
|
||||
|
||||
// WithScopes returns a context with 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 `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.
|
||||
//
|
||||
// Passing an empty list of scopes will virtually remove the scope hints in the
|
||||
// context.
|
||||
//
|
||||
// Reference: https://docs.docker.com/registry/spec/auth/scope/
|
||||
func WithScopes(ctx context.Context, scopes ...string) context.Context {
|
||||
scopes = CleanScopes(scopes)
|
||||
return context.WithValue(ctx, scopesContextKey{}, scopes)
|
||||
}
|
||||
|
||||
// AppendScopes appends additional scopes to the existing scopes in the context
|
||||
// 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 AppendScopes(ctx context.Context, scopes ...string) context.Context {
|
||||
if len(scopes) == 0 {
|
||||
return ctx
|
||||
}
|
||||
return WithScopes(ctx, append(GetScopes(ctx), scopes...)...)
|
||||
}
|
||||
|
||||
// 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 nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Therefore, the output of this function is deterministic.
|
||||
//
|
||||
// If there is a wildcard `*` in the action, other actions in the same resource
|
||||
// type and name are ignored.
|
||||
func CleanScopes(scopes []string) []string {
|
||||
// fast paths
|
||||
switch len(scopes) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
scope := scopes[0]
|
||||
i := strings.LastIndex(scope, ":")
|
||||
if i == -1 {
|
||||
return []string{scope}
|
||||
}
|
||||
actionList := strings.Split(scope[i+1:], ",")
|
||||
actionList = cleanActions(actionList)
|
||||
if len(actionList) == 0 {
|
||||
return nil
|
||||
}
|
||||
actions := strings.Join(actionList, ",")
|
||||
scope = scope[:i+1] + actions
|
||||
return []string{scope}
|
||||
}
|
||||
|
||||
// slow path
|
||||
var result []string
|
||||
|
||||
// merge recognizable scopes
|
||||
resourceTypes := make(map[string]map[string]map[string]struct{})
|
||||
for _, scope := range scopes {
|
||||
// extract resource type
|
||||
i := strings.Index(scope, ":")
|
||||
if i == -1 {
|
||||
result = append(result, scope)
|
||||
continue
|
||||
}
|
||||
resourceType := scope[:i]
|
||||
|
||||
// extract resource name and actions
|
||||
rest := scope[i+1:]
|
||||
i = strings.LastIndex(rest, ":")
|
||||
if i == -1 {
|
||||
result = append(result, scope)
|
||||
continue
|
||||
}
|
||||
resourceName := rest[:i]
|
||||
actions := rest[i+1:]
|
||||
if actions == "" {
|
||||
// drop scope since no action found
|
||||
continue
|
||||
}
|
||||
|
||||
// add to the intermediate map for de-duplication
|
||||
namedActions := resourceTypes[resourceType]
|
||||
if namedActions == nil {
|
||||
namedActions = make(map[string]map[string]struct{})
|
||||
resourceTypes[resourceType] = namedActions
|
||||
}
|
||||
actionSet := namedActions[resourceName]
|
||||
if actionSet == nil {
|
||||
actionSet = make(map[string]struct{})
|
||||
namedActions[resourceName] = actionSet
|
||||
}
|
||||
for _, action := range strings.Split(actions, ",") {
|
||||
if action != "" {
|
||||
actionSet[action] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reconstruct scopes
|
||||
for resourceType, namedActions := range resourceTypes {
|
||||
for resourceName, actionSet := range namedActions {
|
||||
if len(actionSet) == 0 {
|
||||
continue
|
||||
}
|
||||
var actions []string
|
||||
for action := range actionSet {
|
||||
if action == "*" {
|
||||
actions = []string{"*"}
|
||||
break
|
||||
}
|
||||
actions = append(actions, action)
|
||||
}
|
||||
sort.Strings(actions)
|
||||
scope := resourceType + ":" + resourceName + ":" + strings.Join(actions, ",")
|
||||
result = append(result, scope)
|
||||
}
|
||||
}
|
||||
|
||||
// sort and return
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// cleanActions removes the duplicated actions and sort in ascending order.
|
||||
// If there is a wildcard `*` in the action, other actions are ignored.
|
||||
func cleanActions(actions []string) []string {
|
||||
// fast paths
|
||||
switch len(actions) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
if actions[0] == "" {
|
||||
return nil
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// slow path
|
||||
sort.Strings(actions)
|
||||
n := 0
|
||||
for i := 0; i < len(actions); i++ {
|
||||
if actions[i] == "*" {
|
||||
return []string{"*"}
|
||||
}
|
||||
if actions[i] != actions[n] {
|
||||
n++
|
||||
if n != i {
|
||||
actions[n] = actions[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
n++
|
||||
if actions[0] == "" {
|
||||
if n == 1 {
|
||||
return nil
|
||||
}
|
||||
return actions[1:n]
|
||||
}
|
||||
return actions[:n]
|
||||
}
|
128
vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go
vendored
Normal file
128
vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
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 errcode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes
|
||||
// - https://docs.docker.com/registry/spec/api/#errors-2
|
||||
const (
|
||||
ErrorCodeBlobUnknown = "BLOB_UNKNOWN"
|
||||
ErrorCodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID"
|
||||
ErrorCodeBlobUploadUnknown = "BLOB_UPLOAD_UNKNOWN"
|
||||
ErrorCodeDigestInvalid = "DIGEST_INVALID"
|
||||
ErrorCodeManifestBlobUnknown = "MANIFEST_BLOB_UNKNOWN"
|
||||
ErrorCodeManifestInvalid = "MANIFEST_INVALID"
|
||||
ErrorCodeManifestUnknown = "MANIFEST_UNKNOWN"
|
||||
ErrorCodeNameInvalid = "NAME_INVALID"
|
||||
ErrorCodeNameUnknown = "NAME_UNKNOWN"
|
||||
ErrorCodeSizeInvalid = "SIZE_INVALID"
|
||||
ErrorCodeUnauthorized = "UNAUTHORIZED"
|
||||
ErrorCodeDenied = "DENIED"
|
||||
ErrorCodeUnsupported = "UNSUPPORTED"
|
||||
)
|
||||
|
||||
// Error represents a response inner error returned by the remote
|
||||
// registry.
|
||||
// References:
|
||||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes
|
||||
// - https://docs.docker.com/registry/spec/api/#errors-2
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail any `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// Error returns a error string describing the error.
|
||||
func (e Error) Error() string {
|
||||
code := strings.Map(func(r rune) rune {
|
||||
if r == '_' {
|
||||
return ' '
|
||||
}
|
||||
return unicode.ToLower(r)
|
||||
}, e.Code)
|
||||
if e.Message == "" {
|
||||
return code
|
||||
}
|
||||
if e.Detail == nil {
|
||||
return fmt.Sprintf("%s: %s", code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s: %v", code, e.Message, e.Detail)
|
||||
}
|
||||
|
||||
// Errors represents a list of response inner errors returned by the remote
|
||||
// server.
|
||||
// References:
|
||||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes
|
||||
// - https://docs.docker.com/registry/spec/api/#errors-2
|
||||
type Errors []Error
|
||||
|
||||
// Error returns a error string describing the error.
|
||||
func (errs Errors) Error() string {
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
return "<nil>"
|
||||
case 1:
|
||||
return errs[0].Error()
|
||||
}
|
||||
var errmsgs []string
|
||||
for _, err := range errs {
|
||||
errmsgs = append(errmsgs, err.Error())
|
||||
}
|
||||
return strings.Join(errmsgs, "; ")
|
||||
}
|
||||
|
||||
// Unwrap returns the inner error only when there is exactly one error.
|
||||
func (errs Errors) Unwrap() error {
|
||||
if len(errs) == 1 {
|
||||
return errs[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response.
|
||||
type ErrorResponse struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
StatusCode int
|
||||
Errors Errors
|
||||
}
|
||||
|
||||
// Error returns a error string describing the error.
|
||||
func (err *ErrorResponse) Error() string {
|
||||
var errmsg string
|
||||
if len(err.Errors) > 0 {
|
||||
errmsg = err.Errors.Error()
|
||||
} else {
|
||||
errmsg = http.StatusText(err.StatusCode)
|
||||
}
|
||||
return fmt.Sprintf("%s %q: response status code %d: %s", err.Method, err.URL, err.StatusCode, errmsg)
|
||||
}
|
||||
|
||||
// Unwrap returns the internal errors of err if any.
|
||||
func (err *ErrorResponse) Unwrap() error {
|
||||
if len(err.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
return err.Errors
|
||||
}
|
54
vendor/oras.land/oras-go/v2/registry/remote/internal/errutil/errutil.go
vendored
Normal file
54
vendor/oras.land/oras-go/v2/registry/remote/internal/errutil/errutil.go
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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 errutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote/errcode"
|
||||
)
|
||||
|
||||
// maxErrorBytes specifies the default limit on how many response bytes are
|
||||
// allowed in the server's error response.
|
||||
// A typical error message is around 200 bytes. Hence, 8 KiB should be
|
||||
// sufficient.
|
||||
const maxErrorBytes int64 = 8 * 1024 // 8 KiB
|
||||
|
||||
// ParseErrorResponse parses the error returned by the remote registry.
|
||||
func ParseErrorResponse(resp *http.Response) error {
|
||||
resultErr := &errcode.ErrorResponse{
|
||||
Method: resp.Request.Method,
|
||||
URL: resp.Request.URL,
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
var body struct {
|
||||
Errors errcode.Errors `json:"errors"`
|
||||
}
|
||||
lr := io.LimitReader(resp.Body, maxErrorBytes)
|
||||
if err := json.NewDecoder(lr).Decode(&body); err == nil {
|
||||
resultErr.Errors = body.Errors
|
||||
}
|
||||
return resultErr
|
||||
}
|
||||
|
||||
// IsErrorCode returns true if err is an Error and its Code equals to code.
|
||||
func IsErrorCode(err error, code string) bool {
|
||||
var ec errcode.Error
|
||||
return errors.As(err, &ec) && ec.Code == code
|
||||
}
|
58
vendor/oras.land/oras-go/v2/registry/remote/manifest.go
vendored
Normal file
58
vendor/oras.land/oras-go/v2/registry/remote/manifest.go
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 remote
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/internal/docker"
|
||||
)
|
||||
|
||||
// defaultManifestMediaTypes contains the default set of manifests media types.
|
||||
var defaultManifestMediaTypes = []string{
|
||||
docker.MediaTypeManifest,
|
||||
docker.MediaTypeManifestList,
|
||||
ocispec.MediaTypeImageManifest,
|
||||
ocispec.MediaTypeImageIndex,
|
||||
ocispec.MediaTypeArtifactManifest,
|
||||
}
|
||||
|
||||
// defaultManifestAcceptHeader is the default set in the `Accept` header for
|
||||
// resolving manifests from tags.
|
||||
var defaultManifestAcceptHeader = strings.Join(defaultManifestMediaTypes, ", ")
|
||||
|
||||
// isManifest determines if the given descriptor points to a manifest.
|
||||
func isManifest(manifestMediaTypes []string, desc ocispec.Descriptor) bool {
|
||||
if len(manifestMediaTypes) == 0 {
|
||||
manifestMediaTypes = defaultManifestMediaTypes
|
||||
}
|
||||
for _, mediaType := range manifestMediaTypes {
|
||||
if desc.MediaType == mediaType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// manifestAcceptHeader generates the set in the `Accept` header for resolving
|
||||
// manifests from tags.
|
||||
func manifestAcceptHeader(manifestMediaTypes []string) string {
|
||||
if len(manifestMediaTypes) == 0 {
|
||||
return defaultManifestAcceptHeader
|
||||
}
|
||||
return strings.Join(manifestMediaTypes, ", ")
|
||||
}
|
191
vendor/oras.land/oras-go/v2/registry/remote/referrers.go
vendored
Normal file
191
vendor/oras.land/oras-go/v2/registry/remote/referrers.go
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
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 remote
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/internal/descriptor"
|
||||
)
|
||||
|
||||
// zeroDigest represents a digest that consists of zeros. zeroDigest is used
|
||||
// for pinging Referrers API.
|
||||
const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// referrersState represents the state of Referrers API.
|
||||
type referrersState = int32
|
||||
|
||||
const (
|
||||
// referrersStateUnknown represents an unknown state of Referrers API.
|
||||
referrersStateUnknown referrersState = iota
|
||||
// referrersStateSupported represents that the repository is known to
|
||||
// support Referrers API.
|
||||
referrersStateSupported
|
||||
// referrersStateUnsupported represents that the repository is known to
|
||||
// not support Referrers API.
|
||||
referrersStateUnsupported
|
||||
)
|
||||
|
||||
// referrerOperation represents an operation on a referrer.
|
||||
type referrerOperation = int32
|
||||
|
||||
const (
|
||||
// referrerOperationAdd represents an addition operation on a referrer.
|
||||
referrerOperationAdd referrerOperation = iota
|
||||
// referrerOperationRemove represents a removal operation on a referrer.
|
||||
referrerOperationRemove
|
||||
)
|
||||
|
||||
// referrerChange represents a change on a referrer.
|
||||
type referrerChange struct {
|
||||
referrer ocispec.Descriptor
|
||||
operation referrerOperation
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability()
|
||||
// when the Referrers API capability has been already set.
|
||||
ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set")
|
||||
|
||||
// errNoReferrerUpdate is returned by applyReferrerChanges() when there
|
||||
// is no any referrer update.
|
||||
errNoReferrerUpdate = errors.New("no referrer update")
|
||||
)
|
||||
|
||||
// buildReferrersTag builds the referrers tag for the given manifest descriptor.
|
||||
// Format: <algorithm>-<digest>
|
||||
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#unavailable-referrers-api
|
||||
func buildReferrersTag(desc ocispec.Descriptor) string {
|
||||
alg := desc.Digest.Algorithm().String()
|
||||
encoded := desc.Digest.Encoded()
|
||||
return alg + "-" + encoded
|
||||
}
|
||||
|
||||
// isReferrersFilterApplied checks annotations to see if requested is in the
|
||||
// applied filter list.
|
||||
func isReferrersFilterApplied(annotations map[string]string, requested string) bool {
|
||||
applied := annotations[ocispec.AnnotationReferrersFiltersApplied]
|
||||
if applied == "" || requested == "" {
|
||||
return false
|
||||
}
|
||||
filters := strings.Split(applied, ",")
|
||||
for _, f := range filters {
|
||||
if f == requested {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filterReferrers filters a slice of referrers by artifactType in place.
|
||||
// The returned slice contains matching referrers.
|
||||
func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor {
|
||||
if artifactType == "" {
|
||||
return refs
|
||||
}
|
||||
var j int
|
||||
for i, ref := range refs {
|
||||
if ref.ArtifactType == artifactType {
|
||||
if i != j {
|
||||
refs[j] = ref
|
||||
}
|
||||
j++
|
||||
}
|
||||
}
|
||||
return refs[:j]
|
||||
}
|
||||
|
||||
// applyReferrerChanges applies referrerChanges on referrers and returns the
|
||||
// updated referrers.
|
||||
// Returns errNoReferrerUpdate if there is no any referrers updates.
|
||||
func applyReferrerChanges(referrers []ocispec.Descriptor, referrerChanges []referrerChange) ([]ocispec.Descriptor, error) {
|
||||
referrersMap := make(map[descriptor.Descriptor]int, len(referrers)+len(referrerChanges))
|
||||
updatedReferrers := make([]ocispec.Descriptor, 0, len(referrers)+len(referrerChanges))
|
||||
var updateRequired bool
|
||||
for _, r := range referrers {
|
||||
if content.Equal(r, ocispec.Descriptor{}) {
|
||||
// skip bad entry
|
||||
updateRequired = true
|
||||
continue
|
||||
}
|
||||
key := descriptor.FromOCI(r)
|
||||
if _, ok := referrersMap[key]; ok {
|
||||
// skip duplicates
|
||||
updateRequired = true
|
||||
continue
|
||||
}
|
||||
updatedReferrers = append(updatedReferrers, r)
|
||||
referrersMap[key] = len(updatedReferrers) - 1
|
||||
}
|
||||
|
||||
// apply changes
|
||||
for _, change := range referrerChanges {
|
||||
key := descriptor.FromOCI(change.referrer)
|
||||
switch change.operation {
|
||||
case referrerOperationAdd:
|
||||
if _, ok := referrersMap[key]; !ok {
|
||||
// add distinct referrers
|
||||
updatedReferrers = append(updatedReferrers, change.referrer)
|
||||
referrersMap[key] = len(updatedReferrers) - 1
|
||||
}
|
||||
case referrerOperationRemove:
|
||||
if pos, ok := referrersMap[key]; ok {
|
||||
// remove referrers that are already in the map
|
||||
updatedReferrers[pos] = ocispec.Descriptor{}
|
||||
delete(referrersMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip unnecessary update
|
||||
if !updateRequired && len(referrersMap) == len(referrers) {
|
||||
// if the result referrer map contains the same content as the
|
||||
// original referrers, consider that there is no update on the
|
||||
// referrers.
|
||||
for _, r := range referrers {
|
||||
key := descriptor.FromOCI(r)
|
||||
if _, ok := referrersMap[key]; !ok {
|
||||
updateRequired = true
|
||||
}
|
||||
}
|
||||
if !updateRequired {
|
||||
return nil, errNoReferrerUpdate
|
||||
}
|
||||
}
|
||||
|
||||
return removeEmptyDescriptors(updatedReferrers, len(referrersMap)), nil
|
||||
}
|
||||
|
||||
// removeEmptyDescriptors in-place removes empty items from descs, given a hint
|
||||
// of the number of non-empty descriptors.
|
||||
func removeEmptyDescriptors(descs []ocispec.Descriptor, hint int) []ocispec.Descriptor {
|
||||
j := 0
|
||||
for i, r := range descs {
|
||||
if !content.Equal(r, ocispec.Descriptor{}) {
|
||||
if i > j {
|
||||
descs[j] = r
|
||||
}
|
||||
j++
|
||||
}
|
||||
if j == hint {
|
||||
break
|
||||
}
|
||||
}
|
||||
return descs[:j]
|
||||
}
|
175
vendor/oras.land/oras-go/v2/registry/remote/registry.go
vendored
Normal file
175
vendor/oras.land/oras-go/v2/registry/remote/registry.go
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
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 remote provides a client to the remote registry.
|
||||
// Reference: https://github.com/distribution/distribution
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
"oras.land/oras-go/v2/registry/remote/internal/errutil"
|
||||
)
|
||||
|
||||
// RepositoryOptions is an alias of Repository to avoid name conflicts.
|
||||
// It also hides all methods associated with Repository.
|
||||
type RepositoryOptions Repository
|
||||
|
||||
// Registry is an HTTP client to a remote registry.
|
||||
type Registry struct {
|
||||
// RepositoryOptions contains common options for Registry and Repository.
|
||||
// It is also used as a template for derived repositories.
|
||||
RepositoryOptions
|
||||
|
||||
// RepositoryListPageSize specifies the page size when invoking the catalog
|
||||
// API.
|
||||
// If zero, the page size is determined by the remote registry.
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#catalog
|
||||
RepositoryListPageSize int
|
||||
}
|
||||
|
||||
// NewRegistry creates a client to the remote registry with the specified domain
|
||||
// name.
|
||||
// Example: localhost:5000
|
||||
func NewRegistry(name string) (*Registry, error) {
|
||||
ref := registry.Reference{
|
||||
Registry: name,
|
||||
}
|
||||
if err := ref.ValidateRegistry(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Registry{
|
||||
RepositoryOptions: RepositoryOptions{
|
||||
Reference: ref,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// client returns an HTTP client used to access the remote registry.
|
||||
// A default HTTP client is return if the client is not configured.
|
||||
func (r *Registry) client() Client {
|
||||
if r.Client == nil {
|
||||
return auth.DefaultClient
|
||||
}
|
||||
return r.Client
|
||||
}
|
||||
|
||||
// Ping checks whether or not the registry implement Docker Registry API V2 or
|
||||
// OCI Distribution Specification.
|
||||
// Ping can be used to check authentication when an auth client is configured.
|
||||
//
|
||||
// References:
|
||||
// - https://docs.docker.com/registry/spec/api/#base
|
||||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#api
|
||||
func (r *Registry) Ping(ctx context.Context) error {
|
||||
url := buildRegistryBaseURL(r.PlainHTTP, r.Reference)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := r.client().Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusNotFound:
|
||||
return errdef.ErrNotFound
|
||||
default:
|
||||
return errutil.ParseErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
// Repositories lists the name of repositories available in the registry.
|
||||
// See also `RepositoryListPageSize`.
|
||||
//
|
||||
// If `last` is NOT empty, the entries in the response start after the
|
||||
// repo specified by `last`. Otherwise, the response starts from the top
|
||||
// of the Repositories list.
|
||||
//
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#catalog
|
||||
func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error {
|
||||
ctx = auth.AppendScopes(ctx, auth.ScopeRegistryCatalog)
|
||||
url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference)
|
||||
var err error
|
||||
for err == nil {
|
||||
url, err = r.repositories(ctx, last, fn, url)
|
||||
// clear `last` for subsequent pages
|
||||
last = ""
|
||||
}
|
||||
if err != errNoLink {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// repositories returns a single page of repository list with the next link.
|
||||
func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r.RepositoryListPageSize > 0 || last != "" {
|
||||
q := req.URL.Query()
|
||||
if r.RepositoryListPageSize > 0 {
|
||||
q.Set("n", strconv.Itoa(r.RepositoryListPageSize))
|
||||
}
|
||||
if last != "" {
|
||||
q.Set("last", last)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
resp, err := r.client().Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errutil.ParseErrorResponse(resp)
|
||||
}
|
||||
var page struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
lr := limitReader(resp.Body, r.MaxMetadataBytes)
|
||||
if err := json.NewDecoder(lr).Decode(&page); err != nil {
|
||||
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
|
||||
}
|
||||
if err := fn(page.Repositories); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return parseLink(resp)
|
||||
}
|
||||
|
||||
// Repository returns a repository reference by the given name.
|
||||
func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) {
|
||||
ref := registry.Reference{
|
||||
Registry: r.Reference.Registry,
|
||||
Repository: name,
|
||||
}
|
||||
return newRepositoryWithOptions(ref, &r.RepositoryOptions)
|
||||
}
|
1443
vendor/oras.land/oras-go/v2/registry/remote/repository.go
vendored
Normal file
1443
vendor/oras.land/oras-go/v2/registry/remote/repository.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
114
vendor/oras.land/oras-go/v2/registry/remote/retry/client.go
vendored
Normal file
114
vendor/oras.land/oras-go/v2/registry/remote/retry/client.go
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
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 retry
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultClient is a client with the default retry policy.
|
||||
var DefaultClient = NewClient()
|
||||
|
||||
// NewClient creates an HTTP client with the default retry policy.
|
||||
func NewClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: NewTransport(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// Transport is an HTTP transport with retry policy.
|
||||
type Transport struct {
|
||||
// Base is the underlying HTTP transport to use.
|
||||
// If nil, http.DefaultTransport is used for round trips.
|
||||
Base http.RoundTripper
|
||||
|
||||
// Policy returns a retry Policy to use for the request.
|
||||
// If nil, DefaultPolicy is used to determine if the request should be retried.
|
||||
Policy func() Policy
|
||||
}
|
||||
|
||||
// NewTransport creates an HTTP Transport with the default retry policy.
|
||||
func NewTransport(base http.RoundTripper) *Transport {
|
||||
return &Transport{
|
||||
Base: base,
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip executes a single HTTP transaction, returning a Response for the
|
||||
// provided Request.
|
||||
// It relies on the configured Policy to determine if the request should be
|
||||
// retried and to backoff.
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
ctx := req.Context()
|
||||
policy := t.policy()
|
||||
attempt := 0
|
||||
for {
|
||||
resp, respErr := t.roundTrip(req)
|
||||
duration, err := policy.Retry(attempt, resp, respErr)
|
||||
if err != nil {
|
||||
if respErr == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if duration < 0 {
|
||||
return resp, respErr
|
||||
}
|
||||
|
||||
// rewind the body if possible
|
||||
if req.Body != nil {
|
||||
if req.GetBody == nil {
|
||||
// body can't be rewound, so we can't retry
|
||||
return resp, respErr
|
||||
}
|
||||
body, err := req.GetBody()
|
||||
if err != nil {
|
||||
// failed to rewind the body, so we can't retry
|
||||
return resp, respErr
|
||||
}
|
||||
req.Body = body
|
||||
}
|
||||
|
||||
// close the response body if needed
|
||||
if respErr == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
timer := time.NewTimer(duration)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) roundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.Base == nil {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (t *Transport) policy() Policy {
|
||||
if t.Policy == nil {
|
||||
return DefaultPolicy
|
||||
}
|
||||
return t.Policy()
|
||||
}
|
154
vendor/oras.land/oras-go/v2/registry/remote/retry/policy.go
vendored
Normal file
154
vendor/oras.land/oras-go/v2/registry/remote/retry/policy.go
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
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 retry
|
||||
|
||||
import (
|
||||
"hash/maphash"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// headerRetryAfter is the header key for Retry-After.
|
||||
const headerRetryAfter = "Retry-After"
|
||||
|
||||
// DefaultPolicy is a policy with fine-tuned retry parameters.
|
||||
// It uses an exponential backoff with jitter.
|
||||
var DefaultPolicy Policy = &GenericPolicy{
|
||||
Retryable: DefaultPredicate,
|
||||
Backoff: DefaultBackoff,
|
||||
MinWait: 200 * time.Millisecond,
|
||||
MaxWait: 3 * time.Second,
|
||||
MaxRetry: 5,
|
||||
}
|
||||
|
||||
// DefaultPredicate is a predicate that retries on 5xx errors, 429 Too Many
|
||||
// Requests, 408 Request Timeout and on network dial timeout.
|
||||
var DefaultPredicate Predicate = func(resp *http.Response, err error) (bool, error) {
|
||||
if err != nil {
|
||||
// retry on Dial timeout
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooManyRequests {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == 0 || resp.StatusCode >= 500 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DefaultBackoff is a backoff that uses an exponential backoff with jitter.
|
||||
// It uses a base of 250ms, a factor of 2 and a jitter of 10%.
|
||||
var DefaultBackoff Backoff = ExponentialBackoff(250*time.Millisecond, 2, 0.1)
|
||||
|
||||
// Policy is a retry policy.
|
||||
type Policy interface {
|
||||
// Retry returns the duration to wait before retrying the request.
|
||||
// It returns a negative value if the request should not be retried.
|
||||
// The attempt is used to:
|
||||
// - calculate the backoff duration, the default backoff is an exponential backoff.
|
||||
// - determine if the request should be retried.
|
||||
// The attempt starts at 0 and should be less than MaxRetry for the request to
|
||||
// be retried.
|
||||
Retry(attempt int, resp *http.Response, err error) (time.Duration, error)
|
||||
}
|
||||
|
||||
// Predicate is a function that returns true if the request should be retried.
|
||||
type Predicate func(resp *http.Response, err error) (bool, error)
|
||||
|
||||
// Backoff is a function that returns the duration to wait before retrying the
|
||||
// request. The attempt, is the next attempt number. The response is the
|
||||
// response from the previous request.
|
||||
type Backoff func(attempt int, resp *http.Response) time.Duration
|
||||
|
||||
// ExponentialBackoff returns a Backoff that uses an exponential backoff with
|
||||
// jitter. The backoff is calculated as:
|
||||
//
|
||||
// temp = backoff * factor ^ attempt
|
||||
// interval = temp * (1 - jitter) + rand.Int63n(2 * jitter * temp)
|
||||
//
|
||||
// The HTTP response is checked for a Retry-After header. If it is present, the
|
||||
// value is used as the backoff duration.
|
||||
func ExponentialBackoff(backoff time.Duration, factor, jitter float64) Backoff {
|
||||
return func(attempt int, resp *http.Response) time.Duration {
|
||||
var h maphash.Hash
|
||||
h.SetSeed(maphash.MakeSeed())
|
||||
rand := rand.New(rand.NewSource(int64(h.Sum64())))
|
||||
|
||||
// check Retry-After
|
||||
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
|
||||
if v := resp.Header.Get(headerRetryAfter); v != "" {
|
||||
if retryAfter, _ := strconv.ParseInt(v, 10, 64); retryAfter > 0 {
|
||||
return time.Duration(retryAfter) * time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do exponential backoff with jitter
|
||||
temp := float64(backoff) * math.Pow(factor, float64(attempt))
|
||||
return time.Duration(temp*(1-jitter)) + time.Duration(rand.Int63n(int64(2*jitter*temp)))
|
||||
}
|
||||
}
|
||||
|
||||
// GenericPolicy is a generic retry policy.
|
||||
type GenericPolicy struct {
|
||||
// Retryable is a predicate that returns true if the request should be
|
||||
// retried.
|
||||
Retryable Predicate
|
||||
|
||||
// Backoff is a function that returns the duration to wait before retrying.
|
||||
Backoff Backoff
|
||||
|
||||
// MinWait is the minimum duration to wait before retrying.
|
||||
MinWait time.Duration
|
||||
|
||||
// MaxWait is the maximum duration to wait before retrying.
|
||||
MaxWait time.Duration
|
||||
|
||||
// MaxRetry is the maximum number of retries.
|
||||
MaxRetry int
|
||||
}
|
||||
|
||||
// Retry returns the duration to wait before retrying the request.
|
||||
// It returns -1 if the request should not be retried.
|
||||
func (p *GenericPolicy) Retry(attempt int, resp *http.Response, err error) (time.Duration, error) {
|
||||
if attempt >= p.MaxRetry {
|
||||
return -1, nil
|
||||
}
|
||||
if ok, err := p.Retryable(resp, err); err != nil {
|
||||
return -1, err
|
||||
} else if !ok {
|
||||
return -1, nil
|
||||
}
|
||||
backoff := p.Backoff(attempt, resp)
|
||||
if backoff < p.MinWait {
|
||||
backoff = p.MinWait
|
||||
}
|
||||
if backoff > p.MaxWait {
|
||||
backoff = p.MaxWait
|
||||
}
|
||||
return backoff, nil
|
||||
}
|
107
vendor/oras.land/oras-go/v2/registry/remote/url.go
vendored
Normal file
107
vendor/oras.land/oras-go/v2/registry/remote/url.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
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 remote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"oras.land/oras-go/v2/registry"
|
||||
)
|
||||
|
||||
// buildScheme returns HTTP scheme used to access the remote registry.
|
||||
func buildScheme(plainHTTP bool) string {
|
||||
if plainHTTP {
|
||||
return "http"
|
||||
}
|
||||
return "https"
|
||||
}
|
||||
|
||||
// buildRegistryBaseURL builds the URL for accessing the base API.
|
||||
// Format: <scheme>://<registry>/v2/
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#base
|
||||
func buildRegistryBaseURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return fmt.Sprintf("%s://%s/v2/", buildScheme(plainHTTP), ref.Host())
|
||||
}
|
||||
|
||||
// buildRegistryCatalogURL builds the URL for accessing the catalog API.
|
||||
// Format: <scheme>://<registry>/v2/_catalog
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#catalog
|
||||
func buildRegistryCatalogURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return fmt.Sprintf("%s://%s/v2/_catalog", buildScheme(plainHTTP), ref.Host())
|
||||
}
|
||||
|
||||
// buildRepositoryBaseURL builds the base endpoint of the remote repository.
|
||||
// Format: <scheme>://<registry>/v2/<repository>
|
||||
func buildRepositoryBaseURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return fmt.Sprintf("%s://%s/v2/%s", buildScheme(plainHTTP), ref.Host(), ref.Repository)
|
||||
}
|
||||
|
||||
// buildRepositoryTagListURL builds the URL for accessing the tag list API.
|
||||
// Format: <scheme>://<registry>/v2/<repository>/tags/list
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#tags
|
||||
func buildRepositoryTagListURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return buildRepositoryBaseURL(plainHTTP, ref) + "/tags/list"
|
||||
}
|
||||
|
||||
// buildRepositoryManifestURL builds the URL for accessing the manifest API.
|
||||
// Format: <scheme>://<registry>/v2/<repository>/manifests/<digest_or_tag>
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#manifest
|
||||
func buildRepositoryManifestURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return strings.Join([]string{
|
||||
buildRepositoryBaseURL(plainHTTP, ref),
|
||||
"manifests",
|
||||
ref.Reference,
|
||||
}, "/")
|
||||
}
|
||||
|
||||
// buildRepositoryBlobURL builds the URL for accessing the blob API.
|
||||
// Format: <scheme>://<registry>/v2/<repository>/blobs/<digest>
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#blob
|
||||
func buildRepositoryBlobURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return strings.Join([]string{
|
||||
buildRepositoryBaseURL(plainHTTP, ref),
|
||||
"blobs",
|
||||
ref.Reference,
|
||||
}, "/")
|
||||
}
|
||||
|
||||
// buildRepositoryBlobUploadURL builds the URL for blob uploading.
|
||||
// Format: <scheme>://<registry>/v2/<repository>/blobs/uploads/
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#initiate-blob-upload
|
||||
func buildRepositoryBlobUploadURL(plainHTTP bool, ref registry.Reference) string {
|
||||
return buildRepositoryBaseURL(plainHTTP, ref) + "/blobs/uploads/"
|
||||
}
|
||||
|
||||
// buildReferrersURL builds the URL for querying the Referrers API.
|
||||
// Format: <scheme>://<registry>/v2/<repository>/referrers/<digest>?artifactType=<artifactType>
|
||||
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
|
||||
func buildReferrersURL(plainHTTP bool, ref registry.Reference, artifactType string) string {
|
||||
var query string
|
||||
if artifactType != "" {
|
||||
v := url.Values{}
|
||||
v.Set("artifactType", artifactType)
|
||||
query = "?" + v.Encode()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s/referrers/%s%s",
|
||||
buildRepositoryBaseURL(plainHTTP, ref),
|
||||
ref.Reference,
|
||||
query,
|
||||
)
|
||||
}
|
94
vendor/oras.land/oras-go/v2/registry/remote/utils.go
vendored
Normal file
94
vendor/oras.land/oras-go/v2/registry/remote/utils.go
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 remote
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/errdef"
|
||||
)
|
||||
|
||||
// defaultMaxMetadataBytes specifies the default limit on how many response
|
||||
// bytes are allowed in the server's response to the metadata APIs.
|
||||
// See also: Repository.MaxMetadataBytes
|
||||
var defaultMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB
|
||||
|
||||
// errNoLink is returned by parseLink() when no Link header is present.
|
||||
var errNoLink = errors.New("no Link header in response")
|
||||
|
||||
// parseLink returns the URL of the response's "Link" header, if present.
|
||||
func parseLink(resp *http.Response) (string, error) {
|
||||
link := resp.Header.Get("Link")
|
||||
if link == "" {
|
||||
return "", errNoLink
|
||||
}
|
||||
if link[0] != '<' {
|
||||
return "", fmt.Errorf("invalid next link %q: missing '<'", link)
|
||||
}
|
||||
if i := strings.IndexByte(link, '>'); i == -1 {
|
||||
return "", fmt.Errorf("invalid next link %q: missing '>'", link)
|
||||
} else {
|
||||
link = link[1:i]
|
||||
}
|
||||
|
||||
linkURL, err := resp.Request.URL.Parse(link)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return linkURL.String(), nil
|
||||
}
|
||||
|
||||
// limitReader returns a Reader that reads from r but stops with EOF after n
|
||||
// bytes. If n is less than or equal to zero, defaultMaxMetadataBytes is used.
|
||||
func limitReader(r io.Reader, n int64) io.Reader {
|
||||
if n <= 0 {
|
||||
n = defaultMaxMetadataBytes
|
||||
}
|
||||
return io.LimitReader(r, n)
|
||||
}
|
||||
|
||||
// limitSize returns ErrSizeExceedsLimit if the size of desc exceeds the limit n.
|
||||
// If n is less than or equal to zero, defaultMaxMetadataBytes is used.
|
||||
func limitSize(desc ocispec.Descriptor, n int64) error {
|
||||
if n <= 0 {
|
||||
n = defaultMaxMetadataBytes
|
||||
}
|
||||
if desc.Size > n {
|
||||
return fmt.Errorf(
|
||||
"content size %v exceeds MaxMetadataBytes %v: %w",
|
||||
desc.Size,
|
||||
n,
|
||||
errdef.ErrSizeExceedsLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeJSON safely reads the JSON content described by desc, and
|
||||
// decodes it into v.
|
||||
func decodeJSON(r io.Reader, desc ocispec.Descriptor, v any) error {
|
||||
jsonBytes, err := content.ReadAll(r, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(jsonBytes, v)
|
||||
}
|
120
vendor/oras.land/oras-go/v2/registry/repository.go
vendored
Normal file
120
vendor/oras.land/oras-go/v2/registry/repository.go
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
)
|
||||
|
||||
// Repository is an ORAS target and an union of the blob and the manifest CASs.
|
||||
//
|
||||
// As specified by https://docs.docker.com/registry/spec/api/, it is natural to
|
||||
// assume that content.Resolver interface only works for manifests. Tagging a
|
||||
// blob may be resulted in an `ErrUnsupported` error. However, this interface
|
||||
// does not restrict tagging blobs.
|
||||
//
|
||||
// Since a repository is an union of the blob and the manifest CASs, all
|
||||
// operations defined in the `BlobStore` are executed depending on the media
|
||||
// type of the given descriptor accordingly.
|
||||
//
|
||||
// Furthermore, this interface also provides the ability to enforce the
|
||||
// separation of the blob and the manifests CASs.
|
||||
type Repository interface {
|
||||
content.Storage
|
||||
content.Deleter
|
||||
content.TagResolver
|
||||
ReferenceFetcher
|
||||
ReferencePusher
|
||||
ReferrerLister
|
||||
TagLister
|
||||
|
||||
// Blobs provides access to the blob CAS only, which contains config blobs,
|
||||
// layers, and other generic blobs.
|
||||
Blobs() BlobStore
|
||||
|
||||
// Manifests provides access to the manifest CAS only.
|
||||
Manifests() ManifestStore
|
||||
}
|
||||
|
||||
// BlobStore is a CAS with the ability to stat and delete its content.
|
||||
type BlobStore interface {
|
||||
content.Storage
|
||||
content.Deleter
|
||||
content.Resolver
|
||||
ReferenceFetcher
|
||||
}
|
||||
|
||||
// ManifestStore is a CAS with the ability to stat and delete its content.
|
||||
// Besides, ManifestStore provides reference tagging.
|
||||
type ManifestStore interface {
|
||||
BlobStore
|
||||
content.Tagger
|
||||
ReferencePusher
|
||||
}
|
||||
|
||||
// ReferencePusher provides advanced push with the tag service.
|
||||
type ReferencePusher interface {
|
||||
// PushReference pushes the manifest with a reference tag.
|
||||
PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error
|
||||
}
|
||||
|
||||
// ReferenceFetcher provides advanced fetch with the tag service.
|
||||
type ReferenceFetcher interface {
|
||||
// FetchReference fetches the content identified by the reference.
|
||||
FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// ReferrerLister provides the Referrers API.
|
||||
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
|
||||
type ReferrerLister interface {
|
||||
Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error
|
||||
}
|
||||
|
||||
// TagLister lists tags by the tag service.
|
||||
type TagLister interface {
|
||||
// Tags lists the tags available in the repository.
|
||||
// Since the returned tag list may be paginated by the underlying
|
||||
// implementation, a function should be passed in to process the paginated
|
||||
// tag list.
|
||||
// `last` argument is the `last` parameter when invoking the tags API.
|
||||
// If `last` is NOT empty, the entries in the response start after the
|
||||
// tag specified by `last`. Otherwise, the response starts from the top
|
||||
// of the Tags list.
|
||||
// Note: When implemented by a remote registry, the tags API is called.
|
||||
// However, not all registries supports pagination or conforms the
|
||||
// specification.
|
||||
// References:
|
||||
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/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
|
||||
}
|
||||
|
||||
// Tags lists the tags available in the repository.
|
||||
func Tags(ctx context.Context, repo TagLister) ([]string, error) {
|
||||
var res []string
|
||||
if err := repo.Tags(ctx, "", func(tags []string) error {
|
||||
res = append(res, tags...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
Reference in New Issue
Block a user