diff --git a/cli/command/image/push.go b/cli/command/image/push.go index 88d8bec4fe..cd79ff9289 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -16,11 +16,11 @@ 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/api/types/auxprogress" "github.com/docker/docker/api/types/image" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/morikuni/aec" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -105,10 +105,10 @@ To push the complete multi-platform image, remove the --platform flag. } // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, _ := registry.ParseRepositoryInfo(ref) + indexInfo := registry.NewIndexInfo(ref) // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) + authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) if err != nil { return err @@ -134,7 +134,7 @@ To push the complete multi-platform image, remove the --platform flag. defer responseBody.Close() if !opts.untrusted { // TODO pushTrustedReference currently doesn't respect `--quiet` - return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody) + return pushTrustedReference(ctx, dockerCli, indexInfo, ref, authConfig, responseBody) } if opts.quiet { diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index c44b51eb8f..00268e7592 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -13,7 +13,6 @@ import ( "github.com/docker/cli/internal/jsonstream" "github.com/docker/docker/api/types/image" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -42,7 +41,11 @@ func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) ( } // pushTrustedReference pushes a canonical reference to the trust server. -func pushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error { +func pushTrustedReference(ctx context.Context, ioStreams command.Streams, indexInfo *registrytypes.IndexInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error { + repoInfo := &trust.RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + } return trust.PushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in, command.UserAgent()) } diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go index a0c053ff06..33a7179edd 100644 --- a/cli/command/plugin/install.go +++ b/cli/command/plugin/install.go @@ -11,9 +11,9 @@ import ( "github.com/docker/cli/cli/command/image" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/internal/prompt" + "github.com/docker/cli/internal/registry" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -65,8 +65,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti return types.PluginInstallOptions{}, err } - repoInfo, _ := registry.ParseRepositoryInfo(ref) - + indexInfo := registry.NewIndexInfo(ref) remote := ref.String() _, isCanonical := ref.(reference.Canonical) @@ -84,7 +83,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti remote = reference.FamiliarString(trusted) } - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) + authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) if err != nil { return types.PluginInstallOptions{}, err diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go index 4020680347..573ae3d0ae 100644 --- a/cli/command/plugin/push.go +++ b/cli/command/plugin/push.go @@ -8,8 +8,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" + "github.com/docker/cli/internal/registry" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -49,8 +49,8 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error named = reference.TagNameOnly(named) - repoInfo, _ := registry.ParseRepositoryInfo(named) - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) + indexInfo := registry.NewIndexInfo(named) + authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) if err != nil { return err @@ -63,6 +63,10 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error defer responseBody.Close() if !opts.untrusted { + repoInfo := &trust.RepositoryInfo{ + Name: reference.TrimNamed(named), + Index: indexInfo, + } return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent()) } diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 3679e51edd..a216908505 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -15,10 +15,10 @@ 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" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" - "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -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 } diff --git a/cli/command/registry/login_test.go b/cli/command/registry/login_test.go index 58ca73c6a6..d112f08e88 100644 --- a/cli/command/registry/login_test.go +++ b/cli/command/registry/login_test.go @@ -13,11 +13,11 @@ 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" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" - "github.com/docker/docker/registry" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 2af2cdad3f..34498871a5 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -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" ) diff --git a/cli/command/registry/search.go b/cli/command/registry/search.go index 01950acd0e..178a400ddb 100644 --- a/cli/command/registry/search.go +++ b/cli/command/registry/search.go @@ -3,13 +3,13 @@ package registry import ( "context" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/opts" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -52,13 +52,7 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions if options.filter.Value().Contains("is-automated") { _, _ = fmt.Fprintln(dockerCli.Err(), `WARNING: the "is-automated" filter is deprecated, and searching for "is-automated=true" will not yield any results in future.`) } - indexInfo, err := registry.ParseSearchIndexInfo(options.term) - if err != nil { - return err - } - - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + encodedAuth, err := getAuth(dockerCli, options.term) if err != nil { return err } @@ -80,3 +74,37 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions } return SearchWrite(searchCtx, results) } + +// authConfigKey is the key used to store credentials for Docker Hub. It is +// a copy of [registry.IndexServer]. +// +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer +const authConfigKey = "https://index.docker.io/v1/" + +// getAuth will use fetch auth based on the given search-term. If the search +// does not contain a hostname for the registry, it assumes Docker Hub is used, +// and resolves authentication for Docker Hub, otherwise it resolves authentication +// for the given registry. +func getAuth(dockerCLI command.Cli, reposName string) (encodedAuth string, err error) { + authCfgKey := splitReposSearchTerm(reposName) + if authCfgKey == "docker.io" || authCfgKey == "index.docker.io" { + authCfgKey = authConfigKey + } + + // Ignoring errors here, which was the existing behavior (likely + // "no credentials found"). We'll get an error when search failed, + // so fine to ignore in most situations. + authConfig, _ := dockerCLI.ConfigFile().GetAuthConfig(authCfgKey) + return registrytypes.EncodeAuthConfig(registrytypes.AuthConfig(authConfig)) +} + +// splitReposSearchTerm breaks a search term into an index name and remote name +func splitReposSearchTerm(reposName 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 "docker.io" + } + return nameParts[0] +} diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go index 6fb4d129a5..ea79161ad5 100644 --- a/cli/command/service/trust.go +++ b/cli/command/service/trust.go @@ -6,8 +6,8 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/trust" + "github.com/docker/cli/internal/registry" "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -51,9 +51,12 @@ func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm } func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, _ := registry.ParseRepositoryInfo(ref) - authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index) - + indexInfo := registry.NewIndexInfo(ref) + authConfig := command.ResolveAuthConfig(cli.ConfigFile(), indexInfo) + repoInfo := &trust.RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + } notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull") if err != nil { return nil, errors.Wrap(err, "error establishing connection to trust repository") diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 7f82e4e5d8..c892961402 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -19,11 +19,11 @@ 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/api/types/swarm" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" - "github.com/docker/docker/registry" "github.com/docker/go-units" "github.com/spf13/cobra" ) diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index d996ea23f4..726d1ea5a2 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -78,7 +78,6 @@ var sampleInfoNoSwarm = system.Info{ IndexConfigs: map[string]*registrytypes.IndexInfo{ "docker.io": { Name: "docker.io", - Mirrors: nil, Secure: true, Official: true, }, diff --git a/cli/registry/client/endpoint.go b/cli/registry/client/endpoint.go index 8b5431e1f8..ce6e774969 100644 --- a/cli/registry/client/endpoint.go +++ b/cli/registry/client/endpoint.go @@ -1,15 +1,17 @@ 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" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/pkg/errors" ) @@ -32,8 +34,7 @@ func (r repositoryEndpoint) BaseURL() string { func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) { repoName := reference.TrimNamed(ref) - repoInfo, _ := registry.ParseRepositoryInfo(ref) - indexInfo := repoInfo.Index + indexInfo := registry.NewIndexInfo(ref) endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure) if err != nil { @@ -54,7 +55,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 +98,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 +118,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) {} diff --git a/cli/registry/client/fetcher.go b/cli/registry/client/fetcher.go index f270d49432..852bc86e1e 100644 --- a/cli/registry/client/fetcher.go +++ b/cli/registry/client/fetcher.go @@ -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" @@ -221,8 +221,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, } repoName := reference.TrimNamed(namedRef) - repoInfo, _ := registry.ParseRepositoryInfo(namedRef) - indexInfo := repoInfo.Index + indexInfo := registry.NewIndexInfo(namedRef) confirmedTLSRegistries := make(map[string]bool) for _, endpoint := range endpoints { @@ -283,10 +282,9 @@ 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(namedRef)) logrus.Debugf("endpoints for %s: %v", namedRef, endpoints) return endpoints, err } diff --git a/cli/trust/trust.go b/cli/trust/trust.go index 27453ae22e..eeddc912b3 100644 --- a/cli/trust/trust.go +++ b/cli/trust/trust.go @@ -14,11 +14,11 @@ 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" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/docker/go-connections/tlsconfig" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -95,7 +95,7 @@ func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {} // GetNotaryRepository returns a NotaryRepository which stores all the // information needed to operate on a notary repository. // It creates an HTTP transport providing authentication support. -func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) { +func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) { server, err := Server(repoInfo.Index) if err != nil { return nil, err @@ -304,11 +304,18 @@ type ImageRefAndAuth struct { original string authConfig *registrytypes.AuthConfig reference reference.Named - repoInfo *registry.RepositoryInfo + repoInfo *RepositoryInfo tag string digest digest.Digest } +// RepositoryInfo describes a repository +type RepositoryInfo struct { + Name reference.Named + // Index points to registry information + Index *registrytypes.IndexInfo +} + // GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name // as an ImageRefAndAuth struct func GetImageReferencesAndAuth(ctx context.Context, @@ -321,15 +328,18 @@ func GetImageReferencesAndAuth(ctx context.Context, } // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, _ := registry.ParseRepositoryInfo(ref) - authConfig := authResolver(ctx, repoInfo.Index) + indexInfo := registry.NewIndexInfo(ref) + authConfig := authResolver(ctx, indexInfo) return ImageRefAndAuth{ original: imgName, authConfig: &authConfig, reference: ref, - repoInfo: repoInfo, - tag: getTag(ref), - digest: getDigest(ref), + repoInfo: &RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + }, + tag: getTag(ref), + digest: getDigest(ref), }, nil } @@ -366,7 +376,7 @@ func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { } // RepoInfo returns the repository information for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo { +func (imgRefAuth *ImageRefAndAuth) RepoInfo() *RepositoryInfo { return imgRefAuth.repoInfo } diff --git a/cli/trust/trust_push.go b/cli/trust/trust_push.go index 1a8c5e4b72..87a33e7ca2 100644 --- a/cli/trust/trust_push.go +++ b/cli/trust/trust_push.go @@ -13,7 +13,6 @@ import ( "github.com/docker/cli/internal/jsonstream" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/theupdateframework/notary/client" @@ -32,7 +31,7 @@ type Streams interface { // PushTrustedReference pushes a canonical reference to the trust server. // //nolint:gocyclo -func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { +func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { // If it is a trusted push we would like to find the target entry which match the // tag provided in the function and then do an AddTarget later. notaryTarget := &client.Target{} diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 1150064364..96deb61b13 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -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" diff --git a/vendor/github.com/docker/docker/registry/auth.go b/internal/registry/auth.go similarity index 60% rename from vendor/github.com/docker/docker/registry/auth.go rename to internal/registry/auth.go index 1b0eeeed0b..4a042c9a17 100644 --- a/vendor/github.com/docker/docker/registry/auth.go +++ b/internal/registry/auth.go @@ -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/docker/docker/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(auth *registry.AuthConfig) auth.CredentialStore { - return staticCredentialStore{ - auth: auth, - } -} - -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 (scs 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,64 +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(url string) string { - stripped := url - if strings.HasPrefix(stripped, "http://") { - stripped = strings.TrimPrefix(stripped, "http://") - } else if strings.HasPrefix(stripped, "https://") { - stripped = strings.TrimPrefix(stripped, "https://") - } - 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, transport http.RoundTripper) (challenge.Manager, error) { - pingClient := &http.Client{ - Transport: transport, - Timeout: 15 * time.Second, - } +func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) { 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 @@ -193,9 +119,7 @@ func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.M challengeManager := challenge.NewSimpleManager() if err := challengeManager.AddResponse(resp); err != nil { - return nil, PingResponseError{ - Err: err, - } + return nil, err } return challengeManager, nil diff --git a/internal/registry/config.go b/internal/registry/config.go new file mode 100644 index 0000000000..04414cc88b --- /dev/null +++ b/internal/registry/config.go @@ -0,0 +1,310 @@ +// 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 ( + "context" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + + "github.com/containerd/log" + "github.com/distribution/reference" + "github.com/docker/docker/api/types/registry" +) + +// ServiceOptions holds command line options. +// +// TODO(thaJeztah): add CertsDir as option to replace the [CertsDir] function, which sets the location magically. +type ServiceOptions struct { + InsecureRegistries []string `json:"insecure-registries,omitempty"` +} + +// serviceConfig holds daemon configuration for the registry service. +// +// 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 +// are still supported by Docker Hub (and will continue to be supported), but +// there are new domains already in use, and plans to consolidate all legacy +// domains to new "canonical" domains. Once those domains are decided on, we +// should update these consts (but making sure to preserve compatibility with +// existing installs, clients, and user configuration). +const ( + // DefaultNamespace is the default namespace + DefaultNamespace = "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://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 + // 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() + `$`) + }) +) + +// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit], +// but inlining it to prevent adding that as a dependency for docker/cli. +// +// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 +func runningWithRootlessKit() bool { + return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != "" +} + +// CertsDir is the directory where certificates are stored. +// +// - Linux: "/etc/docker/certs.d/" +// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/" +// - Windows: "%PROGRAMDATA%/docker/certs.d/" +// +// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless. +func CertsDir() string { + certsDir := "/etc/docker/certs.d" + if runningWithRootlessKit() { + if configHome, _ := os.UserConfigDir(); configHome != "" { + certsDir = filepath.Join(configHome, "docker", "certs.d") + } + } else if runtime.GOOS == "windows" { + certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d") + } + return certsDir +} + +// newServiceConfig creates a new service config with the given options. +func newServiceConfig(registries []string) (*serviceConfig, error) { + if len(registries) == 0 { + return &serviceConfig{}, nil + } + // 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([]*net.IPNet, 0) + indexConfigs = make(map[string]*registry.IndexInfo) + ) + +skip: + for _, r := range registries { + if scheme, host, ok := strings.Cut(r, "://"); ok { + switch strings.ToLower(scheme) { + case "http", "https": + log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme) + r = host + default: + // unsupported scheme + 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. + for _, value := range insecureRegistryCIDRs { + 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, ipnet) + } else { + if err := validateHostPort(r); err != nil { + 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, + Secure: false, + Official: false, + } + } + } + + // Configure public registry. + indexConfigs[IndexName] = ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + + return &serviceConfig{ + indexConfigs: indexConfigs, + insecureRegistryCIDRs: insecureRegistryCIDRs, + }, nil +} + +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered +// insecure. +// +// indexName should be 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 in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element +// of insecureRegistries. +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 { + return index.Secure + } + + return !isCIDRMatch(config.insecureRegistryCIDRs, indexName) +} + +// for mocking in unit tests. +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 []*net.IPNet, urlHost string) bool { + if len(cidrs) == 0 { + return false + } + + host, _, err := net.SplitHostPort(urlHost) + if err != nil { + // Assume urlHost is a host without port and go on. + host = urlHost + } + + var addresses []net.IP + if ip := net.ParseIP(host); ip != nil { + // Host is an IP-address. + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-address. + addresses, err = lookupIP(host) + if err != nil { + // We failed to resolve the host; assume there's no match. + return false + } + } + + for _, addr := range addresses { + for _, ipnet := range cidrs { + // check if the addr falls in the subnet + if ipnet.Contains(addr) { + return true + } + } + } + + return false +} + +func normalizeIndexName(val string) string { + if val == "index.docker.io" { + return "docker.io" + } + return val +} + +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) + if err != nil { + host = s + port = "" + } + // If match against the `host:port` pattern fails, + // it might be `IPv6:port`, which will be captured by net.ParseIP(host) + if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil { + return invalidParamf("invalid host %q", host) + } + if port != "" { + v, err := strconv.Atoi(port) + if err != nil { + return err + } + if v < 0 || v > 65535 { + return invalidParamf("invalid port %q", port) + } + } + return nil +} + +// NewIndexInfo creates a new [registry.IndexInfo] or the given +// repository-name, and detects whether the registry is considered +// "secure" (non-localhost). +func NewIndexInfo(reposName reference.Named) *registry.IndexInfo { + indexName := normalizeIndexName(reference.Domain(reposName)) + if indexName == IndexName { + return ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + } + + return ®istry.IndexInfo{ + Name: indexName, + Secure: !isInsecure(indexName), + } +} + +// isInsecure is used to detect whether a registry domain or IP-address is allowed +// to use an insecure (non-TLS, or self-signed cert) connection according to the +// defaults, which allows for insecure connections with registries running on a +// loopback address ("localhost", "::1/128", "127.0.0.0/8"). +// +// It is used in situations where we don't have access to the daemon's configuration, +// for example, when used from the client / CLI. +func isInsecure(hostNameOrIP string) bool { + // Attempt to strip port if present; this also strips brackets for + // IPv6 addresses with a port (e.g. "[::1]:5000"). + // + // This is best-effort; we'll continue using the address as-is if it fails. + if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { + hostNameOrIP = host + } + if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { + // Fast path; no need to resolve these, assuming nobody overrides + // "localhost" for anything else than a loopback address (sorry, not sorry). + return true + } + + var addresses []net.IP + if ip := net.ParseIP(hostNameOrIP); ip != nil { + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-addresses. + addrs, _ := lookupIP(hostNameOrIP) + addresses = append(addresses, addrs...) + } + + for _, addr := range addresses { + if addr.IsLoopback() { + return true + } + } + return false +} diff --git a/internal/registry/config_test.go b/internal/registry/config_test.go new file mode 100644 index 0000000000..a5574a298d --- /dev/null +++ b/internal/registry/config_test.go @@ -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)) + } + } +} diff --git a/internal/registry/doc.go b/internal/registry/doc.go new file mode 100644 index 0000000000..0b6a24767c --- /dev/null +++ b/internal/registry/doc.go @@ -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 diff --git a/internal/registry/errors.go b/internal/registry/errors.go new file mode 100644 index 0000000000..e27eb3e7a6 --- /dev/null +++ b/internal/registry/errors.go @@ -0,0 +1,51 @@ +// 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" +) + +func translateV2AuthError(err error) error { + var e *url.Error + if errors.As(err, &e) { + var e2 errcode.Error + if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) { + return unauthorizedErr{err} + } + } + return err +} + +func invalidParam(err error) error { + return invalidParameterErr{err} +} + +func invalidParamf(format string, args ...any) error { + return invalidParameterErr{fmt.Errorf(format, args...)} +} + +type unauthorizedErr struct{ error } + +func (unauthorizedErr) Unauthorized() {} + +func (e unauthorizedErr) Cause() error { + return e.error +} + +func (e unauthorizedErr) Unwrap() error { + return e.error +} + +type invalidParameterErr struct{ error } + +func (invalidParameterErr) InvalidParameter() {} + +func (e invalidParameterErr) Unwrap() error { + return e.error +} diff --git a/vendor/github.com/docker/docker/registry/registry.go b/internal/registry/registry.go similarity index 89% rename from vendor/github.com/docker/docker/registry/registry.go rename to internal/registry/registry.go index d3b3fbc9ba..97946d7fd8 100644 --- a/vendor/github.com/docker/docker/registry/registry.go +++ b/internal/registry/registry.go @@ -4,10 +4,13 @@ package registry import ( "context" "crypto/tls" + "fmt" "net" "net/http" "os" "path/filepath" + "runtime" + "strings" "time" "github.com/containerd/log" @@ -16,16 +19,15 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -// HostCertsDir returns the config directory for a specific host. -// -// Deprecated: this function was only used internally, and will be removed in a future release. -func HostCertsDir(hostname string) string { - return hostCertsDir(hostname) -} - // hostCertsDir returns the config directory for a specific host. -func hostCertsDir(hostname string) string { - return filepath.Join(CertsDir(), cleanPath(hostname)) +func hostCertsDir(hostnameAndPort string) string { + if runtime.GOOS == "windows" { + // Ensure that a directory name is valid; hostnameAndPort may contain + // a colon (:) if a port is included, and Windows does not allow colons + // in directory names. + hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", "")) + } + return filepath.Join(CertsDir(), hostnameAndPort) } // newTLSConfig constructs a client TLS configuration based on server defaults @@ -81,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 } diff --git a/internal/registry/registry_mock_test.go b/internal/registry/registry_mock_test.go new file mode 100644 index 0000000000..13c637852e --- /dev/null +++ b/internal/registry/registry_mock_test.go @@ -0,0 +1,92 @@ +package registry + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containerd/log" + "github.com/docker/docker/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() +} diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go new file mode 100644 index 0000000000..f1747aa86b --- /dev/null +++ b/internal/registry/registry_test.go @@ -0,0 +1,281 @@ +package registry + +import ( + "testing" + + "github.com/distribution/reference" + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestNewIndexInfo(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) + + indexInfo := NewIndexInfo(named) + repoInfoName := reference.TrimNamed(named) + + assert.Check(t, is.DeepEqual(indexInfo, expected.Index)) + assert.Check(t, is.Equal(reference.Path(repoInfoName), expected.RemoteName)) + assert.Check(t, is.Equal(reference.FamiliarName(repoInfoName), expected.LocalName)) + assert.Check(t, is.Equal(repoInfoName.Name(), expected.CanonicalName)) + }) + } +} diff --git a/internal/registry/service.go b/internal/registry/service.go new file mode 100644 index 0000000000..1936a2972a --- /dev/null +++ b/internal/registry/service.go @@ -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/docker/docker/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 +} diff --git a/internal/registry/service_v2.go b/internal/registry/service_v2.go new file mode 100644 index 0000000000..ccfb5ac509 --- /dev/null +++ b/internal/registry/service_v2.go @@ -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 +} diff --git a/vendor.mod b/vendor.mod index f971da50d4..b0744d0334 100644 --- a/vendor.mod +++ b/vendor.mod @@ -9,6 +9,7 @@ go 1.23.0 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 @@ -47,6 +48,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 @@ -70,7 +72,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 @@ -97,7 +98,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/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir.go b/vendor/github.com/docker/docker/pkg/homedir/homedir.go deleted file mode 100644 index c0ab3f5bf3..0000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir.go +++ /dev/null @@ -1,28 +0,0 @@ -package homedir - -import ( - "os" - "os/user" - "runtime" -) - -// Get returns the home directory of the current user with the help of -// environment variables depending on the target operating system. -// Returned path should be used with "path/filepath" to form new paths. -// -// On non-Windows platforms, it falls back to nss lookups, if the home -// directory cannot be obtained from environment-variables. -// -// If linking statically with cgo enabled against glibc, ensure the -// osusergo build tag is used. -// -// If needing to do nss lookups, do not disable cgo or set osusergo. -func Get() string { - home, _ := os.UserHomeDir() - if home == "" && runtime.GOOS != "windows" { - if u, err := user.Current(); err == nil { - return u.HomeDir - } - } - return home -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go b/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go deleted file mode 100644 index 469395f16e..0000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go +++ /dev/null @@ -1,105 +0,0 @@ -package homedir - -import ( - "errors" - "os" - "path/filepath" - "strings" -) - -// GetRuntimeDir returns XDG_RUNTIME_DIR. -// XDG_RUNTIME_DIR is typically configured via pam_systemd. -// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetRuntimeDir() (string, error) { - if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" { - return xdgRuntimeDir, nil - } - return "", errors.New("could not get XDG_RUNTIME_DIR") -} - -// StickRuntimeDirContents sets the sticky bit on files that are under -// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system. -// -// StickyRuntimeDir returns slice of sticked files. -// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func StickRuntimeDirContents(files []string) ([]string, error) { - runtimeDir, err := GetRuntimeDir() - if err != nil { - // ignore error if runtimeDir is empty - return nil, nil - } - runtimeDir, err = filepath.Abs(runtimeDir) - if err != nil { - return nil, err - } - var sticked []string - for _, f := range files { - f, err = filepath.Abs(f) - if err != nil { - return sticked, err - } - if strings.HasPrefix(f, runtimeDir+"/") { - if err = stick(f); err != nil { - return sticked, err - } - sticked = append(sticked, f) - } - } - return sticked, nil -} - -func stick(f string) error { - st, err := os.Stat(f) - if err != nil { - return err - } - m := st.Mode() - m |= os.ModeSticky - return os.Chmod(f, m) -} - -// GetDataHome returns XDG_DATA_HOME. -// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set. -// If HOME and XDG_DATA_HOME are not set, getpwent(3) is consulted to determine the users home directory. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetDataHome() (string, error) { - if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { - return xdgDataHome, nil - } - home := Get() - if home == "" { - return "", errors.New("could not get either XDG_DATA_HOME or HOME") - } - return filepath.Join(home, ".local", "share"), nil -} - -// GetConfigHome returns XDG_CONFIG_HOME. -// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set. -// If HOME and XDG_CONFIG_HOME are not set, getpwent(3) is consulted to determine the users home directory. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetConfigHome() (string, error) { - if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { - return xdgConfigHome, nil - } - home := Get() - if home == "" { - return "", errors.New("could not get either XDG_CONFIG_HOME or HOME") - } - return filepath.Join(home, ".config"), nil -} - -// GetLibHome returns $HOME/.local/lib -// If HOME is not set, getpwent(3) is consulted to determine the users home directory. -func GetLibHome() (string, error) { - home := Get() - if home == "" { - return "", errors.New("could not get HOME") - } - return filepath.Join(home, ".local/lib"), nil -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go b/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go deleted file mode 100644 index 1e41e6aab5..0000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build !linux - -package homedir - -import ( - "errors" -) - -// GetRuntimeDir is unsupported on non-linux system. -func GetRuntimeDir() (string, error) { - return "", errors.New("homedir.GetRuntimeDir() is not supported on this system") -} - -// StickRuntimeDirContents is unsupported on non-linux system. -func StickRuntimeDirContents(files []string) ([]string, error) { - return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system") -} - -// GetDataHome is unsupported on non-linux system. -func GetDataHome() (string, error) { - return "", errors.New("homedir.GetDataHome() is not supported on this system") -} - -// GetConfigHome is unsupported on non-linux system. -func GetConfigHome() (string, error) { - return "", errors.New("homedir.GetConfigHome() is not supported on this system") -} - -// GetLibHome is unsupported on non-linux system. -func GetLibHome() (string, error) { - return "", errors.New("homedir.GetLibHome() is not supported on this system") -} diff --git a/vendor/github.com/docker/docker/registry/config.go b/vendor/github.com/docker/docker/registry/config.go deleted file mode 100644 index 218a12683a..0000000000 --- a/vendor/github.com/docker/docker/registry/config.go +++ /dev/null @@ -1,481 +0,0 @@ -package registry - -import ( - "context" - "net" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/internal/lazyregexp" - "github.com/docker/docker/pkg/homedir" -) - -// ServiceOptions holds command line options. -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 - -// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains -// are here for historic reasons and backward-compatibility. These domains -// are still supported by Docker Hub (and will continue to be supported), but -// there are new domains already in use, and plans to consolidate all legacy -// domains to new "canonical" domains. Once those domains are decided on, we -// should update these consts (but making sure to preserve compatibility with -// existing installs, clients, and user configuration). -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/" - // 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, - } - - validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`) - - // certsDir is used to override defaultCertsDir when running with rootlessKit. - // - // TODO(thaJeztah): change to a sync.OnceValue once we remove [SetCertsDir] - // TODO(thaJeztah): certsDir should not be a package variable, but stored in our config, and passed when needed. - setCertsDirOnce sync.Once - certsDir string -) - -func setCertsDir(dir string) string { - setCertsDirOnce.Do(func() { - if dir != "" { - certsDir = dir - return - } - if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" { - // Configure registry.CertsDir() when running in rootless-mode - // This is the equivalent of [rootless.RunningWithRootlessKit], - // but inlining it to prevent adding that as a dependency - // for docker/cli. - // - // [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 - if configHome, _ := homedir.GetConfigHome(); configHome != "" { - certsDir = filepath.Join(configHome, "docker/certs.d") - return - } - } - certsDir = defaultCertsDir - }) - return certsDir -} - -// SetCertsDir allows the default certs directory to be changed. This function -// is used at daemon startup to set the correct location when running in -// rootless mode. -// -// Deprecated: the cert-directory is now automatically selected when running with rootlessKit, and should no longer be set manually. -func SetCertsDir(path string) { - setCertsDir(path) -} - -// CertsDir is the directory where certificates are stored. -func CertsDir() string { - // call setCertsDir with an empty path to synchronise with [SetCertsDir] - return setCertsDir("") -} - -// 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 - } - 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) - indexConfigs = make(map[string]*registry.IndexInfo) - ) - -skip: - for _, r := range registries { - // validate insecure registry - if _, err := ValidateIndexName(r); err != nil { - return err - } - if strings.HasPrefix(strings.ToLower(r), "http://") { - log.G(context.TODO()).Warnf("insecure registry %s should not contain 'http://' and 'http://' has been removed from the insecure registry config", r) - r = r[7:] - } else if strings.HasPrefix(strings.ToLower(r), "https://") { - log.G(context.TODO()).Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r) - r = r[8:] - } else if hasScheme(r) { - return invalidParamf("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() { - continue skip - } - } - // ipnet is not found, add it in config.InsecureRegistryCIDRs - insecureRegistryCIDRs = append(insecureRegistryCIDRs, data) - } else { - if err := validateHostPort(r); err != nil { - return invalidParamWrapf(err, "insecure registry %s is not valid", r) - } - // Assume `host:port` if not CIDR. - indexConfigs[r] = ®istry.IndexInfo{ - Name: r, - Mirrors: []string{}, - Secure: false, - Official: false, - } - } - } - - // Configure public registry. - indexConfigs[IndexName] = ®istry.IndexInfo{ - Name: IndexName, - Mirrors: config.Mirrors, - Secure: true, - Official: true, - } - config.InsecureRegistryCIDRs = insecureRegistryCIDRs - config.IndexConfigs = indexConfigs - - return nil -} - -// isSecureIndex returns false if the provided indexName is part of the list of insecure registries -// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. -// -// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered -// insecure. -// -// indexName should be 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 in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element -// of insecureRegistries. -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 { - return index.Secure - } - - return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName) -} - -// for mocking in unit tests. -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 { - if len(cidrs) == 0 { - return false - } - - host, _, err := net.SplitHostPort(URLHost) - if err != nil { - // Assume URLHost is a host without port and go on. - host = URLHost - } - - var addresses []net.IP - if ip := net.ParseIP(host); ip != nil { - // Host is an IP-address. - addresses = append(addresses, ip) - } else { - // Try to resolve the host's IP-address. - addresses, err = lookupIP(host) - if err != nil { - // We failed to resolve the host; assume there's no match. - return false - } - } - - for _, addr := range addresses { - for _, ipnet := range cidrs { - // check if the addr falls in the subnet - if (*net.IPNet)(ipnet).Contains(addr) { - return true - } - } - } - - 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) - if err != nil { - host = s - port = "" - } - // If match against the `host:port` pattern fails, - // it might be `IPv6:port`, which will be captured by net.ParseIP(host) - if !validHostPortRegex.MatchString(s) && net.ParseIP(host) == nil { - return invalidParamf("invalid host %q", host) - } - if port != "" { - v, err := strconv.Atoi(port) - if err != nil { - return err - } - if v < 0 || v > 65535 { - return invalidParamf("invalid port %q", port) - } - } - 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 -} - -// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo -func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryInfo { - index := newIndexInfo(config, reference.Domain(name)) - var officialRepo bool - if index.Official { - // RepositoryInfo.Official indicates whether the image repository - // is an official (docker library official images) repository. - // - // We only need to check this if the image-repository is on Docker Hub. - officialRepo = !strings.ContainsRune(reference.FamiliarName(name), '/') - } - - return &RepositoryInfo{ - Name: reference.TrimNamed(name), - Index: index, - Official: officialRepo, - } -} - -// ParseRepositoryInfo performs the breakdown of a repository name into a -// [RepositoryInfo], but lacks registry configuration. -// -// It is used by the Docker cli to interact with registry-related endpoints. -func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { - indexName := normalizeIndexName(reference.Domain(reposName)) - if indexName == IndexName { - return &RepositoryInfo{ - Name: reference.TrimNamed(reposName), - Index: ®istry.IndexInfo{ - Name: IndexName, - Mirrors: []string{}, - Secure: true, - Official: true, - }, - Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'), - }, nil - } - - return &RepositoryInfo{ - Name: reference.TrimNamed(reposName), - Index: ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: !isInsecure(indexName), - }, - }, nil -} - -// isInsecure is used to detect whether a registry domain or IP-address is allowed -// to use an insecure (non-TLS, or self-signed cert) connection according to the -// defaults, which allows for insecure connections with registries running on a -// loopback address ("localhost", "::1/128", "127.0.0.0/8"). -// -// It is used in situations where we don't have access to the daemon's configuration, -// for example, when used from the client / CLI. -func isInsecure(hostNameOrIP string) bool { - // Attempt to strip port if present; this also strips brackets for - // IPv6 addresses with a port (e.g. "[::1]:5000"). - // - // This is best-effort; we'll continue using the address as-is if it fails. - if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { - hostNameOrIP = host - } - if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { - // Fast path; no need to resolve these, assuming nobody overrides - // "localhost" for anything else than a loopback address (sorry, not sorry). - return true - } - - var addresses []net.IP - if ip := net.ParseIP(hostNameOrIP); ip != nil { - addresses = append(addresses, ip) - } else { - // Try to resolve the host's IP-addresses. - addrs, _ := lookupIP(hostNameOrIP) - addresses = append(addresses, addrs...) - } - - for _, addr := range addresses { - if addr.IsLoopback() { - return true - } - } - return false -} diff --git a/vendor/github.com/docker/docker/registry/config_unix.go b/vendor/github.com/docker/docker/registry/config_unix.go deleted file mode 100644 index 6aa6cdcca3..0000000000 --- a/vendor/github.com/docker/docker/registry/config_unix.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !windows - -package registry - -// defaultCertsDir is the platform-specific default directory where certificates -// are stored. On Linux, it may be overridden through certsDir, for example, when -// running in rootless mode. -const defaultCertsDir = "/etc/docker/certs.d" - -// cleanPath is used to ensure that a directory name is valid on the target -// platform. It will be passed in something *similar* to a URL such as -// https:/index.docker.io/v1. Not all platforms support directory names -// which contain those characters (such as : on Windows) -func cleanPath(s string) string { - return s -} diff --git a/vendor/github.com/docker/docker/registry/config_windows.go b/vendor/github.com/docker/docker/registry/config_windows.go deleted file mode 100644 index fd13bffde0..0000000000 --- a/vendor/github.com/docker/docker/registry/config_windows.go +++ /dev/null @@ -1,20 +0,0 @@ -package registry - -import ( - "os" - "path/filepath" - "strings" -) - -// defaultCertsDir is the platform-specific default directory where certificates -// are stored. On Linux, it may be overridden through certsDir, for example, when -// running in rootless mode. -var defaultCertsDir = os.Getenv("programdata") + `\docker\certs.d` - -// cleanPath is used to ensure that a directory name is valid on the target -// platform. It will be passed in something *similar* to a URL such as -// https:\index.docker.io\v1. Not all platforms support directory names -// which contain those characters (such as : on Windows) -func cleanPath(s string) string { - return filepath.FromSlash(strings.ReplaceAll(s, ":", "")) -} diff --git a/vendor/github.com/docker/docker/registry/errors.go b/vendor/github.com/docker/docker/registry/errors.go deleted file mode 100644 index cc3a37da6e..0000000000 --- a/vendor/github.com/docker/docker/registry/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package registry - -import ( - "net/url" - - "github.com/docker/distribution/registry/api/errcode" - "github.com/pkg/errors" -) - -func translateV2AuthError(err error) error { - switch e := err.(type) { - case *url.Error: - switch e2 := e.Err.(type) { - case errcode.Error: - switch e2.Code { - case errcode.ErrorCodeUnauthorized: - return unauthorizedErr{err} - } - } - } - - return err -} - -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...)} -} - -type unauthorizedErr struct{ error } - -func (unauthorizedErr) Unauthorized() {} - -func (e unauthorizedErr) Cause() error { - return e.error -} - -func (e unauthorizedErr) Unwrap() error { - return e.error -} - -type invalidParameterErr struct{ error } - -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 -} diff --git a/vendor/github.com/docker/docker/registry/search.go b/vendor/github.com/docker/docker/registry/search.go deleted file mode 100644 index 26a14298ac..0000000000 --- a/vendor/github.com/docker/docker/registry/search.go +++ /dev/null @@ -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/docker/docker/api/types/filters" - "github.com/docker/docker/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 -} diff --git a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go b/vendor/github.com/docker/docker/registry/search_endpoint_v1.go deleted file mode 100644 index 2ac3cee829..0000000000 --- a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go +++ /dev/null @@ -1,207 +0,0 @@ -package registry - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "net/http" - "net/url" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/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. - return nil, invalidParamf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %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; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) - } - - // 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(transport http.RoundTripper) *http.Client { - return &http.Client{ - Transport: transport, - 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 -} diff --git a/vendor/github.com/docker/docker/registry/search_session.go b/vendor/github.com/docker/docker/registry/search_session.go deleted file mode 100644 index f2886b7d38..0000000000 --- a/vendor/github.com/docker/docker/registry/search_session.go +++ /dev/null @@ -1,247 +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" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/docker/docker/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(fmt.Sprintf("%d", 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 -} diff --git a/vendor/github.com/docker/docker/registry/service.go b/vendor/github.com/docker/docker/registry/service.go deleted file mode 100644 index 85299be32e..0000000000 --- a/vendor/github.com/docker/docker/registry/service.go +++ /dev/null @@ -1,174 +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/docker/docker/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 -} - -// ResolveRepository splits a repository name into its components -// and configuration of the associated registry. -// -// Deprecated: this function was only used internally and is no longer used. It will be removed in the next release. -func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { - s.mu.RLock() - defer s.mu.RUnlock() - // TODO(thaJeztah): remove error return as it's no longer used. - return newRepositoryInfo(s.config, name), nil -} - -// 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 - AllowNondistributableArtifacts bool // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. - Official bool // Deprecated: this field was only used internally, and will be removed in the next release. - TrimHostname bool // Deprecated: hostname is now trimmed unconditionally for remote names. This field will be removed in the next release. - 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) -} diff --git a/vendor/github.com/docker/docker/registry/service_v2.go b/vendor/github.com/docker/docker/registry/service_v2.go deleted file mode 100644 index 6b25a41dc3..0000000000 --- a/vendor/github.com/docker/docker/registry/service_v2.go +++ /dev/null @@ -1,74 +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, - Official: true, - 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 -} diff --git a/vendor/github.com/docker/docker/registry/types.go b/vendor/github.com/docker/docker/registry/types.go deleted file mode 100644 index bb081d5638..0000000000 --- a/vendor/github.com/docker/docker/registry/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package registry - -import ( - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" -) - -// RepositoryInfo describes a repository -type RepositoryInfo struct { - Name reference.Named - // Index points to registry information - Index *registry.IndexInfo - // Official indicates whether the repository is considered official. - // If the registry is official, and the normalized name does not - // contain a '/' (e.g. "foo"), then it is considered an official repo. - // - // Deprecated: this field is no longer used and will be removed in the next release. The information captured in this field can be obtained from the [Name] field instead. - Official bool - // Class represents the class of the repository, such as "plugin" - // or "image". - // - // Deprecated: this field is no longer used, and will be removed in the next release. - Class string -} diff --git a/vendor/modules.txt b/vendor/modules.txt index cebbc521b4..8ccf406adc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -92,12 +92,10 @@ github.com/docker/docker/api/types/volume github.com/docker/docker/client github.com/docker/docker/internal/lazyregexp github.com/docker/docker/internal/multierror -github.com/docker/docker/pkg/homedir github.com/docker/docker/pkg/jsonmessage github.com/docker/docker/pkg/progress github.com/docker/docker/pkg/stdcopy 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