diff --git a/docker/docker_client.go b/docker/docker_client.go index 5900b4581f..ae986de42d 100644 --- a/docker/docker_client.go +++ b/docker/docker_client.go @@ -243,49 +243,58 @@ func (c *dockerClient) getBearerToken(realm, service, scope string) (string, err return tokenStruct.Token, nil } -func getAuth(hostname string) (string, string, error) { +func getAuth(registry string) (string, string, error) { // TODO(runcom): get this from *cli.Context somehow //if username != "" && password != "" { //return username, password, nil //} - if hostname == dockerHostname { - hostname = dockerAuthRegistry - } + var dockerAuth dockerConfigFile dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName) if _, err := os.Stat(dockerCfgPath); err == nil { j, err := ioutil.ReadFile(dockerCfgPath) if err != nil { return "", "", err } - var dockerAuth dockerConfigFile if err := json.Unmarshal(j, &dockerAuth); err != nil { return "", "", err } - // try the normal case - if c, ok := dockerAuth.AuthConfigs[hostname]; ok { - return decodeDockerAuth(c.Auth) - } + } else if os.IsNotExist(err) { + // try old config path oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete)) if _, err := os.Stat(oldDockerCfgPath); err != nil { - return "", "", nil //missing file is not an error + if os.IsNotExist(err) { + return "", "", nil + } + return "", "", fmt.Errorf("%s - %v", oldDockerCfgPath, err) } + j, err := ioutil.ReadFile(oldDockerCfgPath) if err != nil { return "", "", err } - var dockerAuthOld map[string]dockerAuthConfigObsolete - if err := json.Unmarshal(j, &dockerAuthOld); err != nil { + if err := json.Unmarshal(j, &dockerAuth.AuthConfigs); err != nil { return "", "", err } - if c, ok := dockerAuthOld[hostname]; ok { - return decodeDockerAuth(c.Auth) - } - } else { - // if file is there but we can't stat it for any reason other - // than it doesn't exist then stop + + } else if err != nil { return "", "", fmt.Errorf("%s - %v", dockerCfgPath, err) } + + // I'm feeling lucky + if c, exists := dockerAuth.AuthConfigs[registry]; exists { + return decodeDockerAuth(c.Auth) + } + + // bad luck; let's normalize the entries first + registry = normalizeRegistry(registry) + normalizedAuths := map[string]dockerAuthConfig{} + for k, v := range dockerAuth.AuthConfigs { + normalizedAuths[normalizeRegistry(k)] = v + } + if c, exists := normalizedAuths[registry]; exists { + return decodeDockerAuth(c.Auth) + } return "", "", nil } @@ -342,10 +351,6 @@ func getDefaultConfigDir(confPath string) string { return filepath.Join(homedir.Get(), confPath) } -type dockerAuthConfigObsolete struct { - Auth string `json:"auth"` -} - type dockerAuthConfig struct { Auth string `json:"auth,omitempty"` } @@ -368,3 +373,28 @@ func decodeDockerAuth(s string) (string, string, error) { password := strings.Trim(parts[1], "\x00") return user, password, nil } + +// convertToHostname converts a registry url which has http|https prepended +// to just an hostname. +// Copied from github.com/docker/docker/registry/auth.go +func convertToHostname(url string) string { + stripped := url + if strings.HasPrefix(url, "http://") { + stripped = strings.TrimPrefix(url, "http://") + } else if strings.HasPrefix(url, "https://") { + stripped = strings.TrimPrefix(url, "https://") + } + + nameParts := strings.SplitN(stripped, "/", 2) + + return nameParts[0] +} + +func normalizeRegistry(registry string) string { + normalized := convertToHostname(registry) + switch normalized { + case "registry-1.docker.io", "docker.io": + return "index.docker.io" + } + return normalized +} diff --git a/docker/docker_client_test.go b/docker/docker_client_test.go new file mode 100644 index 0000000000..17de920d47 --- /dev/null +++ b/docker/docker_client_test.go @@ -0,0 +1,411 @@ +package docker + +import ( + "encoding/base64" + "encoding/json" + //"fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/docker/docker/pkg/homedir" +) + +func TestGetAuth(t *testing.T) { + origHomeDir := homedir.Get() + tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth") + if err != nil { + t.Fatal(err) + } + t.Logf("using temporary home directory: %q", tmpDir) + // override homedir + os.Setenv(homedir.Key(), tmpDir) + defer func() { + err := os.RemoveAll(tmpDir) + if err != nil { + t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err) + } + os.Setenv(homedir.Key(), origHomeDir) + }() + + configDir := filepath.Join(tmpDir, ".docker") + if err := os.Mkdir(configDir, 0750); err != nil { + t.Fatal(err) + } + configPath := filepath.Join(configDir, "config.json") + + for _, tc := range []struct { + name string + hostname string + authConfig testAuthConfig + expectedUsername string + expectedPassword string + expectedError error + }{ + { + name: "empty hostname", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{"localhost:5000": testAuthConfigData{"bob", "password"}}), + }, + { + name: "no auth config", + hostname: "index.docker.io", + }, + { + name: "match one", + hostname: "example.org", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}), + expectedUsername: "joe", + expectedPassword: "mypass", + }, + { + name: "match none", + hostname: "registry.example.org", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{"example.org": testAuthConfigData{"joe", "mypass"}}), + }, + { + name: "match docker.io", + hostname: "docker.io", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "example.org": testAuthConfigData{"example", "org"}, + "index.docker.io": testAuthConfigData{"index", "docker.io"}, + "docker.io": testAuthConfigData{"docker", "io"}, + }), + expectedUsername: "docker", + expectedPassword: "io", + }, + { + name: "match docker.io normalized", + hostname: "docker.io", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "example.org": testAuthConfigData{"bob", "pw"}, + "https://index.docker.io/v1": testAuthConfigData{"alice", "wp"}, + }), + expectedUsername: "alice", + expectedPassword: "wp", + }, + { + name: "normalize registry", + hostname: "https://docker.io/v1", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "docker.io": testAuthConfigData{"user", "pw"}, + "localhost:5000": testAuthConfigData{"joe", "pass"}, + }), + expectedUsername: "user", + expectedPassword: "pw", + }, + { + name: "match localhost", + hostname: "http://localhost", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "docker.io": testAuthConfigData{"user", "pw"}, + "localhost": testAuthConfigData{"joe", "pass"}, + "example.com": testAuthConfigData{"alice", "pwd"}, + }), + expectedUsername: "joe", + expectedPassword: "pass", + }, + { + name: "match ip", + hostname: "10.10.3.56:5000", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "10.10.30.45": testAuthConfigData{"user", "pw"}, + "localhost": testAuthConfigData{"joe", "pass"}, + "10.10.3.56": testAuthConfigData{"alice", "pwd"}, + "10.10.3.56:5000": testAuthConfigData{"me", "mine"}, + }), + expectedUsername: "me", + expectedPassword: "mine", + }, + { + name: "match port", + hostname: "https://localhost:5000", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "https://127.0.0.1:5000": testAuthConfigData{"user", "pw"}, + "http://localhost": testAuthConfigData{"joe", "pass"}, + "https://localhost:5001": testAuthConfigData{"alice", "pwd"}, + "localhost:5000": testAuthConfigData{"me", "mine"}, + }), + expectedUsername: "me", + expectedPassword: "mine", + }, + } { + contents, err := json.MarshalIndent(&tc.authConfig, "", " ") + if err != nil { + t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err) + continue + } + if err := ioutil.WriteFile(configPath, contents, 0640); err != nil { + t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err) + continue + } + + username, password, err := getAuth(tc.hostname) + if err == nil && tc.expectedError != nil { + t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password) + continue + } + if err != nil && tc.expectedError == nil { + t.Errorf("[%s] got unexpected error: %#+v", tc.name, err) + continue + } + if !reflect.DeepEqual(err, tc.expectedError) { + t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError) + continue + } + + if username != tc.expectedUsername { + t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername) + } + if password != tc.expectedPassword { + t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword) + } + } +} + +func TestGetAuthFromLegacyFile(t *testing.T) { + origHomeDir := homedir.Get() + tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth") + if err != nil { + t.Fatal(err) + } + t.Logf("using temporary home directory: %q", tmpDir) + // override homedir + os.Setenv(homedir.Key(), tmpDir) + defer func() { + err := os.RemoveAll(tmpDir) + if err != nil { + t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err) + } + os.Setenv(homedir.Key(), origHomeDir) + }() + + configPath := filepath.Join(tmpDir, ".dockercfg") + + for _, tc := range []struct { + name string + hostname string + authConfig testAuthConfig + expectedUsername string + expectedPassword string + expectedError error + }{ + { + name: "normalize registry", + hostname: "https://docker.io/v1", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "docker.io": testAuthConfigData{"user", "pw"}, + "localhost:5000": testAuthConfigData{"joe", "pass"}, + }), + expectedUsername: "user", + expectedPassword: "pw", + }, + { + name: "ignore schema and path", + hostname: "http://index.docker.io/v1", + authConfig: makeTestAuthConfig(testAuthConfigDataMap{ + "docker.io/v2": testAuthConfigData{"user", "pw"}, + "https://localhost/v1": testAuthConfigData{"joe", "pwd"}, + }), + expectedUsername: "user", + expectedPassword: "pw", + }, + } { + contents, err := json.MarshalIndent(&tc.authConfig.Auths, "", " ") + if err != nil { + t.Errorf("[%s] failed to marshal authConfig: %v", tc.name, err) + continue + } + if err := ioutil.WriteFile(configPath, contents, 0640); err != nil { + t.Errorf("[%s] failed to write file %q: %v", tc.name, configPath, err) + continue + } + + username, password, err := getAuth(tc.hostname) + if err == nil && tc.expectedError != nil { + t.Errorf("[%s] got unexpected non error and username=%q, password=%q", tc.name, username, password) + continue + } + if err != nil && tc.expectedError == nil { + t.Errorf("[%s] got unexpected error: %#+v", tc.name, err) + continue + } + if !reflect.DeepEqual(err, tc.expectedError) { + t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError) + continue + } + + if username != tc.expectedUsername { + t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, username, tc.expectedUsername) + } + if password != tc.expectedPassword { + t.Errorf("[%s] got unexpected user name: %q != %q", tc.name, password, tc.expectedPassword) + } + } +} + +func TestGetAuthPreferNewConfig(t *testing.T) { + origHomeDir := homedir.Get() + tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth") + if err != nil { + t.Fatal(err) + } + t.Logf("using temporary home directory: %q", tmpDir) + // override homedir + os.Setenv(homedir.Key(), tmpDir) + defer func() { + err := os.RemoveAll(tmpDir) + if err != nil { + t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err) + } + os.Setenv(homedir.Key(), origHomeDir) + }() + + configDir := filepath.Join(tmpDir, ".docker") + if err := os.Mkdir(configDir, 0750); err != nil { + t.Fatal(err) + } + + for _, data := range []struct { + path string + ac interface{} + }{ + { + filepath.Join(configDir, "config.json"), + makeTestAuthConfig(testAuthConfigDataMap{ + "https://index.docker.io/v1/": testAuthConfigData{"alice", "pass"}, + }), + }, + { + filepath.Join(tmpDir, ".dockercfg"), + makeTestAuthConfig(testAuthConfigDataMap{ + "https://index.docker.io/v1/": testAuthConfigData{"bob", "pw"}, + }).Auths, + }, + } { + contents, err := json.MarshalIndent(&data.ac, "", " ") + if err != nil { + t.Fatalf("failed to marshal authConfig: %v", err) + } + if err := ioutil.WriteFile(data.path, contents, 0640); err != nil { + t.Fatalf("failed to write file %q: %v", data.path, err) + } + } + + username, password, err := getAuth("index.docker.io") + if err != nil { + t.Fatalf("got unexpected error: %#+v", err) + } + + if username != "alice" { + t.Fatalf("got unexpected user name: %q != %q", username, "alice") + } + if password != "pass" { + t.Fatalf("got unexpected user name: %q != %q", password, "pass") + } +} + +func TestGetAuthFailsOnBadInput(t *testing.T) { + origHomeDir := homedir.Get() + tmpDir, err := ioutil.TempDir("", "test_docker_client_get_auth") + if err != nil { + t.Fatal(err) + } + t.Logf("using temporary home directory: %q", tmpDir) + // override homedir + os.Setenv(homedir.Key(), tmpDir) + defer func() { + err := os.RemoveAll(tmpDir) + if err != nil { + t.Logf("failed to cleanup temporary home directory %q: %v", tmpDir, err) + } + os.Setenv(homedir.Key(), origHomeDir) + }() + + configDir := filepath.Join(tmpDir, ".docker") + if err := os.Mkdir(configDir, 0750); err != nil { + t.Fatal(err) + } + configPath := filepath.Join(configDir, "config.json") + + // no config file present + username, password, err := getAuth("index.docker.io") + if err != nil { + t.Fatalf("got unexpected error: %#+v", err) + } + if len(username) > 0 || len(password) > 0 { + t.Fatalf("got unexpected not empty username/password: %q/%q", username, password) + } + + if err := ioutil.WriteFile(configPath, []byte("Json rocks! Unless it doesn't."), 0640); err != nil { + t.Fatalf("failed to write file %q: %v", configPath, err) + } + username, password, err = getAuth("index.docker.io") + if err == nil { + t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password) + } + if _, ok := err.(*json.SyntaxError); !ok { + t.Fatalf("expected os.PathError, not: %#+v", err) + } + + // remove the invalid config file + os.RemoveAll(configPath) + // no config file present + username, password, err = getAuth("index.docker.io") + if err != nil { + t.Fatalf("got unexpected error: %#+v", err) + } + if len(username) > 0 || len(password) > 0 { + t.Fatalf("got unexpected not empty username/password: %q/%q", username, password) + } + + configPath = filepath.Join(tmpDir, ".dockercfg") + if err := ioutil.WriteFile(configPath, []byte("I'm certainly not a json string."), 0640); err != nil { + t.Fatalf("failed to write file %q: %v", configPath, err) + } + username, password, err = getAuth("index.docker.io") + if err == nil { + t.Fatalf("got unexpected non-error: username=%q, password=%q", username, password) + } + if _, ok := err.(*json.SyntaxError); !ok { + t.Fatalf("expected os.PathError, not: %#+v", err) + } +} + +type testAuthConfigData struct { + username string + password string +} + +type testAuthConfigDataMap map[string]testAuthConfigData + +type testAuthConfigEntry struct { + Auth string `json:"auth,omitempty"` +} + +type testAuthConfig struct { + Auths map[string]testAuthConfigEntry `json:"auths"` +} + +// encodeAuth creates an auth value from given authConfig data to be stored in auth config file. +// Inspired by github.com/docker/docker/cliconfig/config.go v1.10.3. +func encodeAuth(authConfig *testAuthConfigData) string { + authStr := authConfig.username + ":" + authConfig.password + msg := []byte(authStr) + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) + base64.StdEncoding.Encode(encoded, msg) + return string(encoded) +} + +func makeTestAuthConfig(authConfigData map[string]testAuthConfigData) testAuthConfig { + ac := testAuthConfig{ + Auths: make(map[string]testAuthConfigEntry), + } + for host, data := range authConfigData { + ac.Auths[host] = testAuthConfigEntry{ + Auth: encodeAuth(&data), + } + } + return ac +}