Signed-off-by: Jake Sanders <jsand@google.com>
(cherry picked from commit 07c4b4124b46be30ea3ac7d114c44c4f911ca182)
Signed-off-by: Victor Vieux <vieux@docker.com>
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"runtime" |
| 11 | 11 |
|
| 12 | 12 |
"github.com/docker/docker/api" |
| 13 |
+ "github.com/docker/docker/api/types" |
|
| 13 | 14 |
"github.com/docker/docker/api/types/versions" |
| 14 | 15 |
cliflags "github.com/docker/docker/cli/flags" |
| 15 | 16 |
"github.com/docker/docker/cliconfig" |
| ... | ... |
@@ -86,15 +87,55 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
| 86 | 86 |
return cli.configFile |
| 87 | 87 |
} |
| 88 | 88 |
|
| 89 |
+// GetAllCredentials returns all of the credentials stored in all of the |
|
| 90 |
+// configured credential stores. |
|
| 91 |
+func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) {
|
|
| 92 |
+ auths := make(map[string]types.AuthConfig) |
|
| 93 |
+ for registry := range cli.configFile.CredentialHelpers {
|
|
| 94 |
+ helper := cli.CredentialsStore(registry) |
|
| 95 |
+ newAuths, err := helper.GetAll() |
|
| 96 |
+ if err != nil {
|
|
| 97 |
+ return nil, err |
|
| 98 |
+ } |
|
| 99 |
+ addAll(auths, newAuths) |
|
| 100 |
+ } |
|
| 101 |
+ defaultStore := cli.CredentialsStore("")
|
|
| 102 |
+ newAuths, err := defaultStore.GetAll() |
|
| 103 |
+ if err != nil {
|
|
| 104 |
+ return nil, err |
|
| 105 |
+ } |
|
| 106 |
+ addAll(auths, newAuths) |
|
| 107 |
+ return auths, nil |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+func addAll(to, from map[string]types.AuthConfig) {
|
|
| 111 |
+ for reg, ac := range from {
|
|
| 112 |
+ to[reg] = ac |
|
| 113 |
+ } |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 89 | 116 |
// CredentialsStore returns a new credentials store based |
| 90 |
-// on the settings provided in the configuration file. |
|
| 91 |
-func (cli *DockerCli) CredentialsStore() credentials.Store {
|
|
| 92 |
- if cli.configFile.CredentialsStore != "" {
|
|
| 93 |
- return credentials.NewNativeStore(cli.configFile) |
|
| 117 |
+// on the settings provided in the configuration file. Empty string returns |
|
| 118 |
+// the default credential store. |
|
| 119 |
+func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store {
|
|
| 120 |
+ if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" {
|
|
| 121 |
+ return credentials.NewNativeStore(cli.configFile, helper) |
|
| 94 | 122 |
} |
| 95 | 123 |
return credentials.NewFileStore(cli.configFile) |
| 96 | 124 |
} |
| 97 | 125 |
|
| 126 |
+// getConfiguredCredentialStore returns the credential helper configured for the |
|
| 127 |
+// given registry, the default credsStore, or the empty string if neither are |
|
| 128 |
+// configured. |
|
| 129 |
+func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string {
|
|
| 130 |
+ if c.CredentialHelpers != nil && serverAddress != "" {
|
|
| 131 |
+ if helper, exists := c.CredentialHelpers[serverAddress]; exists {
|
|
| 132 |
+ return helper |
|
| 133 |
+ } |
|
| 134 |
+ } |
|
| 135 |
+ return c.CredentialsStore |
|
| 136 |
+} |
|
| 137 |
+ |
|
| 98 | 138 |
// Initialize the dockerCli runs initialization that must happen after command |
| 99 | 139 |
// line flags are parsed. |
| 100 | 140 |
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
| ... | ... |
@@ -280,7 +280,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
| 280 | 280 |
} |
| 281 | 281 |
} |
| 282 | 282 |
|
| 283 |
- authConfig, _ := dockerCli.CredentialsStore().GetAll() |
|
| 283 |
+ authConfigs, _ := dockerCli.GetAllCredentials() |
|
| 284 | 284 |
buildOptions := types.ImageBuildOptions{
|
| 285 | 285 |
Memory: memory, |
| 286 | 286 |
MemorySwap: memorySwap, |
| ... | ... |
@@ -301,7 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
| 301 | 301 |
ShmSize: shmSize, |
| 302 | 302 |
Ulimits: options.ulimits.GetList(), |
| 303 | 303 |
BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), |
| 304 |
- AuthConfigs: authConfig, |
|
| 304 |
+ AuthConfigs: authConfigs, |
|
| 305 | 305 |
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), |
| 306 | 306 |
CacheFrom: options.cacheFrom, |
| 307 | 307 |
SecurityOpt: options.securityOpt, |
| ... | ... |
@@ -67,7 +67,7 @@ func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes |
| 67 | 67 |
configKey = ElectAuthServer(ctx, cli) |
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 |
- a, _ := cli.CredentialsStore().Get(configKey) |
|
| 70 |
+ a, _ := cli.CredentialsStore(configKey).Get(configKey) |
|
| 71 | 71 |
return a |
| 72 | 72 |
} |
| 73 | 73 |
|
| ... | ... |
@@ -82,7 +82,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD |
| 82 | 82 |
serverAddress = registry.ConvertToHostname(serverAddress) |
| 83 | 83 |
} |
| 84 | 84 |
|
| 85 |
- authconfig, err := cli.CredentialsStore().Get(serverAddress) |
|
| 85 |
+ authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) |
|
| 86 | 86 |
if err != nil {
|
| 87 | 87 |
return authconfig, err |
| 88 | 88 |
} |
| ... | ... |
@@ -74,7 +74,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error {
|
| 74 | 74 |
authConfig.Password = "" |
| 75 | 75 |
authConfig.IdentityToken = response.IdentityToken |
| 76 | 76 |
} |
| 77 |
- if err := dockerCli.CredentialsStore().Store(authConfig); err != nil {
|
|
| 77 |
+ if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil {
|
|
| 78 | 78 |
return fmt.Errorf("Error saving credentials: %v", err)
|
| 79 | 79 |
} |
| 80 | 80 |
|
| ... | ... |
@@ -68,7 +68,7 @@ func runLogout(dockerCli *command.DockerCli, serverAddress string) error {
|
| 68 | 68 |
|
| 69 | 69 |
fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) |
| 70 | 70 |
for _, r := range regsToLogout {
|
| 71 |
- if err := dockerCli.CredentialsStore().Erase(r); err != nil {
|
|
| 71 |
+ if err := dockerCli.CredentialsStore(r).Erase(r); err != nil {
|
|
| 72 | 72 |
fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) |
| 73 | 73 |
} |
| 74 | 74 |
} |
| ... | ... |
@@ -86,7 +86,7 @@ func TestEmptyFile(t *testing.T) {
|
| 86 | 86 |
} |
| 87 | 87 |
} |
| 88 | 88 |
|
| 89 |
-func TestEmptyJson(t *testing.T) {
|
|
| 89 |
+func TestEmptyJSON(t *testing.T) {
|
|
| 90 | 90 |
tmpHome, err := ioutil.TempDir("", "config-test")
|
| 91 | 91 |
if err != nil {
|
| 92 | 92 |
t.Fatal(err) |
| ... | ... |
@@ -193,7 +193,7 @@ func TestOldValidAuth(t *testing.T) {
|
| 193 | 193 |
} |
| 194 | 194 |
} |
| 195 | 195 |
|
| 196 |
-func TestOldJsonInvalid(t *testing.T) {
|
|
| 196 |
+func TestOldJSONInvalid(t *testing.T) {
|
|
| 197 | 197 |
tmpHome, err := ioutil.TempDir("", "config-test")
|
| 198 | 198 |
if err != nil {
|
| 199 | 199 |
t.Fatal(err) |
| ... | ... |
@@ -219,7 +219,7 @@ func TestOldJsonInvalid(t *testing.T) {
|
| 219 | 219 |
} |
| 220 | 220 |
} |
| 221 | 221 |
|
| 222 |
-func TestOldJson(t *testing.T) {
|
|
| 222 |
+func TestOldJSON(t *testing.T) {
|
|
| 223 | 223 |
tmpHome, err := ioutil.TempDir("", "config-test")
|
| 224 | 224 |
if err != nil {
|
| 225 | 225 |
t.Fatal(err) |
| ... | ... |
@@ -265,7 +265,7 @@ func TestOldJson(t *testing.T) {
|
| 265 | 265 |
} |
| 266 | 266 |
} |
| 267 | 267 |
|
| 268 |
-func TestNewJson(t *testing.T) {
|
|
| 268 |
+func TestNewJSON(t *testing.T) {
|
|
| 269 | 269 |
tmpHome, err := ioutil.TempDir("", "config-test")
|
| 270 | 270 |
if err != nil {
|
| 271 | 271 |
t.Fatal(err) |
| ... | ... |
@@ -304,7 +304,7 @@ func TestNewJson(t *testing.T) {
|
| 304 | 304 |
} |
| 305 | 305 |
} |
| 306 | 306 |
|
| 307 |
-func TestNewJsonNoEmail(t *testing.T) {
|
|
| 307 |
+func TestNewJSONNoEmail(t *testing.T) {
|
|
| 308 | 308 |
tmpHome, err := ioutil.TempDir("", "config-test")
|
| 309 | 309 |
if err != nil {
|
| 310 | 310 |
t.Fatal(err) |
| ... | ... |
@@ -343,7 +343,7 @@ func TestNewJsonNoEmail(t *testing.T) {
|
| 343 | 343 |
} |
| 344 | 344 |
} |
| 345 | 345 |
|
| 346 |
-func TestJsonWithPsFormat(t *testing.T) {
|
|
| 346 |
+func TestJSONWithPsFormat(t *testing.T) {
|
|
| 347 | 347 |
tmpHome, err := ioutil.TempDir("", "config-test")
|
| 348 | 348 |
if err != nil {
|
| 349 | 349 |
t.Fatal(err) |
| ... | ... |
@@ -376,6 +376,78 @@ func TestJsonWithPsFormat(t *testing.T) {
|
| 376 | 376 |
} |
| 377 | 377 |
} |
| 378 | 378 |
|
| 379 |
+func TestJSONWithCredentialStore(t *testing.T) {
|
|
| 380 |
+ tmpHome, err := ioutil.TempDir("", "config-test")
|
|
| 381 |
+ if err != nil {
|
|
| 382 |
+ t.Fatal(err) |
|
| 383 |
+ } |
|
| 384 |
+ defer os.RemoveAll(tmpHome) |
|
| 385 |
+ |
|
| 386 |
+ fn := filepath.Join(tmpHome, ConfigFileName) |
|
| 387 |
+ js := `{
|
|
| 388 |
+ "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
|
| 389 |
+ "credsStore": "crazy-secure-storage" |
|
| 390 |
+}` |
|
| 391 |
+ if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil {
|
|
| 392 |
+ t.Fatal(err) |
|
| 393 |
+ } |
|
| 394 |
+ |
|
| 395 |
+ config, err := Load(tmpHome) |
|
| 396 |
+ if err != nil {
|
|
| 397 |
+ t.Fatalf("Failed loading on empty json file: %q", err)
|
|
| 398 |
+ } |
|
| 399 |
+ |
|
| 400 |
+ if config.CredentialsStore != "crazy-secure-storage" {
|
|
| 401 |
+ t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore)
|
|
| 402 |
+ } |
|
| 403 |
+ |
|
| 404 |
+ // Now save it and make sure it shows up in new form |
|
| 405 |
+ configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) |
|
| 406 |
+ if !strings.Contains(configStr, `"credsStore":`) || |
|
| 407 |
+ !strings.Contains(configStr, "crazy-secure-storage") {
|
|
| 408 |
+ t.Fatalf("Should have save in new form: %s", configStr)
|
|
| 409 |
+ } |
|
| 410 |
+} |
|
| 411 |
+ |
|
| 412 |
+func TestJSONWithCredentialHelpers(t *testing.T) {
|
|
| 413 |
+ tmpHome, err := ioutil.TempDir("", "config-test")
|
|
| 414 |
+ if err != nil {
|
|
| 415 |
+ t.Fatal(err) |
|
| 416 |
+ } |
|
| 417 |
+ defer os.RemoveAll(tmpHome) |
|
| 418 |
+ |
|
| 419 |
+ fn := filepath.Join(tmpHome, ConfigFileName) |
|
| 420 |
+ js := `{
|
|
| 421 |
+ "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
|
| 422 |
+ "credHelpers": { "images.io": "images-io", "containers.com": "crazy-secure-storage" }
|
|
| 423 |
+}` |
|
| 424 |
+ if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil {
|
|
| 425 |
+ t.Fatal(err) |
|
| 426 |
+ } |
|
| 427 |
+ |
|
| 428 |
+ config, err := Load(tmpHome) |
|
| 429 |
+ if err != nil {
|
|
| 430 |
+ t.Fatalf("Failed loading on empty json file: %q", err)
|
|
| 431 |
+ } |
|
| 432 |
+ |
|
| 433 |
+ if config.CredentialHelpers == nil {
|
|
| 434 |
+ t.Fatal("config.CredentialHelpers was nil")
|
|
| 435 |
+ } else if config.CredentialHelpers["images.io"] != "images-io" || |
|
| 436 |
+ config.CredentialHelpers["containers.com"] != "crazy-secure-storage" {
|
|
| 437 |
+ t.Fatalf("Credential helpers not deserialized properly: %v\n", config.CredentialHelpers)
|
|
| 438 |
+ } |
|
| 439 |
+ |
|
| 440 |
+ // Now save it and make sure it shows up in new form |
|
| 441 |
+ configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) |
|
| 442 |
+ if !strings.Contains(configStr, `"credHelpers":`) || |
|
| 443 |
+ !strings.Contains(configStr, "images.io") || |
|
| 444 |
+ !strings.Contains(configStr, "images-io") || |
|
| 445 |
+ !strings.Contains(configStr, "containers.com") || |
|
| 446 |
+ !strings.Contains(configStr, "crazy-secure-storage") {
|
|
| 447 |
+ t.Fatalf("Should have save in new form: %s", configStr)
|
|
| 448 |
+ } |
|
| 449 |
+} |
|
| 450 |
+ |
|
| 379 | 451 |
// Save it and make sure it shows up in new form |
| 380 | 452 |
func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string {
|
| 381 | 453 |
if err := config.Save(); err != nil {
|
| ... | ... |
@@ -420,7 +492,7 @@ func TestConfigFile(t *testing.T) {
|
| 420 | 420 |
} |
| 421 | 421 |
} |
| 422 | 422 |
|
| 423 |
-func TestJsonReaderNoFile(t *testing.T) {
|
|
| 423 |
+func TestJSONReaderNoFile(t *testing.T) {
|
|
| 424 | 424 |
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
|
| 425 | 425 |
|
| 426 | 426 |
config, err := LoadFromReader(strings.NewReader(js)) |
| ... | ... |
@@ -435,7 +507,7 @@ func TestJsonReaderNoFile(t *testing.T) {
|
| 435 | 435 |
|
| 436 | 436 |
} |
| 437 | 437 |
|
| 438 |
-func TestOldJsonReaderNoFile(t *testing.T) {
|
|
| 438 |
+func TestOldJSONReaderNoFile(t *testing.T) {
|
|
| 439 | 439 |
js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
|
| 440 | 440 |
|
| 441 | 441 |
config, err := LegacyLoadFromReader(strings.NewReader(js)) |
| ... | ... |
@@ -449,7 +521,7 @@ func TestOldJsonReaderNoFile(t *testing.T) {
|
| 449 | 449 |
} |
| 450 | 450 |
} |
| 451 | 451 |
|
| 452 |
-func TestJsonWithPsFormatNoFile(t *testing.T) {
|
|
| 452 |
+func TestJSONWithPsFormatNoFile(t *testing.T) {
|
|
| 453 | 453 |
js := `{
|
| 454 | 454 |
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
|
| 455 | 455 |
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
|
| ... | ... |
@@ -465,7 +537,7 @@ func TestJsonWithPsFormatNoFile(t *testing.T) {
|
| 465 | 465 |
|
| 466 | 466 |
} |
| 467 | 467 |
|
| 468 |
-func TestJsonSaveWithNoFile(t *testing.T) {
|
|
| 468 |
+func TestJSONSaveWithNoFile(t *testing.T) {
|
|
| 469 | 469 |
js := `{
|
| 470 | 470 |
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } },
|
| 471 | 471 |
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
|
| ... | ... |
@@ -507,7 +579,7 @@ func TestJsonSaveWithNoFile(t *testing.T) {
|
| 507 | 507 |
} |
| 508 | 508 |
} |
| 509 | 509 |
|
| 510 |
-func TestLegacyJsonSaveWithNoFile(t *testing.T) {
|
|
| 510 |
+func TestLegacyJSONSaveWithNoFile(t *testing.T) {
|
|
| 511 | 511 |
|
| 512 | 512 |
js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
|
| 513 | 513 |
config, err := LegacyLoadFromReader(strings.NewReader(js)) |
| ... | ... |
@@ -31,6 +31,7 @@ type ConfigFile struct {
|
| 31 | 31 |
StatsFormat string `json:"statsFormat,omitempty"` |
| 32 | 32 |
DetachKeys string `json:"detachKeys,omitempty"` |
| 33 | 33 |
CredentialsStore string `json:"credsStore,omitempty"` |
| 34 |
+ CredentialHelpers map[string]string `json:"credHelpers,omitempty"` |
|
| 34 | 35 |
Filename string `json:"-"` // Note: for internal use only |
| 35 | 36 |
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` |
| 36 | 37 |
} |
| ... | ... |
@@ -96,7 +97,8 @@ func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
|
| 96 | 96 |
// in this file or not. |
| 97 | 97 |
func (configFile *ConfigFile) ContainsAuth() bool {
|
| 98 | 98 |
return configFile.CredentialsStore != "" || |
| 99 |
- (configFile.AuthConfigs != nil && len(configFile.AuthConfigs) > 0) |
|
| 99 |
+ len(configFile.CredentialHelpers) > 0 || |
|
| 100 |
+ len(configFile.AuthConfigs) > 0 |
|
| 100 | 101 |
} |
| 101 | 102 |
|
| 102 | 103 |
// SaveToWriter encodes and writes out all the authorization information to |
| ... | ... |
@@ -22,8 +22,8 @@ type nativeStore struct {
|
| 22 | 22 |
|
| 23 | 23 |
// NewNativeStore creates a new native store that |
| 24 | 24 |
// uses a remote helper program to manage credentials. |
| 25 |
-func NewNativeStore(file *configfile.ConfigFile) Store {
|
|
| 26 |
- name := remoteCredentialsPrefix + file.CredentialsStore |
|
| 25 |
+func NewNativeStore(file *configfile.ConfigFile, helperSuffix string) Store {
|
|
| 26 |
+ name := remoteCredentialsPrefix + helperSuffix |
|
| 27 | 27 |
return &nativeStore{
|
| 28 | 28 |
programFunc: client.NewShellProgramFunc(name), |
| 29 | 29 |
fileStore: NewFileStore(file), |