Browse code

Add registry-specific credential helper support

Signed-off-by: Jake Sanders <jsand@google.com>
(cherry picked from commit 07c4b4124b46be30ea3ac7d114c44c4f911ca182)
Signed-off-by: Victor Vieux <vieux@docker.com>

Jake Sanders authored on 2016/08/19 06:23:10
Showing 8 changed files
... ...
@@ -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),