Upgrade k8s package verison (#5358)
* upgrade k8s package version Signed-off-by: hongzhouzi <hongzhouzi@kubesphere.io> * Script upgrade and code formatting. Signed-off-by: hongzhouzi <hongzhouzi@kubesphere.io> Signed-off-by: hongzhouzi <hongzhouzi@kubesphere.io>
This commit is contained in:
158
vendor/oras.land/oras-go/pkg/registry/remote/auth/cache.go
vendored
Normal file
158
vendor/oras.land/oras-go/pkg/registry/remote/auth/cache.go
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
errdef "oras.land/oras-go/pkg/content"
|
||||
"oras.land/oras-go/pkg/registry/remote/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)
|
||||
}
|
||||
166
vendor/oras.land/oras-go/pkg/registry/remote/auth/challenge.go
vendored
Normal file
166
vendor/oras.land/oras-go/pkg/registry/remote/auth/challenge.go
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
367
vendor/oras.land/oras-go/pkg/registry/remote/auth/client.go
vendored
Normal file
367
vendor/oras.land/oras-go/pkg/registry/remote/auth/client.go
vendored
Normal file
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
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"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"oras.land/oras-go/pkg/registry/remote/internal/errutil"
|
||||
)
|
||||
|
||||
// DefaultClient is the default auth-decorated client.
|
||||
var DefaultClient = &Client{
|
||||
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"
|
||||
|
||||
// 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.
|
||||
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 with resolving authentication
|
||||
// attempted.
|
||||
// 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) {
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
39
vendor/oras.land/oras-go/pkg/registry/remote/auth/credential.go
vendored
Normal file
39
vendor/oras.land/oras-go/pkg/registry/remote/auth/credential.go
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
231
vendor/oras.land/oras-go/pkg/registry/remote/auth/scope.go
vendored
Normal file
231
vendor/oras.land/oras-go/pkg/registry/remote/auth/scope.go
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
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]
|
||||
}
|
||||
83
vendor/oras.land/oras-go/pkg/registry/remote/internal/errutil/errors.go
vendored
Normal file
83
vendor/oras.land/oras-go/pkg/registry/remote/internal/errutil/errors.go
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// 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.
|
||||
var maxErrorBytes int64 = 8 * 1024 // 8 KiB
|
||||
|
||||
// requestError contains a single error.
|
||||
type requestError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error returns a error string describing the error.
|
||||
func (e requestError) Error() string {
|
||||
code := strings.Map(func(r rune) rune {
|
||||
if r == '_' {
|
||||
return ' '
|
||||
}
|
||||
return unicode.ToLower(r)
|
||||
}, e.Code)
|
||||
if e.Message == "" {
|
||||
return code
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", code, e.Message)
|
||||
}
|
||||
|
||||
// requestErrors is a bundle of requestError.
|
||||
type requestErrors []requestError
|
||||
|
||||
// Error returns a error string describing the error.
|
||||
func (errs requestErrors) 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, "; ")
|
||||
}
|
||||
|
||||
// ParseErrorResponse parses the error returned by the remote registry.
|
||||
func ParseErrorResponse(resp *http.Response) error {
|
||||
var errmsg string
|
||||
var body struct {
|
||||
Errors requestErrors `json:"errors"`
|
||||
}
|
||||
lr := io.LimitReader(resp.Body, maxErrorBytes)
|
||||
if err := json.NewDecoder(lr).Decode(&body); err == nil && len(body.Errors) > 0 {
|
||||
errmsg = body.Errors.Error()
|
||||
} else {
|
||||
errmsg = http.StatusText(resp.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("%s %q: unexpected status code %d: %s", resp.Request.Method, resp.Request.URL, resp.StatusCode, errmsg)
|
||||
}
|
||||
69
vendor/oras.land/oras-go/pkg/registry/remote/internal/syncutil/once.go
vendored
Normal file
69
vendor/oras.land/oras-go/pkg/registry/remote/internal/syncutil/once.go
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 syncutil
|
||||
|
||||
import "context"
|
||||
|
||||
// Once is an object that will perform exactly one action.
|
||||
// Unlike sync.Once, this Once allowes the action to have return values.
|
||||
type Once struct {
|
||||
result interface{}
|
||||
err error
|
||||
status chan bool
|
||||
}
|
||||
|
||||
// NewOnce creates a new Once instance.
|
||||
func NewOnce() *Once {
|
||||
status := make(chan bool, 1)
|
||||
status <- true
|
||||
return &Once{
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// Do calls the function f if and only if Do is being called first time or all
|
||||
// previous function calls are cancelled, deadline exceeded, or panicking.
|
||||
// When `once.Do(ctx, f)` is called multiple times, the return value of the
|
||||
// first call of the function f is stored, and is directly returned for other
|
||||
// calls.
|
||||
// Besides the return value of the function f, including the error, Do returns
|
||||
// true if the function f passed is called first and is not cancelled, deadline
|
||||
// exceeded, or panicking. Otherwise, returns false.
|
||||
func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
o.status <- true
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case inProgress := <-o.status:
|
||||
if !inProgress {
|
||||
return false, o.result, o.err
|
||||
}
|
||||
result, err := f()
|
||||
if err == context.Canceled || err == context.DeadlineExceeded {
|
||||
o.status <- true
|
||||
return false, nil, err
|
||||
}
|
||||
o.result, o.err = result, err
|
||||
close(o.status)
|
||||
return true, result, err
|
||||
case <-ctx.Done():
|
||||
return false, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
171
vendor/oras.land/oras-go/pkg/registry/remote/repository.go
vendored
Normal file
171
vendor/oras.land/oras-go/pkg/registry/remote/repository.go
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
errdef "oras.land/oras-go/pkg/content"
|
||||
"oras.land/oras-go/pkg/registry"
|
||||
"oras.land/oras-go/pkg/registry/remote/auth"
|
||||
"oras.land/oras-go/pkg/registry/remote/internal/errutil"
|
||||
)
|
||||
|
||||
// Client is an interface for a HTTP client.
|
||||
type Client interface {
|
||||
// Do sends an HTTP request and returns an HTTP response.
|
||||
//
|
||||
// Unlike http.RoundTripper, Client can attempt to interpret the response
|
||||
// and handle higher-level protocol details such as redirects and
|
||||
// authentication.
|
||||
//
|
||||
// Like http.RoundTripper, Client should not modify the request, and must
|
||||
// always close the request body.
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// Repository is an HTTP client to a remote repository.
|
||||
type Repository struct {
|
||||
// Client is the underlying HTTP client used to access the remote registry.
|
||||
// If nil, auth.DefaultClient is used.
|
||||
Client Client
|
||||
|
||||
// Reference references the remote repository.
|
||||
Reference registry.Reference
|
||||
|
||||
// PlainHTTP signals the transport to access the remote repository via HTTP
|
||||
// instead of HTTPS.
|
||||
PlainHTTP bool
|
||||
|
||||
// ManifestMediaTypes is used in `Accept` header for resolving manifests from
|
||||
// references. It is also used in identifying manifests and blobs from
|
||||
// descriptors.
|
||||
// If an empty list is present, default manifest media types are used.
|
||||
ManifestMediaTypes []string
|
||||
|
||||
// TagListPageSize specifies the page size when invoking the tag list API.
|
||||
// If zero, the page size is determined by the remote registry.
|
||||
// Reference: https://docs.docker.com/registry/spec/api/#tags
|
||||
TagListPageSize int
|
||||
|
||||
// ReferrerListPageSize specifies the page size when invoking the Referrers
|
||||
// API.
|
||||
// If zero, the page size is determined by the remote registry.
|
||||
// Reference: https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md
|
||||
ReferrerListPageSize int
|
||||
|
||||
// MaxMetadataBytes specifies a limit on how many response bytes are allowed
|
||||
// in the server's response to the metadata APIs, such as catalog list, tag
|
||||
// list, and referrers list.
|
||||
// If zero, a default (currently 4MiB) is used.
|
||||
MaxMetadataBytes int64
|
||||
}
|
||||
|
||||
// NewRepository creates a client to the remote repository identified by a
|
||||
// reference.
|
||||
// Example: localhost:5000/hello-world
|
||||
func NewRepository(reference string) (*Repository, error) {
|
||||
ref, err := registry.ParseReference(reference)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Repository{
|
||||
Reference: ref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// client returns an HTTP client used to access the remote repository.
|
||||
// A default HTTP client is return if the client is not configured.
|
||||
func (r *Repository) client() Client {
|
||||
if r.Client == nil {
|
||||
return auth.DefaultClient
|
||||
}
|
||||
return r.Client
|
||||
}
|
||||
|
||||
// parseReference validates the reference.
|
||||
// Both simplified or fully qualified references are accepted as input.
|
||||
// A fully qualified reference is returned on success.
|
||||
func (r *Repository) parseReference(reference string) (registry.Reference, error) {
|
||||
ref, err := registry.ParseReference(reference)
|
||||
if err != nil {
|
||||
ref = registry.Reference{
|
||||
Registry: r.Reference.Registry,
|
||||
Repository: r.Reference.Repository,
|
||||
Reference: reference,
|
||||
}
|
||||
if err = ref.ValidateReference(); err != nil {
|
||||
return registry.Reference{}, err
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
if ref.Registry == r.Reference.Registry && ref.Repository == r.Reference.Repository {
|
||||
return ref, nil
|
||||
}
|
||||
return registry.Reference{}, fmt.Errorf("%w %q: expect %q", errdef.ErrInvalidReference, ref, r.Reference)
|
||||
}
|
||||
|
||||
// Tags lists the tags available in the repository.
|
||||
func (r *Repository) Tags(ctx context.Context, fn func(tags []string) error) error {
|
||||
ctx = withScopeHint(ctx, r.Reference, auth.ActionPull)
|
||||
url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference)
|
||||
var err error
|
||||
for err == nil {
|
||||
url, err = r.tags(ctx, fn, url)
|
||||
}
|
||||
if err != errNoLink {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tags returns a single page of tag list with the next link.
|
||||
func (r *Repository) tags(ctx context.Context, fn func(tags []string) error, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r.TagListPageSize > 0 {
|
||||
q := req.URL.Query()
|
||||
q.Set("n", strconv.Itoa(r.TagListPageSize))
|
||||
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 {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
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.Tags); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return parseLink(resp)
|
||||
}
|
||||
42
vendor/oras.land/oras-go/pkg/registry/remote/url.go
vendored
Normal file
42
vendor/oras.land/oras-go/pkg/registry/remote/url.go
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"oras.land/oras-go/pkg/registry"
|
||||
)
|
||||
|
||||
// buildScheme returns HTTP scheme used to access the remote registry.
|
||||
func buildScheme(plainHTTP bool) string {
|
||||
if plainHTTP {
|
||||
return "http"
|
||||
}
|
||||
return "https"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
72
vendor/oras.land/oras-go/pkg/registry/remote/utils.go
vendored
Normal file
72
vendor/oras.land/oras-go/pkg/registry/remote/utils.go
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"oras.land/oras-go/pkg/registry"
|
||||
"oras.land/oras-go/pkg/registry/remote/auth"
|
||||
)
|
||||
|
||||
// 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 zero, defaultMaxMetadataBytes is used.
|
||||
func limitReader(r io.Reader, n int64) io.Reader {
|
||||
if n == 0 {
|
||||
n = defaultMaxMetadataBytes
|
||||
}
|
||||
return io.LimitReader(r, n)
|
||||
}
|
||||
|
||||
// withScopeHint adds a hinted scope to the context.
|
||||
func withScopeHint(ctx context.Context, ref registry.Reference, actions ...string) context.Context {
|
||||
scope := auth.ScopeRepository(ref.Repository, actions...)
|
||||
return auth.AppendScopes(ctx, scope)
|
||||
}
|
||||
Reference in New Issue
Block a user