From 67fdf574d5acd6ddccb6ece0ffe0ace1c1608712 Mon Sep 17 00:00:00 2001 From: Noah Treuhaft Date: Tue, 9 May 2017 14:00:31 -0700 Subject: [PATCH] Add daemon option to push foreign layers The --allow-nondistributable-artifacts daemon option specifies registries to which foreign layers should be pushed. (By default, foreign layers are not pushed to registries.) Additionally, to make this option effective, foreign layers are now pulled from the registry if possible, falling back to the URLs in the image manifest otherwise. This option is useful when pushing images containing foreign layers to a registry on an air-gapped network so hosts on that network can pull the images without connecting to another server. Signed-off-by: Noah Treuhaft --- api/types/registry/registry.go | 8 +- cmd/dockerd/daemon_test.go | 2 + contrib/completion/bash/docker | 1 + contrib/completion/zsh/_docker | 1 + daemon/reload.go | 28 +++++ daemon/reload_test.go | 56 +++++++++ distribution/pull_v2_windows.go | 20 ++- distribution/push_v2.go | 13 +- docs/reference/commandline/dockerd.md | 30 +++++ .../docker_cli_events_unix_test.go | 2 +- man/dockerd.8.md | 15 +++ registry/config.go | 83 ++++++++++-- registry/config_test.go | 119 ++++++++++++++++++ registry/registry_test.go | 42 +++++++ registry/service.go | 32 +++-- registry/service_v2.go | 14 ++- 16 files changed, 430 insertions(+), 36 deletions(-) diff --git a/api/types/registry/registry.go b/api/types/registry/registry.go index 28fafab901..008c34cf37 100644 --- a/api/types/registry/registry.go +++ b/api/types/registry/registry.go @@ -7,9 +7,11 @@ import ( // ServiceConfig stores daemon registry services configuration. type ServiceConfig struct { - InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` - IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` - Mirrors []string + AllowNondistributableArtifactsCIDRs []*NetIPNet + AllowNondistributableArtifactsHostnames []string + InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` + IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` + Mirrors []string } // NetIPNet is the net.IPNet type, which can be marshalled and diff --git a/cmd/dockerd/daemon_test.go b/cmd/dockerd/daemon_test.go index 5e60ef55f6..c3ad617c78 100644 --- a/cmd/dockerd/daemon_test.go +++ b/cmd/dockerd/daemon_test.go @@ -131,6 +131,7 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) { func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) { content := `{ + "allow-nondistributable-artifacts": ["allow-nondistributable-artifacts.com"], "registry-mirrors": ["https://mirrors.docker.com"], "insecure-registries": ["https://insecure.docker.com"] }` @@ -142,6 +143,7 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) { require.NoError(t, err) require.NotNil(t, loadedConfig) + assert.Len(t, loadedConfig.AllowNondistributableArtifacts, 1) assert.Len(t, loadedConfig.Mirrors, 1) assert.Len(t, loadedConfig.InsecureRegistries, 1) } diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index b925edf055..d94fd79e3a 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -1962,6 +1962,7 @@ _docker_daemon() { local options_with_args=" $global_options_with_args --add-runtime + --allow-nondistributable-artifacts --api-cors-header --authorization-plugin --bip diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 316caf5d43..0860907839 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -2603,6 +2603,7 @@ __docker_subcommand() { _arguments $(__docker_arguments) \ $opts_help \ "($help)*--add-runtime=[Register an additional OCI compatible runtime]:runtime:__docker_complete_runtimes" \ + "($help)*--allow-nondistributable-artifacts=[Push nondistributable artifacts to specified registries]:registry: " \ "($help)--api-cors-header=[CORS headers in the Engine API]:CORS headers: " \ "($help)*--authorization-plugin=[Authorization plugins to load]" \ "($help -b --bridge)"{-b=,--bridge=}"[Attach containers to a network bridge]:bridge:_net_interfaces" \ diff --git a/daemon/reload.go b/daemon/reload.go index deb13f54bf..e47455946e 100644 --- a/daemon/reload.go +++ b/daemon/reload.go @@ -48,6 +48,9 @@ func (daemon *Daemon) Reload(conf *config.Config) (err error) { if err := daemon.reloadLabels(conf, attributes); err != nil { return err } + if err := daemon.reloadAllowNondistributableArtifacts(conf, attributes); err != nil { + return err + } if err := daemon.reloadInsecureRegistries(conf, attributes); err != nil { return err } @@ -217,6 +220,31 @@ func (daemon *Daemon) reloadLabels(conf *config.Config, attributes map[string]st return nil } +// reloadAllowNondistributableArtifacts updates the configuration with allow-nondistributable-artifacts options +// and updates the passed attributes. +func (daemon *Daemon) reloadAllowNondistributableArtifacts(conf *config.Config, attributes map[string]string) error { + // Update corresponding configuration. + if conf.IsValueSet("allow-nondistributable-artifacts") { + daemon.configStore.AllowNondistributableArtifacts = conf.AllowNondistributableArtifacts + if err := daemon.RegistryService.LoadAllowNondistributableArtifacts(conf.AllowNondistributableArtifacts); err != nil { + return err + } + } + + // Prepare reload event attributes with updatable configurations. + if daemon.configStore.AllowNondistributableArtifacts != nil { + v, err := json.Marshal(daemon.configStore.AllowNondistributableArtifacts) + if err != nil { + return err + } + attributes["allow-nondistributable-artifacts"] = string(v) + } else { + attributes["allow-nondistributable-artifacts"] = "[]" + } + + return nil +} + // reloadInsecureRegistries updates configuration with insecure registry option // and updates the passed attributes func (daemon *Daemon) reloadInsecureRegistries(conf *config.Config, attributes map[string]string) error { diff --git a/daemon/reload_test.go b/daemon/reload_test.go index ba1fd02471..bf11b6bd56 100644 --- a/daemon/reload_test.go +++ b/daemon/reload_test.go @@ -4,6 +4,7 @@ package daemon import ( "reflect" + "sort" "testing" "time" @@ -40,6 +41,61 @@ func TestDaemonReloadLabels(t *testing.T) { } } +func TestDaemonReloadAllowNondistributableArtifacts(t *testing.T) { + daemon := &Daemon{ + configStore: &config.Config{}, + } + + // Initialize daemon with some registries. + daemon.RegistryService = registry.NewService(registry.ServiceOptions{ + AllowNondistributableArtifacts: []string{ + "127.0.0.0/8", + "10.10.1.11:5000", + "10.10.1.22:5000", // This will be removed during reload. + "docker1.com", + "docker2.com", // This will be removed during reload. + }, + }) + + registries := []string{ + "127.0.0.0/8", + "10.10.1.11:5000", + "10.10.1.33:5000", // This will be added during reload. + "docker1.com", + "docker3.com", // This will be added during reload. + } + + newConfig := &config.Config{ + CommonConfig: config.CommonConfig{ + ServiceOptions: registry.ServiceOptions{ + AllowNondistributableArtifacts: registries, + }, + ValuesSet: map[string]interface{}{ + "allow-nondistributable-artifacts": registries, + }, + }, + } + + if err := daemon.Reload(newConfig); err != nil { + t.Fatal(err) + } + + actual := []string{} + serviceConfig := daemon.RegistryService.ServiceConfig() + for _, value := range serviceConfig.AllowNondistributableArtifactsCIDRs { + actual = append(actual, value.String()) + } + for _, value := range serviceConfig.AllowNondistributableArtifactsHostnames { + actual = append(actual, value) + } + + sort.Strings(registries) + sort.Strings(actual) + if !reflect.DeepEqual(registries, actual) { + t.Fatalf("expected %v, got %v\n", registries, actual) + } +} + func TestDaemonReloadMirrors(t *testing.T) { daemon := &Daemon{} daemon.RegistryService = registry.NewService(registry.ServiceOptions{ diff --git a/distribution/pull_v2_windows.go b/distribution/pull_v2_windows.go index aefed86601..543ecc10eb 100644 --- a/distribution/pull_v2_windows.go +++ b/distribution/pull_v2_windows.go @@ -23,20 +23,28 @@ func (ld *v2LayerDescriptor) Descriptor() distribution.Descriptor { } func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekCloser, error) { + blobs := ld.repo.Blobs(ctx) + rsc, err := blobs.Open(ctx, ld.digest) + if len(ld.src.URLs) == 0 { - blobs := ld.repo.Blobs(ctx) - return blobs.Open(ctx, ld.digest) + return rsc, err } - var ( - err error - rsc distribution.ReadSeekCloser - ) + // We're done if the registry has this blob. + if err == nil { + // Seek does an HTTP GET. If it succeeds, the blob really is accessible. + if _, err = rsc.Seek(0, os.SEEK_SET); err == nil { + return rsc, nil + } + rsc.Close() + } // Find the first URL that results in a 200 result code. for _, url := range ld.src.URLs { logrus.Debugf("Pulling %v from foreign URL %v", ld.digest, url) rsc = transport.NewHTTPReadSeeker(http.DefaultClient, url, nil) + + // Seek does an HTTP GET. If it succeeds, the blob really is accessible. _, err = rsc.Seek(0, os.SEEK_SET) if err == nil { break diff --git a/distribution/push_v2.go b/distribution/push_v2.go index 29e44f7c8b..5a6673780c 100644 --- a/distribution/push_v2.go +++ b/distribution/push_v2.go @@ -141,6 +141,7 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, id hmacKey: hmacKey, repoInfo: p.repoInfo.Name, ref: p.ref, + endpoint: p.endpoint, repo: p.repo, pushState: &p.pushState, } @@ -239,6 +240,7 @@ type v2PushDescriptor struct { hmacKey []byte repoInfo reference.Named ref reference.Named + endpoint registry.APIEndpoint repo distribution.Repository pushState *pushState remoteDescriptor distribution.Descriptor @@ -259,10 +261,13 @@ func (pd *v2PushDescriptor) DiffID() layer.DiffID { } func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) { - if fs, ok := pd.layer.(distribution.Describable); ok { - if d := fs.Descriptor(); len(d.URLs) > 0 { - progress.Update(progressOutput, pd.ID(), "Skipped foreign layer") - return d, nil + // Skip foreign layers unless this registry allows nondistributable artifacts. + if !pd.endpoint.AllowNondistributableArtifacts { + if fs, ok := pd.layer.(distribution.Describable); ok { + if d := fs.Descriptor(); len(d.URLs) > 0 { + progress.Update(progressOutput, pd.ID(), "Skipped foreign layer") + return d, nil + } } } diff --git a/docs/reference/commandline/dockerd.md b/docs/reference/commandline/dockerd.md index 5f28f6f21c..93774c841b 100644 --- a/docs/reference/commandline/dockerd.md +++ b/docs/reference/commandline/dockerd.md @@ -23,6 +23,7 @@ A self-sufficient runtime for containers. Options: --add-runtime runtime Register an additional OCI compatible runtime (default []) + --allow-nondistributable-artifacts list Push nondistributable artifacts to specified registries (default []) --api-cors-header string Set CORS headers in the Engine API --authorization-plugin list Authorization plugins to load (default []) --bip string Specify network bridge IP @@ -828,6 +829,32 @@ To set the DNS search domain for all Docker containers, use: $ sudo dockerd --dns-search example.com ``` +#### Allow push of nondistributable artifacts + +Some images (e.g., Windows base images) contain artifacts whose distribution is +restricted by license. When these images are pushed to a registry, restricted +artifacts are not included. + +To override this behavior for specific registries, use the +`--allow-nondistributable-artifacts` option in one of the following forms: + +* `--allow-nondistributable-artifacts myregistry:5000` tells the Docker daemon + to push nondistributable artifacts to myregistry:5000. +* `--allow-nondistributable-artifacts 10.1.0.0/16` tells the Docker daemon to + push nondistributable artifacts to all registries whose resolved IP address + is within the subnet described by the CIDR syntax. + +This option can be used multiple times. + +This option is useful when pushing images containing nondistributable artifacts +to a registry on an air-gapped network so hosts on that network can pull the +images without connecting to another server. + +> **Warning**: Nondistributable artifacts typically have restrictions on how +> and where they can be distributed and shared. Only use this feature to push +> artifacts to private registries and ensure that you are in compliance with +> any terms that cover redistributing nondistributable artifacts. + #### Insecure registries Docker considers a private registry either secure or insecure. In the rest of @@ -1261,6 +1288,7 @@ This is a full example of the allowed configuration options on Linux: "default-gateway-v6": "", "icc": false, "raw-logs": false, + "allow-nondistributable-artifacts": [], "registry-mirrors": [], "seccomp-profile": "", "insecure-registries": [], @@ -1330,6 +1358,7 @@ This is a full example of the allowed configuration options on Windows: "bridge": "", "fixed-cidr": "", "raw-logs": false, + "allow-nondistributable-artifacts": [], "registry-mirrors": [], "insecure-registries": [], "disable-legacy-registry": false @@ -1361,6 +1390,7 @@ The list of currently supported options that can be reconfigured is this: - `runtimes`: it updates the list of available OCI runtimes that can be used to run containers - `authorization-plugin`: specifies the authorization plugins to use. +- `allow-nondistributable-artifacts`: Replaces the set of registries to which the daemon will push nondistributable artifacts with a new set of registries. - `insecure-registries`: it replaces the daemon insecure registries with a new set of insecure registries. If some existing insecure registries in daemon's configuration are not in newly reloaded insecure resgitries, these existing ones will be removed from daemon's config. - `registry-mirrors`: it replaces the daemon registry mirrors with a new set of registry mirrors. If some existing registry mirrors in daemon's configuration are not in newly reloaded registry mirrors, these existing ones will be removed from daemon's config. diff --git a/integration-cli/docker_cli_events_unix_test.go b/integration-cli/docker_cli_events_unix_test.go index 4d5777d128..fe053aa596 100644 --- a/integration-cli/docker_cli_events_unix_test.go +++ b/integration-cli/docker_cli_events_unix_test.go @@ -428,7 +428,7 @@ func (s *DockerDaemonSuite) TestDaemonEvents(c *check.C) { out, err = s.d.Cmd("events", "--since=0", "--until", daemonUnixTime(c)) c.Assert(err, checker.IsNil) - c.Assert(out, checker.Contains, fmt.Sprintf("daemon reload %s (cluster-advertise=, cluster-store=, cluster-store-opts={}, debug=true, default-runtime=runc, default-shm-size=67108864, insecure-registries=[], labels=[\"bar=foo\"], live-restore=false, max-concurrent-downloads=1, max-concurrent-uploads=5, name=%s, registry-mirrors=[], runtimes=runc:{docker-runc []}, shutdown-timeout=10)", daemonID, daemonName)) + c.Assert(out, checker.Contains, fmt.Sprintf("daemon reload %s (allow-nondistributable-artifacts=[], cluster-advertise=, cluster-store=, cluster-store-opts={}, debug=true, default-runtime=runc, default-shm-size=67108864, insecure-registries=[], labels=[\"bar=foo\"], live-restore=false, max-concurrent-downloads=1, max-concurrent-uploads=5, name=%s, registry-mirrors=[], runtimes=runc:{docker-runc []}, shutdown-timeout=10)", daemonID, daemonName)) } func (s *DockerDaemonSuite) TestDaemonEventsWithFilters(c *check.C) { diff --git a/man/dockerd.8.md b/man/dockerd.8.md index 6a12d3f165..a4e079074f 100644 --- a/man/dockerd.8.md +++ b/man/dockerd.8.md @@ -7,6 +7,7 @@ dockerd - Enable daemon mode # SYNOPSIS **dockerd** [**--add-runtime**[=*[]*]] +[**--allow-nondistributable-artifacts**[=*[]*]] [**--api-cors-header**=[=*API-CORS-HEADER*]] [**--authorization-plugin**[=*[]*]] [**-b**|**--bridge**[=*BRIDGE*]] @@ -116,6 +117,20 @@ $ sudo dockerd --add-runtime runc=runc --add-runtime custom=/usr/local/bin/my-ru **Note**: defining runtime arguments via the command line is not supported. +**--allow-nondistributable-artifacts**=[] + Push nondistributable artifacts to the specified registries. + + List can contain elements with CIDR notation to specify a whole subnet. + + This option is useful when pushing images containing nondistributable + artifacts to a registry on an air-gapped network so hosts on that network can + pull the images without connecting to another server. + + **Warning**: Nondistributable artifacts typically have restrictions on how + and where they can be distributed and shared. Only use this feature to push + artifacts to private registries and ensure that you are in compliance with + any terms that cover redistributing nondistributable artifacts. + **--api-cors-header**="" Set CORS headers in the Engine API. Default is cors disabled. Give urls like "http://foo, http://bar, ...". Give "*" to allow all. diff --git a/registry/config.go b/registry/config.go index 7b6703f570..651bd73097 100644 --- a/registry/config.go +++ b/registry/config.go @@ -18,8 +18,9 @@ import ( // ServiceOptions holds command line options. type ServiceOptions struct { - 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"` // V2Only controls access to legacy registries. If it is set to true via the // command line flag the daemon will not attempt to contact v1 legacy registries @@ -74,9 +75,11 @@ var lookupIP = net.LookupIP // InstallCliFlags adds command-line options to the top-level flag parser for // the current process. func (options *ServiceOptions) InstallCliFlags(flags *pflag.FlagSet) { + ana := opts.NewNamedListOptsRef("allow-nondistributable-artifacts", &options.AllowNondistributableArtifacts, ValidateIndexName) mirrors := opts.NewNamedListOptsRef("registry-mirrors", &options.Mirrors, ValidateMirror) insecureRegistries := opts.NewNamedListOptsRef("insecure-registries", &options.InsecureRegistries, ValidateIndexName) + flags.Var(ana, "allow-nondistributable-artifacts", "Allow push of nondistributable artifacts to registry") flags.Var(mirrors, "registry-mirror", "Preferred Docker registry mirror") flags.Var(insecureRegistries, "insecure-registry", "Enable insecure registry communication") @@ -95,12 +98,50 @@ func newServiceConfig(options ServiceOptions) *serviceConfig { V2Only: options.V2Only, } + config.LoadAllowNondistributableArtifacts(options.AllowNondistributableArtifacts) config.LoadMirrors(options.Mirrors) config.LoadInsecureRegistries(options.InsecureRegistries) return config } +// LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config. +func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []string) error { + cidrs := map[string]*registrytypes.NetIPNet{} + hostnames := map[string]bool{} + + for _, r := range registries { + if _, err := ValidateIndexName(r); err != nil { + return err + } + if validateNoScheme(r) != nil { + return fmt.Errorf("allow-nondistributable-artifacts registry %s should not contain '://'", r) + } + + if _, ipnet, err := net.ParseCIDR(r); err == nil { + // Valid CIDR. + cidrs[ipnet.String()] = (*registrytypes.NetIPNet)(ipnet) + } else if err := validateHostPort(r); err == nil { + // Must be `host:port` if not CIDR. + hostnames[r] = true + } else { + return fmt.Errorf("allow-nondistributable-artifacts registry %s is not valid: %v", r, err) + } + } + + config.AllowNondistributableArtifactsCIDRs = make([]*(registrytypes.NetIPNet), 0) + for _, c := range cidrs { + config.AllowNondistributableArtifactsCIDRs = append(config.AllowNondistributableArtifactsCIDRs, c) + } + + config.AllowNondistributableArtifactsHostnames = make([]string, 0) + for h := range hostnames { + config.AllowNondistributableArtifactsHostnames = append(config.AllowNondistributableArtifactsHostnames, h) + } + + return nil +} + // LoadMirrors loads mirrors to config, after removing duplicates. // Returns an error if mirrors contains an invalid mirror. func (config *serviceConfig) LoadMirrors(mirrors []string) error { @@ -211,6 +252,25 @@ skip: return nil } +// allowNondistributableArtifacts returns true if the provided hostname is part of the list of regsitries +// that allow push of nondistributable artifacts. +// +// The list can contain elements with CIDR notation to specify a whole subnet. If the subnet contains an IP +// of the registry specified by hostname, true is returned. +// +// hostname should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved to IP addresses for matching. If +// resolution fails, CIDR matching is not performed. +func allowNondistributableArtifacts(config *serviceConfig, hostname string) bool { + for _, h := range config.AllowNondistributableArtifactsHostnames { + if h == hostname { + return true + } + } + + return isCIDRMatch(config.AllowNondistributableArtifactsCIDRs, hostname) +} + // isSecureIndex returns false if the provided indexName is part of the list of insecure registries // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. // @@ -229,10 +289,17 @@ func isSecureIndex(config *serviceConfig, indexName string) bool { return index.Secure } - host, _, err := net.SplitHostPort(indexName) + return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName) +} + +// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`) +// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be +// resolved to IP addresses for matching. If resolution fails, false is returned. +func isCIDRMatch(cidrs []*registrytypes.NetIPNet, URLHost string) bool { + host, _, err := net.SplitHostPort(URLHost) if err != nil { - // assume indexName is of the form `host` without the port and go on. - host = indexName + // Assume URLHost is of the form `host` without the port and go on. + host = URLHost } addrs, err := lookupIP(host) @@ -249,15 +316,15 @@ func isSecureIndex(config *serviceConfig, indexName string) bool { // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. for _, addr := range addrs { - for _, ipnet := range config.InsecureRegistryCIDRs { + for _, ipnet := range cidrs { // check if the addr falls in the subnet if (*net.IPNet)(ipnet).Contains(addr) { - return false + return true } } } - return true + return false } // ValidateMirror validates an HTTP(S) registry mirror diff --git a/registry/config_test.go b/registry/config_test.go index b57e515b94..8cb7e5a543 100644 --- a/registry/config_test.go +++ b/registry/config_test.go @@ -1,10 +1,129 @@ package registry import ( + "reflect" + "sort" "strings" "testing" ) +func TestLoadAllowNondistributableArtifacts(t *testing.T) { + testCases := []struct { + registries []string + cidrStrs []string + hostnames []string + err string + }{ + { + registries: []string{"1.2.3.0/24"}, + cidrStrs: []string{"1.2.3.0/24"}, + }, + { + registries: []string{"2001:db8::/120"}, + cidrStrs: []string{"2001:db8::/120"}, + }, + { + registries: []string{"127.0.0.1"}, + hostnames: []string{"127.0.0.1"}, + }, + { + registries: []string{"127.0.0.1:8080"}, + hostnames: []string{"127.0.0.1:8080"}, + }, + { + registries: []string{"2001:db8::1"}, + hostnames: []string{"2001:db8::1"}, + }, + { + registries: []string{"[2001:db8::1]:80"}, + hostnames: []string{"[2001:db8::1]:80"}, + }, + { + registries: []string{"[2001:db8::1]:80"}, + hostnames: []string{"[2001:db8::1]:80"}, + }, + { + registries: []string{"1.2.3.0/24", "2001:db8::/120", "127.0.0.1", "127.0.0.1:8080"}, + cidrStrs: []string{"1.2.3.0/24", "2001:db8::/120"}, + hostnames: []string{"127.0.0.1", "127.0.0.1:8080"}, + }, + + { + registries: []string{"http://mytest.com"}, + err: "allow-nondistributable-artifacts registry http://mytest.com should not contain '://'", + }, + { + registries: []string{"https://mytest.com"}, + err: "allow-nondistributable-artifacts registry https://mytest.com should not contain '://'", + }, + { + registries: []string{"HTTP://mytest.com"}, + err: "allow-nondistributable-artifacts registry HTTP://mytest.com should not contain '://'", + }, + { + registries: []string{"svn://mytest.com"}, + err: "allow-nondistributable-artifacts registry svn://mytest.com should not contain '://'", + }, + { + registries: []string{"-invalid-registry"}, + err: "Cannot begin or end with a hyphen", + }, + { + registries: []string{`mytest-.com`}, + err: `allow-nondistributable-artifacts registry mytest-.com is not valid: invalid host "mytest-.com"`, + }, + { + registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`}, + err: `allow-nondistributable-artifacts registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`, + }, + { + registries: []string{`mytest.com:500000`}, + err: `allow-nondistributable-artifacts registry mytest.com:500000 is not valid: invalid port "500000"`, + }, + { + registries: []string{`"mytest.com"`}, + err: `allow-nondistributable-artifacts registry "mytest.com" is not valid: invalid host "\"mytest.com\""`, + }, + { + registries: []string{`"mytest.com:5000"`}, + err: `allow-nondistributable-artifacts registry "mytest.com:5000" is not valid: invalid host "\"mytest.com"`, + }, + } + for _, testCase := range testCases { + config := newServiceConfig(ServiceOptions{}) + err := config.LoadAllowNondistributableArtifacts(testCase.registries) + if testCase.err == "" { + if err != nil { + t.Fatalf("expect no error, got '%s'", err) + } + + cidrStrs := []string{} + for _, c := range config.AllowNondistributableArtifactsCIDRs { + cidrStrs = append(cidrStrs, c.String()) + } + + sort.Strings(testCase.cidrStrs) + sort.Strings(cidrStrs) + if (len(testCase.cidrStrs) > 0 || len(cidrStrs) > 0) && !reflect.DeepEqual(testCase.cidrStrs, cidrStrs) { + t.Fatalf("expect AllowNondistributableArtifactsCIDRs to be '%+v', got '%+v'", testCase.cidrStrs, cidrStrs) + } + + sort.Strings(testCase.hostnames) + sort.Strings(config.AllowNondistributableArtifactsHostnames) + if (len(testCase.hostnames) > 0 || len(config.AllowNondistributableArtifactsHostnames) > 0) && !reflect.DeepEqual(testCase.hostnames, config.AllowNondistributableArtifactsHostnames) { + t.Fatalf("expect AllowNondistributableArtifactsHostnames to be '%+v', got '%+v'", testCase.hostnames, config.AllowNondistributableArtifactsHostnames) + } + } else { + if err == nil { + t.Fatalf("expect error '%s', got no error", testCase.err) + } + if !strings.Contains(err.Error(), testCase.err) { + t.Fatalf("expect error '%s', got '%s'", testCase.err, err) + } + } + } +} + func TestValidateMirror(t *testing.T) { valid := []string{ "http://mirror-1.com", diff --git a/registry/registry_test.go b/registry/registry_test.go index 1cbaaf4d46..d89c46c2c0 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -811,6 +811,48 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { } } +func TestAllowNondistributableArtifacts(t *testing.T) { + tests := []struct { + addr string + registries []string + expected bool + }{ + {IndexName, nil, false}, + {"example.com", []string{}, false}, + {"example.com", []string{"example.com"}, true}, + {"localhost", []string{"localhost:5000"}, false}, + {"localhost:5000", []string{"localhost:5000"}, true}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"127.0.0.1:5000"}, true}, + {"localhost", nil, false}, + {"localhost:5000", nil, false}, + {"127.0.0.1", nil, false}, + {"localhost", []string{"example.com"}, false}, + {"127.0.0.1", []string{"example.com"}, false}, + {"example.com", nil, false}, + {"example.com", []string{"example.com"}, true}, + {"127.0.0.1", []string{"example.com"}, false}, + {"127.0.0.1:5000", []string{"example.com"}, false}, + {"example.com:5000", []string{"42.42.0.0/16"}, true}, + {"example.com", []string{"42.42.0.0/16"}, true}, + {"example.com:5000", []string{"42.42.42.42/8"}, true}, + {"127.0.0.1:5000", []string{"127.0.0.0/8"}, true}, + {"42.42.42.42:5000", []string{"42.1.1.1/8"}, true}, + {"invalid.domain.com", []string{"42.42.0.0/16"}, false}, + {"invalid.domain.com", []string{"invalid.domain.com"}, true}, + {"invalid.domain.com:5000", []string{"invalid.domain.com"}, false}, + {"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, true}, + } + for _, tt := range tests { + config := newServiceConfig(ServiceOptions{ + AllowNondistributableArtifacts: tt.registries, + }) + if v := allowNondistributableArtifacts(config, tt.addr); v != tt.expected { + t.Errorf("allowNondistributableArtifacts failed for %q %v, expected %v got %v", tt.addr, tt.registries, tt.expected, v) + } + } +} + func TestIsSecureIndex(t *testing.T) { tests := []struct { addr string diff --git a/registry/service.go b/registry/service.go index 56dabab754..34e8a13f9e 100644 --- a/registry/service.go +++ b/registry/service.go @@ -31,6 +31,7 @@ type Service interface { 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 LoadInsecureRegistries([]string) error } @@ -56,13 +57,17 @@ func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { defer s.mu.Unlock() servConfig := registrytypes.ServiceConfig{ - InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0), - IndexConfigs: make(map[string]*(registrytypes.IndexInfo)), - Mirrors: make([]string, 0), + AllowNondistributableArtifactsCIDRs: make([]*(registrytypes.NetIPNet), 0), + AllowNondistributableArtifactsHostnames: make([]string, 0), + InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0), + IndexConfigs: make(map[string]*(registrytypes.IndexInfo)), + Mirrors: make([]string, 0), } // construct a new ServiceConfig which will not retrieve s.Config directly, // and look up items in s.config with mu locked + servConfig.AllowNondistributableArtifactsCIDRs = append(servConfig.AllowNondistributableArtifactsCIDRs, s.config.ServiceConfig.AllowNondistributableArtifactsCIDRs...) + servConfig.AllowNondistributableArtifactsHostnames = append(servConfig.AllowNondistributableArtifactsHostnames, s.config.ServiceConfig.AllowNondistributableArtifactsHostnames...) servConfig.InsecureRegistryCIDRs = append(servConfig.InsecureRegistryCIDRs, s.config.ServiceConfig.InsecureRegistryCIDRs...) for key, value := range s.config.ServiceConfig.IndexConfigs { @@ -74,6 +79,14 @@ func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { return &servConfig } +// LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries for Service. +func (s *DefaultService) LoadAllowNondistributableArtifacts(registries []string) error { + s.mu.Lock() + defer s.mu.Unlock() + + return s.config.LoadAllowNondistributableArtifacts(registries) +} + // LoadMirrors loads registry mirrors for Service func (s *DefaultService) LoadMirrors(mirrors []string) error { s.mu.Lock() @@ -235,12 +248,13 @@ func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInf // APIEndpoint represents a remote API endpoint type APIEndpoint struct { - Mirror bool - URL *url.URL - Version APIVersion - Official bool - TrimHostname bool - TLSConfig *tls.Config + Mirror bool + URL *url.URL + Version APIVersion + AllowNondistributableArtifacts bool + Official bool + TrimHostname bool + TLSConfig *tls.Config } // ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint diff --git a/registry/service_v2.go b/registry/service_v2.go index 228d745f8c..68466f823f 100644 --- a/registry/service_v2.go +++ b/registry/service_v2.go @@ -44,6 +44,8 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp return endpoints, nil } + ana := allowNondistributableArtifacts(s.config, hostname) + tlsConfig, err = s.tlsConfig(hostname) if err != nil { return nil, err @@ -55,9 +57,10 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp Scheme: "https", Host: hostname, }, - Version: APIVersion2, - TrimHostname: true, - TLSConfig: tlsConfig, + Version: APIVersion2, + AllowNondistributableArtifacts: ana, + TrimHostname: true, + TLSConfig: tlsConfig, }, } @@ -67,8 +70,9 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp Scheme: "http", Host: hostname, }, - Version: APIVersion2, - TrimHostname: true, + Version: APIVersion2, + AllowNondistributableArtifacts: ana, + TrimHostname: true, // used to check if supposed to be secure via InsecureSkipVerify TLSConfig: tlsConfig, })