diff --git a/daemon/daemon.go b/daemon/daemon.go index 198423140b..5642785319 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -183,10 +183,8 @@ func (daemon *Daemon) RegistryHosts() docker.RegistryHosts { } for k, v := range m { - if d, err := registry.HostCertsDir(k); err == nil { - v.TLSConfigDir = []string{d} - m[k] = v - } + v.TLSConfigDir = []string{registry.HostCertsDir(k)} + m[k] = v } certsDir := registry.CertsDir() diff --git a/daemon/images/image_search_test.go b/daemon/images/image_search_test.go index 4fef86b6f2..725349bbae 100644 --- a/daemon/images/image_search_test.go +++ b/daemon/images/image_search_test.go @@ -11,16 +11,15 @@ import ( "github.com/docker/docker/registry" ) -type FakeService struct { - registry.DefaultService - +type fakeService struct { + registry.Service shouldReturnError bool term string results []registrytypes.SearchResult } -func (s *FakeService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { +func (s *fakeService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { if s.shouldReturnError { return nil, errors.New("Search unknown error") } @@ -76,7 +75,7 @@ func TestSearchRegistryForImagesErrors(t *testing.T) { } for index, e := range errorCases { daemon := &ImageService{ - registryService: &FakeService{ + registryService: &fakeService{ shouldReturnError: e.shouldReturnError, }, } @@ -322,7 +321,7 @@ func TestSearchRegistryForImages(t *testing.T) { } for index, s := range successCases { daemon := &ImageService{ - registryService: &FakeService{ + registryService: &fakeService{ term: term, results: s.registryResults, }, diff --git a/registry/auth.go b/registry/auth.go index 81533d269f..9a4c670213 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -10,15 +10,13 @@ import ( "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -const ( - // AuthClientID is used the ClientID used for the token server - AuthClientID = "docker" -) +// AuthClientID is used the ClientID used for the token server +const AuthClientID = "docker" type loginCredentialStore struct { authConfig *types.AuthConfig @@ -80,7 +78,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin var ( endpointStr = strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" modifiers = Headers(userAgent, nil) - authTransport = transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...) + authTransport = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) credentialAuthConfig = *authConfig creds = loginCredentialStore{authConfig: &credentialAuthConfig} ) @@ -109,8 +107,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin } // TODO(dmcgowan): Attempt to further interpret result, status code and error code string - err = errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) - return "", "", err + return "", "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) } func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) { @@ -129,10 +126,9 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) - tr := transport.NewTransport(authTransport, modifiers...) return &http.Client{ - Transport: tr, + Transport: transport.NewTransport(authTransport, modifiers...), Timeout: 15 * time.Second, }, nil } @@ -146,14 +142,11 @@ func ConvertToHostname(url string) string { } else if strings.HasPrefix(url, "https://") { stripped = strings.TrimPrefix(url, "https://") } - - nameParts := strings.SplitN(stripped, "/", 2) - - return nameParts[0] + return strings.SplitN(stripped, "/", 2)[0] } // ResolveAuthConfig matches an auth configuration to a server address or a URL -func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig { +func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registry.IndexInfo) types.AuthConfig { configKey := GetAuthConfigKey(index) // First try the happy case if c, found := authConfigs[configKey]; found || index.Official { diff --git a/registry/auth_test.go b/registry/auth_test.go index 511b2ef8bd..6e8809260b 100644 --- a/registry/auth_test.go +++ b/registry/auth_test.go @@ -4,14 +4,15 @@ import ( "testing" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" ) func buildAuthConfigs() map[string]types.AuthConfig { authConfigs := map[string]types.AuthConfig{} - for _, registry := range []string{"testIndex", IndexServer} { - authConfigs[registry] = types.AuthConfig{ + for _, reg := range []string{"testIndex", IndexServer} { + authConfigs[reg] = types.AuthConfig{ Username: "docker-user", Password: "docker-pass", } @@ -24,18 +25,18 @@ func TestResolveAuthConfigIndexServer(t *testing.T) { authConfigs := buildAuthConfigs() indexConfig := authConfigs[IndexServer] - officialIndex := ®istrytypes.IndexInfo{ + officialIndex := ®istry.IndexInfo{ Official: true, } - privateIndex := ®istrytypes.IndexInfo{ + privateIndex := ®istry.IndexInfo{ Official: false, } resolved := ResolveAuthConfig(authConfigs, officialIndex) - assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") + assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") resolved = ResolveAuthConfig(authConfigs, privateIndex) - assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServer") + assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer") } func TestResolveAuthConfigFullURL(t *testing.T) { @@ -87,19 +88,19 @@ func TestResolveAuthConfigFullURL(t *testing.T) { if !ok { t.Fail() } - index := ®istrytypes.IndexInfo{ + index := ®istry.IndexInfo{ Name: configKey, } - for _, registry := range registries { - authConfigs[registry] = configured + 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", registry, resolved, configured) + t.Errorf("%s -> %v != %v\n", reg, resolved, configured) } - delete(authConfigs, registry) + delete(authConfigs, reg) resolved = ResolveAuthConfig(authConfigs, index) if resolved.Username == configured.Username || resolved.Password == configured.Password { - t.Errorf("%s -> %v == %v\n", registry, resolved, configured) + t.Errorf("%s -> %v == %v\n", reg, resolved, configured) } } } diff --git a/registry/config.go b/registry/config.go index 977b0fd024..0d92a52813 100644 --- a/registry/config.go +++ b/registry/config.go @@ -1,7 +1,6 @@ package registry // import "github.com/docker/docker/registry" import ( - "fmt" "net" "net/url" "regexp" @@ -9,8 +8,7 @@ import ( "strings" "github.com/docker/distribution/reference" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" + "github.com/docker/docker/api/types/registry" "github.com/sirupsen/logrus" ) @@ -22,9 +20,7 @@ type ServiceOptions struct { } // serviceConfig holds daemon configuration for the registry service. -type serviceConfig struct { - registrytypes.ServiceConfig -} +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 @@ -58,10 +54,6 @@ var ( Host: DefaultRegistryHost, } - // ErrInvalidRepositoryName is an error returned if the repository name did - // not have the correct form - ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") - emptyServiceConfig, _ = newServiceConfig(ServiceOptions{}) validHostPortRegex = regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) @@ -71,57 +63,65 @@ var ( // newServiceConfig returns a new instance of ServiceConfig func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { - config := &serviceConfig{ - ServiceConfig: registrytypes.ServiceConfig{ - InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0), - IndexConfigs: make(map[string]*registrytypes.IndexInfo), - // Hack: Bypass setting the mirrors to IndexConfigs since they are going away - // and Mirrors are only for the official registry anyways. - }, - } - if err := config.LoadAllowNondistributableArtifacts(options.AllowNondistributableArtifacts); err != nil { + config := &serviceConfig{} + if err := config.loadAllowNondistributableArtifacts(options.AllowNondistributableArtifacts); err != nil { return nil, err } - if err := config.LoadMirrors(options.Mirrors); err != nil { + if err := config.loadMirrors(options.Mirrors); err != nil { return nil, err } - if err := config.LoadInsecureRegistries(options.InsecureRegistries); err != nil { + if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil { return nil, err } return config, nil } -// LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config. -func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []string) error { - cidrs := map[string]*registrytypes.NetIPNet{} +// 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{ + AllowNondistributableArtifactsCIDRs: append([]*registry.NetIPNet(nil), config.AllowNondistributableArtifactsCIDRs...), + AllowNondistributableArtifactsHostnames: append([]string(nil), config.AllowNondistributableArtifactsHostnames...), + InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...), + IndexConfigs: ic, + Mirrors: append([]string(nil), config.Mirrors...), + } +} + +// loadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config. +func (config *serviceConfig) loadAllowNondistributableArtifacts(registries []string) error { + cidrs := map[string]*registry.NetIPNet{} hostnames := map[string]bool{} for _, r := range registries { if _, err := ValidateIndexName(r); err != nil { return err } - if validateNoScheme(r) != nil { - return fmt.Errorf("allow-nondistributable-artifacts registry %s should not contain '://'", r) + if hasScheme(r) { + return invalidParamf("allow-nondistributable-artifacts registry %s should not contain '://'", r) } if _, ipnet, err := net.ParseCIDR(r); err == nil { // Valid CIDR. - cidrs[ipnet.String()] = (*registrytypes.NetIPNet)(ipnet) - } else if err := validateHostPort(r); err == nil { + cidrs[ipnet.String()] = (*registry.NetIPNet)(ipnet) + } else if err = validateHostPort(r); err == nil { // Must be `host:port` if not CIDR. hostnames[r] = true } else { - return fmt.Errorf("allow-nondistributable-artifacts registry %s is not valid: %v", r, err) + return invalidParamWrapf(err, "allow-nondistributable-artifacts registry %s is not valid", r) } } - config.AllowNondistributableArtifactsCIDRs = make([]*(registrytypes.NetIPNet), 0) + config.AllowNondistributableArtifactsCIDRs = make([]*registry.NetIPNet, 0, len(cidrs)) for _, c := range cidrs { config.AllowNondistributableArtifactsCIDRs = append(config.AllowNondistributableArtifactsCIDRs, c) } - config.AllowNondistributableArtifactsHostnames = make([]string, 0) + config.AllowNondistributableArtifactsHostnames = make([]string, 0, len(hostnames)) for h := range hostnames { config.AllowNondistributableArtifactsHostnames = append(config.AllowNondistributableArtifactsHostnames, h) } @@ -129,9 +129,9 @@ func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []str return nil } -// LoadMirrors loads mirrors to config, after removing duplicates. +// loadMirrors loads mirrors to config, after removing duplicates. // Returns an error if mirrors contains an invalid mirror. -func (config *serviceConfig) LoadMirrors(mirrors []string) error { +func (config *serviceConfig) loadMirrors(mirrors []string) error { mMap := map[string]struct{}{} unique := []string{} @@ -149,40 +149,33 @@ func (config *serviceConfig) LoadMirrors(mirrors []string) error { config.Mirrors = unique // Configure public registry since mirrors may have changed. - config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{ - Name: IndexName, - Mirrors: config.Mirrors, - Secure: true, - Official: true, + 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 (especially on Boot2docker). - // - // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change - // daemon flags on boot2docker? +// 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, "127.0.0.0/8") - // Store original InsecureRegistryCIDRs and IndexConfigs - // Clean InsecureRegistryCIDRs and IndexConfigs in config, as passed registries has all insecure registry info. - originalCIDRs := config.ServiceConfig.InsecureRegistryCIDRs - originalIndexInfos := config.ServiceConfig.IndexConfigs - - config.ServiceConfig.InsecureRegistryCIDRs = make([]*registrytypes.NetIPNet, 0) - config.ServiceConfig.IndexConfigs = make(map[string]*registrytypes.IndexInfo) + 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 { - // before returning err, roll back to original data - config.ServiceConfig.InsecureRegistryCIDRs = originalCIDRs - config.ServiceConfig.IndexConfigs = originalIndexInfos return err } if strings.HasPrefix(strings.ToLower(r), "http://") { @@ -191,35 +184,27 @@ skip: } else if strings.HasPrefix(strings.ToLower(r), "https://") { logrus.Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r) r = r[8:] - } else if validateNoScheme(r) != nil { - // Insecure registry should not contain '://' - // before returning err, roll back to original data - config.ServiceConfig.InsecureRegistryCIDRs = originalCIDRs - config.ServiceConfig.IndexConfigs = originalIndexInfos - return fmt.Errorf("insecure registry %s should not contain '://'", r) + } 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 := (*registrytypes.NetIPNet)(ipnet) - for _, value := range config.InsecureRegistryCIDRs { + 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 - config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, data) - + insecureRegistryCIDRs = append(insecureRegistryCIDRs, data) } else { if err := validateHostPort(r); err != nil { - config.ServiceConfig.InsecureRegistryCIDRs = originalCIDRs - config.ServiceConfig.IndexConfigs = originalIndexInfos - return fmt.Errorf("insecure registry %s is not valid: %v", r, err) - + return invalidParamWrapf(err, "insecure registry %s is not valid", r) } // Assume `host:port` if not CIDR. - config.IndexConfigs[r] = ®istrytypes.IndexInfo{ + indexConfigs[r] = ®istry.IndexInfo{ Name: r, Mirrors: make([]string, 0), Secure: false, @@ -229,12 +214,14 @@ skip: } // Configure public registry. - config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{ + indexConfigs[IndexName] = ®istry.IndexInfo{ Name: IndexName, Mirrors: config.Mirrors, Secure: true, Official: true, } + config.InsecureRegistryCIDRs = insecureRegistryCIDRs + config.IndexConfigs = indexConfigs return nil } @@ -248,7 +235,7 @@ skip: // hostname 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 to IP addresses for matching. If // resolution fails, CIDR matching is not performed. -func allowNondistributableArtifacts(config *serviceConfig, hostname string) bool { +func (config *serviceConfig) allowNondistributableArtifacts(hostname string) bool { for _, h := range config.AllowNondistributableArtifactsHostnames { if h == hostname { return true @@ -269,7 +256,7 @@ func allowNondistributableArtifacts(config *serviceConfig, hostname string) bool // 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 isSecureIndex(config *serviceConfig, indexName string) bool { +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 { @@ -282,7 +269,7 @@ func isSecureIndex(config *serviceConfig, indexName string) bool { // 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 []*registrytypes.NetIPNet, URLHost string) bool { +func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool { host, _, err := net.SplitHostPort(URLHost) if err != nil { // Assume URLHost is of the form `host` without the port and go on. @@ -318,18 +305,18 @@ func isCIDRMatch(cidrs []*registrytypes.NetIPNet, URLHost string) bool { func ValidateMirror(val string) (string, error) { uri, err := url.Parse(val) if err != nil { - return "", fmt.Errorf("invalid mirror: %q is not a valid URI", val) + return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", val) } if uri.Scheme != "http" && uri.Scheme != "https" { - return "", fmt.Errorf("invalid mirror: unsupported scheme %q in %q", uri.Scheme, uri) + return "", invalidParamf("invalid mirror: unsupported scheme %q in %q", uri.Scheme, uri) } if (uri.Path != "" && uri.Path != "/") || uri.RawQuery != "" || uri.Fragment != "" { - return "", fmt.Errorf("invalid mirror: path, query, or fragment at end of the URI %q", uri) + return "", invalidParamf("invalid mirror: path, 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 "", fmt.Errorf("invalid mirror: username/password not allowed in URI %q", uri) + return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri) } return strings.TrimSuffix(val, "/") + "/", nil } @@ -341,17 +328,13 @@ func ValidateIndexName(val string) (string, error) { val = "docker.io" } if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") { - return "", fmt.Errorf("invalid index name (%s). Cannot begin or end with a hyphen", val) + return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val) } return val, nil } -func validateNoScheme(reposName string) error { - if strings.Contains(reposName, "://") { - // It cannot contain a scheme! - return ErrInvalidRepositoryName - } - return nil +func hasScheme(reposName string) bool { + return strings.Contains(reposName, "://") } func validateHostPort(s string) error { @@ -364,7 +347,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 { - return fmt.Errorf("invalid host %q", host) + return invalidParamf("invalid host %q", host) } if port != "" { v, err := strconv.Atoi(port) @@ -372,14 +355,14 @@ func validateHostPort(s string) error { return err } if v < 0 || v > 65535 { - return fmt.Errorf("invalid port %q", port) + return invalidParamf("invalid port %q", port) } } return nil } // newIndexInfo returns IndexInfo configuration from indexName -func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.IndexInfo, error) { +func newIndexInfo(config *serviceConfig, indexName string) (*registry.IndexInfo, error) { var err error indexName, err = ValidateIndexName(indexName) if err != nil { @@ -392,18 +375,17 @@ func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.Index } // Construct a non-configured index info. - index := ®istrytypes.IndexInfo{ + return ®istry.IndexInfo{ Name: indexName, Mirrors: make([]string, 0), + Secure: config.isSecureIndex(indexName), Official: false, - } - index.Secure = isSecureIndex(config, indexName) - return index, nil + }, nil } // 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 *registrytypes.IndexInfo) string { +func GetAuthConfigKey(index *registry.IndexInfo) string { if index.Official { return IndexServer } @@ -432,7 +414,12 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { } // ParseSearchIndexInfo will use repository name to get back an indexInfo. -func ParseSearchIndexInfo(reposName string) (*registrytypes.IndexInfo, error) { +// +// 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) indexInfo, err := newIndexInfo(emptyServiceConfig, indexName) diff --git a/registry/config_test.go b/registry/config_test.go index c939eff56f..123cedee0f 100644 --- a/registry/config_test.go +++ b/registry/config_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/docker/docker/errdefs" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -94,7 +95,7 @@ func TestLoadAllowNondistributableArtifacts(t *testing.T) { } for _, testCase := range testCases { config := emptyServiceConfig - err := config.LoadAllowNondistributableArtifacts(testCase.registries) + err := config.loadAllowNondistributableArtifacts(testCase.registries) if testCase.err == "" { if err != nil { t.Fatalf("expect no error, got '%s'", err) @@ -237,7 +238,7 @@ func TestLoadInsecureRegistries(t *testing.T) { } for _, testCase := range testCases { config := emptyServiceConfig - err := config.LoadInsecureRegistries(testCase.registries) + err := config.loadInsecureRegistries(testCase.registries) if testCase.err == "" { if err != nil { t.Fatalf("expect no error, got '%s'", err) @@ -255,9 +256,8 @@ func TestLoadInsecureRegistries(t *testing.T) { if err == nil { t.Fatalf("expect error '%s', got no error", testCase.err) } - if !strings.Contains(err.Error(), testCase.err) { - t.Fatalf("expect error '%s', got '%s'", testCase.err, err) - } + assert.ErrorContains(t, err, testCase.err) + assert.Check(t, errdefs.IsInvalidParameter(err)) } } } @@ -313,6 +313,7 @@ func TestNewServiceConfig(t *testing.T) { _, err := newServiceConfig(testCase.opts) if testCase.errStr != "" { assert.Check(t, is.Error(err, testCase.errStr)) + assert.Check(t, errdefs.IsInvalidParameter(err)) } else { assert.Check(t, err) } @@ -377,5 +378,6 @@ func TestValidateIndexNameWithError(t *testing.T) { for _, testCase := range invalid { _, err := ValidateIndexName(testCase.index) assert.Check(t, is.Error(err, testCase.err)) + assert.Check(t, errdefs.IsInvalidParameter(err)) } } diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go index 9268c3a4f0..e36db56a33 100644 --- a/registry/endpoint_test.go +++ b/registry/endpoint_test.go @@ -63,9 +63,9 @@ func TestValidateEndpoint(t *testing.T) { t.Fatal(err) } - testEndpoint := V1Endpoint{ + testEndpoint := v1Endpoint{ URL: testServerURL, - client: HTTPClient(NewTransport(nil)), + client: httpClient(newTransport(nil)), } if err = validateEndpoint(&testEndpoint); err != nil { diff --git a/registry/endpoint_v1.go b/registry/endpoint_v1.go index 684c330dc3..c7e930c8ad 100644 --- a/registry/endpoint_v1.go +++ b/registry/endpoint_v1.go @@ -3,27 +3,39 @@ package registry // import "github.com/docker/docker/registry" import ( "crypto/tls" "encoding/json" - "fmt" "io" "net/http" "net/url" "strings" "github.com/docker/distribution/registry/client/transport" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" "github.com/sirupsen/logrus" ) -// V1Endpoint stores basic information about a V1 registry endpoint. -type V1Endpoint struct { +// v1PingResult contains the information returned when pinging a registry. It +// indicates the registry's version and whether the registry claims to be a +// standalone registry. +type v1PingResult struct { + // Version is the registry version supplied by the registry in an HTTP + // header + Version string `json:"version"` + // 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. +// newV1Endpoint parses the given address to return a registry endpoint. // TODO: remove. This is only used by search. -func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { +func newV1Endpoint(index *registry.IndexInfo, userAgent string, metaHeaders http.Header) (*v1Endpoint, error) { tlsConfig, err := newTLSConfig(index.Name, index.Secure) if err != nil { return nil, err @@ -42,28 +54,28 @@ func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders return endpoint, nil } -func validateEndpoint(endpoint *V1Endpoint) error { +func validateEndpoint(endpoint *v1Endpoint) error { logrus.Debugf("pinging registry endpoint %s", endpoint) // Try HTTPS ping to registry endpoint.URL.Scheme = "https" - if _, err := endpoint.Ping(); err != nil { + if _, err := endpoint.ping(); err != nil { 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 fallback to HTTP. - return fmt.Errorf("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) + return 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) } // If registry is insecure and HTTPS failed, fallback to HTTP. - logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err) + logrus.WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint) endpoint.URL.Scheme = "http" var err2 error - if _, err2 = endpoint.Ping(); err2 == nil { + if _, err2 = endpoint.ping(); err2 == nil { return nil } - return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) + return invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) } return nil @@ -72,28 +84,23 @@ func validateEndpoint(endpoint *V1Endpoint) error { // trimV1Address trims the version off the address and returns the // trimmed address or an error if there is a non-V1 version. func trimV1Address(address string) (string, error) { - var ( - chunks []string - apiVersionStr string - ) - address = strings.TrimSuffix(address, "/") - chunks = strings.Split(address, "/") - apiVersionStr = chunks[len(chunks)-1] + chunks := strings.Split(address, "/") + apiVersionStr := chunks[len(chunks)-1] if apiVersionStr == "v1" { return strings.Join(chunks[:len(chunks)-1], "/"), nil } for k, v := range apiVersions { if k != APIVersion1 && apiVersionStr == v { - return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr) + return "", invalidParamf("unsupported V1 version path %s", apiVersionStr) } } return address, nil } -func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { +func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*v1Endpoint, error) { if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { address = "https://" + address } @@ -105,69 +112,64 @@ func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent strin uri, err := url.Parse(address) if err != nil { - return nil, err + return nil, invalidParam(err) } // TODO(tiborvass): make sure a ConnectTimeout transport is used - tr := NewTransport(tlsConfig) + tr := newTransport(tlsConfig) - return &V1Endpoint{ + return &v1Endpoint{ IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify, URL: uri, - client: HTTPClient(transport.NewTransport(tr, Headers(userAgent, metaHeaders)...)), + client: httpClient(transport.NewTransport(tr, Headers(userAgent, metaHeaders)...)), }, nil } // Get the formatted URL for the root of this registry Endpoint -func (e *V1Endpoint) String() string { +func (e *v1Endpoint) String() string { return e.URL.String() + "/v1/" } -// Path returns a formatted string for the URL -// of this endpoint with the given path appended. -func (e *V1Endpoint) Path(path string) string { - return e.URL.String() + "/v1/" + path -} - -// Ping returns a PingResult which indicates whether the registry is standalone or not. -func (e *V1Endpoint) Ping() (PingResult, error) { +// ping returns a v1PingResult which indicates whether the registry is standalone or not. +func (e *v1Endpoint) ping() (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 PingResult{}, nil + return v1PingResult{}, nil } logrus.Debugf("attempting v1 ping for registry endpoint %s", e) - req, err := http.NewRequest(http.MethodGet, e.Path("_ping"), nil) + pingURL := e.String() + "_ping" + req, err := http.NewRequest(http.MethodGet, pingURL, nil) if err != nil { - return PingResult{}, err + return v1PingResult{}, invalidParam(err) } resp, err := e.client.Do(req) if err != nil { - return PingResult{}, err + return v1PingResult{}, invalidParam(err) } defer resp.Body.Close() jsonString, err := io.ReadAll(resp.Body) if err != nil { - return PingResult{}, fmt.Errorf("error while reading the http response: %s", err) + return v1PingResult{}, invalidParamWrapf(err, "error while reading response from %s", pingURL) } // If the header is absent, we assume true for compatibility with earlier // versions of the registry. default to true - info := PingResult{ + info := v1PingResult{ Standalone: true, } if err := json.Unmarshal(jsonString, &info); err != nil { - logrus.Debugf("Error unmarshaling the _ping PingResult: %s", err) + logrus.WithError(err).Debug("error unmarshaling _ping response") // don't stop here. Just assume sane defaults } if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" { info.Version = hdr } - logrus.Debugf("PingResult.Version: %q", info.Version) + logrus.Debugf("v1PingResult.Version: %q", info.Version) standalone := resp.Header.Get("X-Docker-Registry-Standalone") @@ -178,6 +180,6 @@ func (e *V1Endpoint) Ping() (PingResult, error) { // there is a header set, and it is not "true" or "1", so assume fails info.Standalone = false } - logrus.Debugf("PingResult.Standalone: %t", info.Standalone) + logrus.Debugf("v1PingResult.Standalone: %t", info.Standalone) return info, nil } diff --git a/registry/errors.go b/registry/errors.go index 4906303efc..7dc20ad8ff 100644 --- a/registry/errors.go +++ b/registry/errors.go @@ -5,6 +5,7 @@ import ( "github.com/docker/distribution/registry/api/errcode" "github.com/docker/docker/errdefs" + "github.com/pkg/errors" ) func translateV2AuthError(err error) error { @@ -21,3 +22,15 @@ func translateV2AuthError(err error) error { return err } + +func invalidParam(err error) error { + return errdefs.InvalidParameter(err) +} + +func invalidParamf(format string, args ...interface{}) error { + return errdefs.InvalidParameter(errors.Errorf(format, args...)) +} + +func invalidParamWrapf(err error, format string, args ...interface{}) error { + return errdefs.InvalidParameter(errors.Wrapf(err, format, args...)) +} diff --git a/registry/registry.go b/registry/registry.go index 42f0f4dc9f..5ff39ce5e7 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -3,7 +3,6 @@ package registry // import "github.com/docker/docker/registry" import ( "crypto/tls" - "fmt" "net" "net/http" "os" @@ -16,15 +15,12 @@ import ( "github.com/sirupsen/logrus" ) -// HostCertsDir returns the config directory for a specific host -func HostCertsDir(hostname string) (string, error) { - certsDir := CertsDir() - - hostDir := filepath.Join(certsDir, cleanPath(hostname)) - - return hostDir, nil +// HostCertsDir returns the config directory for a specific host. +func HostCertsDir(hostname string) string { + return filepath.Join(CertsDir(), cleanPath(hostname)) } +// newTLSConfig constructs a client TLS configuration based on server defaults func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { // PreferredServerCipherSuites should have no effect tlsConfig := tlsconfig.ServerDefault() @@ -32,11 +28,7 @@ func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { tlsConfig.InsecureSkipVerify = !isSecure if isSecure && CertsDir() != "" { - hostDir, err := HostCertsDir(hostname) - if err != nil { - return nil, err - } - + hostDir := HostCertsDir(hostname) logrus.Debugf("hostDir: %s", hostDir) if err := ReadCertsDirectory(tlsConfig, hostDir); err != nil { return nil, err @@ -61,7 +53,7 @@ func hasFile(files []os.DirEntry, name string) bool { func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { fs, err := os.ReadDir(directory) if err != nil && !os.IsNotExist(err) { - return err + return invalidParam(err) } for _, f := range fs { @@ -69,7 +61,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { if tlsConfig.RootCAs == nil { systemPool, err := tlsconfig.SystemCertPool() if err != nil { - return fmt.Errorf("unable to get system cert pool: %v", err) + return invalidParamWrapf(err, "unable to get system cert pool") } tlsConfig.RootCAs = systemPool } @@ -85,7 +77,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { keyName := certName[:len(certName)-5] + ".key" logrus.Debugf("cert: %s", filepath.Join(directory, f.Name())) if !hasFile(fs, keyName) { - return fmt.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName) + return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName) } cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) if err != nil { @@ -98,7 +90,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { certName := keyName[:len(keyName)-4] + ".cert" logrus.Debugf("key: %s", filepath.Join(directory, f.Name())) if !hasFile(fs, certName) { - return fmt.Errorf("Missing client certificate %s for key %s", certName, keyName) + return invalidParamf("missing client certificate %s for key %s", certName, keyName) } } } @@ -120,9 +112,9 @@ func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModif return modifiers } -// HTTPClient returns an HTTP client structure which uses the given transport +// 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(transport http.RoundTripper) *http.Client { return &http.Client{ Transport: transport, CheckRedirect: addRequiredHeadersToRedirectedRequests, @@ -165,9 +157,9 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque return nil } -// NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the +// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the // default TLS configuration. -func NewTransport(tlsConfig *tls.Config) *http.Transport { +func newTransport(tlsConfig *tls.Config) *http.Transport { if tlsConfig == nil { tlsConfig = tlsconfig.ServerDefault() } @@ -177,7 +169,7 @@ func NewTransport(tlsConfig *tls.Config) *http.Transport { KeepAlive: 30 * time.Second, } - base := &http.Transport{ + return &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: direct.DialContext, TLSHandshakeTimeout: 10 * time.Second, @@ -185,6 +177,4 @@ func NewTransport(tlsConfig *tls.Config) *http.Transport { // TODO(dmcgowan): Call close idle connections when complete and use keep alive DisableKeepAlives: true, } - - return base } diff --git a/registry/registry_mock_test.go b/registry/registry_mock_test.go index 74c5a2b168..2baa215868 100644 --- a/registry/registry_mock_test.go +++ b/registry/registry_mock_test.go @@ -3,93 +3,21 @@ package registry // import "github.com/docker/docker/registry" import ( "encoding/json" "errors" - "fmt" "io" "net" "net/http" "net/http/httptest" - "net/url" - "strconv" - "strings" "testing" - "time" - "github.com/docker/distribution/reference" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" "github.com/gorilla/mux" - "github.com/sirupsen/logrus" + "gotest.tools/v3/assert" ) var ( testHTTPServer *httptest.Server testHTTPSServer *httptest.Server - testLayers = map[string]map[string]string{ - "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": { - "json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", - "comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00", - "container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0, - "CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false, - "Tty":false,"OpenStdin":false,"StdinOnce":false, - "Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null, - "VolumesFrom":"","Entrypoint":null},"Size":424242}`, - "checksum_simple": "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", - "checksum_tarsum": "tarsum+sha256:4409a0685741ca86d38df878ed6f8cbba4c99de5dc73cd71aef04be3bb70be7c", - "ancestry": `["77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`, - "layer": string([]byte{ - 0x1f, 0x8b, 0x08, 0x08, 0x0e, 0xb0, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65, - 0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd2, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05, - 0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0xed, 0x38, 0x4e, 0xce, 0x13, 0x44, 0x2b, 0x66, - 0x62, 0x24, 0x8e, 0x4f, 0xa0, 0x15, 0x63, 0xb6, 0x20, 0x21, 0xfc, 0x96, 0xbf, 0x78, - 0xb0, 0xf5, 0x1d, 0x16, 0x98, 0x8e, 0x88, 0x8a, 0x2a, 0xbe, 0x33, 0xef, 0x49, 0x31, - 0xed, 0x79, 0x40, 0x8e, 0x5c, 0x44, 0x85, 0x88, 0x33, 0x12, 0x73, 0x2c, 0x02, 0xa8, - 0xf0, 0x05, 0xf7, 0x66, 0xf5, 0xd6, 0x57, 0x69, 0xd7, 0x7a, 0x19, 0xcd, 0xf5, 0xb1, - 0x6d, 0x1b, 0x1f, 0xf9, 0xba, 0xe3, 0x93, 0x3f, 0x22, 0x2c, 0xb6, 0x36, 0x0b, 0xf6, - 0xb0, 0xa9, 0xfd, 0xe7, 0x94, 0x46, 0xfd, 0xeb, 0xd1, 0x7f, 0x2c, 0xc4, 0xd2, 0xfb, - 0x97, 0xfe, 0x02, 0x80, 0xe4, 0xfd, 0x4f, 0x77, 0xae, 0x6d, 0x3d, 0x81, 0x73, 0xce, - 0xb9, 0x7f, 0xf3, 0x04, 0x41, 0xc1, 0xab, 0xc6, 0x00, 0x0a, 0x00, 0x00, - }), - }, - "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d": { - "json": `{"id":"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", - "parent":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", - "comment":"test base image","created":"2013-03-23T12:55:11.10432-07:00", - "container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0, - "CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false, - "Tty":false,"OpenStdin":false,"StdinOnce":false, - "Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null, - "VolumesFrom":"","Entrypoint":null},"Size":424242}`, - "checksum_simple": "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2", - "checksum_tarsum": "tarsum+sha256:68fdb56fb364f074eec2c9b3f85ca175329c4dcabc4a6a452b7272aa613a07a2", - "ancestry": `["42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", - "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`, - "layer": string([]byte{ - 0x1f, 0x8b, 0x08, 0x08, 0xbd, 0xb3, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65, - 0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd1, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05, - 0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0x9d, 0x38, 0x8e, 0xcf, 0x53, 0x51, 0xaa, 0x56, - 0xea, 0x44, 0x82, 0xc4, 0xf1, 0x09, 0xb4, 0xea, 0x98, 0x2d, 0x48, 0x08, 0xbf, 0xe5, - 0x2f, 0x1e, 0xfc, 0xf5, 0xdd, 0x00, 0xdd, 0x11, 0x91, 0x8a, 0xe0, 0x27, 0xd3, 0x9e, - 0x14, 0xe2, 0x9e, 0x07, 0xf4, 0xc1, 0x2b, 0x0b, 0xfb, 0xa4, 0x82, 0xe4, 0x3d, 0x93, - 0x02, 0x0a, 0x7c, 0xc1, 0x23, 0x97, 0xf1, 0x5e, 0x5f, 0xc9, 0xcb, 0x38, 0xb5, 0xee, - 0xea, 0xd9, 0x3c, 0xb7, 0x4b, 0xbe, 0x7b, 0x9c, 0xf9, 0x23, 0xdc, 0x50, 0x6e, 0xb9, - 0xb8, 0xf2, 0x2c, 0x5d, 0xf7, 0x4f, 0x31, 0xb6, 0xf6, 0x4f, 0xc7, 0xfe, 0x41, 0x55, - 0x63, 0xdd, 0x9f, 0x89, 0x09, 0x90, 0x6c, 0xff, 0xee, 0xae, 0xcb, 0xba, 0x4d, 0x17, - 0x30, 0xc6, 0x18, 0xf3, 0x67, 0x5e, 0xc1, 0xed, 0x21, 0x5d, 0x00, 0x0a, 0x00, 0x00, - }), - }, - } - testRepositories = map[string]map[string]string{ - "foo42/bar": { - "latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", - "test": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", - }, - } - 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")}, - } ) func init() { @@ -97,14 +25,6 @@ func init() { // /v1/ r.HandleFunc("/v1/_ping", handlerGetPing).Methods(http.MethodGet) - r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|ancestry}", handlerGetImage).Methods(http.MethodGet) - r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|checksum}", handlerPutImage).Methods(http.MethodPut) - r.HandleFunc("/v1/repositories/{repository:.+}/tags", handlerGetDeleteTags).Methods(http.MethodGet, http.MethodDelete) - r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerGetTag).Methods(http.MethodGet) - r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerPutTag).Methods(http.MethodPut) - r.HandleFunc("/v1/users{null:.*}", handlerUsers).Methods(http.MethodGet, http.MethodPost, http.MethodPut) - r.HandleFunc("/v1/repositories/{repository:.+}{action:/images|/}", handlerImages).Methods(http.MethodGet, http.MethodPut, http.MethodDelete) - r.HandleFunc("/v1/repositories/{repository:.+}/auth", handlerAuth).Methods(http.MethodPut) r.HandleFunc("/v1/search", handlerSearch).Methods(http.MethodGet) // /v2/ @@ -119,6 +39,12 @@ func init() { // I believe in future Go versions this will fail, so let's fix it later return net.LookupIP(host) } + 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")}, + } for h, addrs := range mockHosts { if host == h { return addrs, nil @@ -135,7 +61,7 @@ func init() { func handlerAccessLog(handler http.Handler) http.Handler { logHandler := func(w http.ResponseWriter, r *http.Request) { - logrus.Debugf("%s \"%s %s\"", r.RemoteAddr, r.Method, r.URL) + logrus.Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL) handler.ServeHTTP(w, r) } return http.HandlerFunc(logHandler) @@ -149,22 +75,22 @@ func makeHTTPSURL(req string) string { return testHTTPSServer.URL + req } -func makeIndex(req string) *registrytypes.IndexInfo { - index := ®istrytypes.IndexInfo{ +func makeIndex(req string) *registry.IndexInfo { + index := ®istry.IndexInfo{ Name: makeURL(req), } return index } -func makeHTTPSIndex(req string) *registrytypes.IndexInfo { - index := ®istrytypes.IndexInfo{ +func makeHTTPSIndex(req string) *registry.IndexInfo { + index := ®istry.IndexInfo{ Name: makeHTTPSURL(req), } return index } -func makePublicIndex() *registrytypes.IndexInfo { - index := ®istrytypes.IndexInfo{ +func makePublicIndex() *registry.IndexInfo { + index := ®istry.IndexInfo{ Name: IndexServer, Secure: true, Official: true, @@ -203,252 +129,15 @@ func writeResponse(w http.ResponseWriter, message interface{}, code int) { w.Write(body) } -func readJSON(r *http.Request, dest interface{}) error { - body, err := io.ReadAll(r.Body) - if err != nil { - return err - } - return json.Unmarshal(body, dest) -} - -func apiError(w http.ResponseWriter, message string, code int) { - body := map[string]string{ - "error": message, - } - writeResponse(w, body, code) -} - -func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { - if a == b { - return - } - if len(message) == 0 { - message = fmt.Sprintf("%v != %v", a, b) - } - t.Fatal(message) -} - -func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) { - if a != b { - return - } - if len(message) == 0 { - message = fmt.Sprintf("%v == %v", a, b) - } - t.Fatal(message) -} - -// Similar to assertEqual, but does not stop test -func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { - if a == b { - return - } - message := fmt.Sprintf("%v != %v", a, b) - if len(messagePrefix) != 0 { - message = messagePrefix + ": " + message - } - t.Error(message) -} - -// Similar to assertNotEqual, but does not stop test -func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { - if a != b { - return - } - message := fmt.Sprintf("%v == %v", a, b) - if len(messagePrefix) != 0 { - message = messagePrefix + ": " + message - } - t.Error(message) -} - -func requiresAuth(w http.ResponseWriter, r *http.Request) bool { - writeCookie := func() { - value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()) - cookie := &http.Cookie{Name: "session", Value: value, MaxAge: 3600} - http.SetCookie(w, cookie) - // FIXME(sam): this should be sent only on Index routes - value = fmt.Sprintf("FAKE-TOKEN-%d", time.Now().UnixNano()) - w.Header().Add("X-Docker-Token", value) - } - if len(r.Cookies()) > 0 { - writeCookie() - return true - } - if len(r.Header.Get("Authorization")) > 0 { - writeCookie() - return true - } - w.Header().Add("WWW-Authenticate", "token") - apiError(w, "Wrong auth", http.StatusUnauthorized) - return false -} - func handlerGetPing(w http.ResponseWriter, r *http.Request) { writeResponse(w, true, http.StatusOK) } -func handlerGetImage(w http.ResponseWriter, r *http.Request) { - if !requiresAuth(w, r) { - return - } - vars := mux.Vars(r) - layer, exists := testLayers[vars["image_id"]] - if !exists { - http.NotFound(w, r) - return - } - writeHeaders(w) - layerSize := len(layer["layer"]) - w.Header().Add("X-Docker-Size", strconv.Itoa(layerSize)) - io.WriteString(w, layer[vars["action"]]) -} - -func handlerPutImage(w http.ResponseWriter, r *http.Request) { - if !requiresAuth(w, r) { - return - } - vars := mux.Vars(r) - imageID := vars["image_id"] - action := vars["action"] - layer, exists := testLayers[imageID] - if !exists { - if action != "json" { - http.NotFound(w, r) - return - } - layer = make(map[string]string) - testLayers[imageID] = layer - } - if checksum := r.Header.Get("X-Docker-Checksum"); checksum != "" { - if checksum != layer["checksum_simple"] && checksum != layer["checksum_tarsum"] { - apiError(w, "Wrong checksum", http.StatusBadRequest) - return - } - } - body, err := io.ReadAll(r.Body) - if err != nil { - apiError(w, fmt.Sprintf("Error: %s", err), http.StatusInternalServerError) - return - } - layer[action] = string(body) - writeResponse(w, true, http.StatusOK) -} - -func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) { - if !requiresAuth(w, r) { - return - } - repositoryName, err := reference.WithName(mux.Vars(r)["repository"]) - if err != nil { - apiError(w, "Could not parse repository", http.StatusBadRequest) - return - } - tags, exists := testRepositories[repositoryName.String()] - if !exists { - apiError(w, "Repository not found", http.StatusNotFound) - return - } - if r.Method == http.MethodDelete { - delete(testRepositories, repositoryName.String()) - writeResponse(w, true, http.StatusOK) - return - } - writeResponse(w, tags, http.StatusOK) -} - -func handlerGetTag(w http.ResponseWriter, r *http.Request) { - if !requiresAuth(w, r) { - return - } - vars := mux.Vars(r) - repositoryName, err := reference.WithName(vars["repository"]) - if err != nil { - apiError(w, "Could not parse repository", http.StatusBadRequest) - return - } - tagName := vars["tag"] - tags, exists := testRepositories[repositoryName.String()] - if !exists { - apiError(w, "Repository not found", http.StatusNotFound) - return - } - tag, exists := tags[tagName] - if !exists { - apiError(w, "Tag not found", http.StatusNotFound) - return - } - writeResponse(w, tag, http.StatusOK) -} - -func handlerPutTag(w http.ResponseWriter, r *http.Request) { - if !requiresAuth(w, r) { - return - } - vars := mux.Vars(r) - repositoryName, err := reference.WithName(vars["repository"]) - if err != nil { - apiError(w, "Could not parse repository", http.StatusBadRequest) - return - } - tagName := vars["tag"] - tags, exists := testRepositories[repositoryName.String()] - if !exists { - tags = make(map[string]string) - testRepositories[repositoryName.String()] = tags - } - tagValue := "" - readJSON(r, tagValue) - tags[tagName] = tagValue - writeResponse(w, true, http.StatusOK) -} - -func handlerUsers(w http.ResponseWriter, r *http.Request) { - code := http.StatusOK - if r.Method == http.MethodPost { - code = http.StatusCreated - } else if r.Method == http.MethodPut { - code = http.StatusNoContent - } - writeResponse(w, "", code) -} - -func handlerImages(w http.ResponseWriter, r *http.Request) { - u, _ := url.Parse(testHTTPServer.URL) - w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s , %s ", u.Host, "test.example.com")) - w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())) - if r.Method == http.MethodPut { - if strings.HasSuffix(r.URL.Path, "images") { - writeResponse(w, "", http.StatusNoContent) - return - } - writeResponse(w, "", http.StatusOK) - return - } - if r.Method == http.MethodDelete { - writeResponse(w, "", http.StatusNoContent) - return - } - var images []map[string]string - for imageID, layer := range testLayers { - image := make(map[string]string) - image["id"] = imageID - image["checksum"] = layer["checksum_tarsum"] - image["Tag"] = "latest" - images = append(images, image) - } - writeResponse(w, images, http.StatusOK) -} - -func handlerAuth(w http.ResponseWriter, r *http.Request) { - writeResponse(w, "OK", http.StatusOK) -} - func handlerSearch(w http.ResponseWriter, r *http.Request) { - result := ®istrytypes.SearchResults{ + result := ®istry.SearchResults{ Query: "fakequery", NumResults: 1, - Results: []registrytypes.SearchResult{{Name: "fakeimage", StarCount: 42}}, + Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}}, } writeResponse(w, result, http.StatusOK) } @@ -458,18 +147,6 @@ func TestPing(t *testing.T) { if err != nil { t.Fatal(err) } - assertEqual(t, res.StatusCode, http.StatusOK, "") - assertEqual(t, res.Header.Get("X-Docker-Registry-Config"), "mock", - "This is not a Mocked Registry") + assert.Equal(t, res.StatusCode, http.StatusOK, "") + assert.Equal(t, res.Header.Get("X-Docker-Registry-Config"), "mock", "This is not a Mocked Registry") } - -/* Uncomment this to test Mocked Registry locally with curl - * WARNING: Don't push on the repos uncommented, it'll block the tests - * -func TestWait(t *testing.T) { - logrus.Println("Test HTTP server ready and waiting:", testHTTPServer.URL) - c := make(chan int) - <-c -} - -//*/ diff --git a/registry/registry_test.go b/registry/registry_test.go index 03ac260d8d..e0f29404e6 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -10,25 +10,28 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" ) -func spawnTestRegistrySession(t *testing.T) *Session { +func spawnTestRegistrySession(t *testing.T) *session { authConfig := &types.AuthConfig{} - endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil) + endpoint, err := newV1Endpoint(makeIndex("/v1/"), "", nil) if err != nil { t.Fatal(err) } userAgent := "docker test client" - var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log} + var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log} tr = transport.NewTransport(AuthTransport(tr, authConfig, false), Headers(userAgent, nil)...) - client := HTTPClient(tr) - r, err := NewSession(client, authConfig, endpoint) - if err != nil { + client := httpClient(tr) + + if err := authorizeClient(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. @@ -45,17 +48,17 @@ func spawnTestRegistrySession(t *testing.T) *Session { func TestPingRegistryEndpoint(t *testing.T) { skip.If(t, os.Getuid() != 0, "skipping test that requires root") - testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) { - ep, err := NewV1Endpoint(index, "", nil) + testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) { + ep, err := newV1Endpoint(index, "", nil) if err != nil { t.Fatal(err) } - regInfo, err := ep.Ping() + regInfo, err := ep.ping() if err != nil { t.Fatal(err) } - assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage) + assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage) } testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") @@ -66,61 +69,59 @@ func TestPingRegistryEndpoint(t *testing.T) { func TestEndpoint(t *testing.T) { skip.If(t, os.Getuid() != 0, "skipping test that requires root") // Simple wrapper to fail test if err != nil - expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint { - endpoint, err := NewV1Endpoint(index, "", nil) + expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint { + endpoint, err := newV1Endpoint(index, "", nil) if err != nil { t.Fatal(err) } return endpoint } - assertInsecureIndex := func(index *registrytypes.IndexInfo) { + assertInsecureIndex := func(index *registry.IndexInfo) { index.Secure = true - _, err := NewV1Endpoint(index, "", nil) - assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index") - assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index") + _, err := newV1Endpoint(index, "", nil) + assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index") index.Secure = false } - assertSecureIndex := func(index *registrytypes.IndexInfo) { + assertSecureIndex := func(index *registry.IndexInfo) { index.Secure = true - _, err := NewV1Endpoint(index, "", nil) - assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index") - assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index") + _, err := newV1Endpoint(index, "", nil) + assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index") index.Secure = false } - index := ®istrytypes.IndexInfo{} + index := ®istry.IndexInfo{} index.Name = makeURL("/v1/") endpoint := expandEndpoint(index) - assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) assertInsecureIndex(index) index.Name = makeURL("") endpoint = expandEndpoint(index) - assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + 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) - assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") + assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") assertInsecureIndex(index) index.Name = makeHTTPSURL("/v1/") endpoint = expandEndpoint(index) - assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) assertSecureIndex(index) index.Name = makeHTTPSURL("") endpoint = expandEndpoint(index) - assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + 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) - assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") + assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") assertSecureIndex(index) badEndpoints := []string{ @@ -132,14 +133,14 @@ func TestEndpoint(t *testing.T) { } for _, address := range badEndpoints { index.Name = address - _, err := NewV1Endpoint(index, "", nil) - checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") + _, err := newV1Endpoint(index, "", nil) + assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address) } } func TestParseRepositoryInfo(t *testing.T) { type staticRepositoryInfo struct { - Index *registrytypes.IndexInfo + Index *registry.IndexInfo RemoteName string CanonicalName string LocalName string @@ -148,7 +149,7 @@ func TestParseRepositoryInfo(t *testing.T) { expectedRepoInfos := map[string]staticRepositoryInfo{ "fooo/bar": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -158,7 +159,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "library/ubuntu": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -168,7 +169,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: true, }, "nonlibrary/ubuntu": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -178,7 +179,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "ubuntu": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -188,7 +189,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: true, }, "other/library": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -198,7 +199,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "127.0.0.1:8000/private/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "127.0.0.1:8000", Official: false, }, @@ -208,7 +209,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "127.0.0.1:8000/privatebase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "127.0.0.1:8000", Official: false, }, @@ -218,7 +219,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "localhost:8000/private/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "localhost:8000", Official: false, }, @@ -228,7 +229,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "localhost:8000/privatebase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "localhost:8000", Official: false, }, @@ -238,7 +239,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "example.com/private/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "example.com", Official: false, }, @@ -248,7 +249,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "example.com/privatebase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "example.com", Official: false, }, @@ -258,7 +259,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "example.com:8000/private/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "example.com:8000", Official: false, }, @@ -268,7 +269,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "example.com:8000/privatebase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "example.com:8000", Official: false, }, @@ -278,7 +279,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "localhost/private/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "localhost", Official: false, }, @@ -288,7 +289,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "localhost/privatebase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: "localhost", Official: false, }, @@ -298,7 +299,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, IndexName + "/public/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -308,7 +309,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "index." + IndexName + "/public/moonbase": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -318,7 +319,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: false, }, "ubuntu-12.04-base": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -328,7 +329,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: true, }, IndexName + "/ubuntu-12.04-base": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -338,7 +339,7 @@ func TestParseRepositoryInfo(t *testing.T) { Official: true, }, "index." + IndexName + "/ubuntu-12.04-base": { - Index: ®istrytypes.IndexInfo{ + Index: ®istry.IndexInfo{ Name: IndexName, Official: true, }, @@ -359,34 +360,34 @@ func TestParseRepositoryInfo(t *testing.T) { if err != nil { t.Error(err) } else { - checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName) - checkEqual(t, reference.Path(repoInfo.Name), expectedRepoInfo.RemoteName, reposName) - checkEqual(t, reference.FamiliarName(repoInfo.Name), expectedRepoInfo.LocalName, reposName) - checkEqual(t, repoInfo.Name.Name(), expectedRepoInfo.CanonicalName, reposName) - checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName) - checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName) + assert.Check(t, is.Equal(repoInfo.Index.Name, expectedRepoInfo.Index.Name), reposName) + assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expectedRepoInfo.RemoteName), reposName) + assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expectedRepoInfo.LocalName), reposName) + assert.Check(t, is.Equal(repoInfo.Name.Name(), expectedRepoInfo.CanonicalName), reposName) + assert.Check(t, is.Equal(repoInfo.Index.Official, expectedRepoInfo.Index.Official), reposName) + assert.Check(t, is.Equal(repoInfo.Official, expectedRepoInfo.Official), reposName) } } } func TestNewIndexInfo(t *testing.T) { - testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registrytypes.IndexInfo) { + testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registry.IndexInfo) { for indexName, expectedIndexInfo := range expectedIndexInfos { index, err := newIndexInfo(config, indexName) if err != nil { t.Fatal(err) } else { - checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name") - checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official") - checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure") - checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors") + assert.Check(t, is.Equal(index.Name, expectedIndexInfo.Name), indexName+" name") + assert.Check(t, is.Equal(index.Official, expectedIndexInfo.Official), indexName+" is official") + assert.Check(t, is.Equal(index.Secure, expectedIndexInfo.Secure), indexName+" is secure") + assert.Check(t, is.Equal(len(index.Mirrors), len(expectedIndexInfo.Mirrors)), indexName+" mirrors") } } } config := emptyServiceConfig var noMirrors []string - expectedIndexInfos := map[string]*registrytypes.IndexInfo{ + expectedIndexInfos := map[string]*registry.IndexInfo{ IndexName: { Name: IndexName, Official: true, @@ -421,7 +422,7 @@ func TestNewIndexInfo(t *testing.T) { t.Fatal(err) } - expectedIndexInfos = map[string]*registrytypes.IndexInfo{ + expectedIndexInfos = map[string]*registry.IndexInfo{ IndexName: { Name: IndexName, Official: true, @@ -471,7 +472,7 @@ func TestNewIndexInfo(t *testing.T) { if err != nil { t.Fatal(err) } - expectedIndexInfos = map[string]*registrytypes.IndexInfo{ + expectedIndexInfos = map[string]*registry.IndexInfo{ "example.com": { Name: "example.com", Official: false, @@ -520,7 +521,7 @@ func TestMirrorEndpointLookup(t *testing.T) { if err != nil { t.Fatal(err) } - s := DefaultService{config: cfg} + s := defaultService{config: cfg} imageName, err := reference.WithName(IndexName + "/test/image") if err != nil { @@ -545,16 +546,16 @@ func TestMirrorEndpointLookup(t *testing.T) { func TestSearchRepositories(t *testing.T) { r := spawnTestRegistrySession(t) - results, err := r.SearchRepositories("fakequery", 25) + results, err := r.searchRepositories("fakequery", 25) if err != nil { t.Fatal(err) } if results == nil { t.Fatal("Expected non-nil SearchResults object") } - assertEqual(t, results.NumResults, 1, "Expected 1 search results") - assertEqual(t, results.Query, "fakequery", "Expected 'fakequery' as query") - assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") + 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 TestTrustedLocation(t *testing.T) { @@ -580,7 +581,7 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { reqFrom.Header.Add("Authorization", "super_secret") reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil) - addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) + _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) if len(reqTo.Header) != 1 { t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header)) @@ -604,7 +605,7 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { reqFrom.Header.Add("Authorization", "super_secret") reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil) - addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) + _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) if len(reqTo.Header) != 2 { t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header)) @@ -659,7 +660,7 @@ func TestAllowNondistributableArtifacts(t *testing.T) { if err != nil { t.Error(err) } - if v := allowNondistributableArtifacts(config, tt.addr); v != tt.expected { + if v := config.allowNondistributableArtifacts(tt.addr); v != tt.expected { t.Errorf("allowNondistributableArtifacts failed for %q %v, expected %v got %v", tt.addr, tt.registries, tt.expected, v) } } @@ -702,7 +703,7 @@ func TestIsSecureIndex(t *testing.T) { if err != nil { t.Error(err) } - if sec := isSecureIndex(config, tt.addr); sec != tt.expected { + if sec := config.isSecureIndex(tt.addr); sec != tt.expected { t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec) } } diff --git a/registry/service.go b/registry/service.go index 276b04724a..0cda3a8806 100644 --- a/registry/service.go +++ b/registry/service.go @@ -11,9 +11,8 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" "github.com/docker/docker/errdefs" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -28,85 +27,64 @@ type Service interface { LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) ResolveRepository(name reference.Named) (*RepositoryInfo, error) - Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) - ServiceConfig() *registrytypes.ServiceConfig + Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) + ServiceConfig() *registry.ServiceConfig TLSConfig(hostname string) (*tls.Config, error) LoadAllowNondistributableArtifacts([]string) error LoadMirrors([]string) error LoadInsecureRegistries([]string) error } -// DefaultService is a registry service. It tracks configuration data such as a list +// defaultService is a registry service. It tracks configuration data such as a list // of mirrors. -type DefaultService struct { +type defaultService struct { config *serviceConfig - mu sync.Mutex + mu sync.RWMutex } -// NewService returns a new instance of DefaultService ready to be +// NewService returns a new instance of defaultService ready to be // installed into an engine. -func NewService(options ServiceOptions) (*DefaultService, error) { +func NewService(options ServiceOptions) (Service, error) { config, err := newServiceConfig(options) - return &DefaultService{config: config}, err + return &defaultService{config: config}, err } -// ServiceConfig returns the public registry service configuration. -func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { - s.mu.Lock() - defer s.mu.Unlock() - - servConfig := registrytypes.ServiceConfig{ - AllowNondistributableArtifactsCIDRs: make([]*(registrytypes.NetIPNet), 0), - AllowNondistributableArtifactsHostnames: make([]string, 0), - InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0), - IndexConfigs: make(map[string]*(registrytypes.IndexInfo)), - Mirrors: make([]string, 0), - } - - // construct a new ServiceConfig which will not retrieve s.Config directly, - // and look up items in s.config with mu locked - servConfig.AllowNondistributableArtifactsCIDRs = append(servConfig.AllowNondistributableArtifactsCIDRs, s.config.ServiceConfig.AllowNondistributableArtifactsCIDRs...) - servConfig.AllowNondistributableArtifactsHostnames = append(servConfig.AllowNondistributableArtifactsHostnames, s.config.ServiceConfig.AllowNondistributableArtifactsHostnames...) - servConfig.InsecureRegistryCIDRs = append(servConfig.InsecureRegistryCIDRs, s.config.ServiceConfig.InsecureRegistryCIDRs...) - - for key, value := range s.config.ServiceConfig.IndexConfigs { - servConfig.IndexConfigs[key] = value - } - - servConfig.Mirrors = append(servConfig.Mirrors, s.config.ServiceConfig.Mirrors...) - - return &servConfig +// ServiceConfig returns a copy of the public registry service's configuration. +func (s *defaultService) ServiceConfig() *registry.ServiceConfig { + s.mu.RLock() + defer s.mu.RUnlock() + return s.config.copy() } // LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries for Service. -func (s *DefaultService) LoadAllowNondistributableArtifacts(registries []string) error { +func (s *defaultService) LoadAllowNondistributableArtifacts(registries []string) error { s.mu.Lock() defer s.mu.Unlock() - return s.config.LoadAllowNondistributableArtifacts(registries) + return s.config.loadAllowNondistributableArtifacts(registries) } // LoadMirrors loads registry mirrors for Service -func (s *DefaultService) LoadMirrors(mirrors []string) error { +func (s *defaultService) LoadMirrors(mirrors []string) error { s.mu.Lock() defer s.mu.Unlock() - return s.config.LoadMirrors(mirrors) + return s.config.loadMirrors(mirrors) } // LoadInsecureRegistries loads insecure registries for Service -func (s *DefaultService) LoadInsecureRegistries(registries []string) error { +func (s *defaultService) LoadInsecureRegistries(registries []string) error { s.mu.Lock() defer s.mu.Unlock() - return s.config.LoadInsecureRegistries(registries) + return s.config.loadInsecureRegistries(registries) } // 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 *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) { +func (s *defaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) { // TODO Use ctx when searching for repositories var registryHostName = IndexHostname @@ -117,7 +95,7 @@ func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, } u, err := url.Parse(serverAddress) if err != nil { - return "", "", errdefs.InvalidParameter(errors.Errorf("unable to parse server address: %v", err)) + return "", "", invalidParamWrapf(err, "unable to parse server address") } registryHostName = u.Host } @@ -127,7 +105,7 @@ func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, // to a mirror. endpoints, err := s.LookupPushEndpoints(registryHostName) if err != nil { - return "", "", errdefs.InvalidParameter(err) + return "", "", invalidParam(err) } for _, endpoint := range endpoints { @@ -159,25 +137,28 @@ func splitReposSearchTerm(reposName string) (string, string) { // Search queries the public registry for images matching the specified // search terms, and returns the results. -func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { +func (s *defaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) { // TODO Use ctx when searching for repositories - if err := validateNoScheme(term); err != nil { - return nil, err + 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.Lock() + s.mu.RLock() index, err := newIndexInfo(s.config, indexName) - s.mu.Unlock() + s.mu.RUnlock() if err != nil { return nil, err } + if index.Official { + // If pull "library/foo", it's stored locally under "foo" + remoteName = strings.TrimPrefix(remoteName, "library/") + } - // *TODO: Search multiple indexes. - endpoint, err := NewV1Endpoint(index, userAgent, headers) + endpoint, err := newV1Endpoint(index, userAgent, headers) if err != nil { return nil, err } @@ -196,7 +177,7 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes) if err != nil { if fErr, ok := err.(fallbackError); ok { - logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err) + logrus.WithError(fErr.err).Error("cannot use identity token for search, v2 auth not supported") } else { return nil, err } @@ -218,20 +199,14 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut } } - r := newSession(client, authConfig, endpoint) - - if index.Official { - // If pull "library/foo", it's stored locally under "foo" - remoteName = strings.TrimPrefix(remoteName, "library/") - } - return r.SearchRepositories(remoteName, limit) + return newSession(client, endpoint).searchRepositories(remoteName, limit) } // ResolveRepository splits a repository name into its components // and configuration of the associated registry. -func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (s *defaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { + s.mu.RLock() + defer s.mu.RUnlock() return newRepositoryInfo(s.config, name) } @@ -247,32 +222,28 @@ type APIEndpoint struct { } // TLSConfig constructs a client TLS configuration based on server defaults -func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (s *defaultService) TLSConfig(hostname string) (*tls.Config, error) { + s.mu.RLock() + secure := s.config.isSecureIndex(hostname) + s.mu.RUnlock() - return s.tlsConfig(hostname) -} - -// tlsConfig constructs a client TLS configuration based on server defaults -func (s *DefaultService) tlsConfig(hostname string) (*tls.Config, error) { - return newTLSConfig(hostname, isSecureIndex(s.config, hostname)) + return newTLSConfig(hostname, secure) } // 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 *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { - s.mu.Lock() - defer s.mu.Unlock() +func (s *defaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { + s.mu.RLock() + defer s.mu.RUnlock() return s.lookupV2Endpoints(hostname) } // 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 *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { - s.mu.Lock() - defer s.mu.Unlock() +func (s *defaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { + s.mu.RLock() + defer s.mu.RUnlock() allEndpoints, err := s.lookupV2Endpoints(hostname) if err == nil { diff --git a/registry/service_v2.go b/registry/service_v2.go index 46f28ebccf..f147af0faa 100644 --- a/registry/service_v2.go +++ b/registry/service_v2.go @@ -7,8 +7,7 @@ import ( "github.com/docker/go-connections/tlsconfig" ) -func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { - tlsConfig := tlsconfig.ServerDefault() +func (s *defaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { if hostname == DefaultNamespace || hostname == IndexHostname { for _, mirror := range s.config.Mirrors { if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { @@ -16,9 +15,9 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp } mirrorURL, err := url.Parse(mirror) if err != nil { - return nil, err + return nil, invalidParam(err) } - mirrorTLSConfig, err := s.tlsConfig(mirrorURL.Host) + mirrorTLSConfig, err := newTLSConfig(mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host)) if err != nil { return nil, err } @@ -35,19 +34,18 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp Version: APIVersion2, Official: true, TrimHostname: true, - TLSConfig: tlsConfig, + TLSConfig: tlsconfig.ServerDefault(), }) return endpoints, nil } - ana := allowNondistributableArtifacts(s.config, hostname) - - tlsConfig, err = s.tlsConfig(hostname) + tlsConfig, err := newTLSConfig(hostname, s.config.isSecureIndex(hostname)) if err != nil { return nil, err } + ana := s.config.allowNondistributableArtifacts(hostname) endpoints = []APIEndpoint{ { URL: &url.URL{ diff --git a/registry/session.go b/registry/session.go index d34dc1e58a..bf222e43e8 100644 --- a/registry/session.go +++ b/registry/session.go @@ -12,7 +12,7 @@ import ( "sync" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/jsonmessage" @@ -21,13 +21,11 @@ import ( "github.com/sirupsen/logrus" ) -// A Session is used to communicate with a V1 registry -type Session struct { - indexEndpoint *V1Endpoint +// A session is used to communicate with a V1 registry +type session struct { + indexEndpoint *v1Endpoint client *http.Client - // TODO(tiborvass): remove authConfig - authConfig *types.AuthConfig - id string + id string } type authTransport struct { @@ -149,13 +147,13 @@ func (tr *authTransport) CancelRequest(req *http.Request) { } } -func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error { +func authorizeClient(client *http.Client, authConfig *types.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() + info, err := endpoint.ping() if err != nil { return err } @@ -171,43 +169,32 @@ func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint jar, err := cookiejar.New(nil) if err != nil { - return errors.New("cookiejar.New is not supposed to return an error") + return errdefs.System(errors.New("cookiejar.New is not supposed to return an error")) } client.Jar = jar return nil } -func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session { - return &Session{ - authConfig: authConfig, +func newSession(client *http.Client, endpoint *v1Endpoint) *session { + return &session{ client: client, indexEndpoint: endpoint, id: stringid.GenerateRandomID(), } } -// NewSession creates a new session -// TODO(tiborvass): remove authConfig param once registry client v2 is vendored -func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) { - if err := authorizeClient(client, authConfig, endpoint); err != nil { - return nil, err - } - - return newSession(client, authConfig, endpoint), nil -} - -// SearchRepositories performs a search against the remote repository -func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) { +// searchRepositories performs a search against the remote repository +func (r *session) searchRepositories(term string, limit int) (*registry.SearchResults, error) { if limit < 1 || limit > 100 { - return nil, errdefs.InvalidParameter(errors.Errorf("Limit %d is outside the range of [1, 100]", limit)) + return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit) } logrus.Debugf("Index server: %s", r.indexEndpoint) u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { - return nil, errors.Wrap(errdefs.InvalidParameter(err), "Error building request") + return nil, invalidParamWrapf(err, "error building request") } // Have the AuthTransport send authentication, when logged in. req.Header.Set("X-Docker-Token", "true") @@ -222,6 +209,6 @@ func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.Sea Code: res.StatusCode, } } - result := new(registrytypes.SearchResults) + result := new(registry.SearchResults) return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results") } diff --git a/registry/types.go b/registry/types.go index 073e244ba8..37094737f2 100644 --- a/registry/types.go +++ b/registry/types.go @@ -2,39 +2,9 @@ package registry // import "github.com/docker/docker/registry" import ( "github.com/docker/distribution/reference" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/api/types/registry" ) -// RepositoryData tracks the image list, list of endpoints for a repository -type RepositoryData struct { - // ImgList is a list of images in the repository - ImgList map[string]*ImgData - // Endpoints is a list of endpoints returned in X-Docker-Endpoints - Endpoints []string -} - -// ImgData is used to transfer image checksums to and from the registry -type ImgData struct { - // ID is an opaque string that identifies the image - ID string `json:"id"` - Checksum string `json:"checksum,omitempty"` - ChecksumPayload string `json:"-"` - Tag string `json:",omitempty"` -} - -// PingResult contains the information returned when pinging a registry. It -// indicates the registry's version and whether the registry claims to be a -// standalone registry. -type PingResult struct { - // Version is the registry version supplied by the registry in an HTTP - // header - Version string `json:"version"` - // 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"` -} - // APIVersion is an integral representation of an API version (presently // either 1 or 2) type APIVersion int @@ -58,7 +28,7 @@ var apiVersions = map[APIVersion]string{ type RepositoryInfo struct { Name reference.Named // Index points to registry information - Index *registrytypes.IndexInfo + 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.