Merge pull request #6207 from thaJeztah/fork_registry
add internal fork of docker/docker/registry
This commit is contained in:
@ -16,8 +16,8 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/moby/moby/api/types/auxprogress"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@ -15,8 +15,8 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
@ -288,7 +288,7 @@ func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*regis
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, token, err := svc.Auth(ctx, &auth, command.UserAgent())
|
||||
token, err := svc.Auth(ctx, &auth, command.UserAgent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -13,8 +13,8 @@ import (
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@ -19,8 +19,8 @@ import (
|
||||
"github.com/docker/cli/cli/debug"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
|
||||
@ -78,7 +78,6 @@ var sampleInfoNoSwarm = system.Info{
|
||||
IndexConfigs: map[string]*registrytypes.IndexInfo{
|
||||
"docker.io": {
|
||||
Name: "docker.io",
|
||||
Mirrors: nil,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/registry"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -54,7 +56,7 @@ func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEn
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoName))
|
||||
endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoName))
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
@ -97,7 +99,7 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
|
||||
if len(actions) == 0 {
|
||||
actions = []string{"pull"}
|
||||
}
|
||||
creds := registry.NewStaticCredentialStore(&authConfig)
|
||||
creds := &staticCredentialStore{authConfig: &authConfig}
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
@ -117,3 +119,23 @@ func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, _ map[string
|
||||
func (*existingTokenHandler) Scheme() string {
|
||||
return "bearer"
|
||||
}
|
||||
|
||||
type staticCredentialStore struct {
|
||||
authConfig *registrytypes.AuthConfig
|
||||
}
|
||||
|
||||
func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
|
||||
if scs.authConfig == nil {
|
||||
return "", ""
|
||||
}
|
||||
return scs.authConfig.Username, scs.authConfig.Password
|
||||
}
|
||||
|
||||
func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
|
||||
if scs.authConfig == nil {
|
||||
return ""
|
||||
}
|
||||
return scs.authConfig.IdentityToken
|
||||
}
|
||||
|
||||
func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/ocischema"
|
||||
@ -13,7 +14,6 @@ import (
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
v2 "github.com/docker/distribution/registry/api/v2"
|
||||
distclient "github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
@ -283,10 +283,10 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi
|
||||
}
|
||||
registryService, err := registry.NewService(serviceOpts)
|
||||
if err != nil {
|
||||
return []registry.APIEndpoint{}, err
|
||||
return nil, err
|
||||
}
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
|
||||
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
|
||||
endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoInfo.Name))
|
||||
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
|
||||
return endpoints, err
|
||||
}
|
||||
|
||||
@ -14,10 +14,10 @@ import (
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/moby/moby/api/types"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
@ -14,8 +14,8 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/oauth"
|
||||
"github.com/docker/cli/internal/oauth/api"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@ -12,7 +13,6 @@ import (
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// AuthClientID is used the ClientID used for the token server
|
||||
@ -34,35 +34,6 @@ func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token strin
|
||||
lcs.authConfig.IdentityToken = token
|
||||
}
|
||||
|
||||
type staticCredentialStore struct {
|
||||
auth *registry.AuthConfig
|
||||
}
|
||||
|
||||
// NewStaticCredentialStore returns a credential store
|
||||
// which always returns the same credential values.
|
||||
func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
|
||||
return staticCredentialStore{
|
||||
auth: ac,
|
||||
}
|
||||
}
|
||||
|
||||
func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
|
||||
if scs.auth == nil {
|
||||
return "", ""
|
||||
}
|
||||
return scs.auth.Username, scs.auth.Password
|
||||
}
|
||||
|
||||
func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
|
||||
if scs.auth == nil {
|
||||
return ""
|
||||
}
|
||||
return scs.auth.IdentityToken
|
||||
}
|
||||
|
||||
func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
||||
}
|
||||
|
||||
// loginV2 tries to login to the v2 registry server. The given registry
|
||||
// endpoint will be pinged to get authorization challenges. These challenges
|
||||
// will be used to authenticate against the registry to validate credentials.
|
||||
@ -96,7 +67,7 @@ func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIE
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
|
||||
return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
return "", fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return credentialAuthConfig.IdentityToken, nil
|
||||
@ -127,67 +98,19 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConvertToHostname normalizes a registry URL which has http|https prepended
|
||||
// to just its hostname. It is used to match credentials, which may be either
|
||||
// stored as hostname or as hostname including scheme (in legacy configuration
|
||||
// files).
|
||||
func ConvertToHostname(maybeURL string) string {
|
||||
stripped := maybeURL
|
||||
if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
|
||||
switch scheme {
|
||||
case "http", "https":
|
||||
stripped = remainder
|
||||
default:
|
||||
// unknown, or no scheme; doing nothing for now, as we never did.
|
||||
}
|
||||
}
|
||||
stripped, _, _ = strings.Cut(stripped, "/")
|
||||
return stripped
|
||||
}
|
||||
|
||||
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
||||
func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
|
||||
configKey := GetAuthConfigKey(index)
|
||||
// First try the happy case
|
||||
if c, found := authConfigs[configKey]; found || index.Official {
|
||||
return c
|
||||
}
|
||||
|
||||
// Maybe they have a legacy config file, we will iterate the keys converting
|
||||
// them to the new format and testing
|
||||
for registryURL, ac := range authConfigs {
|
||||
if configKey == ConvertToHostname(registryURL) {
|
||||
return ac
|
||||
}
|
||||
}
|
||||
|
||||
// When all else fails, return an empty auth config
|
||||
return registry.AuthConfig{}
|
||||
}
|
||||
|
||||
// PingResponseError is used when the response from a ping
|
||||
// was received but invalid.
|
||||
type PingResponseError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (err PingResponseError) Error() string {
|
||||
return err.Err.Error()
|
||||
}
|
||||
|
||||
// PingV2Registry attempts to ping a v2 registry and on success return a
|
||||
// challenge manager for the supported authentication types.
|
||||
// If a response is received but cannot be interpreted, a PingResponseError will be returned.
|
||||
func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
|
||||
pingClient := &http.Client{
|
||||
Transport: authTransport,
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
|
||||
req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pingClient := &http.Client{
|
||||
Transport: authTransport,
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
resp, err := pingClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -196,9 +119,7 @@ func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challen
|
||||
|
||||
challengeManager := challenge.NewSimpleManager()
|
||||
if err := challengeManager.AddResponse(resp); err != nil {
|
||||
return nil, PingResponseError{
|
||||
Err: err,
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return challengeManager, nil
|
||||
@ -5,6 +5,7 @@ package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -21,13 +22,19 @@ import (
|
||||
)
|
||||
|
||||
// ServiceOptions holds command line options.
|
||||
//
|
||||
// TODO(thaJeztah): add CertsDir as option to replace the [CertsDir] function, which sets the location magically.
|
||||
type ServiceOptions struct {
|
||||
Mirrors []string `json:"registry-mirrors,omitempty"`
|
||||
InsecureRegistries []string `json:"insecure-registries,omitempty"`
|
||||
}
|
||||
|
||||
// serviceConfig holds daemon configuration for the registry service.
|
||||
type serviceConfig registry.ServiceConfig
|
||||
//
|
||||
// It's a reduced version of [registry.ServiceConfig] for the CLI.
|
||||
type serviceConfig struct {
|
||||
insecureRegistryCIDRs []*net.IPNet
|
||||
indexConfigs map[string]*registry.IndexInfo
|
||||
}
|
||||
|
||||
// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
|
||||
// are here for historic reasons and backward-compatibility. These domains
|
||||
@ -39,27 +46,22 @@ type serviceConfig registry.ServiceConfig
|
||||
const (
|
||||
// DefaultNamespace is the default namespace
|
||||
DefaultNamespace = "docker.io"
|
||||
// DefaultRegistryHost is the hostname for the default (Docker Hub) registry
|
||||
// used for pushing and pulling images. This hostname is hard-coded to handle
|
||||
// the conversion from image references without registry name (e.g. "ubuntu",
|
||||
// or "ubuntu:latest"), as well as references using the "docker.io" domain
|
||||
// name, which is used as canonical reference for images on Docker Hub, but
|
||||
// does not match the domain-name of Docker Hub's registry.
|
||||
DefaultRegistryHost = "registry-1.docker.io"
|
||||
// IndexHostname is the index hostname, used for authentication and image search.
|
||||
IndexHostname = "index.docker.io"
|
||||
// IndexServer is used for user auth and image search
|
||||
IndexServer = "https://" + IndexHostname + "/v1/"
|
||||
IndexServer = "https://index.docker.io/v1/"
|
||||
// IndexName is the name of the index
|
||||
IndexName = "docker.io"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultV2Registry is the URI of the default (Docker Hub) registry.
|
||||
DefaultV2Registry = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: DefaultRegistryHost,
|
||||
}
|
||||
// DefaultV2Registry is the URI of the default (Docker Hub) registry
|
||||
// used for pushing and pulling images. This hostname is hard-coded to handle
|
||||
// the conversion from image references without registry name (e.g. "ubuntu",
|
||||
// or "ubuntu:latest"), as well as references using the "docker.io" domain
|
||||
// name, which is used as canonical reference for images on Docker Hub, but
|
||||
// does not match the domain-name of Docker Hub's registry.
|
||||
DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"}
|
||||
|
||||
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
|
||||
@ -93,81 +95,22 @@ func CertsDir() string {
|
||||
return certsDir
|
||||
}
|
||||
|
||||
// newServiceConfig returns a new instance of ServiceConfig
|
||||
func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
||||
config := &serviceConfig{}
|
||||
if err := config.loadMirrors(options.Mirrors); err != nil {
|
||||
return nil, err
|
||||
// newServiceConfig creates a new service config with the given options.
|
||||
func newServiceConfig(registries []string) (*serviceConfig, error) {
|
||||
if len(registries) == 0 {
|
||||
return &serviceConfig{}, nil
|
||||
}
|
||||
if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// copy constructs a new ServiceConfig with a copy of the configuration in config.
|
||||
func (config *serviceConfig) copy() *registry.ServiceConfig {
|
||||
ic := make(map[string]*registry.IndexInfo)
|
||||
for key, value := range config.IndexConfigs {
|
||||
ic[key] = value
|
||||
}
|
||||
return ®istry.ServiceConfig{
|
||||
InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
|
||||
IndexConfigs: ic,
|
||||
Mirrors: append([]string(nil), config.Mirrors...),
|
||||
}
|
||||
}
|
||||
|
||||
// loadMirrors loads mirrors to config, after removing duplicates.
|
||||
// Returns an error if mirrors contains an invalid mirror.
|
||||
func (config *serviceConfig) loadMirrors(mirrors []string) error {
|
||||
mMap := map[string]struct{}{}
|
||||
unique := []string{}
|
||||
|
||||
for _, mirror := range mirrors {
|
||||
m, err := ValidateMirror(mirror)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exist := mMap[m]; !exist {
|
||||
mMap[m] = struct{}{}
|
||||
unique = append(unique, m)
|
||||
}
|
||||
}
|
||||
|
||||
config.Mirrors = unique
|
||||
|
||||
// Configure public registry since mirrors may have changed.
|
||||
config.IndexConfigs = map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Mirrors: unique,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadInsecureRegistries loads insecure registries to config
|
||||
func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
|
||||
// Localhost is by default considered as an insecure registry. This is a
|
||||
// stop-gap for people who are running a private registry on localhost.
|
||||
registries = append(registries, "::1/128", "127.0.0.0/8")
|
||||
|
||||
var (
|
||||
insecureRegistryCIDRs = make([]*registry.NetIPNet, 0)
|
||||
insecureRegistryCIDRs = make([]*net.IPNet, 0)
|
||||
indexConfigs = make(map[string]*registry.IndexInfo)
|
||||
)
|
||||
|
||||
skip:
|
||||
for _, r := range registries {
|
||||
// validate insecure registry
|
||||
if _, err := ValidateIndexName(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if scheme, host, ok := strings.Cut(r, "://"); ok {
|
||||
switch strings.ToLower(scheme) {
|
||||
case "http", "https":
|
||||
@ -175,29 +118,27 @@ skip:
|
||||
r = host
|
||||
default:
|
||||
// unsupported scheme
|
||||
return invalidParamf("insecure registry %s should not contain '://'", r)
|
||||
return nil, invalidParam(fmt.Errorf("insecure registry %s should not contain '://'", r))
|
||||
}
|
||||
}
|
||||
// Check if CIDR was passed to --insecure-registry
|
||||
_, ipnet, err := net.ParseCIDR(r)
|
||||
if err == nil {
|
||||
// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
|
||||
data := (*registry.NetIPNet)(ipnet)
|
||||
for _, value := range insecureRegistryCIDRs {
|
||||
if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
|
||||
if value.IP.String() == ipnet.IP.String() && value.Mask.String() == ipnet.Mask.String() {
|
||||
continue skip
|
||||
}
|
||||
}
|
||||
// ipnet is not found, add it in config.InsecureRegistryCIDRs
|
||||
insecureRegistryCIDRs = append(insecureRegistryCIDRs, data)
|
||||
insecureRegistryCIDRs = append(insecureRegistryCIDRs, ipnet)
|
||||
} else {
|
||||
if err := validateHostPort(r); err != nil {
|
||||
return invalidParamWrapf(err, "insecure registry %s is not valid", r)
|
||||
return nil, invalidParam(fmt.Errorf("insecure registry %s is not valid: %w", r, err))
|
||||
}
|
||||
// Assume `host:port` if not CIDR.
|
||||
indexConfigs[r] = ®istry.IndexInfo{
|
||||
Name: r,
|
||||
Mirrors: []string{},
|
||||
Secure: false,
|
||||
Official: false,
|
||||
}
|
||||
@ -207,14 +148,14 @@ skip:
|
||||
// Configure public registry.
|
||||
indexConfigs[IndexName] = ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: config.Mirrors,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
config.InsecureRegistryCIDRs = insecureRegistryCIDRs
|
||||
config.IndexConfigs = indexConfigs
|
||||
|
||||
return nil
|
||||
return &serviceConfig{
|
||||
indexConfigs: indexConfigs,
|
||||
insecureRegistryCIDRs: insecureRegistryCIDRs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
|
||||
@ -231,11 +172,11 @@ skip:
|
||||
func (config *serviceConfig) isSecureIndex(indexName string) bool {
|
||||
// Check for configured index, first. This is needed in case isSecureIndex
|
||||
// is called from anything besides newIndexInfo, in order to honor per-index configurations.
|
||||
if index, ok := config.IndexConfigs[indexName]; ok {
|
||||
if index, ok := config.indexConfigs[indexName]; ok {
|
||||
return index.Secure
|
||||
}
|
||||
|
||||
return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName)
|
||||
return !isCIDRMatch(config.insecureRegistryCIDRs, indexName)
|
||||
}
|
||||
|
||||
// for mocking in unit tests.
|
||||
@ -244,7 +185,7 @@ var lookupIP = net.LookupIP
|
||||
// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host")
|
||||
// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
|
||||
// resolved to IP addresses for matching. If resolution fails, false is returned.
|
||||
func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
|
||||
func isCIDRMatch(cidrs []*net.IPNet, urlHost string) bool {
|
||||
if len(cidrs) == 0 {
|
||||
return false
|
||||
}
|
||||
@ -271,7 +212,7 @@ func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
|
||||
for _, addr := range addresses {
|
||||
for _, ipnet := range cidrs {
|
||||
// check if the addr falls in the subnet
|
||||
if (*net.IPNet)(ipnet).Contains(addr) {
|
||||
if ipnet.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -280,58 +221,13 @@ func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It
|
||||
// returns an error if the given mirrorURL is invalid, or the normalized
|
||||
// format for the URL otherwise.
|
||||
//
|
||||
// It is used by the daemon to validate the daemon configuration.
|
||||
func ValidateMirror(mirrorURL string) (string, error) {
|
||||
// Fast path for missing scheme, as url.Parse splits by ":", which can
|
||||
// cause the hostname to be considered the "scheme" when using "hostname:port".
|
||||
if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" {
|
||||
return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL)
|
||||
}
|
||||
uri, err := url.Parse(mirrorURL)
|
||||
if err != nil {
|
||||
return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL)
|
||||
}
|
||||
if uri.Scheme != "http" && uri.Scheme != "https" {
|
||||
return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri)
|
||||
}
|
||||
if uri.RawQuery != "" || uri.Fragment != "" {
|
||||
return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri)
|
||||
}
|
||||
if uri.User != nil {
|
||||
// strip password from output
|
||||
uri.User = url.UserPassword(uri.User.Username(), "xxxxx")
|
||||
return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
|
||||
}
|
||||
return strings.TrimSuffix(mirrorURL, "/") + "/", nil
|
||||
}
|
||||
|
||||
// ValidateIndexName validates an index name. It is used by the daemon to
|
||||
// validate the daemon configuration.
|
||||
func ValidateIndexName(val string) (string, error) {
|
||||
val = normalizeIndexName(val)
|
||||
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
||||
return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func normalizeIndexName(val string) string {
|
||||
// TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/".
|
||||
// TODO: upstream this to check to reference package
|
||||
if val == "index.docker.io" {
|
||||
return "docker.io"
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func hasScheme(reposName string) bool {
|
||||
return strings.Contains(reposName, "://")
|
||||
}
|
||||
|
||||
func validateHostPort(s string) error {
|
||||
// Split host and port, and in case s can not be split, assume host only
|
||||
host, port, err := net.SplitHostPort(s)
|
||||
@ -356,32 +252,6 @@ func validateHostPort(s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newIndexInfo returns IndexInfo configuration from indexName
|
||||
func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
|
||||
indexName = normalizeIndexName(indexName)
|
||||
|
||||
// Return any configured index info, first.
|
||||
if index, ok := config.IndexConfigs[indexName]; ok {
|
||||
return index
|
||||
}
|
||||
|
||||
// Construct a non-configured index info.
|
||||
return ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Mirrors: []string{},
|
||||
Secure: config.isSecureIndex(indexName),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthConfigKey special-cases using the full index address of the official
|
||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||
func GetAuthConfigKey(index *registry.IndexInfo) string {
|
||||
if index.Official {
|
||||
return IndexServer
|
||||
}
|
||||
return index.Name
|
||||
}
|
||||
|
||||
// ParseRepositoryInfo performs the breakdown of a repository name into a
|
||||
// [RepositoryInfo], but lacks registry configuration.
|
||||
//
|
||||
@ -393,7 +263,6 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
||||
Name: reference.TrimNamed(reposName),
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
@ -403,9 +272,8 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
||||
return &RepositoryInfo{
|
||||
Name: reference.TrimNamed(reposName),
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Mirrors: []string{},
|
||||
Secure: !isInsecure(indexName),
|
||||
Name: indexName,
|
||||
Secure: !isInsecure(indexName),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
92
internal/registry/config_test.go
Normal file
92
internal/registry/config_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestLoadInsecureRegistries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
registries []string
|
||||
index string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
registries: []string{"127.0.0.1"},
|
||||
index: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
registries: []string{"127.0.0.1:8080"},
|
||||
index: "127.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
registries: []string{"2001:db8::1"},
|
||||
index: "2001:db8::1",
|
||||
},
|
||||
{
|
||||
registries: []string{"[2001:db8::1]:80"},
|
||||
index: "[2001:db8::1]:80",
|
||||
},
|
||||
{
|
||||
registries: []string{"http://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"https://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"HTTP://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"svn://myregistry.example.com"},
|
||||
err: "insecure registry svn://myregistry.example.com should not contain '://'",
|
||||
},
|
||||
{
|
||||
registries: []string{`mytest-.com`},
|
||||
err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
|
||||
err: `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`myregistry.example.com:500000`},
|
||||
err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`"myregistry.example.com"`},
|
||||
err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`,
|
||||
},
|
||||
{
|
||||
registries: []string{`"myregistry.example.com:5000"`},
|
||||
err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
config, err := newServiceConfig(testCase.registries)
|
||||
if testCase.err == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got '%s'", err)
|
||||
}
|
||||
match := false
|
||||
for index := range config.indexConfigs {
|
||||
if index == testCase.index {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.indexConfigs)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("expect error '%s', got no error", testCase.err)
|
||||
}
|
||||
assert.ErrorContains(t, err, testCase.err)
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
12
internal/registry/doc.go
Normal file
12
internal/registry/doc.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Package registry is a fork of [github.com/docker/docker/registry], taken
|
||||
// at commit [moby@49306c6]. Git history was not preserved in this fork,
|
||||
// but can be found using the URLs provided.
|
||||
//
|
||||
// This fork was created to remove the dependency on the "Moby" codebase,
|
||||
// and because the CLI only needs a subset of its features. The original
|
||||
// package was written specifically for use in the daemon code, and includes
|
||||
// functionality that cannot be used in the CLI.
|
||||
//
|
||||
// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
|
||||
// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry
|
||||
package registry
|
||||
@ -1,10 +1,14 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.23
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func translateV2AuthError(err error) error {
|
||||
@ -22,12 +26,8 @@ func invalidParam(err error) error {
|
||||
return invalidParameterErr{err}
|
||||
}
|
||||
|
||||
func invalidParamf(format string, args ...interface{}) error {
|
||||
return invalidParameterErr{errors.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
func invalidParamWrapf(err error, format string, args ...interface{}) error {
|
||||
return invalidParameterErr{errors.Wrapf(err, format, args...)}
|
||||
func invalidParamf(format string, args ...any) error {
|
||||
return invalidParameterErr{fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
type unauthorizedErr struct{ error }
|
||||
@ -49,19 +49,3 @@ func (invalidParameterErr) InvalidParameter() {}
|
||||
func (e invalidParameterErr) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
type systemErr struct{ error }
|
||||
|
||||
func (systemErr) System() {}
|
||||
|
||||
func (e systemErr) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
type errUnknown struct{ error }
|
||||
|
||||
func (errUnknown) Unknown() {}
|
||||
|
||||
func (e errUnknown) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
@ -4,6 +4,7 @@ package registry
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -82,7 +83,7 @@ func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config)
|
||||
if tlsConfig.RootCAs == nil {
|
||||
systemPool, err := tlsconfig.SystemCertPool()
|
||||
if err != nil {
|
||||
return invalidParamWrapf(err, "unable to get system cert pool")
|
||||
return invalidParam(fmt.Errorf("unable to get system cert pool: %w", err))
|
||||
}
|
||||
tlsConfig.RootCAs = systemPool
|
||||
}
|
||||
92
internal/registry/registry_mock_test.go
Normal file
92
internal/registry/registry_mock_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
var testHTTPServer *httptest.Server
|
||||
|
||||
func init() {
|
||||
r := http.NewServeMux()
|
||||
|
||||
// /v1/
|
||||
r.HandleFunc("/v1/_ping", handlerGetPing)
|
||||
r.HandleFunc("/v1/search", handlerSearch)
|
||||
|
||||
// /v2/
|
||||
r.HandleFunc("/v2/version", handlerGetPing)
|
||||
|
||||
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
||||
}
|
||||
|
||||
func handlerAccessLog(handler http.Handler) http.Handler {
|
||||
logHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL)
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(logHandler)
|
||||
}
|
||||
|
||||
func makeURL(req string) string {
|
||||
return testHTTPServer.URL + req
|
||||
}
|
||||
|
||||
func writeHeaders(w http.ResponseWriter) {
|
||||
h := w.Header()
|
||||
h.Add("Server", "docker-tests/mock")
|
||||
h.Add("Expires", "-1")
|
||||
h.Add("Content-Type", "application/json")
|
||||
h.Add("Pragma", "no-cache")
|
||||
h.Add("Cache-Control", "no-cache")
|
||||
}
|
||||
|
||||
func writeResponse(w http.ResponseWriter, message any, code int) {
|
||||
writeHeaders(w)
|
||||
w.WriteHeader(code)
|
||||
body, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
writeResponse(w, true, http.StatusOK)
|
||||
}
|
||||
|
||||
func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
result := ®istry.SearchResults{
|
||||
Query: "fakequery",
|
||||
NumResults: 1,
|
||||
Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
||||
}
|
||||
writeResponse(w, result, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
res, err := http.Get(makeURL("/v1/_ping"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, res.StatusCode, http.StatusOK, "")
|
||||
assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
281
internal/registry/registry_test.go
Normal file
281
internal/registry/registry_test.go
Normal file
@ -0,0 +1,281 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestParseRepositoryInfo(t *testing.T) {
|
||||
type staticRepositoryInfo struct {
|
||||
Index *registry.IndexInfo
|
||||
RemoteName string
|
||||
CanonicalName string
|
||||
LocalName string
|
||||
}
|
||||
|
||||
tests := map[string]staticRepositoryInfo{
|
||||
"fooo/bar": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "fooo/bar",
|
||||
LocalName: "fooo/bar",
|
||||
CanonicalName: "docker.io/fooo/bar",
|
||||
},
|
||||
"library/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"nonlibrary/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "nonlibrary/ubuntu",
|
||||
LocalName: "nonlibrary/ubuntu",
|
||||
CanonicalName: "docker.io/nonlibrary/ubuntu",
|
||||
},
|
||||
"ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"other/library": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "other/library",
|
||||
LocalName: "other/library",
|
||||
CanonicalName: "docker.io/other/library",
|
||||
},
|
||||
"127.0.0.1:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "127.0.0.1:8000/private/moonbase",
|
||||
CanonicalName: "127.0.0.1:8000/private/moonbase",
|
||||
},
|
||||
"127.0.0.1:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "127.0.0.1:8000/privatebase",
|
||||
CanonicalName: "127.0.0.1:8000/privatebase",
|
||||
},
|
||||
"[::1]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::1]:8000/private/moonbase",
|
||||
CanonicalName: "[::1]:8000/private/moonbase",
|
||||
},
|
||||
"[::1]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::1]:8000/privatebase",
|
||||
CanonicalName: "[::1]:8000/privatebase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::2]:8000/private/moonbase",
|
||||
CanonicalName: "[::2]:8000/private/moonbase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::2]:8000/privatebase",
|
||||
CanonicalName: "[::2]:8000/privatebase",
|
||||
},
|
||||
"localhost:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost:8000/private/moonbase",
|
||||
CanonicalName: "localhost:8000/private/moonbase",
|
||||
},
|
||||
"localhost:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost:8000/privatebase",
|
||||
CanonicalName: "localhost:8000/privatebase",
|
||||
},
|
||||
"example.com/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com/private/moonbase",
|
||||
CanonicalName: "example.com/private/moonbase",
|
||||
},
|
||||
"example.com/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com/privatebase",
|
||||
CanonicalName: "example.com/privatebase",
|
||||
},
|
||||
"example.com:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com:8000/private/moonbase",
|
||||
CanonicalName: "example.com:8000/private/moonbase",
|
||||
},
|
||||
"example.com:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com:8000/privatebase",
|
||||
CanonicalName: "example.com:8000/privatebase",
|
||||
},
|
||||
"localhost/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost/private/moonbase",
|
||||
CanonicalName: "localhost/private/moonbase",
|
||||
},
|
||||
"localhost/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost/privatebase",
|
||||
CanonicalName: "localhost/privatebase",
|
||||
},
|
||||
IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"index." + IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
"index." + IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
}
|
||||
|
||||
for reposName, expected := range tests {
|
||||
t.Run(reposName, func(t *testing.T) {
|
||||
named, err := reference.ParseNormalizedNamed(reposName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
repoInfo, err := ParseRepositoryInfo(named)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index))
|
||||
assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName))
|
||||
assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName))
|
||||
assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName))
|
||||
})
|
||||
}
|
||||
}
|
||||
86
internal/registry/service.go
Normal file
86
internal/registry/service.go
Normal file
@ -0,0 +1,86 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/log"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
)
|
||||
|
||||
// Service is a registry service. It tracks configuration data such as a list
|
||||
// of mirrors.
|
||||
type Service struct {
|
||||
config *serviceConfig
|
||||
}
|
||||
|
||||
// NewService returns a new instance of [Service] ready to be installed into
|
||||
// an engine.
|
||||
func NewService(options ServiceOptions) (*Service, error) {
|
||||
config, err := newServiceConfig(options.InsecureRegistries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Service{config: config}, nil
|
||||
}
|
||||
|
||||
// Auth contacts the public registry with the provided credentials,
|
||||
// and returns OK if authentication was successful.
|
||||
// It can be used to verify the validity of a client's credentials.
|
||||
func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (token string, _ error) {
|
||||
registryHostName := IndexHostname
|
||||
|
||||
if authConfig.ServerAddress != "" {
|
||||
serverAddress := authConfig.ServerAddress
|
||||
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
||||
serverAddress = "https://" + serverAddress
|
||||
}
|
||||
u, err := url.Parse(serverAddress)
|
||||
if err != nil {
|
||||
return "", invalidParam(fmt.Errorf("unable to parse server address: %w", err))
|
||||
}
|
||||
registryHostName = u.Host
|
||||
}
|
||||
|
||||
// Lookup endpoints for authentication.
|
||||
endpoints, err := s.Endpoints(ctx, registryHostName)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return "", err
|
||||
}
|
||||
return "", invalidParam(err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, endpoint := range endpoints {
|
||||
authToken, err := loginV2(ctx, authConfig, endpoint, userAgent)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
|
||||
// Failed to authenticate; don't continue with (non-TLS) endpoints.
|
||||
return "", err
|
||||
}
|
||||
// Try next endpoint
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"error": err,
|
||||
"endpoint": endpoint,
|
||||
}).Infof("Error logging in to endpoint, trying next endpoint")
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
return authToken, nil
|
||||
}
|
||||
|
||||
return "", lastErr
|
||||
}
|
||||
|
||||
// APIEndpoint represents a remote API endpoint
|
||||
type APIEndpoint struct {
|
||||
URL *url.URL
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
37
internal/registry/service_v2.go
Normal file
37
internal/registry/service_v2.go
Normal file
@ -0,0 +1,37 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
)
|
||||
|
||||
func (s *Service) Endpoints(ctx context.Context, hostname string) ([]APIEndpoint, error) {
|
||||
if hostname == DefaultNamespace || hostname == IndexHostname {
|
||||
return []APIEndpoint{{
|
||||
URL: DefaultV2Registry,
|
||||
TLSConfig: tlsconfig.ServerDefault(),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoints := []APIEndpoint{{
|
||||
URL: &url.URL{Scheme: "https", Host: hostname},
|
||||
TLSConfig: tlsConfig,
|
||||
}}
|
||||
|
||||
if tlsConfig.InsecureSkipVerify {
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: &url.URL{Scheme: "http", Host: hostname},
|
||||
// used to check if supposed to be secure via InsecureSkipVerify
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
@ -15,6 +15,7 @@ replace (
|
||||
require (
|
||||
dario.cat/mergo v1.0.1
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/platforms v1.0.0-rc.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||
github.com/creack/pty v1.1.24
|
||||
@ -55,6 +56,7 @@ require (
|
||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
|
||||
@ -78,7 +80,6 @@ require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
@ -105,7 +106,6 @@ require (
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
|
||||
170
vendor/github.com/docker/docker/registry/search.go
generated
vendored
170
vendor/github.com/docker/docker/registry/search.go
generated
vendored
@ -1,170 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/moby/moby/api/types/filters"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var acceptedSearchFilterTags = map[string]bool{
|
||||
"is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
|
||||
"is-official": true,
|
||||
"stars": true,
|
||||
}
|
||||
|
||||
// Search queries the public registry for repositories matching the specified
|
||||
// search term and filters.
|
||||
func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
|
||||
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// "is-automated" is deprecated and filtering for `true` will yield no results.
|
||||
if isAutomated {
|
||||
return []registry.SearchResult{}, nil
|
||||
}
|
||||
|
||||
isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasStarFilter := 0
|
||||
if searchFilters.Contains("stars") {
|
||||
hasStars := searchFilters.Get("stars")
|
||||
for _, hasStar := range hasStars {
|
||||
iHasStar, err := strconv.Atoi(hasStar)
|
||||
if err != nil {
|
||||
return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
|
||||
}
|
||||
if iHasStar > hasStarFilter {
|
||||
hasStarFilter = iHasStar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredResults := []registry.SearchResult{}
|
||||
for _, result := range unfilteredResult.Results {
|
||||
if searchFilters.Contains("is-official") {
|
||||
if isOfficial != result.IsOfficial {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if searchFilters.Contains("stars") {
|
||||
if result.StarCount < hasStarFilter {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// "is-automated" is deprecated and the value in Docker Hub search
|
||||
// results is untrustworthy. Force it to false so as to not mislead our
|
||||
// clients.
|
||||
result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated)
|
||||
filteredResults = append(filteredResults, result)
|
||||
}
|
||||
|
||||
return filteredResults, nil
|
||||
}
|
||||
|
||||
func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
|
||||
if hasScheme(term) {
|
||||
return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
|
||||
}
|
||||
|
||||
indexName, remoteName := splitReposSearchTerm(term)
|
||||
|
||||
// Search is a long-running operation, just lock s.config to avoid block others.
|
||||
s.mu.RLock()
|
||||
index := newIndexInfo(s.config, indexName)
|
||||
s.mu.RUnlock()
|
||||
if index.Official {
|
||||
// If pull "library/foo", it's stored locally under "foo"
|
||||
remoteName = strings.TrimPrefix(remoteName, "library/")
|
||||
}
|
||||
|
||||
endpoint, err := newV1Endpoint(ctx, index, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var client *http.Client
|
||||
if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
|
||||
creds := NewStaticCredentialStore(authConfig)
|
||||
|
||||
// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
|
||||
modifiers := Headers(headers.Get("User-Agent"), nil)
|
||||
v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
|
||||
auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Copy non transport http client features
|
||||
v2Client.Timeout = endpoint.client.Timeout
|
||||
v2Client.CheckRedirect = endpoint.client.CheckRedirect
|
||||
v2Client.Jar = endpoint.client.Jar
|
||||
|
||||
log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
|
||||
client = v2Client
|
||||
} else {
|
||||
client = endpoint.client
|
||||
if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit)
|
||||
}
|
||||
|
||||
// splitReposSearchTerm breaks a search term into an index name and remote name
|
||||
func splitReposSearchTerm(reposName string) (string, string) {
|
||||
nameParts := strings.SplitN(reposName, "/", 2)
|
||||
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
|
||||
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
||||
// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
|
||||
// use the default Docker Hub registry (docker.io)
|
||||
return IndexName, reposName
|
||||
}
|
||||
return nameParts[0], nameParts[1]
|
||||
}
|
||||
|
||||
// ParseSearchIndexInfo will use repository name to get back an indexInfo.
|
||||
//
|
||||
// TODO(thaJeztah) this function is only used by the CLI, and used to get
|
||||
// information of the registry (to provide credentials if needed). We should
|
||||
// move this function (or equivalent) to the CLI, as it's doing too much just
|
||||
// for that.
|
||||
func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
|
||||
indexName, _ := splitReposSearchTerm(reposName)
|
||||
indexName = normalizeIndexName(indexName)
|
||||
if indexName == IndexName {
|
||||
return ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Mirrors: []string{},
|
||||
Secure: !isInsecure(indexName),
|
||||
}, nil
|
||||
}
|
||||
213
vendor/github.com/docker/docker/registry/search_endpoint_v1.go
generated
vendored
213
vendor/github.com/docker/docker/registry/search_endpoint_v1.go
generated
vendored
@ -1,213 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
)
|
||||
|
||||
// v1PingResult contains the information returned when pinging a registry. It
|
||||
// indicates whether the registry claims to be a standalone registry.
|
||||
type v1PingResult struct {
|
||||
// Standalone is set to true if the registry indicates it is a
|
||||
// standalone registry in the X-Docker-Registry-Standalone
|
||||
// header
|
||||
Standalone bool `json:"standalone"`
|
||||
}
|
||||
|
||||
// v1Endpoint stores basic information about a V1 registry endpoint.
|
||||
type v1Endpoint struct {
|
||||
client *http.Client
|
||||
URL *url.URL
|
||||
IsSecure bool
|
||||
}
|
||||
|
||||
// newV1Endpoint parses the given address to return a registry endpoint.
|
||||
// TODO: remove. This is only used by search.
|
||||
func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
|
||||
tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endpoint.String() == IndexServer {
|
||||
// Skip the check, we know this one is valid
|
||||
// (and we never want to fall back to http in case of error)
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// Try HTTPS ping to registry
|
||||
endpoint.URL.Scheme = "https"
|
||||
if _, err := endpoint.ping(ctx); err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
if endpoint.IsSecure {
|
||||
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
|
||||
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP.
|
||||
hint := fmt.Sprintf(
|
||||
". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+
|
||||
"In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt",
|
||||
endpoint.URL.Host,
|
||||
)
|
||||
return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
|
||||
}
|
||||
|
||||
// registry is insecure and HTTPS failed, fallback to HTTP.
|
||||
log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
|
||||
endpoint.URL.Scheme = "http"
|
||||
if _, err2 := endpoint.ping(ctx); err2 != nil {
|
||||
return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// trimV1Address trims the "v1" version suffix off the address and returns
|
||||
// the trimmed address. It returns an error on "v2" endpoints.
|
||||
func trimV1Address(address string) (string, error) {
|
||||
trimmed := strings.TrimSuffix(address, "/")
|
||||
if strings.HasSuffix(trimmed, "/v2") {
|
||||
return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
|
||||
}
|
||||
return strings.TrimSuffix(trimmed, "/v1"), nil
|
||||
}
|
||||
|
||||
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
|
||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
||||
address = "https://" + address
|
||||
}
|
||||
|
||||
address, err := trimV1Address(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return nil, invalidParam(err)
|
||||
}
|
||||
|
||||
// TODO(tiborvass): make sure a ConnectTimeout transport is used
|
||||
tr := newTransport(tlsConfig)
|
||||
|
||||
return &v1Endpoint{
|
||||
IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
|
||||
URL: uri,
|
||||
client: httpClient(transport.NewTransport(tr, Headers("", headers)...)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get the formatted URL for the root of this registry Endpoint
|
||||
func (e *v1Endpoint) String() string {
|
||||
return e.URL.String() + "/v1/"
|
||||
}
|
||||
|
||||
// ping returns a v1PingResult which indicates whether the registry is standalone or not.
|
||||
func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
|
||||
if e.String() == IndexServer {
|
||||
// Skip the check, we know this one is valid
|
||||
// (and we never want to fallback to http in case of error)
|
||||
return v1PingResult{}, nil
|
||||
}
|
||||
|
||||
pingURL := e.String() + "_ping"
|
||||
log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody)
|
||||
if err != nil {
|
||||
return v1PingResult{}, invalidParam(err)
|
||||
}
|
||||
|
||||
resp, err := e.client.Do(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return v1PingResult{}, err
|
||||
}
|
||||
return v1PingResult{}, invalidParam(err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
|
||||
info := v1PingResult{}
|
||||
// Accepted values are "1", and "true" (case-insensitive).
|
||||
if v == "1" || strings.EqualFold(v, "true") {
|
||||
info.Standalone = true
|
||||
}
|
||||
log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// If the header is absent, we assume true for compatibility with earlier
|
||||
// versions of the registry. default to true
|
||||
info := v1PingResult{
|
||||
Standalone: true,
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
|
||||
// don't stop here. Just assume sane defaults
|
||||
}
|
||||
|
||||
log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// httpClient returns an HTTP client structure which uses the given transport
|
||||
// and contains the necessary headers for redirected requests
|
||||
func httpClient(tr http.RoundTripper) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: addRequiredHeadersToRedirectedRequests,
|
||||
}
|
||||
}
|
||||
|
||||
func trustedLocation(req *http.Request) bool {
|
||||
var (
|
||||
trusteds = []string{"docker.com", "docker.io"}
|
||||
hostname = strings.SplitN(req.Host, ":", 2)[0]
|
||||
)
|
||||
if req.URL.Scheme != "https" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, trusted := range trusteds {
|
||||
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
|
||||
// for redirected requests
|
||||
func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
||||
if len(via) != 0 && via[0] != nil {
|
||||
if trustedLocation(req) && trustedLocation(via[0]) {
|
||||
req.Header = via[0].Header
|
||||
return nil
|
||||
}
|
||||
for k, v := range via[0].Header {
|
||||
if k != "Authorization" {
|
||||
for _, vv := range v {
|
||||
req.Header.Add(k, vv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
248
vendor/github.com/docker/docker/registry/search_session.go
generated
vendored
248
vendor/github.com/docker/docker/registry/search_session.go
generated
vendored
@ -1,248 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
// this is required for some certificates
|
||||
"context"
|
||||
_ "crypto/sha512"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// A session is used to communicate with a V1 registry
|
||||
type session struct {
|
||||
indexEndpoint *v1Endpoint
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type authTransport struct {
|
||||
base http.RoundTripper
|
||||
authConfig *registry.AuthConfig
|
||||
|
||||
alwaysSetBasicAuth bool
|
||||
token []string
|
||||
|
||||
mu sync.Mutex // guards modReq
|
||||
modReq map[*http.Request]*http.Request // original -> modified
|
||||
}
|
||||
|
||||
// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official)
|
||||
//
|
||||
// For private v1 registries, set alwaysSetBasicAuth to true.
|
||||
//
|
||||
// For the official v1 registry, if there isn't already an Authorization header in the request,
|
||||
// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
|
||||
// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
|
||||
// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
|
||||
// requests.
|
||||
//
|
||||
// If the server sends a token without the client having requested it, it is ignored.
|
||||
//
|
||||
// This RoundTripper also has a CancelRequest method important for correct timeout handling.
|
||||
func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
|
||||
if base == nil {
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
return &authTransport{
|
||||
base: base,
|
||||
authConfig: authConfig,
|
||||
alwaysSetBasicAuth: alwaysSetBasicAuth,
|
||||
modReq: make(map[*http.Request]*http.Request),
|
||||
}
|
||||
}
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
|
||||
return r2
|
||||
}
|
||||
|
||||
// onEOFReader wraps an io.ReadCloser and a function
|
||||
// the function will run at the end of file or close the file.
|
||||
type onEOFReader struct {
|
||||
Rc io.ReadCloser
|
||||
Fn func()
|
||||
}
|
||||
|
||||
func (r *onEOFReader) Read(p []byte) (int, error) {
|
||||
n, err := r.Rc.Read(p)
|
||||
if err == io.EOF {
|
||||
r.runFunc()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the file and run the function.
|
||||
func (r *onEOFReader) Close() error {
|
||||
err := r.Rc.Close()
|
||||
r.runFunc()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *onEOFReader) runFunc() {
|
||||
if fn := r.Fn; fn != nil {
|
||||
fn()
|
||||
r.Fn = nil
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip changes an HTTP request's headers to add the necessary
|
||||
// authentication-related headers
|
||||
func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
||||
// Authorization should not be set on 302 redirect for untrusted locations.
|
||||
// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
|
||||
// As the authorization logic is currently implemented in RoundTrip,
|
||||
// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
|
||||
// This is safe as Docker doesn't set Referrer in other scenarios.
|
||||
if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
|
||||
return tr.base.RoundTrip(orig)
|
||||
}
|
||||
|
||||
req := cloneRequest(orig)
|
||||
tr.mu.Lock()
|
||||
tr.modReq[orig] = req
|
||||
tr.mu.Unlock()
|
||||
|
||||
if tr.alwaysSetBasicAuth {
|
||||
if tr.authConfig == nil {
|
||||
return nil, errors.New("unexpected error: empty auth config")
|
||||
}
|
||||
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
|
||||
return tr.base.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Don't override
|
||||
if req.Header.Get("Authorization") == "" {
|
||||
if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
|
||||
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
|
||||
} else if len(tr.token) > 0 {
|
||||
req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
|
||||
}
|
||||
}
|
||||
resp, err := tr.base.RoundTrip(req)
|
||||
if err != nil {
|
||||
tr.mu.Lock()
|
||||
delete(tr.modReq, orig)
|
||||
tr.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Header["X-Docker-Token"]) > 0 {
|
||||
tr.token = resp.Header["X-Docker-Token"]
|
||||
}
|
||||
resp.Body = &onEOFReader{
|
||||
Rc: resp.Body,
|
||||
Fn: func() {
|
||||
tr.mu.Lock()
|
||||
delete(tr.modReq, orig)
|
||||
tr.mu.Unlock()
|
||||
},
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CancelRequest cancels an in-flight request by closing its connection.
|
||||
func (tr *authTransport) CancelRequest(req *http.Request) {
|
||||
type canceler interface {
|
||||
CancelRequest(*http.Request)
|
||||
}
|
||||
if cr, ok := tr.base.(canceler); ok {
|
||||
tr.mu.Lock()
|
||||
modReq := tr.modReq[req]
|
||||
delete(tr.modReq, req)
|
||||
tr.mu.Unlock()
|
||||
cr.CancelRequest(modReq)
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
|
||||
var alwaysSetBasicAuth bool
|
||||
|
||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
||||
// alongside all our requests.
|
||||
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
|
||||
info, err := endpoint.ping(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Standalone && authConfig != nil {
|
||||
log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
|
||||
alwaysSetBasicAuth = true
|
||||
}
|
||||
}
|
||||
|
||||
// Annotate the transport unconditionally so that v2 can
|
||||
// properly fallback on v1 when an image is not found.
|
||||
client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
|
||||
}
|
||||
client.Jar = jar
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSession(client *http.Client, endpoint *v1Endpoint) *session {
|
||||
return &session{
|
||||
client: client,
|
||||
indexEndpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// defaultSearchLimit is the default value for maximum number of returned search results.
|
||||
const defaultSearchLimit = 25
|
||||
|
||||
// searchRepositories performs a search against the remote repository
|
||||
func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
|
||||
if limit == 0 {
|
||||
limit = defaultSearchLimit
|
||||
}
|
||||
if limit < 1 || limit > 100 {
|
||||
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
|
||||
}
|
||||
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit))
|
||||
log.G(ctx).WithField("url", u).Debug("searchRepositories")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, invalidParamWrapf(err, "error building request")
|
||||
}
|
||||
// Have the AuthTransport send authentication, when logged in.
|
||||
req.Header.Set("X-Docker-Token", "true")
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, systemErr{err}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
// TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286).
|
||||
// TODO(thaJeztah): handle other status-codes to return correct error-type
|
||||
return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
|
||||
}
|
||||
result := ®istry.SearchResults{}
|
||||
err = json.NewDecoder(res.Body).Decode(result)
|
||||
if err != nil {
|
||||
return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
160
vendor/github.com/docker/docker/registry/service.go
generated
vendored
160
vendor/github.com/docker/docker/registry/service.go
generated
vendored
@ -1,160 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/log"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
)
|
||||
|
||||
// Service is a registry service. It tracks configuration data such as a list
|
||||
// of mirrors.
|
||||
type Service struct {
|
||||
config *serviceConfig
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewService returns a new instance of [Service] ready to be installed into
|
||||
// an engine.
|
||||
func NewService(options ServiceOptions) (*Service, error) {
|
||||
config, err := newServiceConfig(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{config: config}, err
|
||||
}
|
||||
|
||||
// ServiceConfig returns a copy of the public registry service's configuration.
|
||||
func (s *Service) ServiceConfig() *registry.ServiceConfig {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.config.copy()
|
||||
}
|
||||
|
||||
// ReplaceConfig prepares a transaction which will atomically replace the
|
||||
// registry service's configuration when the returned commit function is called.
|
||||
func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
|
||||
config, err := newServiceConfig(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.config = config
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Auth contacts the public registry with the provided credentials,
|
||||
// and returns OK if authentication was successful.
|
||||
// It can be used to verify the validity of a client's credentials.
|
||||
func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
|
||||
// TODO Use ctx when searching for repositories
|
||||
registryHostName := IndexHostname
|
||||
|
||||
if authConfig.ServerAddress != "" {
|
||||
serverAddress := authConfig.ServerAddress
|
||||
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
||||
serverAddress = "https://" + serverAddress
|
||||
}
|
||||
u, err := url.Parse(serverAddress)
|
||||
if err != nil {
|
||||
return "", "", invalidParamWrapf(err, "unable to parse server address")
|
||||
}
|
||||
registryHostName = u.Host
|
||||
}
|
||||
|
||||
// Lookup endpoints for authentication but exclude mirrors to prevent
|
||||
// sending credentials of the upstream registry to a mirror.
|
||||
s.mu.RLock()
|
||||
endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
|
||||
s.mu.RUnlock()
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return "", "", err
|
||||
}
|
||||
return "", "", invalidParam(err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, endpoint := range endpoints {
|
||||
authToken, err := loginV2(ctx, authConfig, endpoint, userAgent)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
|
||||
// Failed to authenticate; don't continue with (non-TLS) endpoints.
|
||||
return "", "", err
|
||||
}
|
||||
// Try next endpoint
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"error": err,
|
||||
"endpoint": endpoint,
|
||||
}).Infof("Error logging in to endpoint, trying next endpoint")
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here?
|
||||
return "Login Succeeded", authToken, nil
|
||||
}
|
||||
|
||||
return "", "", lastErr
|
||||
}
|
||||
|
||||
// ResolveAuthConfig looks up authentication for the given reference from the
|
||||
// given authConfigs.
|
||||
//
|
||||
// IMPORTANT: This function is for internal use and should not be used by external projects.
|
||||
func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
// Simplified version of "newIndexInfo" without handling of insecure
|
||||
// registries and mirrors, as we don't need that information to resolve
|
||||
// the auth-config.
|
||||
indexName := normalizeIndexName(reference.Domain(ref))
|
||||
registryInfo, ok := s.config.IndexConfigs[indexName]
|
||||
if !ok {
|
||||
registryInfo = ®istry.IndexInfo{Name: indexName}
|
||||
}
|
||||
return ResolveAuthConfig(authConfigs, registryInfo)
|
||||
}
|
||||
|
||||
// APIEndpoint represents a remote API endpoint
|
||||
type APIEndpoint struct {
|
||||
Mirror bool
|
||||
URL *url.URL
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
|
||||
// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
|
||||
func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.lookupV2Endpoints(context.TODO(), hostname, true)
|
||||
}
|
||||
|
||||
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
|
||||
// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
|
||||
func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.lookupV2Endpoints(context.TODO(), hostname, false)
|
||||
}
|
||||
|
||||
// IsInsecureRegistry returns true if the registry at given host is configured as
|
||||
// insecure registry.
|
||||
func (s *Service) IsInsecureRegistry(host string) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return !s.config.isSecureIndex(host)
|
||||
}
|
||||
73
vendor/github.com/docker/docker/registry/service_v2.go
generated
vendored
73
vendor/github.com/docker/docker/registry/service_v2.go
generated
vendored
@ -1,73 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
)
|
||||
|
||||
func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) {
|
||||
var endpoints []APIEndpoint
|
||||
if hostname == DefaultNamespace || hostname == IndexHostname {
|
||||
if includeMirrors {
|
||||
for _, mirror := range s.config.Mirrors {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
|
||||
mirror = "https://" + mirror
|
||||
}
|
||||
mirrorURL, err := url.Parse(mirror)
|
||||
if err != nil {
|
||||
return nil, invalidParam(err)
|
||||
}
|
||||
// TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time.
|
||||
mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: mirrorURL,
|
||||
Mirror: true,
|
||||
TLSConfig: mirrorTLSConfig,
|
||||
})
|
||||
}
|
||||
}
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: DefaultV2Registry,
|
||||
TLSConfig: tlsconfig.ServerDefault(),
|
||||
})
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoints = []APIEndpoint{
|
||||
{
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: hostname,
|
||||
},
|
||||
TLSConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if tlsConfig.InsecureSkipVerify {
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: hostname,
|
||||
},
|
||||
// used to check if supposed to be secure via InsecureSkipVerify
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -71,7 +71,6 @@ github.com/docker/docker/pkg/jsonmessage
|
||||
github.com/docker/docker/pkg/process
|
||||
github.com/docker/docker/pkg/progress
|
||||
github.com/docker/docker/pkg/streamformatter
|
||||
github.com/docker/docker/registry
|
||||
# github.com/docker/docker-credential-helpers v0.9.3
|
||||
## explicit; go 1.21
|
||||
github.com/docker/docker-credential-helpers/client
|
||||
|
||||
Reference in New Issue
Block a user