diff --git a/cli/command/image/push.go b/cli/command/image/push.go index 88d8bec4fe..ff2bb39b91 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" diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index c44b51eb8f..e0288b9cba 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -11,9 +11,9 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" + "github.com/docker/cli/internal/registry" "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" diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go index a0c053ff06..a737f8d9c2 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" diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go index 4020680347..30e9c7f5d1 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" ) diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 3679e51edd..54c3e462d6 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" 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/service/trust.go b/cli/command/service/trust.go index 6fb4d129a5..49c65d668d 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" 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/registry/client/endpoint.go b/cli/registry/client/endpoint.go index 8b5431e1f8..1b966e330d 100644 --- a/cli/registry/client/endpoint.go +++ b/cli/registry/client/endpoint.go @@ -6,10 +6,10 @@ import ( "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" ) diff --git a/cli/registry/client/fetcher.go b/cli/registry/client/fetcher.go index f270d49432..e169179d16 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" diff --git a/cli/trust/trust.go b/cli/trust/trust.go index 27453ae22e..abfc8fdb5f 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" diff --git a/cli/trust/trust_push.go b/cli/trust/trust_push.go index 1a8c5e4b72..047990c453 100644 --- a/cli/trust/trust_push.go +++ b/cli/trust/trust_push.go @@ -11,9 +11,9 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/jsonstream" + "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/opencontainers/go-digest" "github.com/pkg/errors" "github.com/theupdateframework/notary/client" 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 90% rename from vendor/github.com/docker/docker/registry/auth.go rename to internal/registry/auth.go index 1b0eeeed0b..f99c58a121 100644 --- a/vendor/github.com/docker/docker/registry/auth.go +++ b/internal/registry/auth.go @@ -40,9 +40,9 @@ type staticCredentialStore struct { // NewStaticCredentialStore returns a credential store // which always returns the same credential values. -func NewStaticCredentialStore(auth *registry.AuthConfig) auth.CredentialStore { +func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore { return staticCredentialStore{ - auth: auth, + auth: ac, } } @@ -60,7 +60,7 @@ func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { return scs.auth.IdentityToken } -func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) { +func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) { } // loginV2 tries to login to the v2 registry server. The given registry @@ -131,12 +131,15 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi // 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://") +func ConvertToHostname(maybeURL string) string { + stripped := maybeURL + if scheme, remainder, ok := strings.Cut(stripped, "://"); ok { + switch scheme { + case "http", "https": + stripped = remainder + default: + // unknown, or no scheme; doing nothing for now, as we never did. + } } stripped, _, _ = strings.Cut(stripped, "/") return stripped @@ -175,9 +178,9 @@ func (err PingResponseError) Error() string { // 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) { +func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) { pingClient := &http.Client{ - Transport: transport, + Transport: authTransport, Timeout: 15 * time.Second, } endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" diff --git a/internal/registry/auth_test.go b/internal/registry/auth_test.go new file mode 100644 index 0000000000..927254177d --- /dev/null +++ b/internal/registry/auth_test.go @@ -0,0 +1,106 @@ +package registry + +import ( + "testing" + + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" +) + +func buildAuthConfigs() map[string]registry.AuthConfig { + authConfigs := map[string]registry.AuthConfig{} + + for _, reg := range []string{"testIndex", IndexServer} { + authConfigs[reg] = registry.AuthConfig{ + Username: "docker-user", + Password: "docker-pass", + } + } + + return authConfigs +} + +func TestResolveAuthConfigIndexServer(t *testing.T) { + authConfigs := buildAuthConfigs() + indexConfig := authConfigs[IndexServer] + + officialIndex := ®istry.IndexInfo{ + Official: true, + } + privateIndex := ®istry.IndexInfo{ + Official: false, + } + + resolved := ResolveAuthConfig(authConfigs, officialIndex) + assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") + + resolved = ResolveAuthConfig(authConfigs, privateIndex) + assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer") +} + +func TestResolveAuthConfigFullURL(t *testing.T) { + authConfigs := buildAuthConfigs() + + registryAuth := registry.AuthConfig{ + Username: "foo-user", + Password: "foo-pass", + } + localAuth := registry.AuthConfig{ + Username: "bar-user", + Password: "bar-pass", + } + officialAuth := registry.AuthConfig{ + Username: "baz-user", + Password: "baz-pass", + } + authConfigs[IndexServer] = officialAuth + + expectedAuths := map[string]registry.AuthConfig{ + "registry.example.com": registryAuth, + "localhost:8000": localAuth, + "example.com": localAuth, + } + + validRegistries := map[string][]string{ + "registry.example.com": { + "https://registry.example.com/v1/", + "http://registry.example.com/v1/", + "registry.example.com", + "registry.example.com/v1/", + }, + "localhost:8000": { + "https://localhost:8000/v1/", + "http://localhost:8000/v1/", + "localhost:8000", + "localhost:8000/v1/", + }, + "example.com": { + "https://example.com/v1/", + "http://example.com/v1/", + "example.com", + "example.com/v1/", + }, + } + + for configKey, registries := range validRegistries { + configured, ok := expectedAuths[configKey] + if !ok { + t.Fail() + } + index := ®istry.IndexInfo{ + Name: configKey, + } + for _, reg := range registries { + authConfigs[reg] = configured + resolved := ResolveAuthConfig(authConfigs, index) + if resolved.Username != configured.Username || resolved.Password != configured.Password { + t.Errorf("%s -> %v != %v\n", reg, resolved, configured) + } + delete(authConfigs, reg) + resolved = ResolveAuthConfig(authConfigs, index) + if resolved.Username == configured.Username || resolved.Password == configured.Password { + t.Errorf("%s -> %v == %v\n", reg, resolved, configured) + } + } + } +} diff --git a/vendor/github.com/docker/docker/registry/config.go b/internal/registry/config.go similarity index 79% rename from vendor/github.com/docker/docker/registry/config.go rename to internal/registry/config.go index 218a12683a..1aa466147c 100644 --- a/vendor/github.com/docker/docker/registry/config.go +++ b/internal/registry/config.go @@ -1,3 +1,6 @@ +// 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 ( @@ -6,6 +9,8 @@ import ( "net/url" "os" "path/filepath" + "regexp" + "runtime" "strconv" "strings" "sync" @@ -13,8 +18,6 @@ import ( "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. @@ -58,52 +61,36 @@ var ( 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 + validHostPortRegex = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`^` + reference.DomainRegexp.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. +// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit], +// but inlining it to prevent adding that as a dependency for docker/cli. // -// 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) +// [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 { - // call setCertsDir with an empty path to synchronise with [SetCertsDir] - return setCertsDir("") + 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 returns a new instance of ServiceConfig @@ -181,14 +168,15 @@ skip: 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) + 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 invalidParamf("insecure registry %s should not contain '://'", r) + } } // Check if CIDR was passed to --insecure-registry _, ipnet, err := net.ParseCIDR(r) @@ -253,18 +241,18 @@ func (config *serviceConfig) isSecureIndex(indexName string) bool { // 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`) +// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host") // where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be // resolved to IP addresses for matching. If resolution fails, false is returned. -func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool { +func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool { if len(cidrs) == 0 { return false } - host, _, err := net.SplitHostPort(URLHost) + host, _, err := net.SplitHostPort(urlHost) if err != nil { - // Assume URLHost is a host without port and go on. - host = URLHost + // Assume urlHost is a host without port and go on. + host = urlHost } var addresses []net.IP @@ -353,7 +341,7 @@ func validateHostPort(s string) error { } // 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 { + if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil { return invalidParamf("invalid host %q", host) } if port != "" { @@ -394,25 +382,6 @@ func GetAuthConfigKey(index *registry.IndexInfo) string { 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. // @@ -428,7 +397,6 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { Secure: true, Official: true, }, - Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'), }, nil } diff --git a/internal/registry/config_test.go b/internal/registry/config_test.go new file mode 100644 index 0000000000..699e763be6 --- /dev/null +++ b/internal/registry/config_test.go @@ -0,0 +1,340 @@ +package registry + +import ( + "testing" + + cerrdefs "github.com/containerd/errdefs" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestValidateMirror(t *testing.T) { + tests := []struct { + input string + output string + expectedErr string + }{ + // Valid cases + { + input: "http://mirror-1.example.com", + output: "http://mirror-1.example.com/", + }, + { + input: "http://mirror-1.example.com/", + output: "http://mirror-1.example.com/", + }, + { + input: "https://mirror-1.example.com", + output: "https://mirror-1.example.com/", + }, + { + input: "https://mirror-1.example.com/", + output: "https://mirror-1.example.com/", + }, + { + input: "http://localhost", + output: "http://localhost/", + }, + { + input: "https://localhost", + output: "https://localhost/", + }, + { + input: "http://localhost:5000", + output: "http://localhost:5000/", + }, + { + input: "https://localhost:5000", + output: "https://localhost:5000/", + }, + { + input: "http://127.0.0.1", + output: "http://127.0.0.1/", + }, + { + input: "https://127.0.0.1", + output: "https://127.0.0.1/", + }, + { + input: "http://127.0.0.1:5000", + output: "http://127.0.0.1:5000/", + }, + { + input: "https://127.0.0.1:5000", + output: "https://127.0.0.1:5000/", + }, + { + input: "http://mirror-1.example.com/v1/", + output: "http://mirror-1.example.com/v1/", + }, + { + input: "https://mirror-1.example.com/v1/", + output: "https://mirror-1.example.com/v1/", + }, + + // Invalid cases + { + input: "!invalid!://%as%", + expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`, + }, + { + input: "mirror-1.example.com", + expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`, + }, + { + input: "mirror-1.example.com:5000", + expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`, + }, + { + input: "ftp://mirror-1.example.com", + expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`, + }, + { + input: "http://mirror-1.example.com/?q=foo", + expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`, + }, + { + input: "http://mirror-1.example.com/v1/?q=foo", + expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`, + }, + { + input: "http://mirror-1.example.com/v1/?q=foo#frag", + expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`, + }, + { + input: "http://mirror-1.example.com?q=foo", + expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`, + }, + { + input: "https://mirror-1.example.com#frag", + expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`, + }, + { + input: "https://mirror-1.example.com/#frag", + expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`, + }, + { + input: "http://foo:bar@mirror-1.example.com/", + expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`, + }, + { + input: "https://mirror-1.example.com/v1/#frag", + expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`, + }, + { + input: "https://mirror-1.example.com?q", + expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`, + }, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + out, err := ValidateMirror(tc.input) + if tc.expectedErr != "" { + assert.Error(t, err, tc.expectedErr) + } else { + assert.NilError(t, err) + } + assert.Check(t, is.Equal(out, tc.output)) + }) + } +} + +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{"-invalid-registry"}, + err: "Cannot begin or end with a hyphen", + }, + { + 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 := &serviceConfig{} + err := config.loadInsecureRegistries(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)) + } + } +} + +func TestNewServiceConfig(t *testing.T) { + tests := []struct { + doc string + opts ServiceOptions + errStr string + }{ + { + doc: "empty config", + }, + { + doc: "invalid mirror", + opts: ServiceOptions{ + Mirrors: []string{"example.com:5000"}, + }, + errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`, + }, + { + doc: "valid mirror", + opts: ServiceOptions{ + Mirrors: []string{"https://example.com:5000"}, + }, + }, + { + doc: "invalid insecure registry", + opts: ServiceOptions{ + InsecureRegistries: []string{"[fe80::]/64"}, + }, + errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`, + }, + { + doc: "valid insecure registry", + opts: ServiceOptions{ + InsecureRegistries: []string{"102.10.8.1/24"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + _, err := newServiceConfig(tc.opts) + if tc.errStr != "" { + assert.Check(t, is.Error(err, tc.errStr)) + assert.Check(t, cerrdefs.IsInvalidArgument(err)) + } else { + assert.Check(t, err) + } + }) + } +} + +func TestValidateIndexName(t *testing.T) { + valid := []struct { + index string + expect string + }{ + { + index: "index.docker.io", + expect: "docker.io", + }, + { + index: "example.com", + expect: "example.com", + }, + { + index: "127.0.0.1:8080", + expect: "127.0.0.1:8080", + }, + { + index: "mytest-1.com", + expect: "mytest-1.com", + }, + { + index: "mirror-1.example.com/v1/?q=foo", + expect: "mirror-1.example.com/v1/?q=foo", + }, + } + + for _, testCase := range valid { + result, err := ValidateIndexName(testCase.index) + if assert.Check(t, err) { + assert.Check(t, is.Equal(testCase.expect, result)) + } + } +} + +func TestValidateIndexNameWithError(t *testing.T) { + invalid := []struct { + index string + err string + }{ + { + index: "docker.io-", + err: "invalid index name (docker.io-). Cannot begin or end with a hyphen", + }, + { + index: "-example.com", + err: "invalid index name (-example.com). Cannot begin or end with a hyphen", + }, + { + index: "mirror-1.example.com/v1/?q=foo-", + err: "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen", + }, + } + for _, testCase := range invalid { + _, err := ValidateIndexName(testCase.index) + assert.Check(t, is.Error(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/vendor/github.com/docker/docker/registry/errors.go b/internal/registry/errors.go similarity index 85% rename from vendor/github.com/docker/docker/registry/errors.go rename to internal/registry/errors.go index cc3a37da6e..d37155a789 100644 --- a/vendor/github.com/docker/docker/registry/errors.go +++ b/internal/registry/errors.go @@ -8,17 +8,13 @@ import ( ) 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} - } + 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 } diff --git a/vendor/github.com/docker/docker/registry/registry.go b/internal/registry/registry.go similarity index 91% rename from vendor/github.com/docker/docker/registry/registry.go rename to internal/registry/registry.go index d3b3fbc9ba..d69c1d9ec7 100644 --- a/vendor/github.com/docker/docker/registry/registry.go +++ b/internal/registry/registry.go @@ -8,6 +8,8 @@ import ( "net/http" "os" "path/filepath" + "runtime" + "strings" "time" "github.com/containerd/log" @@ -16,16 +18,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 diff --git a/internal/registry/registry_mock_test.go b/internal/registry/registry_mock_test.go new file mode 100644 index 0000000000..eb6b16de79 --- /dev/null +++ b/internal/registry/registry_mock_test.go @@ -0,0 +1,120 @@ +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 + testHTTPSServer *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)) + testHTTPSServer = httptest.NewTLSServer(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 makeHTTPSURL(req string) string { + return testHTTPSServer.URL + req +} + +func makeIndex(req string) *registry.IndexInfo { + return ®istry.IndexInfo{ + Name: makeURL(req), + } +} + +func makeHTTPSIndex(req string) *registry.IndexInfo { + return ®istry.IndexInfo{ + Name: makeHTTPSURL(req), + } +} + +func makePublicIndex() *registry.IndexInfo { + return ®istry.IndexInfo{ + Name: IndexServer, + Secure: true, + Official: true, + } +} + +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 interface{}, 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..8495174c36 --- /dev/null +++ b/internal/registry/registry_test.go @@ -0,0 +1,637 @@ +package registry + +import ( + "errors" + "net" + "testing" + + "github.com/distribution/reference" + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +// overrideLookupIP overrides net.LookupIP for testing. +func overrideLookupIP(t *testing.T) { + t.Helper() + restoreLookup := lookupIP + + // override net.LookupIP + lookupIP = func(host string) ([]net.IP, error) { + mockHosts := map[string][]net.IP{ + "": {net.ParseIP("0.0.0.0")}, + "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + "example.com": {net.ParseIP("42.42.42.42")}, + "other.com": {net.ParseIP("43.43.43.43")}, + } + if addrs, ok := mockHosts[host]; ok { + return addrs, nil + } + return nil, errors.New("lookup: no such host") + } + t.Cleanup(func() { + lookupIP = restoreLookup + }) +} + +func TestParseRepositoryInfo(t *testing.T) { + type staticRepositoryInfo struct { + Index *registry.IndexInfo + RemoteName string + CanonicalName string + LocalName string + } + + tests := map[string]staticRepositoryInfo{ + "fooo/bar": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "docker.io/fooo/bar", + }, + "library/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "nonlibrary/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "docker.io/nonlibrary/ubuntu", + }, + "ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "other/library": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "[::2]:8000/privatebase", + CanonicalName: "[::2]:8000/privatebase", + }, + "localhost:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + }, + "example.com/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + 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", + Mirrors: []string{}, + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + }, + "localhost/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Mirrors: []string{}, + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + }, + "localhost/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Mirrors: []string{}, + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + }, + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "index." + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Mirrors: []string{}, + 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, + Mirrors: []string{}, + 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, + Mirrors: []string{}, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + } + + for reposName, expected := range tests { + t.Run(reposName, func(t *testing.T) { + named, err := reference.ParseNormalizedNamed(reposName) + assert.NilError(t, err) + + repoInfo, err := ParseRepositoryInfo(named) + assert.NilError(t, err) + + assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index)) + assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName)) + assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName)) + assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName)) + }) + } +} + +func TestNewIndexInfo(t *testing.T) { + overrideLookupIP(t) + + // ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128" + ipv6Loopback := &net.IPNet{ + IP: net.IPv6loopback, + Mask: net.CIDRMask(128, 128), + } + + // ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8") + ipv4Loopback := &net.IPNet{ + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + } + + // emptyServiceConfig is a default service-config for situations where + // no config-file is available (e.g. when used in the CLI). It won't + // have mirrors configured, but does have the default insecure registry + // CIDRs for loopback interfaces configured. + emptyServiceConfig := &serviceConfig{ + IndexConfigs: map[string]*registry.IndexInfo{ + IndexName: { + Name: IndexName, + Mirrors: []string{}, + Secure: true, + Official: true, + }, + }, + InsecureRegistryCIDRs: []*registry.NetIPNet{ + (*registry.NetIPNet)(ipv6Loopback), + (*registry.NetIPNet)(ipv4Loopback), + }, + } + + expectedIndexInfos := map[string]*registry.IndexInfo{ + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: []string{}, + }, + "index." + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: []string{}, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + } + t.Run("no mirrors", func(t *testing.T) { + for indexName, expected := range expectedIndexInfos { + t.Run(indexName, func(t *testing.T) { + actual := newIndexInfo(emptyServiceConfig, indexName) + assert.Check(t, is.DeepEqual(actual, expected)) + }) + } + }) + + expectedIndexInfos = map[string]*registry.IndexInfo{ + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"}, + }, + "index." + IndexName: { + Name: IndexName, + Official: true, + Secure: true, + Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"}, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "127.255.255.255": { + Name: "127.255.255.255", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "127.255.255.255:5000": { + Name: "127.255.255.255:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "::1": { + Name: "::1", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "[::1]:5000": { + Name: "[::1]:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "::2": { + Name: "::2", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:5000": { + Name: "[::2]:5000", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + } + t.Run("mirrors", func(t *testing.T) { + // Note that newServiceConfig calls ValidateMirror internally, which normalizes + // mirror-URLs to have a trailing slash. + config, err := newServiceConfig(ServiceOptions{ + Mirrors: []string{"http://mirror1.local", "http://mirror2.local"}, + InsecureRegistries: []string{"example.com"}, + }) + assert.NilError(t, err) + for indexName, expected := range expectedIndexInfos { + t.Run(indexName, func(t *testing.T) { + actual := newIndexInfo(config, indexName) + assert.Check(t, is.DeepEqual(actual, expected)) + }) + } + }) + + expectedIndexInfos = map[string]*registry.IndexInfo{ + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "42.42.0.1:5000": { + Name: "42.42.0.1:5000", + Official: false, + Secure: false, + Mirrors: []string{}, + }, + "42.43.0.1:5000": { + Name: "42.43.0.1:5000", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: []string{}, + }, + } + t.Run("custom insecure", func(t *testing.T) { + config, err := newServiceConfig(ServiceOptions{ + InsecureRegistries: []string{"42.42.0.0/16"}, + }) + assert.NilError(t, err) + for indexName, expected := range expectedIndexInfos { + t.Run(indexName, func(t *testing.T) { + actual := newIndexInfo(config, indexName) + assert.Check(t, is.DeepEqual(actual, expected)) + }) + } + }) +} + +func TestMirrorEndpointLookup(t *testing.T) { + containsMirror := func(endpoints []APIEndpoint) bool { + for _, pe := range endpoints { + if pe.URL.Host == "my.mirror" { + return true + } + } + return false + } + cfg, err := newServiceConfig(ServiceOptions{ + Mirrors: []string{"https://my.mirror"}, + }) + assert.NilError(t, err) + s := Service{config: cfg} + + imageName, err := reference.WithName(IndexName + "/test/image") + if err != nil { + t.Error(err) + } + pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName)) + if err != nil { + t.Fatal(err) + } + if containsMirror(pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") + } + + pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName)) + if err != nil { + t.Fatal(err) + } + if !containsMirror(pullAPIEndpoints) { + t.Fatal("Pull endpoint should contain mirror") + } +} + +func TestIsSecureIndex(t *testing.T) { + overrideLookupIP(t) + tests := []struct { + addr string + insecureRegistries []string + expected bool + }{ + {IndexName, nil, true}, + {"example.com", []string{}, true}, + {"example.com", []string{"example.com"}, false}, + {"localhost", []string{"localhost:5000"}, false}, + {"localhost:5000", []string{"localhost:5000"}, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false}, + {"localhost", nil, false}, + {"localhost:5000", nil, false}, + {"127.0.0.1", nil, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"example.com", nil, true}, + {"example.com", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"example.com"}, false}, + {"example.com:5000", []string{"42.42.0.0/16"}, false}, + {"example.com", []string{"42.42.0.0/16"}, false}, + {"example.com:5000", []string{"42.42.42.42/8"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.0/8"}, false}, + {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false}, + {"invalid.example.com", []string{"42.42.0.0/16"}, true}, + {"invalid.example.com", []string{"invalid.example.com"}, false}, + {"invalid.example.com:5000", []string{"invalid.example.com"}, true}, + {"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false}, + } + for _, tc := range tests { + config, err := newServiceConfig(ServiceOptions{ + InsecureRegistries: tc.insecureRegistries, + }) + assert.NilError(t, err) + + sec := config.isSecureIndex(tc.addr) + assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec) + } +} diff --git a/vendor/github.com/docker/docker/registry/search.go b/internal/registry/search.go similarity index 100% rename from vendor/github.com/docker/docker/registry/search.go rename to internal/registry/search.go diff --git a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go b/internal/registry/search_endpoint_v1.go similarity index 92% rename from vendor/github.com/docker/docker/registry/search_endpoint_v1.go rename to internal/registry/search_endpoint_v1.go index 2ac3cee829..2b20c6044e 100644 --- a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go +++ b/internal/registry/search_endpoint_v1.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/json" "errors" + "fmt" "net/http" "net/url" "strings" @@ -58,7 +59,12 @@ func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http. 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) + hint := fmt.Sprintf( + ". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+ + "In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt", + endpoint.URL.Host, + ) + return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint) } // registry is insecure and HTTPS failed, fallback to HTTP. @@ -163,9 +169,9 @@ func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) { // 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 { +func httpClient(tr http.RoundTripper) *http.Client { return &http.Client{ - Transport: transport, + Transport: tr, CheckRedirect: addRequiredHeadersToRedirectedRequests, } } diff --git a/internal/registry/search_endpoint_v1_test.go b/internal/registry/search_endpoint_v1_test.go new file mode 100644 index 0000000000..a430f34cc1 --- /dev/null +++ b/internal/registry/search_endpoint_v1_test.go @@ -0,0 +1,237 @@ +package registry + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestV1EndpointPing(t *testing.T) { + testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) { + ep, err := newV1Endpoint(context.Background(), index, nil) + if err != nil { + t.Fatal(err) + } + regInfo, err := ep.ping(context.Background()) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage) + } + + testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makePublicIndex(), false, "Expected standalone to be false for public index") +} + +func TestV1Endpoint(t *testing.T) { + // Simple wrapper to fail test if err != nil + expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint { + endpoint, err := newV1Endpoint(context.Background(), index, nil) + if err != nil { + t.Fatal(err) + } + return endpoint + } + + assertInsecureIndex := func(index *registry.IndexInfo) { + index.Secure = true + _, err := newV1Endpoint(context.Background(), index, nil) + assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index") + index.Secure = false + } + + assertSecureIndex := func(index *registry.IndexInfo) { + index.Secure = true + _, err := newV1Endpoint(context.Background(), index, nil) + assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index") + index.Secure = false + } + + index := ®istry.IndexInfo{} + index.Name = makeURL("/v1/") + endpoint := expandEndpoint(index) + assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + assertInsecureIndex(index) + + index.Name = makeURL("") + endpoint = expandEndpoint(index) + assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + assertInsecureIndex(index) + + httpURL := makeURL("") + index.Name = strings.SplitN(httpURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") + assertInsecureIndex(index) + + index.Name = makeHTTPSURL("/v1/") + endpoint = expandEndpoint(index) + assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + assertSecureIndex(index) + + index.Name = makeHTTPSURL("") + endpoint = expandEndpoint(index) + assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + assertSecureIndex(index) + + httpsURL := makeHTTPSURL("") + index.Name = strings.SplitN(httpsURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") + assertSecureIndex(index) + + badEndpoints := []string{ + "http://127.0.0.1/v1/", + "https://127.0.0.1/v1/", + "http://127.0.0.1", + "https://127.0.0.1", + "127.0.0.1", + } + for _, address := range badEndpoints { + index.Name = address + _, err := newV1Endpoint(context.Background(), index, nil) + assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address) + } +} + +func TestV1EndpointParse(t *testing.T) { + tests := []struct { + address string + expected string + expectedErr string + }{ + { + address: IndexServer, + expected: IndexServer, + }, + { + address: "https://0.0.0.0:5000/v1/", + expected: "https://0.0.0.0:5000/v1/", + }, + { + address: "https://0.0.0.0:5000", + expected: "https://0.0.0.0:5000/v1/", + }, + { + address: "0.0.0.0:5000", + expected: "https://0.0.0.0:5000/v1/", + }, + { + address: "https://0.0.0.0:5000/nonversion/", + expected: "https://0.0.0.0:5000/nonversion/v1/", + }, + { + address: "https://0.0.0.0:5000/v0/", + expected: "https://0.0.0.0:5000/v0/v1/", + }, + { + address: "https://0.0.0.0:5000/v2/", + expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/", + }, + } + for _, tc := range tests { + t.Run(tc.address, func(t *testing.T) { + ep, err := newV1EndpointFromStr(tc.address, nil, nil) + if tc.expectedErr != "" { + assert.Check(t, is.Error(err, tc.expectedErr)) + assert.Check(t, is.Nil(ep)) + } else { + assert.NilError(t, err) + assert.Check(t, is.Equal(ep.String(), tc.expected)) + } + }) + } +} + +// Ensure that a registry endpoint that responds with a 401 only is determined +// to be a valid v1 registry endpoint +func TestV1EndpointValidate(t *testing.T) { + requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`) + w.WriteHeader(http.StatusUnauthorized) + }) + + // Make a test server which should validate as a v1 server. + testServer := httptest.NewServer(requireBasicAuthHandler) + defer testServer.Close() + + testEndpoint, err := newV1Endpoint(context.Background(), ®istry.IndexInfo{Name: testServer.URL}, nil) + if err != nil { + t.Fatal(err) + } + + if testEndpoint.URL.Scheme != "http" { + t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String()) + } +} + +func TestTrustedLocation(t *testing.T) { + for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} { + req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) + assert.Check(t, !trustedLocation(req)) + } + + for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} { + req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) + assert.Check(t, trustedLocation(req)) + } +} + +func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { + for _, urls := range [][]string{ + {"http://docker.io", "https://docker.com"}, + {"https://foo.docker.io:7777", "http://bar.docker.com"}, + {"https://foo.docker.io", "https://example.com"}, + } { + reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) + reqFrom.Header.Add("Content-Type", "application/json") + reqFrom.Header.Add("Authorization", "super_secret") + reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) + + _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) + + if len(reqTo.Header) != 1 { + t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header)) + } + + if reqTo.Header.Get("Content-Type") != "application/json" { + t.Fatal("'Content-Type' should be 'application/json'") + } + + if reqTo.Header.Get("Authorization") != "" { + t.Fatal("'Authorization' should be empty") + } + } + + for _, urls := range [][]string{ + {"https://docker.io", "https://docker.com"}, + {"https://foo.docker.io:7777", "https://bar.docker.com"}, + } { + reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) + reqFrom.Header.Add("Content-Type", "application/json") + reqFrom.Header.Add("Authorization", "super_secret") + reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) + + _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) + + if len(reqTo.Header) != 2 { + t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header)) + } + + if reqTo.Header.Get("Content-Type") != "application/json" { + t.Fatal("'Content-Type' should be 'application/json'") + } + + if reqTo.Header.Get("Authorization") != "super_secret" { + t.Fatal("'Authorization' should be 'super_secret'") + } + } +} diff --git a/vendor/github.com/docker/docker/registry/search_session.go b/internal/registry/search_session.go similarity index 98% rename from vendor/github.com/docker/docker/registry/search_session.go rename to internal/registry/search_session.go index f2886b7d38..825777ac2c 100644 --- a/vendor/github.com/docker/docker/registry/search_session.go +++ b/internal/registry/search_session.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/cookiejar" "net/url" + "strconv" "strings" "sync" @@ -219,7 +220,7 @@ func (r *session) searchRepositories(ctx context.Context, term string, limit int 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)) + u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit)) log.G(ctx).WithField("url", u).Debug("searchRepositories") req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) @@ -236,7 +237,7 @@ func (r *session) searchRepositories(ctx context.Context, term string, limit int 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)} + return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)} } result := ®istry.SearchResults{} err = json.NewDecoder(res.Body).Decode(result) diff --git a/internal/registry/search_test.go b/internal/registry/search_test.go new file mode 100644 index 0000000000..d9f563df17 --- /dev/null +++ b/internal/registry/search_test.go @@ -0,0 +1,418 @@ +package registry + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/http/httputil" + "testing" + + cerrdefs "github.com/containerd/errdefs" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" +) + +func spawnTestRegistrySession(t *testing.T) *session { + t.Helper() + authConfig := ®istry.AuthConfig{} + endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil) + if err != nil { + t.Fatal(err) + } + userAgent := "docker test client" + var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log} + tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...) + client := httpClient(tr) + + if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil { + t.Fatal(err) + } + r := newSession(client, endpoint) + + // In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true` + // header while authenticating, in order to retrieve a token that can be later used to + // perform authenticated actions. + // + // The mock v1 registry does not support that, (TODO(tiborvass): support it), instead, + // it will consider authenticated any request with the header `X-Docker-Token: fake-token`. + // + // Because we know that the client's transport is an `*authTransport` we simply cast it, + // in order to set the internal cached token to the fake token, and thus send that fake token + // upon every subsequent requests. + r.client.Transport.(*authTransport).token = []string{"fake-token"} + return r +} + +type debugTransport struct { + http.RoundTripper + log func(...interface{}) +} + +func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { + dump, err := httputil.DumpRequestOut(req, false) + if err != nil { + tr.log("could not dump request") + } + tr.log(string(dump)) + resp, err := tr.RoundTripper.RoundTrip(req) + if err != nil { + return nil, err + } + dump, err = httputil.DumpResponse(resp, false) + if err != nil { + tr.log("could not dump response") + } + tr.log(string(dump)) + return resp, err +} + +func TestSearchRepositories(t *testing.T) { + r := spawnTestRegistrySession(t) + results, err := r.searchRepositories(context.Background(), "fakequery", 25) + if err != nil { + t.Fatal(err) + } + if results == nil { + t.Fatal("Expected non-nil SearchResults object") + } + assert.Equal(t, results.NumResults, 1, "Expected 1 search results") + assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query") + assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") +} + +func TestSearchErrors(t *testing.T) { + errorCases := []struct { + filtersArgs filters.Args + shouldReturnError bool + expectedError string + }{ + { + expectedError: "unexpected status code 500", + shouldReturnError: true, + }, + { + filtersArgs: filters.NewArgs(filters.Arg("type", "custom")), + expectedError: "invalid filter 'type'", + }, + { + filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")), + expectedError: "invalid filter 'is-automated=[invalid]'", + }, + { + filtersArgs: filters.NewArgs( + filters.Arg("is-automated", "true"), + filters.Arg("is-automated", "false"), + ), + expectedError: "invalid filter 'is-automated", + }, + { + filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")), + expectedError: "invalid filter 'is-official=[invalid]'", + }, + { + filtersArgs: filters.NewArgs( + filters.Arg("is-official", "true"), + filters.Arg("is-official", "false"), + ), + expectedError: "invalid filter 'is-official", + }, + { + filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")), + expectedError: "invalid filter 'stars=invalid'", + }, + { + filtersArgs: filters.NewArgs( + filters.Arg("stars", "1"), + filters.Arg("stars", "invalid"), + ), + expectedError: "invalid filter 'stars=invalid'", + }, + } + for _, tc := range errorCases { + t.Run(tc.expectedError, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !tc.shouldReturnError { + t.Errorf("unexpected HTTP request") + } + http.Error(w, "no search for you", http.StatusInternalServerError) + })) + defer srv.Close() + + // Construct the search term by cutting the 'http://' prefix off srv.URL. + term := srv.URL[7:] + "/term" + + reg, err := NewService(ServiceOptions{}) + assert.NilError(t, err) + _, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{}) + assert.ErrorContains(t, err, tc.expectedError) + if tc.shouldReturnError { + assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err) + return + } + assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err) + }) + } +} + +func TestSearch(t *testing.T) { + const term = "term" + successCases := []struct { + name string + filtersArgs filters.Args + registryResults []registry.SearchResult + expectedResults []registry.SearchResult + }{ + { + name: "empty results", + registryResults: []registry.SearchResult{}, + expectedResults: []registry.SearchResult{}, + }, + { + name: "no filter", + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + }, + }, + }, + { + name: "is-automated=true, no results", + filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + }, + }, + expectedResults: []registry.SearchResult{}, + }, + { + name: "is-automated=true", + filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). + }, + }, + expectedResults: []registry.SearchResult{}, + }, + { + name: "is-automated=false, IsAutomated reset to false", + filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated). + }, + }, + }, + { + name: "is-automated=false", + filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + }, + }, + }, + { + name: "is-official=true, no results", + filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + }, + }, + expectedResults: []registry.SearchResult{}, + }, + { + name: "is-official=true", + filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsOfficial: true, + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsOfficial: true, + }, + }, + }, + { + name: "is-official=false, no results", + filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsOfficial: true, + }, + }, + expectedResults: []registry.SearchResult{}, + }, + { + name: "is-official=false", + filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsOfficial: false, + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + IsOfficial: false, + }, + }, + }, + { + name: "stars=0", + filtersArgs: filters.NewArgs(filters.Arg("stars", "0")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + StarCount: 0, + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + StarCount: 0, + }, + }, + }, + { + name: "stars=0, no results", + filtersArgs: filters.NewArgs(filters.Arg("stars", "1")), + registryResults: []registry.SearchResult{ + { + Name: "name", + Description: "description", + StarCount: 0, + }, + }, + expectedResults: []registry.SearchResult{}, + }, + { + name: "stars=1", + filtersArgs: filters.NewArgs(filters.Arg("stars", "1")), + registryResults: []registry.SearchResult{ + { + Name: "name0", + Description: "description0", + StarCount: 0, + }, + { + Name: "name1", + Description: "description1", + StarCount: 1, + }, + }, + expectedResults: []registry.SearchResult{ + { + Name: "name1", + Description: "description1", + StarCount: 1, + }, + }, + }, + { + name: "stars=1, is-official=true, is-automated=true", + filtersArgs: filters.NewArgs( + filters.Arg("stars", "1"), + filters.Arg("is-official", "true"), + filters.Arg("is-automated", "true"), + ), + registryResults: []registry.SearchResult{ + { + Name: "name0", + Description: "description0", + StarCount: 0, + IsOfficial: true, + IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). + }, + { + Name: "name1", + Description: "description1", + StarCount: 1, + IsOfficial: false, + IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). + }, + { + Name: "name2", + Description: "description2", + StarCount: 1, + IsOfficial: true, + }, + { + Name: "name3", + Description: "description3", + StarCount: 2, + IsOfficial: true, + IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). + }, + }, + expectedResults: []registry.SearchResult{}, + }, + } + for _, tc := range successCases { + t.Run(tc.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-type", "application/json") + json.NewEncoder(w).Encode(registry.SearchResults{ + Query: term, + NumResults: len(tc.registryResults), + Results: tc.registryResults, + }) + })) + defer srv.Close() + + // Construct the search term by cutting the 'http://' prefix off srv.URL. + searchTerm := srv.URL[7:] + "/" + term + + reg, err := NewService(ServiceOptions{}) + assert.NilError(t, err) + results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{}) + assert.NilError(t, err) + assert.DeepEqual(t, results, tc.expectedResults) + }) + } +} diff --git a/vendor/github.com/docker/docker/registry/service.go b/internal/registry/service.go similarity index 82% rename from vendor/github.com/docker/docker/registry/service.go rename to internal/registry/service.go index 85299be32e..73c9edfcd1 100644 --- a/vendor/github.com/docker/docker/registry/service.go +++ b/internal/registry/service.go @@ -108,17 +108,6 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use 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. // @@ -139,12 +128,9 @@ func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, // 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 + Mirror bool + URL *url.URL + TLSConfig *tls.Config } // LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference. diff --git a/vendor/github.com/docker/docker/registry/service_v2.go b/internal/registry/service_v2.go similarity index 98% rename from vendor/github.com/docker/docker/registry/service_v2.go rename to internal/registry/service_v2.go index 6b25a41dc3..df343a155d 100644 --- a/vendor/github.com/docker/docker/registry/service_v2.go +++ b/internal/registry/service_v2.go @@ -37,7 +37,6 @@ func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includ } endpoints = append(endpoints, APIEndpoint{ URL: DefaultV2Registry, - Official: true, TLSConfig: tlsconfig.ServerDefault(), }) diff --git a/internal/registry/types.go b/internal/registry/types.go new file mode 100644 index 0000000000..c35eb9fad2 --- /dev/null +++ b/internal/registry/types.go @@ -0,0 +1,13 @@ +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 +} 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_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/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