From 0bb32212d07d21b0704ef3b3197fad118ae87e7f Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Mon, 2 Jul 2018 13:37:34 +0200 Subject: [PATCH 3/6] PRIVATE-REGISTRY: 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 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 --- api/types/registry/registry.go | 144 +++++++++++++++++++++++++++++++++ daemon/config/config.go | 4 + daemon/reload.go | 33 ++++++++ daemon/reload_test.go | 95 ++++++++++++++++++++++ distribution/pull.go | 2 +- distribution/pull_v2.go | 2 +- distribution/push.go | 2 +- registry/config.go | 126 ++++++++++++++++++++++++++++- registry/config_test.go | 142 ++++++++++++++++++++++++++++++++ registry/registry_test.go | 99 ++++++++++++++++++++--- registry/service.go | 43 +++++++--- registry/service_v2.go | 64 +++++++++++---- 12 files changed, 710 insertions(+), 46 deletions(-) diff --git a/api/types/registry/registry.go b/api/types/registry/registry.go index 53e47084c8d5..b4bb9ef805d3 100644 --- a/api/types/registry/registry.go +++ b/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" v1 "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/daemon/config/config.go b/daemon/config/config.go index 4990727597c9..f3a53c692d73 100644 --- a/daemon/config/config.go +++ b/daemon/config/config.go @@ -482,6 +482,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/daemon/reload.go b/daemon/reload.go index 72379c054ef6..1e4afe9b3b03 100644 --- a/daemon/reload.go +++ b/daemon/reload.go @@ -22,8 +22,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{} @@ -69,6 +75,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) } @@ -320,6 +329,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/daemon/reload_test.go b/daemon/reload_test.go index 4a8466616dee..46664f4b1eda 100644 --- a/daemon/reload_test.go +++ b/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" @@ -211,6 +212,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/distribution/pull.go b/distribution/pull.go index c8ddd4c5cfcd..b17e9d25d6c2 100644 --- a/distribution/pull.go +++ b/distribution/pull.go @@ -61,7 +61,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/distribution/pull_v2.go b/distribution/pull_v2.go index 023ee2e71efd..e14cdd16b410 100644 --- a/distribution/pull_v2.go +++ b/distribution/pull_v2.go @@ -431,7 +431,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/distribution/push.go b/distribution/push.go index 5617a4c95f49..0a24aebed968 100644 --- a/distribution/push.go +++ b/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/registry/config.go b/registry/config.go index 54b83fa40aab..e1ba24b83bdd 100644 --- a/registry/config.go +++ b/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. @@ -59,8 +60,21 @@ 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), @@ -78,10 +92,106 @@ 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() { + regURL := reg.URL.URL() + mirrorURL := mirror.URL.URL() + logrus.Warnf("registry '%s' and mirror '%s' have different security levels", ®URL, &mirrorURL) + } + } + 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{} @@ -122,6 +232,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{} @@ -151,6 +265,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/registry/config_test.go b/registry/config_test.go index ae8cb23f94b6..7f31b1eb2bf4 100644 --- a/registry/config_test.go +++ b/registry/config_test.go @@ -6,10 +6,152 @@ import ( "strings" "testing" + registrytypes "github.com/docker/docker/api/types/registry" "gotest.tools/v3/assert" is "gotest.tools/v3/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, err = newServiceConfig( + ServiceOptions{ + Mirrors: officialMirrors, + Registries: []registrytypes.Registry{secReg, insecReg}, + }) + if err != nil { + t.Fatal(err) + } + + // 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, err = newServiceConfig( + ServiceOptions{ + Registries: []registrytypes.Registry{regA, regB}, + }) + if err != nil { + t.Fatal(err) + } + + // no match -> nil + reg := config.FindRegistry("foo") + assert.Assert(t, is.Nil(reg)) + + // prefix match -> registry + reg = config.FindRegistry("registry-a.com/my-prefix/image:latest") + assert.Assert(t, reg != nil) + 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.Assert(t, is.Nil(reg)) + + // prefix match -> registry + reg = config.FindRegistry("registry-b.com/image:latest") + assert.Assert(t, reg != nil) + assert.Equal(t, "registry-b.com", reg.URL.Host()) + // prefix match -> registry + reg = config.FindRegistry("registry-b.com/also-in-namespaces/image:latest") + assert.Assert(t, reg != nil) + assert.Equal(t, "registry-b.com", reg.URL.Host()) +} + func TestLoadAllowNondistributableArtifacts(t *testing.T) { testCases := []struct { registries []string diff --git a/registry/registry_test.go b/registry/registry_test.go index 417c9574bc5d..b3a978474ec1 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -507,40 +507,119 @@ func TestNewIndexInfo(t *testing.T) { } func TestMirrorEndpointLookup(t *testing.T) { - skip.If(t, os.Getuid() != 0, "skipping test that requires root") - containsMirror := func(endpoints []APIEndpoint) bool { + var ( + registries []registrytypes.Registry + secReg registrytypes.Registry + pushAPIEndpoints []APIEndpoint + pullAPIEndpoints []APIEndpoint + err error + ) + + // 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) + } + registries = append(registries, secReg) + + // docker.io mirrors to test backwards compatibility + officialMirrors := []string{"https://official.mirror1.com/", "https://official.mirror2.com/"} + + containsMirror := func(needle string, endpoints []APIEndpoint) bool { for _, pe := range endpoints { - if pe.URL.Host == "my.mirror" { + if pe.URL.String() == needle { return true } } return false } - cfg, err := makeServiceConfig([]string{"https://my.mirror"}, nil) + cfg, err := newServiceConfig(ServiceOptions{ + Mirrors: officialMirrors, + Registries: registries, + }) 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") + } + 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(officialMirrors[0], pullAPIEndpoints) { + t.Fatal("Pull endpoint should contain mirror") } - pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName)) + 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(pushAPIEndpoints) { + 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 TestSearchRepositories(t *testing.T) { diff --git a/registry/service.go b/registry/service.go index 3b08e39da2c2..62556ba1ba70 100644 --- a/registry/service.go +++ b/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. @@ -230,7 +244,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) @@ -270,22 +284,25 @@ func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, er return s.tlsConfig(mirrorURL.Host) } -// 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) { +// 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.lookupV2Endpoints(hostname) + return s.lookupV2Endpoints(reference) } -// 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) { +// 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.lookupV2Endpoints(hostname) + allEndpoints, err := s.lookupV2Endpoints(reference) if err == nil { for _, endpoint := range allEndpoints { if !endpoint.Mirror { diff --git a/registry/service_v2.go b/registry/service_v2.go index 3e3a5b41ffbd..451a6f874bc1 100644 --- a/registry/service_v2.go +++ b/registry/service_v2.go @@ -1,39 +1,71 @@ 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 { - 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, Version: APIVersion2, Mirror: true, TrimHostname: true, TLSConfig: mirrorTLSConfig, }) } + // 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, }) @@ -45,7 +77,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.33.0