feat: load model from oci image

This commit is contained in:
2023-05-05 17:07:29 +02:00
parent b57698380e
commit 9fb01f7be9
441 changed files with 61395 additions and 15356 deletions

View 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. BackusNaur 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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]
}

View 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
}

View 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
}

View 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, ", ")
}

View 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]
}

View 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)
}

File diff suppressed because it is too large Load Diff

View 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()
}

View 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
}

View 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,
)
}

View 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)
}

View 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
}