diff --git a/docs/misc/deprecated.md b/docs/misc/deprecated.md index 4b63716a1e..d1a48d320c 100644 --- a/docs/misc/deprecated.md +++ b/docs/misc/deprecated.md @@ -100,3 +100,9 @@ docker was automatically creating the `/host/path` if it didn't already exist. This auto-creation of the host path is deprecated and docker will error out if the path does not exist. + +### Interacting with V1 registries + +Version 1.9 adds a flag (`--no-legacy-registry=false`) which prevents the docker daemon from `pull`, `push`, and `login` operations against v1 registries. Though disabled by default, this signals the intent to deprecate the v1 protocol. + + diff --git a/docs/reference/commandline/daemon.md b/docs/reference/commandline/daemon.md index 8b343373c1..e3ed62ef42 100644 --- a/docs/reference/commandline/daemon.md +++ b/docs/reference/commandline/daemon.md @@ -49,6 +49,7 @@ weight=1 --log-driver="json-file" Default driver for container logs --log-opt=[] Log driver specific options --mtu=0 Set the containers network MTU + --no-legacy-registry=false Do not contact legacy registries -p, --pidfile="/var/run/docker.pid" Path to use for daemon PID file --registry-mirror=[] Preferred Docker registry mirror -s, --storage-driver="" Storage driver to use @@ -457,6 +458,10 @@ because its use creates security vulnerabilities it should ONLY be enabled for testing purposes. For increased security, users should add their CA to their system's list of trusted CAs instead of enabling `--insecure-registry`. +## Legacy Registries + +Enabling `--no-legacy-registry` forces a docker daemon to only interact with registries which support the V2 protocol. Specifically, the daemon will not attempt `push`, `pull` and `login` to v1 registries. The exception to this is `search` which can still be performed on v1 registries. + ## Running a Docker daemon behind a HTTPS_PROXY When running inside a LAN that uses a `HTTPS` proxy, the Docker Hub diff --git a/graph/pull_v1.go b/graph/pull_v1.go index afd235c4e9..04bb8fa2dc 100644 --- a/graph/pull_v1.go +++ b/graph/pull_v1.go @@ -60,6 +60,9 @@ func (p *v1Puller) Pull(tag string) (fallback bool, err error) { // TODO(dmcgowan): Check if should fallback return false, err } + out := p.config.OutStream + out.Write(p.sf.FormatStatus("", "%s: this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.", p.repoInfo.CanonicalName)) + return false, nil } diff --git a/integration-cli/check_test.go b/integration-cli/check_test.go index 438ca2a8ab..becbc9a8d7 100644 --- a/integration-cli/check_test.go +++ b/integration-cli/check_test.go @@ -32,10 +32,13 @@ func init() { type DockerRegistrySuite struct { ds *DockerSuite reg *testRegistryV2 + d *Daemon } func (s *DockerRegistrySuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux) s.reg = setupRegistry(c) + s.d = NewDaemon(c) } func (s *DockerRegistrySuite) TearDownTest(c *check.C) { @@ -45,6 +48,7 @@ func (s *DockerRegistrySuite) TearDownTest(c *check.C) { if s.ds != nil { s.ds.TearDownTest(c) } + s.d.Stop() } func init() { diff --git a/integration-cli/docker_cli_v2_only.go b/integration-cli/docker_cli_v2_only.go new file mode 100644 index 0000000000..3920fd7728 --- /dev/null +++ b/integration-cli/docker_cli_v2_only.go @@ -0,0 +1,147 @@ +package main + +import ( + "fmt" + "github.com/go-check/check" + "io/ioutil" + "net/http" + "os" +) + +func makefile(contents string) (string, func(), error) { + cleanup := func() { + + } + + f, err := ioutil.TempFile(".", "tmp") + if err != nil { + return "", cleanup, err + } + err = ioutil.WriteFile(f.Name(), []byte(contents), os.ModePerm) + if err != nil { + return "", cleanup, err + } + + cleanup = func() { + err := os.Remove(f.Name()) + if err != nil { + fmt.Println("Error removing tmpfile") + } + } + return f.Name(), cleanup, nil + +} + +// TestV2Only ensures that a daemon in v2-only mode does not +// attempt to contact any v1 registry endpoints. +func (s *DockerRegistrySuite) TestV2Only(c *check.C) { + reg, err := newTestRegistry(c) + if err != nil { + c.Fatal(err.Error()) + } + + reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + }) + + reg.registerHandler("/v1/.*", func(w http.ResponseWriter, r *http.Request) { + c.Fatal("V1 registry contacted") + }) + + repoName := fmt.Sprintf("%s/busybox", reg.hostport) + + err = s.d.Start("--insecure-registry", reg.hostport, "--no-legacy-registry=true") + if err != nil { + c.Fatalf("Error starting daemon: %s", err.Error()) + } + + dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport)) + if err != nil { + c.Fatalf("Unable to create test dockerfile") + } + defer cleanup() + + s.d.Cmd("build", "--file", dockerfileName, ".") + + s.d.Cmd("run", repoName) + s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport) + s.d.Cmd("tag", "busybox", repoName) + s.d.Cmd("push", repoName) + s.d.Cmd("pull", repoName) +} + +// TestV1 starts a daemon in 'normal' mode +// and ensure v1 endpoints are hit for the following operations: +// login, push, pull, build & run +func (s *DockerRegistrySuite) TestV1(c *check.C) { + reg, err := newTestRegistry(c) + if err != nil { + c.Fatal(err.Error()) + } + + v2Pings := 0 + reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) { + v2Pings++ + // V2 ping 404 causes fallback to v1 + w.WriteHeader(404) + }) + + v1Pings := 0 + reg.registerHandler("/v1/_ping", func(w http.ResponseWriter, r *http.Request) { + v1Pings++ + }) + + v1Logins := 0 + reg.registerHandler("/v1/users/", func(w http.ResponseWriter, r *http.Request) { + v1Logins++ + }) + + v1Repo := 0 + reg.registerHandler("/v1/repositories/busybox/", func(w http.ResponseWriter, r *http.Request) { + v1Repo++ + }) + + reg.registerHandler("/v1/repositories/busybox/images", func(w http.ResponseWriter, r *http.Request) { + v1Repo++ + }) + + err = s.d.Start("--insecure-registry", reg.hostport, "--no-legacy-registry=false") + if err != nil { + c.Fatalf("Error starting daemon: %s", err.Error()) + } + + dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport)) + if err != nil { + c.Fatalf("Unable to create test dockerfile") + } + defer cleanup() + + s.d.Cmd("build", "--file", dockerfileName, ".") + if v1Repo == 0 { + c.Errorf("Expected v1 repository access after build") + } + + repoName := fmt.Sprintf("%s/busybox", reg.hostport) + s.d.Cmd("run", repoName) + if v1Repo == 1 { + c.Errorf("Expected v1 repository access after run") + } + + s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport) + if v1Logins == 0 { + c.Errorf("Expected v1 login attempt") + } + + s.d.Cmd("tag", "busybox", repoName) + s.d.Cmd("push", repoName) + + if v1Repo != 2 || v1Pings != 1 { + c.Error("Not all endpoints contacted after push", v1Repo, v1Pings) + } + + s.d.Cmd("pull", repoName) + if v1Repo != 3 { + c.Errorf("Expected v1 repository access after pull") + } + +} diff --git a/integration-cli/registry_mock.go b/integration-cli/registry_mock.go new file mode 100644 index 0000000000..e5fb64c184 --- /dev/null +++ b/integration-cli/registry_mock.go @@ -0,0 +1,56 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "regexp" + "strings" + "sync" + + "github.com/go-check/check" +) + +type handlerFunc func(w http.ResponseWriter, r *http.Request) + +type testRegistry struct { + server *httptest.Server + hostport string + handlers map[string]handlerFunc + mu sync.Mutex +} + +func (tr *testRegistry) registerHandler(path string, h handlerFunc) { + tr.mu.Lock() + defer tr.mu.Unlock() + tr.handlers[path] = h +} + +func newTestRegistry(c *check.C) (*testRegistry, error) { + testReg := &testRegistry{handlers: make(map[string]handlerFunc)} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + url := r.URL.String() + + var matched bool + var err error + for re, function := range testReg.handlers { + matched, err = regexp.MatchString(re, url) + if err != nil { + c.Fatalf("Error with handler regexp") + return + } + if matched { + function(w, r) + break + } + } + + if !matched { + c.Fatal("Unable to match", url, "with regexp") + } + })) + + testReg.server = ts + testReg.hostport = strings.Replace(ts.URL, "http://", "", 1) + return testReg, nil +} diff --git a/man/docker-daemon.8.md b/man/docker-daemon.8.md index 3f1932d07d..20e5e5b370 100644 --- a/man/docker-daemon.8.md +++ b/man/docker-daemon.8.md @@ -40,6 +40,7 @@ docker-daemon - Enable daemon mode [**--log-driver**[=*json-file*]] [**--log-opt**[=*map[]*]] [**--mtu**[=*0*]] +[**--no-legacy-registry**[=*false*]] [**-p**|**--pidfile**[=*/var/run/docker.pid*]] [**--registry-mirror**[=*[]*]] [**-s**|**--storage-driver**[=*STORAGE-DRIVER*]] @@ -170,6 +171,9 @@ unix://[/path/to/socket] to use. **--mtu**=VALUE Set the containers network mtu. Default is `0`. +**--no-legacy-registry**=*true*|*false* + Do not contact legacy registries + **-p**, **--pidfile**="" Path to use for daemon PID file. Default is `/var/run/docker.pid` diff --git a/registry/config.go b/registry/config.go index 5fca9df07c..5ab3e08ccd 100644 --- a/registry/config.go +++ b/registry/config.go @@ -44,6 +44,10 @@ var ( ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") emptyServiceConfig = NewServiceConfig(nil) + + // 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 + V2Only = false ) // InstallFlags adds command-line options to the top-level flag parser for @@ -53,6 +57,7 @@ func (options *Options) InstallFlags(cmd *flag.FlagSet, usageFn func(string) str cmd.Var(&options.Mirrors, []string{"-registry-mirror"}, usageFn("Preferred Docker registry mirror")) options.InsecureRegistries = opts.NewListOpts(ValidateIndexName) cmd.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, usageFn("Enable insecure registry communication")) + cmd.BoolVar(&V2Only, []string{"-no-legacy-registry"}, false, "Do not contact legacy registries") } type netIPNet net.IPNet diff --git a/registry/endpoint.go b/registry/endpoint.go index b7aaedaaac..20805767c6 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -42,8 +42,9 @@ func scanForAPIVersion(address string) (string, APIVersion) { return address, APIVersionUnknown } -// NewEndpoint parses the given address to return a registry endpoint. -func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) { +// NewEndpoint parses the given address to return a registry endpoint. v can be used to +// specify a specific endpoint version +func NewEndpoint(index *IndexInfo, metaHeaders http.Header, v APIVersion) (*Endpoint, error) { tlsConfig, err := newTLSConfig(index.Name, index.Secure) if err != nil { return nil, err @@ -52,6 +53,9 @@ func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) { if err != nil { return nil, err } + if v != APIVersionUnknown { + endpoint.Version = v + } if err := validateEndpoint(endpoint); err != nil { return nil, err } @@ -111,11 +115,6 @@ func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) return endpoint, nil } -// GetEndpoint returns a new endpoint with the specified headers -func (repoInfo *RepositoryInfo) GetEndpoint(metaHeaders http.Header) (*Endpoint, error) { - return NewEndpoint(repoInfo.Index, metaHeaders) -} - // Endpoint stores basic information about a registry endpoint. type Endpoint struct { client *http.Client diff --git a/registry/registry.go b/registry/registry.go index 408bc8e1fd..389bd959df 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -49,6 +49,10 @@ func init() { httpVersion = append(httpVersion, useragent.VersionInfo{"arch", runtime.GOARCH}) dockerUserAgent = useragent.AppendVersions("", httpVersion...) + + if runtime.GOOS != "linux" { + V2Only = true + } } func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { diff --git a/registry/registry_test.go b/registry/registry_test.go index 160d34405d..f75d7d665c 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -23,7 +23,7 @@ const ( func spawnTestRegistrySession(t *testing.T) *Session { authConfig := &cliconfig.AuthConfig{} - endpoint, err := NewEndpoint(makeIndex("/v1/"), nil) + endpoint, err := NewEndpoint(makeIndex("/v1/"), nil, APIVersionUnknown) if err != nil { t.Fatal(err) } @@ -50,7 +50,7 @@ func spawnTestRegistrySession(t *testing.T) *Session { func TestPingRegistryEndpoint(t *testing.T) { testPing := func(index *IndexInfo, expectedStandalone bool, assertMessage string) { - ep, err := NewEndpoint(index, nil) + ep, err := NewEndpoint(index, nil, APIVersionUnknown) if err != nil { t.Fatal(err) } @@ -70,7 +70,7 @@ func TestPingRegistryEndpoint(t *testing.T) { func TestEndpoint(t *testing.T) { // Simple wrapper to fail test if err != nil expandEndpoint := func(index *IndexInfo) *Endpoint { - endpoint, err := NewEndpoint(index, nil) + endpoint, err := NewEndpoint(index, nil, APIVersionUnknown) if err != nil { t.Fatal(err) } @@ -79,7 +79,7 @@ func TestEndpoint(t *testing.T) { assertInsecureIndex := func(index *IndexInfo) { index.Secure = true - _, err := NewEndpoint(index, nil) + _, err := NewEndpoint(index, nil, APIVersionUnknown) assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index") assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index") index.Secure = false @@ -87,7 +87,7 @@ func TestEndpoint(t *testing.T) { assertSecureIndex := func(index *IndexInfo) { index.Secure = true - _, err := NewEndpoint(index, nil) + _, err := NewEndpoint(index, nil, APIVersionUnknown) assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index") assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index") index.Secure = false @@ -153,7 +153,7 @@ func TestEndpoint(t *testing.T) { } for _, address := range badEndpoints { index.Name = address - _, err := NewEndpoint(index, nil) + _, err := NewEndpoint(index, nil, APIVersionUnknown) checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") } } diff --git a/registry/service.go b/registry/service.go index 36d63091f7..1335fe3a07 100644 --- a/registry/service.go +++ b/registry/service.go @@ -2,15 +2,11 @@ package registry import ( "crypto/tls" - "fmt" "net/http" "net/url" - "runtime" - "strings" "github.com/docker/distribution/registry/client/auth" "github.com/docker/docker/cliconfig" - "github.com/docker/docker/pkg/tlsconfig" ) // Service is a registry service. It tracks configuration data such as a list @@ -40,7 +36,14 @@ func (s *Service) Auth(authConfig *cliconfig.AuthConfig) (string, error) { if err != nil { return "", err } - endpoint, err := NewEndpoint(index, nil) + + endpointVersion := APIVersion(APIVersionUnknown) + if V2Only { + // Override the endpoint to only attempt a v2 ping + endpointVersion = APIVersion2 + } + + endpoint, err := NewEndpoint(index, nil, endpointVersion) if err != nil { return "", err } @@ -57,10 +60,11 @@ func (s *Service) Search(term string, authConfig *cliconfig.AuthConfig, headers } // *TODO: Search multiple indexes. - endpoint, err := repoInfo.GetEndpoint(http.Header(headers)) + endpoint, err := NewEndpoint(repoInfo.Index, http.Header(headers), APIVersionUnknown) if err != nil { return nil, err } + r, err := NewSession(endpoint.client, authConfig, endpoint) if err != nil { return nil, err @@ -132,97 +136,20 @@ func (s *Service) LookupPushEndpoints(repoName string) (endpoints []APIEndpoint, } func (s *Service) lookupEndpoints(repoName string) (endpoints []APIEndpoint, err error) { - var cfg = tlsconfig.ServerDefault - tlsConfig := &cfg - if strings.HasPrefix(repoName, DefaultNamespace+"/") { - // v2 mirrors - for _, mirror := range s.Config.Mirrors { - mirrorTLSConfig, err := s.tlsConfigForMirror(mirror) - if err != nil { - return nil, err - } - endpoints = append(endpoints, APIEndpoint{ - URL: mirror, - // guess mirrors are v2 - Version: APIVersion2, - Mirror: true, - TrimHostname: true, - TLSConfig: mirrorTLSConfig, - }) - } - // v2 registry - endpoints = append(endpoints, APIEndpoint{ - URL: DefaultV2Registry, - Version: APIVersion2, - Official: true, - TrimHostname: true, - TLSConfig: tlsConfig, - }) - if runtime.GOOS == "linux" { // do not inherit legacy API for OSes supported in the future - // v1 registry - endpoints = append(endpoints, APIEndpoint{ - URL: DefaultV1Registry, - Version: APIVersion1, - Official: true, - TrimHostname: true, - TLSConfig: tlsConfig, - }) - } - return endpoints, nil - } - - slashIndex := strings.IndexRune(repoName, '/') - if slashIndex <= 0 { - return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName) - } - hostname := repoName[:slashIndex] - - tlsConfig, err = s.TLSConfig(hostname) + endpoints, err = s.lookupV2Endpoints(repoName) if err != nil { return nil, err } - isSecure := !tlsConfig.InsecureSkipVerify - v2Versions := []auth.APIVersion{ - { - Type: "registry", - Version: "2.0", - }, - } - endpoints = []APIEndpoint{ - { - URL: "https://" + hostname, - Version: APIVersion2, - TrimHostname: true, - TLSConfig: tlsConfig, - VersionHeader: DefaultRegistryVersionHeader, - Versions: v2Versions, - }, - { - URL: "https://" + hostname, - Version: APIVersion1, - TrimHostname: true, - TLSConfig: tlsConfig, - }, + if V2Only { + return endpoints, nil } - if !isSecure { - endpoints = append(endpoints, APIEndpoint{ - URL: "http://" + hostname, - Version: APIVersion2, - TrimHostname: true, - // used to check if supposed to be secure via InsecureSkipVerify - TLSConfig: tlsConfig, - VersionHeader: DefaultRegistryVersionHeader, - Versions: v2Versions, - }, APIEndpoint{ - URL: "http://" + hostname, - Version: APIVersion1, - TrimHostname: true, - // used to check if supposed to be secure via InsecureSkipVerify - TLSConfig: tlsConfig, - }) + legacyEndpoints, err := s.lookupV1Endpoints(repoName) + if err != nil { + return nil, err } + endpoints = append(endpoints, legacyEndpoints...) return endpoints, nil } diff --git a/registry/service_v1.go b/registry/service_v1.go new file mode 100644 index 0000000000..ddb78ee60a --- /dev/null +++ b/registry/service_v1.go @@ -0,0 +1,54 @@ +package registry + +import ( + "fmt" + "strings" + + "github.com/docker/docker/pkg/tlsconfig" +) + +func (s *Service) lookupV1Endpoints(repoName string) (endpoints []APIEndpoint, err error) { + var cfg = tlsconfig.ServerDefault + tlsConfig := &cfg + if strings.HasPrefix(repoName, DefaultNamespace+"/") { + endpoints = append(endpoints, APIEndpoint{ + URL: DefaultV1Registry, + Version: APIVersion1, + Official: true, + TrimHostname: true, + TLSConfig: tlsConfig, + }) + return endpoints, nil + } + + slashIndex := strings.IndexRune(repoName, '/') + if slashIndex <= 0 { + return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName) + } + hostname := repoName[:slashIndex] + + tlsConfig, err = s.TLSConfig(hostname) + if err != nil { + return nil, err + } + + endpoints = []APIEndpoint{ + { + URL: "https://" + hostname, + Version: APIVersion1, + TrimHostname: true, + TLSConfig: tlsConfig, + }, + } + + if tlsConfig.InsecureSkipVerify { + endpoints = append(endpoints, APIEndpoint{ // or this + URL: "http://" + hostname, + Version: APIVersion1, + TrimHostname: true, + // used to check if supposed to be secure via InsecureSkipVerify + TLSConfig: tlsConfig, + }) + } + return endpoints, nil +} diff --git a/registry/service_v2.go b/registry/service_v2.go new file mode 100644 index 0000000000..70d5fd710e --- /dev/null +++ b/registry/service_v2.go @@ -0,0 +1,83 @@ +package registry + +import ( + "fmt" + "strings" + + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/docker/pkg/tlsconfig" +) + +func (s *Service) lookupV2Endpoints(repoName string) (endpoints []APIEndpoint, err error) { + var cfg = tlsconfig.ServerDefault + tlsConfig := &cfg + if strings.HasPrefix(repoName, DefaultNamespace+"/") { + // v2 mirrors + for _, mirror := range s.Config.Mirrors { + mirrorTLSConfig, err := s.tlsConfigForMirror(mirror) + if err != nil { + return nil, err + } + endpoints = append(endpoints, APIEndpoint{ + URL: mirror, + // guess mirrors are v2 + Version: APIVersion2, + Mirror: true, + TrimHostname: true, + TLSConfig: mirrorTLSConfig, + }) + } + // v2 registry + endpoints = append(endpoints, APIEndpoint{ + URL: DefaultV2Registry, + Version: APIVersion2, + Official: true, + TrimHostname: true, + TLSConfig: tlsConfig, + }) + + return endpoints, nil + } + + slashIndex := strings.IndexRune(repoName, '/') + if slashIndex <= 0 { + return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName) + } + hostname := repoName[:slashIndex] + + tlsConfig, err = s.TLSConfig(hostname) + if err != nil { + return nil, err + } + + v2Versions := []auth.APIVersion{ + { + Type: "registry", + Version: "2.0", + }, + } + endpoints = []APIEndpoint{ + { + URL: "https://" + hostname, + Version: APIVersion2, + TrimHostname: true, + TLSConfig: tlsConfig, + VersionHeader: DefaultRegistryVersionHeader, + Versions: v2Versions, + }, + } + + if tlsConfig.InsecureSkipVerify { + endpoints = append(endpoints, APIEndpoint{ + URL: "http://" + hostname, + Version: APIVersion2, + TrimHostname: true, + // used to check if supposed to be secure via InsecureSkipVerify + TLSConfig: tlsConfig, + VersionHeader: DefaultRegistryVersionHeader, + Versions: v2Versions, + }) + } + + return endpoints, nil +}