From 69d43a9550cdedf86b0d4b29e9d737af90221109 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Mon, 2 Jul 2018 13:37:34 +0200 Subject: [PATCH] Add private-registry mirror support NOTE: This is a backport/downstream patch of the upstream pull-request for Moby, which is still subject to changes. Please visit https://github.com/moby/moby/pull/34319 for the current status. Add support for mirroring private registries. The daemon.json config can now be configured as exemplified below: ```json { "registries": [ { "Prefix": "docker.io/library/alpine", "Mirrors": [ { "URL": "http://local-alpine-mirror.lan" } ] }, { "Prefix": "registry.suse.com", "Mirrors": [ { "URL": "https://remote.suse.mirror.com" } ] }, { "Prefix": "http://insecure.registry.org:5000" } ], "registry-mirrors": ["https://deprecated-mirror.com"] } ``` With the new semantics, a mirror will be selected as an endpoint if the specified prefix matches the prefix of the requested resource (e.g., an image reference). In the upper example, "local-alpine-mirror" will only serve as a mirror for docker.io if the requested resource matches the "alpine" prefix, such as "alpine:latest" or "alpine-foo/bar". Furthermore, private registries can now be mirrored as well. In the example above, "remote.suse.mirror.com" will serve as a mirror for all requests to "registry.suse.com". Notice that if no http{s,} scheme is specified, the URI will always default to https without fallback to http. An insecure registry can now be specified by adding the "http://" scheme to the corresponding prefix. Note that the configuration is sanity checked, so that a given mirror can serve multiple prefixes if they all point to the same registry, while a registry cannot simultaneously serve as a mirror. The daemon will warn in case the URI schemes of a registry and one of its mirrors do not correspond. This change deprecates the "insecure-regestries" and "registry-mirrors" options, while the "insecure-registries" cannot be used simultaneously with the new "registries", which doesn't allow a fallback from https to http for security reasons. Signed-off-by: Flavio Castelli Signed-off-by: Valentin Rothberg Signed-off-by: Aleksa Sarai --- .../engine/api/types/registry/registry.go | 144 ++++++++++++++++++ components/engine/daemon/config/config.go | 4 + components/engine/daemon/reload.go | 33 ++++ components/engine/daemon/reload_test.go | 95 ++++++++++++ components/engine/distribution/pull.go | 2 +- components/engine/distribution/pull_v2.go | 2 +- components/engine/distribution/push.go | 2 +- components/engine/registry/config.go | 124 ++++++++++++++- components/engine/registry/config_test.go | 136 +++++++++++++++++ components/engine/registry/registry_test.go | 91 ++++++++++- components/engine/registry/service.go | 45 ++++-- components/engine/registry/service_v2.go | 66 +++++--- 12 files changed, 697 insertions(+), 47 deletions(-) diff --git a/components/engine/api/types/registry/registry.go b/components/engine/api/types/registry/registry.go index 8789ad3b3210..c663fec7d881 100644 --- a/components/engine/api/types/registry/registry.go +++ b/components/engine/api/types/registry/registry.go @@ -2,7 +2,10 @@ package registry // import "github.com/docker/docker/api/types/registry" import ( "encoding/json" + "fmt" "net" + "net/url" + "strings" "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -14,6 +17,147 @@ type ServiceConfig struct { InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` Mirrors []string + Registries map[string]Registry +} + +// Registry holds information for a registry and its mirrors. +type Registry struct { + // Prefix is used for the lookup of endpoints, where the given registry + // is selected when its Prefix is a prefix of the passed reference, for + // instance, Prefix:"docker.io/opensuse" will match a `docker pull + // opensuse:tumleweed`. + URL RegURL `json:"Prefix"` + // The mirrors will be selected prior to the registry during lookup of + // endpoints. + Mirrors []Mirror `json:"Mirrors,omitempty"` +} + +// NewRegistry returns a Registry and interprets input as a URL. +func NewRegistry(input string) (Registry, error) { + reg := Registry{} + err := reg.URL.Parse(input) + return reg, err +} + +// AddMirror interprets input as a URL and adds it as a new mirror. +func (r *Registry) AddMirror(input string) error { + mir, err := NewMirror(input) + if err != nil { + return err + } + r.Mirrors = append(r.Mirrors, mir) + return nil +} + +// ContainsMirror returns true if the URL of any mirror equals input. +func (r *Registry) ContainsMirror(input string) bool { + for _, m := range r.Mirrors { + if m.URL.String() == input { + return true + } + } + return false +} + +// Mirror holds information for a given registry mirror. +type Mirror struct { + // The URL of the mirror. + URL RegURL `json:"URL,omitempty"` +} + +// NewMirror returns a Registry and interprets input as a URL. +func NewMirror(input string) (Mirror, error) { + mir := Mirror{} + err := mir.URL.Parse(input) + return mir, err +} + +// RegURL is a wrapper for url.URL to unmarshal it from the JSON config and to +// make it an embedded type for its users. +type RegURL struct { + // rURL is a simple url.URL. Notice it is no pointer to avoid potential + // null pointer dereferences. + rURL url.URL +} + +// UnmarshalJSON unmarshals the byte array into the RegURL pointer. +func (r *RegURL) UnmarshalJSON(b []byte) error { + var input string + if err := json.Unmarshal(b, &input); err != nil { + return err + } + return r.Parse(input) +} + +// MarshalJSON marshals the RegURL. +func (r *RegURL) MarshalJSON() ([]byte, error) { + return json.Marshal(r.String()) +} + +// Parse parses input as a URL. +func (r *RegURL) Parse(input string) error { + input = strings.ToLower(input) + uri, err := url.Parse(input) + if err == nil { + r.rURL = *uri + } else { + return err + } + // default to https if no URI scheme is specified + if uri.Scheme == "" { + // we have to parse again to update all associated data + return r.Parse("https://" + input) + } + + // sanity checks + if uri.Scheme != "http" && uri.Scheme != "https" { + return fmt.Errorf("invalid url: unsupported scheme %q in %q", uri.Scheme, uri) + } + if uri.Host == "" { + return fmt.Errorf("invalid url: unspecified hostname in %s", uri) + } + if uri.User != nil { + // strip password from output + uri.User = url.UserPassword(uri.User.Username(), "xxxxx") + return fmt.Errorf("invalid url: username/password not allowed in URI %q", uri) + } + + return nil +} + +// Host returns the host:port of the URL. +func (r *RegURL) Host() string { + return r.rURL.Host +} + +// Prefix returns the host:port/path of the URL. +func (r *RegURL) Prefix() string { + return r.rURL.Host + r.rURL.Path +} + +// IsOfficial returns true if the URL points to an official "docker.io" host. +func (r *RegURL) IsOfficial() bool { + return r.rURL.Hostname() == "docker.io" +} + +// IsSecure returns true if the URI scheme of the URL is "https". +func (r *RegURL) IsSecure() bool { + return r.Scheme() == "https" +} + +// Scheme returns the URI scheme. +func (r *RegURL) Scheme() string { + return r.rURL.Scheme +} + +// URL return URL of the RegURL. +func (r *RegURL) URL() url.URL { + return r.rURL +} + +// String return URL as a string. +func (r *RegURL) String() string { + return r.rURL.String() } // NetIPNet is the net.IPNet type, which can be marshalled and diff --git a/components/engine/daemon/config/config.go b/components/engine/daemon/config/config.go index 80ecbbd9550d..8ce69714d9bf 100644 --- a/components/engine/daemon/config/config.go +++ b/components/engine/daemon/config/config.go @@ -467,6 +467,10 @@ func findConfigurationConflicts(config map[string]interface{}, flags *pflag.Flag // 1. Search keys from the file that we don't recognize as flags. unknownKeys := make(map[string]interface{}) for key, value := range config { + // skip complex config-only options (daemon.json) + if key == "registries" { + continue + } if flag := flags.Lookup(key); flag == nil && !skipValidateOptions[key] { unknownKeys[key] = value } diff --git a/components/engine/daemon/reload.go b/components/engine/daemon/reload.go index a31dd0cb87c1..99cc4a65a79d 100644 --- a/components/engine/daemon/reload.go +++ b/components/engine/daemon/reload.go @@ -21,8 +21,14 @@ import ( // - Daemon labels // - Insecure registries // - Registry mirrors +// - Registries // - Daemon live restore func (daemon *Daemon) Reload(conf *config.Config) (err error) { + // check for incompatible options + if err := conf.ServiceOptions.CompatCheck(); err != nil { + return err + } + daemon.configStore.Lock() attributes := map[string]string{} @@ -65,6 +71,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) } @@ -295,6 +304,30 @@ 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 restore option // and updates the passed attributes func (daemon *Daemon) reloadLiveRestore(conf *config.Config, attributes map[string]string) error { diff --git a/components/engine/daemon/reload_test.go b/components/engine/daemon/reload_test.go index ffad297f71b7..21733c3f1e33 100644 --- a/components/engine/daemon/reload_test.go +++ b/components/engine/daemon/reload_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/daemon/config" "github.com/docker/docker/daemon/images" "github.com/docker/docker/pkg/discovery" @@ -201,6 +202,100 @@ func TestDaemonReloadMirrors(t *testing.T) { } } +func TestDaemonReloadRegistries(t *testing.T) { + daemon := &Daemon{ + imageService: images.NewImageService(images.ImageServiceConfig{}), + } + + // create registries: note that this is done implicitly when loading + // daemon.json file. + var ( + err error + regA registrytypes.Registry // no change + regB registrytypes.Registry // will be changed + regC registrytypes.Registry // will be added + ) + + regA, err = registrytypes.NewRegistry("https://registry-a.com") + if err != nil { + t.Fatal(err) + } + if err := regA.AddMirror("https://mirror-a.com"); err != nil { + t.Fatal(err) + } + + // we'll add a 2nd mirror before reloading + regB, err = registrytypes.NewRegistry("https://registry-b.com") + if err != nil { + t.Fatal(err) + } + if err := regB.AddMirror("https://mirror1-b.com"); err != nil { + t.Fatal(err) + } + + // insecure regC will be added before reloading + regC, err = registrytypes.NewRegistry("http://registry-c.com") + if err != nil { + t.Fatal(err) + } + + daemon.RegistryService, err = registry.NewService(registry.ServiceOptions{ + Registries: []registrytypes.Registry{regA, regB}, + }) + if err != nil { + t.Fatal(err) + } + + daemon.configStore = &config.Config{} + + if err := regB.AddMirror("https://mirror2-b.com"); err != nil { + t.Fatal(err) + } + + registries := []registrytypes.Registry{regA, regB, regC} + + valuesSets := make(map[string]interface{}) + valuesSets["registries"] = registries + + newConfig := &config.Config{ + CommonConfig: config.CommonConfig{ + ServiceOptions: registry.ServiceOptions{ + Registries: registries, + }, + ValuesSet: valuesSets, + }, + } + + if err := daemon.Reload(newConfig); err != nil { + t.Fatal(err) + } + + registryService := daemon.RegistryService.ServiceConfig() + + if reg, exists := registryService.Registries["registry-a.com"]; !exists { + t.Fatal("registry should exist but doesn't") + } else { + if !reg.ContainsMirror("https://mirror-a.com") { + t.Fatal("registry should contain mirror but doesn't") + } + } + + if reg, exists := registryService.Registries["registry-b.com"]; !exists { + t.Fatal("registry should exist but doesn't") + } else { + if !reg.ContainsMirror("https://mirror1-b.com") { + t.Fatal("registry should contain mirror but doesn't") + } + if !reg.ContainsMirror("https://mirror2-b.com") { + t.Fatal("registry should contain mirror but doesn't") + } + } + + if _, exists := registryService.Registries["registry-c.com"]; !exists { + t.Fatal("registry should exist but doesn't") + } +} + func TestDaemonReloadInsecureRegistries(t *testing.T) { daemon := &Daemon{ imageService: images.NewImageService(images.ImageServiceConfig{}), diff --git a/components/engine/distribution/pull.go b/components/engine/distribution/pull.go index be366ce4a99b..49e0d0352778 100644 --- a/components/engine/distribution/pull.go +++ b/components/engine/distribution/pull.go @@ -58,7 +58,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo return err } - endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) + endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(ref.Name()) if err != nil { return err } diff --git a/components/engine/distribution/pull_v2.go b/components/engine/distribution/pull_v2.go index dd91ff2157b1..2640f6134e5d 100644 --- a/components/engine/distribution/pull_v2.go +++ b/components/engine/distribution/pull_v2.go @@ -379,7 +379,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.Infof("Pulling ref %s from V2 registry %s", reference.FamiliarString(ref), p.endpoint.URL) progress.Message(p.config.ProgressOutput, tagOrDigest, "Pulling from "+reference.FamiliarName(p.repo.Named())) var ( diff --git a/components/engine/distribution/push.go b/components/engine/distribution/push.go index 5617a4c95f49..0a24aebed968 100644 --- a/components/engine/distribution/push.go +++ b/components/engine/distribution/push.go @@ -58,7 +58,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo return err } - endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(reference.Domain(repoInfo.Name)) + endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(ref.Name()) if err != nil { return err } diff --git a/components/engine/registry/config.go b/components/engine/registry/config.go index 6bb9258c9b6f..f1945237d235 100644 --- a/components/engine/registry/config.go +++ b/components/engine/registry/config.go @@ -14,11 +14,12 @@ import ( "github.com/sirupsen/logrus" ) -// ServiceOptions holds command line options. +// ServiceOptions holds the user-specified configuration options. type ServiceOptions struct { - AllowNondistributableArtifacts []string `json:"allow-nondistributable-artifacts,omitempty"` - Mirrors []string `json:"registry-mirrors,omitempty"` - InsecureRegistries []string `json:"insecure-registries,omitempty"` + AllowNondistributableArtifacts []string `json:"allow-nondistributable-artifacts,omitempty"` + Mirrors []string `json:"registry-mirrors,omitempty"` + InsecureRegistries []string `json:"insecure-registries,omitempty"` + Registries []registrytypes.Registry `json:"registries,omitempty"` } // serviceConfig holds daemon configuration for the registry service. @@ -62,8 +63,21 @@ var ( // for mocking in unit tests var lookupIP = net.LookupIP +// CompatCheck performs some compatibility checks among the config options and +// returns an error in case of conflicts. +func (options *ServiceOptions) CompatCheck() error { + if len(options.InsecureRegistries) > 0 && len(options.Registries) > 0 { + return fmt.Errorf("usage of \"registries\" with deprecated option \"insecure-registries\" is not supported") + } + return nil +} + // newServiceConfig returns a new instance of ServiceConfig func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { + if err := options.CompatCheck(); err != nil { + panic(fmt.Sprintf("error loading config: %v", err)) + } + config := &serviceConfig{ ServiceConfig: registrytypes.ServiceConfig{ InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0), @@ -81,10 +95,104 @@ 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, fmt.Errorf("error loading registries: %v", err) + } return config, nil } +// checkRegistries makes sure that no mirror serves more than one registry and +// that no host is used as a registry and as a mirror simultaneously. Notice +// that different registry prefixes can share a mirror as long as they point to +// the same registry. It also warns if the URI schemes of a given registry and +// one of its mirrors differ. +func (config *serviceConfig) checkRegistries() error { + inUse := make(map[string]string) // key: host, value: user + + // make sure that each mirror serves only one registry + for _, reg := range config.Registries { + for _, mirror := range reg.Mirrors { + if used, conflict := inUse[mirror.URL.Host()]; conflict { + if used != reg.URL.Host() { + return fmt.Errorf("mirror '%s' can only serve one registry host", mirror.URL.Host()) + } + } + // docker.io etc. is reserved + if mirror.URL.IsOfficial() { + return fmt.Errorf("mirror '%s' cannot be used (reserved host)", mirror.URL.Host()) + } + inUse[mirror.URL.Host()] = reg.URL.Host() + // also warnf if seucurity levels differ + if reg.URL.IsSecure() != mirror.URL.IsSecure() { + logrus.Warnf("registry '%s' and mirror '%s' have different security levels", reg.URL.URL(), mirror.URL.URL()) + } + } + if reg.URL.IsSecure() && len(reg.Mirrors) == 0 { + logrus.Warnf("specifying secure registry '%s' without mirrors has no effect", reg.URL.Prefix()) + } + } + + // make sure that no registry host is used as a mirror + for _, reg := range config.Registries { + if _, conflict := inUse[reg.URL.Host()]; conflict { + return fmt.Errorf("registry '%s' cannot simultaneously serve as a mirror for '%s'", reg.URL.Host(), inUse[reg.URL.Host()]) + } + } + return nil +} + +// FindRegistry returns a Registry pointer based on the passed reference. If +// more than one index-prefix match the reference, the longest index is +// returned. In case of no match, nil is returned. +func (config *serviceConfig) FindRegistry(reference string) *registrytypes.Registry { + prefixStr := "" + prefixLen := 0 + for _, reg := range config.Registries { + if strings.HasPrefix(reference, reg.URL.Prefix()) { + length := len(reg.URL.Prefix()) + if length > prefixLen { + prefixStr = reg.URL.Prefix() + prefixLen = length + } + } + } + if prefixLen > 0 { + reg := config.Registries[prefixStr] + return ® + } + return nil +} + +// LoadRegistries loads the user-specified configuration options for registries. +func (config *serviceConfig) LoadRegistries(registries []registrytypes.Registry) error { + config.Registries = make(map[string]registrytypes.Registry) + + for _, reg := range registries { + config.Registries[reg.URL.Prefix()] = reg + } + + // backwards compatability to the "registry-mirrors" config + if len(config.Mirrors) > 0 { + reg := registrytypes.Registry{} + if officialReg, exists := config.Registries[IndexName]; exists { + reg = officialReg + } else { + var err error + reg, err = registrytypes.NewRegistry(IndexName) + if err != nil { + return err + } + } + for _, mirrorStr := range config.Mirrors { + reg.AddMirror(mirrorStr) + } + config.Registries[IndexName] = reg + } + + return config.checkRegistries() +} + // LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config. func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []string) error { cidrs := map[string]*registrytypes.NetIPNet{} @@ -125,6 +233,10 @@ func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []str // LoadMirrors loads mirrors to config, after removing duplicates. // Returns an error if mirrors contains an invalid mirror. func (config *serviceConfig) LoadMirrors(mirrors []string) error { + if len(mirrors) > 0 { + logrus.Infof("usage of deprecated 'registry-mirrors' option: please use 'registries' instead") + } + mMap := map[string]struct{}{} unique := []string{} @@ -154,6 +266,10 @@ func (config *serviceConfig) LoadMirrors(mirrors []string) error { // LoadInsecureRegistries loads insecure registries to config func (config *serviceConfig) LoadInsecureRegistries(registries []string) error { + if len(registries) > 0 { + logrus.Info("usage of deprecated 'insecure-registries' option: please use 'registries' instead") + } + // 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). // diff --git a/components/engine/registry/config_test.go b/components/engine/registry/config_test.go index 30a257e32556..78a4fadd733f 100644 --- a/components/engine/registry/config_test.go +++ b/components/engine/registry/config_test.go @@ -6,10 +6,146 @@ import ( "strings" "testing" + registrytypes "github.com/docker/docker/api/types/registry" "gotest.tools/assert" is "gotest.tools/assert/cmp" ) +func TestLoadValidRegistries(t *testing.T) { + var ( + secReg registrytypes.Registry + insecReg registrytypes.Registry + config *serviceConfig + err error + ) + // secure with mirrors + secReg, err = registrytypes.NewRegistry("https://secure.registry.com") + secMirrors := []string{"https://secure.mirror1.com", "https://secure.mirror2.com"} + if err != nil { + t.Fatal(err) + } + if err := secReg.AddMirror(secMirrors[0]); err != nil { + t.Fatal(err) + } + if err := secReg.AddMirror(secMirrors[1]); err != nil { + t.Fatal(err) + } + + // insecure without mirrors + insecReg, err = registrytypes.NewRegistry("http://insecure.registry.com") + if err != nil { + t.Fatal(err) + } + + // docker.io mirrors to test backwards compatibility + officialMirrors := []string{"https://official.mirror1.com", "https://official.mirror2.com"} + + // create serciveConfig + config = newServiceConfig( + ServiceOptions{ + Mirrors: officialMirrors, + Registries: []registrytypes.Registry{secReg, insecReg}, + }) + + // now test if the config looks as expected + getMirrors := func(reg registrytypes.Registry) []string { + mirrors := []string{} + for _, mir := range reg.Mirrors { + mirrors = append(mirrors, mir.URL.String()) + } + return mirrors + } + + if reg, loaded := config.Registries["secure.registry.com"]; !loaded { + t.Fatalf("registry not loaded") + } else { + assert.Equal(t, true, reg.URL.IsSecure()) + assert.Equal(t, false, reg.URL.IsOfficial()) + mirrors := getMirrors(reg) + assert.Equal(t, len(secMirrors), len(mirrors)) + sort.Strings(mirrors) + sort.Strings(secMirrors) + assert.Equal(t, secMirrors[0], mirrors[0]) + assert.Equal(t, secMirrors[1], mirrors[1]) + } + + if reg, loaded := config.Registries["insecure.registry.com"]; !loaded { + t.Fatalf("registry not loaded") + } else { + assert.Equal(t, false, reg.URL.IsSecure()) + assert.Equal(t, false, reg.URL.IsOfficial()) + mirrors := getMirrors(reg) + assert.Equal(t, 0, len(mirrors)) + } + + // backwards compatibility: "docker.io" will be loaded due to the config.Mirrors + if reg, loaded := config.Registries["docker.io"]; !loaded { + t.Fatalf("registry not loaded") + } else { + assert.Equal(t, true, reg.URL.IsSecure()) + assert.Equal(t, true, reg.URL.IsOfficial()) + mirrors := getMirrors(reg) + assert.Equal(t, len(officialMirrors), len(mirrors)) + sort.Strings(mirrors) + sort.Strings(officialMirrors) + // append '/' (see ValidateMirror()) + assert.Equal(t, officialMirrors[0]+"/", mirrors[0]) + assert.Equal(t, officialMirrors[1]+"/", mirrors[1]) + } +} + +//func TestLoadInvalidRegistries(t *testing.T) { +// XXX: this has to be tested manually as the v17.09.X doesn't have a proper +// error handling for service configs (errors are silently ignored), so +// the backported patch panics() instead. +//} + +func TestFindRegistry(t *testing.T) { + var ( + regA registrytypes.Registry + regB registrytypes.Registry + config *serviceConfig + err error + ) + + regA, err = registrytypes.NewRegistry("https://registry-a.com/my-prefix") + if err != nil { + t.Fatal(err) + } + + regB, err = registrytypes.NewRegistry("http://registry-b.com") + if err != nil { + t.Fatal(err) + } + + // create serciveConfig + config = newServiceConfig( + ServiceOptions{ + Registries: []registrytypes.Registry{regA, regB}, + }) + + // no match -> nil + reg := config.FindRegistry("foo") + assert.Nil(t, reg) + + // prefix match -> registry + reg = config.FindRegistry("registry-a.com/my-prefix/image:latest") + assert.NotNil(t, reg) + assert.Equal(t, "registry-a.com", reg.URL.Host()) + // no prefix match -> nil + reg = config.FindRegistry("registry-a.com/not-my-prefix/image:42") + assert.Nil(t, reg) + + // prefix match -> registry + reg = config.FindRegistry("registry-b.com/image:latest") + assert.NotNil(t, reg) + assert.Equal(t, "registry-b.com", reg.URL.Host()) + // prefix match -> registry + reg = config.FindRegistry("registry-b.com/also-in-namespaces/image:latest") + assert.NotNil(t, reg) + assert.Equal(t, "registry-b.com", reg.URL.Host()) +} + func TestLoadAllowNondistributableArtifacts(t *testing.T) { testCases := []struct { registries []string diff --git a/components/engine/registry/registry_test.go b/components/engine/registry/registry_test.go index b7459471b3f6..1e0d53e7dc21 100644 --- a/components/engine/registry/registry_test.go +++ b/components/engine/registry/registry_test.go @@ -665,7 +665,32 @@ func TestNewIndexInfo(t *testing.T) { } func TestMirrorEndpointLookup(t *testing.T) { + var ( + secReg registrytypes.Registry + config *serviceConfig + pushAPIEndpoints []APIEndpoint + pullAPIEndpoints []APIEndpoint + err error + ) + skip.If(t, os.Getuid() != 0, "skipping test that requires root") + + // secure with mirrors + secReg, err = registrytypes.NewRegistry("https://secure.registry.com/test-prefix/") + secMirrors := []string{"https://secure.mirror1.com/", "https://secure.mirror2.com/"} + if err != nil { + t.Fatal(err) + } + if err := secReg.AddMirror(secMirrors[0]); err != nil { + t.Fatal(err) + } + if err := secReg.AddMirror(secMirrors[1]); err != nil { + t.Fatal(err) + } + + // docker.io mirrors to test backwards compatibility + officialMirrors := []string{"https://official.mirror1.com/", "https://official.mirror2.com/"} + containsMirror := func(endpoints []APIEndpoint) bool { for _, pe := range endpoints { if pe.URL.Host == "my.mirror" { @@ -674,31 +699,83 @@ func TestMirrorEndpointLookup(t *testing.T) { } return false } - cfg, err := makeServiceConfig([]string{"https://my.mirror"}, nil) + cfg, err := makeServiceConfig(officialMirrors, nil) if err != nil { t.Fatal(err) } s := DefaultService{config: cfg} - imageName, err := reference.WithName(IndexName + "/test/image") + // lookups for "docker.io" + officialRef := "docker.io/test/image:latest" + pushAPIEndpoints, err = s.LookupPushEndpoints(officialRef) if err != nil { - t.Error(err) + t.Fatal(err) + } + if containsMirror(officialMirrors[0], pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") } - pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName)) + if containsMirror(officialMirrors[1], pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") + } + + pullAPIEndpoints, err = s.LookupPullEndpoints(officialRef) if err != nil { t.Fatal(err) } - if containsMirror(pushAPIEndpoints) { + if !containsMirror(officialMirrors[0], pullAPIEndpoints) { + t.Fatal("Pull endpoint should contain mirror") + } + if !containsMirror(officialMirrors[1], pullAPIEndpoints) { + t.Fatal("Pull endpoint should contain mirror") + } + + // prefix lookups + prefixRef := "secure.registry.com/test-prefix/foo:latest" + pushAPIEndpoints, err = s.LookupPushEndpoints(prefixRef) + if err != nil { + t.Fatal(err) + } + if containsMirror(secMirrors[0], pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") + } + if containsMirror(secMirrors[1], pushAPIEndpoints) { t.Fatal("Push endpoint should not contain mirror") } - pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName)) + pullAPIEndpoints, err = s.LookupPullEndpoints(prefixRef) if err != nil { t.Fatal(err) } - if !containsMirror(pullAPIEndpoints) { + if !containsMirror(secMirrors[0], pullAPIEndpoints) { t.Fatal("Pull endpoint should contain mirror") } + if !containsMirror(secMirrors[1], pullAPIEndpoints) { + t.Fatal("Pull endpoint should contain mirror") + } + + // lookups without matching prefix -> no mirrors + noPrefixRef := "secure.registry.com/no-matching-prefix/foo:latest" + pushAPIEndpoints, err = s.LookupPushEndpoints(noPrefixRef) + if err != nil { + t.Fatal(err) + } + if containsMirror(secMirrors[0], pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") + } + if containsMirror(secMirrors[1], pushAPIEndpoints) { + t.Fatal("Push endpoint should not contain mirror") + } + + pullAPIEndpoints, err = s.LookupPullEndpoints(noPrefixRef) + if err != nil { + t.Fatal(err) + } + if containsMirror(secMirrors[0], pullAPIEndpoints) { + t.Fatal("Pull endpoint should not contain mirror") + } + if containsMirror(secMirrors[1], pullAPIEndpoints) { + t.Fatal("Pull endpoint should not contain mirror") + } } func TestPushRegistryTag(t *testing.T) { diff --git a/components/engine/registry/service.go b/components/engine/registry/service.go index 08f5c7a4e12c..ee0c97a8a21b 100644 --- a/components/engine/registry/service.go +++ b/components/engine/registry/service.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - "github.com/docker/distribution/reference" + dref "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" @@ -25,14 +25,15 @@ const ( // Service is the interface defining what a registry service should implement. type Service interface { Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) - LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) - LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) - ResolveRepository(name reference.Named) (*RepositoryInfo, error) + LookupPullEndpoints(reference string) (endpoints []APIEndpoint, err error) + LookupPushEndpoints(reference string) (endpoints []APIEndpoint, err error) + ResolveRepository(name dref.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 TLSConfig(hostname string) (*tls.Config, error) LoadAllowNondistributableArtifacts([]string) error LoadMirrors([]string) error + LoadRegistries([]registrytypes.Registry) error LoadInsecureRegistries([]string) error } @@ -61,6 +62,7 @@ func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { AllowNondistributableArtifactsHostnames: make([]string, 0), InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0), IndexConfigs: make(map[string]*(registrytypes.IndexInfo)), + Registries: make(map[string]registrytypes.Registry), Mirrors: make([]string, 0), } @@ -76,6 +78,10 @@ func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { servConfig.Mirrors = append(servConfig.Mirrors, s.config.ServiceConfig.Mirrors...) + for key, value := range s.config.ServiceConfig.Registries { + servConfig.Registries[key] = value + } + return &servConfig } @@ -103,6 +109,14 @@ func (s *DefaultService) LoadInsecureRegistries(registries []string) error { return s.config.LoadInsecureRegistries(registries) } +// LoadRegistries loads registries for Service +func (s *DefaultService) LoadRegistries(registries []registrytypes.Registry) 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. @@ -241,7 +255,7 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut // ResolveRepository splits a repository name into its components // and configuration of the associated registry. -func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { +func (s *DefaultService) ResolveRepository(name dref.Named) (*RepositoryInfo, error) { s.mu.Lock() defer s.mu.Unlock() return newRepositoryInfo(s.config, name) @@ -280,24 +294,25 @@ func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, er return s.tlsConfig(mirrorURL.Host) } -// LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference. -// It gives preference to v2 endpoints over v1, mirrors over the actual -// registry, and HTTPS over plain HTTP. -func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { +// LookupPullEndpoints creates a list of endpoints based on the provided +// reference to try to pull from, in order of preference. It gives preference +// to v2 endpoints over v1, mirrors over the actual registry, and HTTPS over +// plain HTTP. +func (s *DefaultService) LookupPullEndpoints(reference string) (endpoints []APIEndpoint, err error) { s.mu.Lock() defer s.mu.Unlock() - return s.lookupEndpoints(hostname) + return s.lookupEndpoints(reference) } -// LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference. -// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP. -// Mirrors are not included. -func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { +// LookupPushEndpoints creates a list of endpoints based on the provided +// reference to try to push to, in order of preference. It gives preference to +// v2 endpoints over v1, and HTTPS over plain HTTP. Mirrors are not included. +func (s *DefaultService) LookupPushEndpoints(reference string) (endpoints []APIEndpoint, err error) { s.mu.Lock() defer s.mu.Unlock() - allEndpoints, err := s.lookupEndpoints(hostname) + allEndpoints, err := s.lookupEndpoints(reference) if err == nil { for _, endpoint := range allEndpoints { if !endpoint.Mirror { diff --git a/components/engine/registry/service_v2.go b/components/engine/registry/service_v2.go index 1a4c9e310547..efebb4f41486 100644 --- a/components/engine/registry/service_v2.go +++ b/components/engine/registry/service_v2.go @@ -1,30 +1,51 @@ package registry // import "github.com/docker/docker/registry" import ( + "fmt" "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) { +func (s *DefaultService) lookupV2Endpoints(reference 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 - } - mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL) + + // extraxt the hostname from the reference + refURL := reference + if !strings.HasPrefix(refURL, "http://") && !strings.HasPrefix(refURL, "https://") { + refURL = "https://" + refURL + } + u, err := url.Parse(refURL) + if err != nil { + return nil, fmt.Errorf("SUSE PATCH [lookupV2Endpoints]: error parsing reference %s: %s", reference, err) + } + hostname := u.Host // hostname + port (if present) + if hostname == "" { + return nil, fmt.Errorf("SUSE PATCH [lookupV2Endpoints]: cannot determine hostname of reference %s", reference) + } + + // create endpoints for official and configured registries + official := false + if hostname == "docker.io" { + official = true + } + reg := s.config.FindRegistry(reference) + + if reg != nil || official { + if reg == nil { + reg = ®istrytypes.Registry{} + } + // if present, add mirrors prior to the registry + for _, mirror := range reg.Mirrors { + mURL := mirror.URL.URL() + mirrorTLSConfig, err := s.tlsConfigForMirror(&mURL) if err != nil { - return nil, err + return nil, fmt.Errorf("SUSE PATCH [lookupV2Endpoints]: %s", err) } endpoints = append(endpoints, APIEndpoint{ - URL: mirrorURL, + URL: &mURL, // guess mirrors are v2 Version: APIVersion2, Mirror: true, @@ -32,11 +53,20 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp TLSConfig: mirrorTLSConfig, }) } - // v2 registry + // add the registry + var endpointURL *url.URL + if official { + endpointURL = DefaultV2Registry + } else { + endpointURL = &url.URL{ + Scheme: reg.URL.Scheme(), + Host: reg.URL.Host(), + } + } endpoints = append(endpoints, APIEndpoint{ - URL: DefaultV2Registry, + URL: endpointURL, Version: APIVersion2, - Official: true, + Official: official, TrimHostname: true, TLSConfig: tlsConfig, }) @@ -48,7 +78,7 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp tlsConfig, err = s.tlsConfig(hostname) if err != nil { - return nil, err + return nil, fmt.Errorf("SUSE PATCH [lookupV2Enpoints]: %s", err) } endpoints = []APIEndpoint{ -- 2.22.0