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, })