From ca795c91b91ea38ce26616825c646f59a746edde Mon Sep 17 00:00:00 2001 From: jiangpengfei Date: Mon, 30 Sep 2019 14:15:45 -0400 Subject: [PATCH] docker: support private registry reason: 1. add registries config to support downnload private registry image 2. add LLT testcases for registries config Change-Id: Icd77363c6c2024e9ece0b79e65aeaee3af928caa Signed-off-by: jiangpengfei --- components/engine/api/types/registry/registry.go | 162 ++++++++++++++++++++- .../engine/api/types/registry/registry_test.go | 73 ++++++++++ components/engine/cmd/dockerd/daemon_test.go | 24 +++ components/engine/daemon/config/config.go | 13 ++ components/engine/daemon/reload.go | 26 ++++ components/engine/distribution/pull_v2.go | 26 +++- components/engine/distribution/push_v2_test.go | 4 + components/engine/opts/opts.go | 34 +++++ components/engine/registry/config.go | 24 ++- components/engine/registry/service.go | 12 ++ components/engine/registry/service_v2.go | 98 +++++++++---- components/engine/registry/service_v2_test.go | 104 +++++++++++++ 12 files changed, 564 insertions(+), 36 deletions(-) create mode 100644 components/engine/api/types/registry/registry_test.go create mode 100644 components/engine/registry/service_v2_test.go diff --git a/components/engine/api/types/registry/registry.go b/components/engine/api/types/registry/registry.go index 8789ad3..1ebf18b 100644 --- a/components/engine/api/types/registry/registry.go +++ b/components/engine/api/types/registry/registry.go @@ -2,9 +2,25 @@ package registry // import "github.com/docker/docker/api/types/registry" import ( "encoding/json" + "fmt" "net" + "net/url" + "regexp" + "strings" - "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/docker/distribution/reference" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +var ( + // DefaultEndpoint for docker.io + DefaultEndpoint = Endpoint{ + Address: "https://registry-1.docker.io", + url: url.URL{ + Scheme: "https", + Host: "registry-1.docker.io", + }, + } ) // ServiceConfig stores daemon registry services configuration. @@ -14,6 +30,150 @@ type ServiceConfig struct { InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` Mirrors []string + Registries Registries +} + +// Registries is a slice of type Registry. +type Registries []Registry + +// Registry includes all data relevant for the lookup of push and pull +// endpoints. +type Registry struct { + // Pattern is a string contains the registry domain name which pull/push + // images directly, don't need to convert to pull from mirror registry + Pattern string `json:"pattern"` + // Mirrors is a slice contains registry mirror url address + Mirrors []Endpoint `json:"mirrors"` + patternRegexp *regexp.Regexp +} + +// Endpoint includes all data associated with a given registry endpoint. +type Endpoint struct { + // Address is the endpoints base URL when assembling a repository in a + // registry (e.g., "registry.com:5000/v2"). + Address string `json:"address"` + // url is used during endpoint lookup and avoids to redundantly parse + // Address when the Endpoint is used. + url url.URL + // InsecureSkipVerify: if true, TLS accepts any certificate presented + // by the server and any host name in that certificate. In this mode, + // TLS is susceptible to man-in-the-middle attacks. This should be used + // only for testing + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` +} + +// RewriteReference strips the prefix from ref and appends it to registry. +// If the prefix is empty, ref remains unchanged. An error is returned if +// prefix doesn't prefix ref. +func RewriteReference(ref reference.Named, prefix string, registry *url.URL) (reference.Named, error) { + // Sanity check the provided arguments + if ref == nil { + return nil, fmt.Errorf("provided reference is nil") + } + if registry == nil { + return nil, fmt.Errorf("provided registry is nil") + } + + // don't rewrite the default endpoints + if *registry == DefaultEndpoint.url { + return ref, nil + } + + if prefix == "" { + return ref, nil + } + + baseAddress := strings.TrimPrefix(registry.String(), registry.Scheme+"://") + + refStr := ref.String() + if !strings.HasPrefix(refStr, prefix) { + return nil, fmt.Errorf("unable to rewrite reference %q with prefix %q", refStr, prefix) + } + remainder := strings.TrimPrefix(refStr, prefix) + remainder = strings.TrimPrefix(remainder, "/") + baseAddress = strings.TrimSuffix(baseAddress, "/") + + newRefStr := baseAddress + "/" + remainder + newRef, err := reference.ParseNamed(newRefStr) + if err != nil { + return nil, fmt.Errorf("unable to rewrite reference %q with prefix %q to %q: %v", refStr, prefix, newRefStr, err) + } + return newRef, nil +} + +// GetURL returns the Endpoint's URL. +func (r *Endpoint) GetURL() *url.URL { + // return the pointer of a copy + url := r.url + return &url +} + +// MatchWhiteList return reference match the r.whiteListRegexp or not +func (r *Registry) MatchPattern(reference string) bool { + if r.patternRegexp == nil { + return false + } + + return r.patternRegexp.MatchString(reference) +} + +// FindRegistry returns the Registry mirror url address if reference not in the whitelist +// or nil if reference in the Registry whitelist. +func (r Registries) FindRegistry(reference string) *Registry { + var reg *Registry = nil + for i := range r { + match := r[i].MatchPattern(reference) + if match { + reg = &r[i] + break + } + } + + return reg +} + +// Prepare must be called on each new Registry. It sets up all specified push +// and pull endpoints +func (r *Registry) Prepare() error { + var err error + r.patternRegexp, err = regexp.Compile(r.Pattern) + if err != nil { + return fmt.Errorf("invalid pattern: %v", err) + } + + prepareEndpoints := func(endpoints []Endpoint) ([]Endpoint, error) { + for i := range endpoints { + if err := endpoints[i].Prepare(); err != nil { + return nil, err + } + } + + return endpoints, nil + } + + if r.Mirrors, err = prepareEndpoints(r.Mirrors); err != nil { + return err + } + + if len(r.Mirrors) == 0 { + return fmt.Errorf("Registry with whitelist %v without mirror endpoints", r.Pattern) + } + + return nil +} + +// Prepare sets up the Endpoint. +func (r *Endpoint) Prepare() error { + if !strings.HasPrefix(r.Address, "http://") && !strings.HasPrefix(r.Address, "https://") { + return fmt.Errorf("%s: address must start with %q or %q", r.Address, "http://", "https://") + } + + u, err := url.Parse(r.Address) + if err != nil { + return err + } + r.url = *u + return nil } // NetIPNet is the net.IPNet type, which can be marshalled and diff --git a/components/engine/api/types/registry/registry_test.go b/components/engine/api/types/registry/registry_test.go new file mode 100644 index 0000000..e532d4d --- /dev/null +++ b/components/engine/api/types/registry/registry_test.go @@ -0,0 +1,73 @@ +package registry + +import ( + "net/url" + "testing" + + "github.com/docker/distribution/reference" + "gotest.tools/assert" +) + +func TestRewriteReference(t *testing.T) { + var ref reference.Named + var prefix string + var registry *url.URL + + // case 1: ref is nil + _, err := RewriteReference(ref, prefix, registry) + assert.ErrorContains(t, err, "provided reference is nil") + + ref, err = reference.ParseNormalizedNamed("hello.com/official/busybox") + assert.NilError(t, err) + + // case 2: registry is nil + _, err = RewriteReference(ref, prefix, registry) + assert.ErrorContains(t, err, "provided registry is nil") + + registry = &url.URL{ + Scheme: "https", + Host: "exapmle.com", + } + + // case 3: prefix is empty, expect nil + rewriteRef, err := RewriteReference(ref, prefix, registry) + assert.NilError(t, err) + assert.Equal(t, rewriteRef, ref) + + // case 4: registry equal to DefaultEndpoint.url + registry = &url.URL{ + Scheme: "https", + Host: "registry-1.docker.io", + } + rewriteRef, err = RewriteReference(ref, prefix, registry) + assert.NilError(t, err) + assert.Equal(t, rewriteRef, ref) + + // case 5: ref.String() doesn't have prefix + registry = &url.URL{ + Scheme: "https", + Host: "test.io", + } + prefix = "example.com" + rewriteRef, err = RewriteReference(ref, prefix, registry) + assert.ErrorContains(t, err, "unable to rewrite reference") + + // case 6: registry host is invalid + prefix = "hello.com" + registry = &url.URL{ + Scheme: "https", + Host: "[?f,*fds", + } + rewriteRef, err = RewriteReference(ref, prefix, registry) + assert.ErrorContains(t, err, "unable to rewrite reference") + + // case 7: everything is ok + registry = &url.URL{ + Scheme: "https", + Host: "test.io", + } + prefix = "hello.com" + rewriteRef, err = RewriteReference(ref, prefix, registry) + assert.NilError(t, err) + assert.Equal(t, rewriteRef.String(), "test.io/official/busybox") +} diff --git a/components/engine/cmd/dockerd/daemon_test.go b/components/engine/cmd/dockerd/daemon_test.go index ad447e3..681bf87 100644 --- a/components/engine/cmd/dockerd/daemon_test.go +++ b/components/engine/cmd/dockerd/daemon_test.go @@ -180,3 +180,27 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) { assert.Check(t, is.Len(loadedConfig.Mirrors, 1)) assert.Check(t, is.Len(loadedConfig.InsecureRegistries, 1)) } + +func TestLoadDaemonConfigWithRegistriesOptions(t *testing.T) { + content := `{ + "registries": [ + { + "pattern": "xxx.com", + "mirrors": [ + { + "address": "http://hello.mirror.com" + } + ] + } + ] + }` + tempFile := fs.NewFile(t, "config", fs.WithContent(content)) + defer tempFile.Remove() + + opts := defaultOptions(tempFile.Path()) + loadedConfig, err := loadDaemonCliConfig(opts) + assert.NilError(t, err) + assert.Assert(t, loadedConfig != nil) + + assert.Check(t, is.Len(loadedConfig.Registries, 1)) +} diff --git a/components/engine/daemon/config/config.go b/components/engine/daemon/config/config.go index 2141ce8..07d4c89 100644 --- a/components/engine/daemon/config/config.go +++ b/components/engine/daemon/config/config.go @@ -435,6 +435,10 @@ func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Con return nil, err } + if len(config.Mirrors) > 0 && len(config.Registries) > 0 { + return nil, fmt.Errorf("registry-mirror config conflict with registries config") + } + if config.RootDeprecated != "" { logrus.Warn(`The "graph" config file option is deprecated. Please use "data-root" instead.`) @@ -472,6 +476,10 @@ func findConfigurationConflicts(config map[string]interface{}, flags *pflag.Flag unknownKeys := make(map[string]interface{}) for key, value := range config { if flag := flags.Lookup(key); flag == nil && !skipValidateOptions[key] { + // skip config-only flags + if key == "registries" { + continue + } unknownKeys[key] = value } } @@ -579,6 +587,11 @@ func Validate(config *Config) error { } } + // validate registries mirror settings + if err := opts.ValidateRegistries(config.Registries); err != nil { + return err + } + // validate platform-specific settings return config.ValidatePlatformConfig() } diff --git a/components/engine/daemon/reload.go b/components/engine/daemon/reload.go index 026d7dd..b8132cc 100644 --- a/components/engine/daemon/reload.go +++ b/components/engine/daemon/reload.go @@ -65,6 +65,9 @@ func (daemon *Daemon) Reload(conf *config.Config) (err error) { if err := daemon.reloadLiveRestore(conf, attributes); err != nil { return err } + if err := daemon.reloadRegistries(conf, attributes);err != nil { + return err + } return daemon.reloadNetworkDiagnosticPort(conf, attributes) } @@ -294,6 +297,29 @@ func (daemon *Daemon) reloadRegistryMirrors(conf *config.Config, attributes map[ return nil } +// reloadRegistries updates the registries configuration and the passed attributes +func (daemon *Daemon) reloadRegistries(conf *config.Config, attributes map[string]string) error { + // update corresponding configuration + if conf.IsValueSet("registries") { + daemon.configStore.Registries = conf.Registries + if err := daemon.RegistryService.LoadRegistries(conf.Registries); err != nil { + return err + } + } + + // prepare reload event attributes with updatable configurations + if daemon.configStore.Registries != nil { + registries, err := json.Marshal(daemon.configStore.Registries) + if err != nil { + return err + } + attributes["registries"] = string(registries) + } else { + attributes["registries"] = "[]" + } + return nil +} + // reloadLiveRestore updates configuration with live retore option // and updates the passed attributes func (daemon *Daemon) reloadLiveRestore(conf *config.Config, attributes map[string]string) error { diff --git a/components/engine/distribution/pull_v2.go b/components/engine/distribution/pull_v2.go index 99cee79..4150241 100644 --- a/components/engine/distribution/pull_v2.go +++ b/components/engine/distribution/pull_v2.go @@ -20,10 +20,11 @@ import ( "github.com/docker/distribution/registry/api/errcode" "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/distribution/metadata" "github.com/docker/docker/distribution/xfer" "github.com/docker/docker/image" - "github.com/docker/docker/image/v1" + v1 "github.com/docker/docker/image/v1" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" @@ -66,6 +67,10 @@ type v2Puller struct { func (p *v2Puller) Pull(ctx context.Context, ref reference.Named, platform *specs.Platform) (err error) { // TODO(tiborvass): was ReceiveTimeout + if p.endpoint.Prefix != "" { + p.config.MetaHeaders["Docker-Prefix"] = []string{p.endpoint.Prefix} + } + p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") if err != nil { logrus.Warnf("Error getting v2 registry: %v", err) @@ -334,6 +339,17 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named, platform return false, err } + var pullRef reference.Named = ref + if len(p.endpoint.Prefix) != 0 { + // Note that pullRef is only used for pulling while ref is used as + // the reference for storing the image + pullRef, err = registrytypes.RewriteReference(ref, p.endpoint.Prefix, p.endpoint.URL) + if err != nil { + return false, err + } + logrus.Infof("rewriting %q to %q", ref.String(), pullRef.String()) + } + var ( manifest distribution.Manifest tagOrDigest string // Used for logging/progress only @@ -379,7 +395,7 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named, platform // the other side speaks the v2 protocol. p.confirmedV2 = true - logrus.Debugf("Pulling ref from V2 registry: %s", reference.FamiliarString(ref)) + logrus.Debugf("Pulling ref %q from V2 registry: %s", ref, p.endpoint.URL) progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+reference.FamiliarName(p.repo.Named())) var ( @@ -392,18 +408,18 @@ func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named, platform if p.config.RequireSchema2 { return false, fmt.Errorf("invalid manifest: not schema2") } - id, manifestDigest, err = p.pullSchema1(ctx, ref, v, platform) + id, manifestDigest, err = p.pullSchema1(ctx, pullRef, v, platform) if err != nil { return false, err } case *schema2.DeserializedManifest: - id, manifestDigest, err = p.pullSchema2(ctx, ref, v, platform) + id, manifestDigest, err = p.pullSchema2(ctx, pullRef, v, platform) if err != nil { logrus.Errorf("try to pull schema2 failed. manifest: %+v", manifest.References()) return false, err } case *manifestlist.DeserializedManifestList: - id, manifestDigest, err = p.pullManifestList(ctx, ref, v, platform) + id, manifestDigest, err = p.pullManifestList(ctx, pullRef, v, platform) if err != nil { logrus.Errorf("try to get manifest data from storage failed. manifest: %+v", manifest.References()) return false, err diff --git a/components/engine/distribution/push_v2_test.go b/components/engine/distribution/push_v2_test.go index 436b4a1..8d39403 100644 --- a/components/engine/distribution/push_v2_test.go +++ b/components/engine/distribution/push_v2_test.go @@ -488,6 +488,10 @@ func (s *mockReferenceStore) Get(ref reference.Named) (digest.Digest, error) { return "", nil } +func (s *mockReferenceStore) List() []digest.Digest { + return []digest.Digest{} +} + func TestWhenEmptyAuthConfig(t *testing.T) { for _, authInfo := range []struct { username string diff --git a/components/engine/opts/opts.go b/components/engine/opts/opts.go index de8aacb..db63aa6 100644 --- a/components/engine/opts/opts.go +++ b/components/engine/opts/opts.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + "github.com/docker/docker/api/types/registry" "github.com/docker/go-units" ) @@ -15,6 +16,11 @@ var ( domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`) ) +const ( + maxRegistryNum = 100 + maxMirrorNumber = 100 +) + // ListOpts holds a list of values and a validation function. type ListOpts struct { values *[]string @@ -273,6 +279,34 @@ func ValidateSingleGenericResource(val string) (string, error) { return val, nil } +func ValidateRegistries(registries registry.Registries) error { + if len(registries) == 0 { + return nil + } + + if len(registries) > maxRegistryNum { + return fmt.Errorf("registries config registry number should not larger than %d", maxRegistryNum) + } + + for _, reg := range registries { + if len(reg.Pattern) == 0 || len(reg.Mirrors) == 0 { + return fmt.Errorf("registry pattern and mirrors is required, should not be empty") + } + + if len(reg.Mirrors) > maxMirrorNumber { + return fmt.Errorf("registry mirrors number should not larger than %d", maxMirrorNumber) + } + + for _, mirror := range reg.Mirrors { + if len(mirror.Address) == 0 { + return fmt.Errorf("mirror address is required, should not be empty") + } + } + } + + return nil +} + // ParseLink parses and validates the specified string as a link format (name:alias) func ParseLink(val string) (string, string, error) { if val == "" { diff --git a/components/engine/registry/config.go b/components/engine/registry/config.go index ea491b9..9c2b762 100644 --- a/components/engine/registry/config.go +++ b/components/engine/registry/config.go @@ -20,6 +20,10 @@ type ServiceOptions struct { Mirrors []string `json:"registry-mirrors,omitempty"` InsecureRegistries []string `json:"insecure-registries,omitempty"` + // Registries holds information associated with registries and their + // push and pull mirrors. + Registries registrytypes.Registries `json:"registries,omitempty"` + // V2Only controls access to legacy registries. If it is set to true via the // command line flag the daemon will not attempt to contact v1 legacy registries V2Only bool `json:"disable-legacy-registry,omitempty"` @@ -97,6 +101,9 @@ func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { if err := config.LoadInsecureRegistries(options.InsecureRegistries); err != nil { return nil, err } + if err := config.LoadRegistries(options.Registries); err != nil { + return nil, err + } return config, nil } @@ -248,7 +255,22 @@ skip: return nil } -// allowNondistributableArtifacts returns true if the provided hostname is part of the list of registries +// LoadRegistries loads the user-specified configuration options for registries +func (config *serviceConfig) LoadRegistries(registries registrytypes.Registries) error { + for _, registry := range registries { + if err := registry.Prepare(); err != nil { + return err + } + config.Registries = append(config.Registries, registry) + } + + for i, r := range config.Registries { + logrus.Infof("REGISTRY %d: %v", i, r) + } + return nil +} + +// allowNondistributableArtifacts returns true if the provided hostname is part of the list of regsitries // that allow push of nondistributable artifacts. // // The list can contain elements with CIDR notation to specify a whole subnet. If the subnet contains an IP diff --git a/components/engine/registry/service.go b/components/engine/registry/service.go index d38f44b..8530f97 100644 --- a/components/engine/registry/service.go +++ b/components/engine/registry/service.go @@ -34,6 +34,7 @@ type Service interface { LoadAllowNondistributableArtifacts([]string) error LoadMirrors([]string) error LoadInsecureRegistries([]string) error + LoadRegistries(registrytypes.Registries) error } // DefaultService is a registry service. It tracks configuration data such as a list @@ -64,6 +65,7 @@ func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0), IndexConfigs: make(map[string]*(registrytypes.IndexInfo)), Mirrors: make([]string, 0), + Registries: make([]registrytypes.Registry, 0), } // construct a new ServiceConfig which will not retrieve s.Config directly, @@ -77,6 +79,7 @@ func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { } servConfig.Mirrors = append(servConfig.Mirrors, s.config.ServiceConfig.Mirrors...) + servConfig.Registries = append(servConfig.Registries, s.config.ServiceConfig.Registries...) return &servConfig } @@ -105,6 +108,14 @@ func (s *DefaultService) LoadInsecureRegistries(registries []string) error { return s.config.LoadInsecureRegistries(registries) } +// LoadRegistries loads registries for Service +func (s *DefaultService) LoadRegistries(registries registrytypes.Registries) error { + s.mu.Lock() + defer s.mu.Unlock() + + return s.config.LoadRegistries(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. @@ -258,6 +269,7 @@ type APIEndpoint struct { Official bool TrimHostname bool TLSConfig *tls.Config + Prefix string } // ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint diff --git a/components/engine/registry/service_v2.go b/components/engine/registry/service_v2.go index 3a56dc9..adeb10c 100644 --- a/components/engine/registry/service_v2.go +++ b/components/engine/registry/service_v2.go @@ -1,47 +1,87 @@ package registry // import "github.com/docker/docker/registry" import ( + "crypto/tls" "net/url" "strings" + registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/go-connections/tlsconfig" ) func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { - tlsConfig := tlsconfig.ServerDefault() - if hostname == DefaultNamespace || hostname == IndexHostname { - // v2 mirrors - for _, mirror := range s.config.Mirrors { - if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { - mirror = "https://" + mirror - } - mirrorURL, err := url.Parse(mirror) - if err != nil { - return nil, err + var tlsConfig *tls.Config + + // if s.config.Registries is set, lookup regsitry mirror addr from s.config.Registries + if len(s.config.Registries) > 0 { + reg := s.config.Registries.FindRegistry(hostname) + + if reg != nil { + var regEndpoints []registrytypes.Endpoint = reg.Mirrors + + lastIndex := len(regEndpoints) - 1 + for i, regEP := range regEndpoints { + official := regEP.Address == registrytypes.DefaultEndpoint.Address + regURL := regEP.GetURL() + + if official { + tlsConfig = tlsconfig.ServerDefault() + } else { + tlsConfig, err = s.tlsConfigForMirror(regURL) + if err != nil { + return nil, err + } + } + tlsConfig.InsecureSkipVerify = regEP.InsecureSkipVerify + endpoints = append(endpoints, APIEndpoint{ + URL: regURL, + Version: APIVersion2, + Official: official, + TrimHostname: true, + TLSConfig: tlsConfig, + Prefix: hostname, + // the last endpoint is not considered a mirror + Mirror: i != lastIndex, + }) } - mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL) - if err != nil { - return nil, err + return endpoints, nil + } + } else { + tlsConfig = tlsconfig.ServerDefault() + if hostname == DefaultNamespace || hostname == IndexHostname { + // v2 mirrors + for _, mirror := range s.config.Mirrors { + if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { + mirror = "https://" + mirror + } + mirrorURL, err := url.Parse(mirror) + if err != nil { + return nil, err + } + mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL) + if err != nil { + return nil, err + } + endpoints = append(endpoints, APIEndpoint{ + URL: mirrorURL, + // guess mirrors are v2 + Version: APIVersion2, + Mirror: true, + TrimHostname: true, + TLSConfig: mirrorTLSConfig, + }) } + // v2 registry endpoints = append(endpoints, APIEndpoint{ - URL: mirrorURL, - // guess mirrors are v2 + URL: DefaultV2Registry, Version: APIVersion2, - Mirror: true, + Official: true, TrimHostname: true, - TLSConfig: mirrorTLSConfig, + TLSConfig: tlsConfig, }) - } - // v2 registry - endpoints = append(endpoints, APIEndpoint{ - URL: DefaultV2Registry, - Version: APIVersion2, - Official: true, - TrimHostname: true, - TLSConfig: tlsConfig, - }) - return endpoints, nil + return endpoints, nil + } } ana := allowNondistributableArtifacts(s.config, hostname) @@ -57,7 +97,7 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp Scheme: "https", Host: hostname, }, - Version: APIVersion2, + Version: APIVersion2, AllowNondistributableArtifacts: ana, TrimHostname: true, TLSConfig: tlsConfig, @@ -70,7 +110,7 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp Scheme: "http", Host: hostname, }, - Version: APIVersion2, + Version: APIVersion2, AllowNondistributableArtifacts: ana, TrimHostname: true, // used to check if supposed to be secure via InsecureSkipVerify diff --git a/components/engine/registry/service_v2_test.go b/components/engine/registry/service_v2_test.go new file mode 100644 index 0000000..02c954b --- /dev/null +++ b/components/engine/registry/service_v2_test.go @@ -0,0 +1,104 @@ +package registry + +import ( + "testing" + "gotest.tools/assert" + + registrytypes "github.com/docker/docker/api/types/registry" +) + +func TestLookupV2Endpoints(t *testing.T) { + // case 1: doesn't call r.Prepare(), expect use default + r := registrytypes.Registry{ + Pattern: "hello.com", + Mirrors: []registrytypes.Endpoint{ + { + Address: "http://docker.com", + InsecureSkipVerify: false, + }, + }, + } + + s, err := NewService(ServiceOptions{ + Registries: registrytypes.Registries{ + r, + }, + }) + + _, err = s.lookupV2Endpoints("hello.com") + assert.NilError(t, err) + + // case 2: everything is ok + err = r.Prepare() + assert.NilError(t, err) + + if err != nil { + t.Fatal(err) + } + + _, err = s.lookupV2Endpoints("hello.com") + assert.NilError(t, err) + + // case 3: Mirror Address is invalid, without http:// or https:// prefix + r = registrytypes.Registry{ + Pattern: "hello.com", + Mirrors: []registrytypes.Endpoint{ + { + Address: "docker.com", + InsecureSkipVerify: false, + }, + }, + } + + err = r.Prepare() + assert.ErrorContains(t, err, "address must start with") + + // case 4: invalid pattern + r = registrytypes.Registry{ + Pattern: "`[@1xxfdsaf", + Mirrors: []registrytypes.Endpoint{ + { + Address: "https://docker.com", + InsecureSkipVerify: false, + }, + }, + } + + err = r.Prepare() + assert.ErrorContains(t, err, "invalid pattern") + + // case 5: r.Mirrors is empty, expect error + r = registrytypes.Registry{ + Pattern: "hello.com", + Mirrors: []registrytypes.Endpoint{}, + } + + err = r.Prepare() + assert.ErrorContains(t, err, "without mirror endpoints") + + // case 6: lookupV2Endpoints doesn't match to registry pattern, expect no error, return default endpoints + r = registrytypes.Registry{ + Pattern: "hello.com", + Mirrors: []registrytypes.Endpoint{ + { + Address: "http://docker.com", + InsecureSkipVerify: false, + }, + }, + } + + err = r.Prepare() + assert.NilError(t, err) + + s, err = NewService(ServiceOptions{ + Registries: registrytypes.Registries{ + r, + }, + }) + if err != nil { + t.Fatal(err) + } + + _, err = s.lookupV2Endpoints("example.com") + assert.NilError(t, err) +} -- 1.8.3.1