docker/0003-PRIVATE-REGISTRY-add-private-registry-mirror-support.patch

1147 lines
38 KiB
Diff
Raw Normal View History

From cf3c6ccefda39c60dc656404674628ac48b55de9 Mon Sep 17 00:00:00 2001
From: Valentin Rothberg <vrothberg@suse.com>
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
<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>
---
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 123abf6b497a..097ead45d0fd 100644
--- a/distribution/pull_v2.go
+++ b/distribution/pull_v2.go
@@ -432,7 +432,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", &regURL, &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 &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{}
@@ -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 = &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,
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.36.1