File private-registry-0001-Add-private-registry-mirror-support.patch of Package docker.13548

From 69d43a9550cdedf86b0d4b29e9d737af90221109 Mon Sep 17 00:00:00 2001
From: Valentin Rothberg <vrothberg@suse.com>
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 <fcastelli@suse.com>
Signed-off-by: Valentin Rothberg <vrothberg@suse.com>
Signed-off-by: Aleksa Sarai <asarai@suse.de>
---
 .../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 &reg
+	}
+	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 = &registrytypes.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

openSUSE Build Service is sponsored by