Skip to content

Commit

Permalink
PRIVATE-REGISTRY: add private-registry mirror support
Browse files Browse the repository at this point in the history
NOTE: This is a backport/downstream patch of the upstream pull-request
      for Moby, which is still subject to changes.  Please visit
      <moby#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>
  • Loading branch information
vrothberg authored and cyphar committed Feb 10, 2021
1 parent eaedebb commit 14f385e
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 46 deletions.
144 changes: 144 additions & 0 deletions api/types/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions daemon/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
33 changes: 33 additions & 0 deletions daemon/reload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down
95 changes: 95 additions & 0 deletions daemon/reload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{}),
Expand Down
2 changes: 1 addition & 1 deletion distribution/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion distribution/pull_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion distribution/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 14f385e

Please sign in to comment.