diff --git a/distribution/errors.go b/distribution/errors.go index 1a2923a8cf..0ebcdc34a3 100644 --- a/distribution/errors.go +++ b/distribution/errors.go @@ -84,7 +84,9 @@ func continueOnError(err error) bool { func retryOnError(err error) error { switch v := err.(type) { case errcode.Errors: - return retryOnError(v[0]) + if len(v) != 0 { + return retryOnError(v[0]) + } case errcode.Error: switch v.Code { case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeUnsupported, errcode.ErrorCodeDenied: diff --git a/integration-cli/check_test.go b/integration-cli/check_test.go index 7eb40dadb9..6ce801bc1b 100644 --- a/integration-cli/check_test.go +++ b/integration-cli/check_test.go @@ -49,7 +49,7 @@ type DockerRegistrySuite struct { func (s *DockerRegistrySuite) SetUpTest(c *check.C) { testRequires(c, DaemonIsLinux, RegistryHosting) - s.reg = setupRegistry(c, false, false) + s.reg = setupRegistry(c, false, "", "") s.d = NewDaemon(c) } @@ -77,7 +77,7 @@ type DockerSchema1RegistrySuite struct { func (s *DockerSchema1RegistrySuite) SetUpTest(c *check.C) { testRequires(c, DaemonIsLinux, RegistryHosting) - s.reg = setupRegistry(c, true, false) + s.reg = setupRegistry(c, true, "", "") s.d = NewDaemon(c) } @@ -92,24 +92,24 @@ func (s *DockerSchema1RegistrySuite) TearDownTest(c *check.C) { } func init() { - check.Suite(&DockerRegistryAuthSuite{ + check.Suite(&DockerRegistryAuthHtpasswdSuite{ ds: &DockerSuite{}, }) } -type DockerRegistryAuthSuite struct { +type DockerRegistryAuthHtpasswdSuite struct { ds *DockerSuite reg *testRegistryV2 d *Daemon } -func (s *DockerRegistryAuthSuite) SetUpTest(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) SetUpTest(c *check.C) { testRequires(c, DaemonIsLinux, RegistryHosting) - s.reg = setupRegistry(c, false, true) + s.reg = setupRegistry(c, false, "htpasswd", "") s.d = NewDaemon(c) } -func (s *DockerRegistryAuthSuite) TearDownTest(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TearDownTest(c *check.C) { if s.reg != nil { out, err := s.d.Cmd("logout", privateRegistryURL) c.Assert(err, check.IsNil, check.Commentf(out)) @@ -121,6 +121,42 @@ func (s *DockerRegistryAuthSuite) TearDownTest(c *check.C) { s.ds.TearDownTest(c) } +func init() { + check.Suite(&DockerRegistryAuthTokenSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerRegistryAuthTokenSuite struct { + ds *DockerSuite + reg *testRegistryV2 + d *Daemon +} + +func (s *DockerRegistryAuthTokenSuite) SetUpTest(c *check.C) { + testRequires(c, DaemonIsLinux, RegistryHosting) + s.d = NewDaemon(c) +} + +func (s *DockerRegistryAuthTokenSuite) TearDownTest(c *check.C) { + if s.reg != nil { + out, err := s.d.Cmd("logout", privateRegistryURL) + c.Assert(err, check.IsNil, check.Commentf(out)) + s.reg.Close() + } + if s.d != nil { + s.d.Stop() + } + s.ds.TearDownTest(c) +} + +func (s *DockerRegistryAuthTokenSuite) setupRegistryWithTokenService(c *check.C, tokenURL string) { + if s == nil { + c.Fatal("registry suite isn't initialized") + } + s.reg = setupRegistry(c, false, "token", tokenURL) +} + func init() { check.Suite(&DockerDaemonSuite{ ds: &DockerSuite{}, @@ -159,7 +195,7 @@ type DockerTrustSuite struct { func (s *DockerTrustSuite) SetUpTest(c *check.C) { testRequires(c, RegistryHosting, NotaryServerHosting) - s.reg = setupRegistry(c, false, false) + s.reg = setupRegistry(c, false, "", "") s.not = setupNotary(c) } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 48e3f72705..ad52222628 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -6596,7 +6596,7 @@ func (s *DockerSuite) TestBuildWorkdirWindowsPath(c *check.C) { } } -func (s *DockerRegistryAuthSuite) TestBuildFromAuthenticatedRegistry(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestBuildFromAuthenticatedRegistry(c *check.C) { dockerCmd(c, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) baseImage := privateRegistryURL + "/baseimage" @@ -6619,7 +6619,7 @@ func (s *DockerRegistryAuthSuite) TestBuildFromAuthenticatedRegistry(c *check.C) c.Assert(err, checker.IsNil) } -func (s *DockerRegistryAuthSuite) TestBuildWithExternalAuth(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestBuildWithExternalAuth(c *check.C) { osPath := os.Getenv("PATH") defer os.Setenv("PATH", osPath) diff --git a/integration-cli/docker_cli_login_test.go b/integration-cli/docker_cli_login_test.go index 204d032e10..01de75d985 100644 --- a/integration-cli/docker_cli_login_test.go +++ b/integration-cli/docker_cli_login_test.go @@ -19,7 +19,7 @@ func (s *DockerSuite) TestLoginWithoutTTY(c *check.C) { c.Assert(err, checker.NotNil) //"Expected non nil err when loginning in & TTY not available" } -func (s *DockerRegistryAuthSuite) TestLoginToPrivateRegistry(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestLoginToPrivateRegistry(c *check.C) { // wrong credentials out, _, err := dockerCmdWithError("login", "-u", s.reg.username, "-p", "WRONGPASSWORD", privateRegistryURL) c.Assert(err, checker.NotNil, check.Commentf(out)) @@ -29,7 +29,7 @@ func (s *DockerRegistryAuthSuite) TestLoginToPrivateRegistry(c *check.C) { dockerCmd(c, "login", "-u", s.reg.username, "-p", s.reg.password, privateRegistryURL) } -func (s *DockerRegistryAuthSuite) TestLoginToPrivateRegistryDeprecatedEmailFlag(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestLoginToPrivateRegistryDeprecatedEmailFlag(c *check.C) { // Test to make sure login still works with the deprecated -e and --email flags // wrong credentials out, _, err := dockerCmdWithError("login", "-u", s.reg.username, "-p", "WRONGPASSWORD", "-e", s.reg.email, privateRegistryURL) diff --git a/integration-cli/docker_cli_logout_test.go b/integration-cli/docker_cli_logout_test.go index 8b756146f1..6658da54c2 100644 --- a/integration-cli/docker_cli_logout_test.go +++ b/integration-cli/docker_cli_logout_test.go @@ -10,7 +10,7 @@ import ( "github.com/go-check/check" ) -func (s *DockerRegistryAuthSuite) TestLogoutWithExternalAuth(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestLogoutWithExternalAuth(c *check.C) { osPath := os.Getenv("PATH") defer os.Setenv("PATH", osPath) diff --git a/integration-cli/docker_cli_pull_local_test.go b/integration-cli/docker_cli_pull_local_test.go index a9614fdcd2..1f8583204e 100644 --- a/integration-cli/docker_cli_pull_local_test.go +++ b/integration-cli/docker_cli_pull_local_test.go @@ -387,7 +387,7 @@ func (s *DockerRegistrySuite) TestPullManifestList(c *check.C) { dockerCmd(c, "rmi", repoName) } -func (s *DockerRegistryAuthSuite) TestPullWithExternalAuth(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestPullWithExternalAuth(c *check.C) { osPath := os.Getenv("PATH") defer os.Setenv("PATH", osPath) diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index 8722405a43..ac211d2519 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -255,7 +255,7 @@ func (s *DockerHubPullSuite) TestPullClientDisconnect(c *check.C) { c.Assert(err, checker.NotNil, check.Commentf("image was pulled after client disconnected")) } -func (s *DockerRegistryAuthSuite) TestPullNoCredentialsNotFound(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestPullNoCredentialsNotFound(c *check.C) { // we don't care about the actual image, we just want to see image not found // because that means v2 call returned 401 and we fell back to v1 which usually // gives a 404 (in this case the test registry doesn't handle v1 at all) diff --git a/integration-cli/docker_cli_push_test.go b/integration-cli/docker_cli_push_test.go index b085ea455a..f1ca005a62 100644 --- a/integration-cli/docker_cli_push_test.go +++ b/integration-cli/docker_cli_push_test.go @@ -4,6 +4,8 @@ import ( "archive/tar" "fmt" "io/ioutil" + "net/http" + "net/http/httptest" "os" "os/exec" "path/filepath" @@ -528,7 +530,7 @@ func (s *DockerTrustSuite) TestTrustedPushWithReleasesDelegation(c *check.C) { c.Assert(string(contents), checker.Contains, `"latest"`, check.Commentf(string(contents))) } -func (s *DockerRegistryAuthSuite) TestPushNoCredentialsNoRetry(c *check.C) { +func (s *DockerRegistryAuthHtpasswdSuite) TestPushNoCredentialsNoRetry(c *check.C) { repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) dockerCmd(c, "tag", "busybox", repoName) out, _, err := dockerCmdWithError("push", repoName) @@ -546,3 +548,33 @@ func (s *DockerSuite) TestPushToCentralRegistryUnauthorized(c *check.C) { c.Assert(err, check.NotNil, check.Commentf(out)) c.Assert(out, checker.Contains, "unauthorized: access to the requested resource is not authorized") } + +func (s *DockerRegistryAuthTokenSuite) TestPushTokenServiceUnauthResponse(c *check.C) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"errors": [{"Code":"UNAUTHORIZED", "message": "a message", "detail": null}]}`)) + })) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "unauthorized: a message") +} + +func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponse(c *check.C) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Header().Set("Content-Type", "application/json") + // this will make the daemon panics if no check is performed in retryOnError + w.Write([]byte(`{"error": "unauthorized"}`)) + })) + defer ts.Close() + s.setupRegistryWithTokenService(c, ts.URL) + repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) + dockerCmd(c, "tag", "busybox", repoName) + out, _, err := dockerCmdWithError("push", repoName) + c.Assert(err, check.NotNil, check.Commentf(out)) +} diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index 5cf8097111..63955970af 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -1237,8 +1237,8 @@ func daemonTime(c *check.C) time.Time { return dt } -func setupRegistry(c *check.C, schema1, auth bool) *testRegistryV2 { - reg, err := newTestRegistryV2(c, schema1, auth) +func setupRegistry(c *check.C, schema1 bool, auth, tokenURL string) *testRegistryV2 { + reg, err := newTestRegistryV2(c, schema1, auth, tokenURL) c.Assert(err, check.IsNil) // Wait for registry to be ready to serve requests. diff --git a/integration-cli/fixtures/registry/cert.pem b/integration-cli/fixtures/registry/cert.pem new file mode 100644 index 0000000000..376054033a --- /dev/null +++ b/integration-cli/fixtures/registry/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIJAKZjzF7N4zFJMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjAzMTQxOTAzMDZa +Fw0xNzAzMTQxOTAzMDZaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0 +IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMAVEPA6tSNy +MoExHvT8CWvbe0MyYqZjMmUUdGVYyAaoZgmj9HvtGKaUWY/hCtgTond3OKhPq69u +fQSDlHQA/scq4KZovKQJhvBaRb2DqD31KcbcDyh5KUAL1aalbjTLbKmAYSFSoY93 +57KiBei2BmvS55HLhOiO8ccQOq3feH/J/XcszAdAaiGXW3woDOIumYzur6Q8Suyn +cIUEX5Ik7mxS7oGYN1IM++Y+B6aAFT7htAZEvF7RF7sjG7QBfxNPOFg9lBWXzVSv +0vRbVme9OCDD2QOpj8O7XAPuLDwW5b2A8Iex3CJRngBI9vAK5h1Wssst8117bur9 +AiubOrF6cxUCAwEAAaNQME4wHQYDVR0OBBYEFNTGYK7uX19yjCPeGXhmel98amoA +MB8GA1UdIwQYMBaAFNTGYK7uX19yjCPeGXhmel98amoAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBACW/oF6RgLbTPxb8oPI9424Uv/erYYdxdqIaO3Mz +fQfBEvGu62A0ZLH+av4BTeqBM6iVhN6/Y3hUb8UzbbZAIo/dVJSglW7PXAfUITMM +ca9U2r2cFqgXELZkhde6mTFTYwM3swMCP0HUEo+Hu62NX5gunKr4QMNfTlE3vHEj +jitnkTR0ZVEKHvmdTJC9S92j+NuaJVcwe5UNP1Nj/Ksd/iUUCa2DBnw2N7YwHTDB +jb9cQb8aNVNSrjKP3sknMslVy1JVbUB1LXsth/h+kkVFNP4dsk+dZHn20uIA/VeJ +mJ3Wo54CeTAa3DysiWbIIYsFSASCPvki08ZKI373tCf2RvE= +-----END CERTIFICATE----- diff --git a/integration-cli/registry.go b/integration-cli/registry.go index 28fc054ed1..fa3fb87565 100644 --- a/integration-cli/registry.go +++ b/integration-cli/registry.go @@ -20,12 +20,13 @@ const ( type testRegistryV2 struct { cmd *exec.Cmd dir string + auth string username string password string email string } -func newTestRegistryV2(c *check.C, schema1, auth bool) (*testRegistryV2, error) { +func newTestRegistryV2(c *check.C, schema1 bool, auth, tokenURL string) (*testRegistryV2, error) { tmp, err := ioutil.TempDir("", "registry-test-") if err != nil { return nil, err @@ -39,12 +40,13 @@ http: addr: %s %s` var ( - htpasswd string - username string - password string - email string + authTemplate string + username string + password string + email string ) - if auth { + switch auth { + case "htpasswd": htpasswdPath := filepath.Join(tmp, "htpasswd") // generated with: htpasswd -Bbn testuser testpassword userpasswd := "testuser:$2y$05$sBsSqk0OpSD1uTZkHXc4FeJ0Z70wLQdAX/82UiHuQOKbNbBrzs63m" @@ -54,11 +56,19 @@ http: if err := ioutil.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0644)); err != nil { return nil, err } - htpasswd = fmt.Sprintf(`auth: + authTemplate = fmt.Sprintf(`auth: htpasswd: realm: basic-realm path: %s `, htpasswdPath) + case "token": + authTemplate = fmt.Sprintf(`auth: + token: + realm: %s + service: "registry" + issuer: "auth-registry" + rootcertbundle: "fixtures/registry/cert.pem" +`, tokenURL) } confPath := filepath.Join(tmp, "config.yaml") @@ -66,7 +76,7 @@ http: if err != nil { return nil, err } - if _, err := fmt.Fprintf(config, template, tmp, privateRegistryURL, htpasswd); err != nil { + if _, err := fmt.Fprintf(config, template, tmp, privateRegistryURL, authTemplate); err != nil { os.RemoveAll(tmp) return nil, err } @@ -86,6 +96,7 @@ http: return &testRegistryV2{ cmd: cmd, dir: tmp, + auth: auth, username: username, password: password, email: email, @@ -101,7 +112,7 @@ func (t *testRegistryV2) Ping() error { resp.Body.Close() fail := resp.StatusCode != http.StatusOK - if t.username != "" { + if t.auth != "" { // unauthorized is a _good_ status when pinging v2/ and it needs auth fail = fail && resp.StatusCode != http.StatusUnauthorized }