Browse code

Add .docker/config.json and support for HTTP Headers

This PR does the following:
- migrated ~/.dockerfg to ~/.docker/config.json. The data is migrated
but the old file remains in case its needed
- moves the auth json in that fie into an "auth" property so we can add new
top-level properties w/o messing with the auth stuff
- adds support for an HttpHeaders property in ~/.docker/config.json
which adds these http headers to all msgs from the cli

In a follow-on PR I'll move the config file process out from under
"registry" since it not specific to that any more. I didn't do it here
because I wanted the diff to be smaller so people can make sure I didn't
break/miss any auth code during my edits.

Signed-off-by: Doug Davis <dug@us.ibm.com>

Doug Davis authored on 2015/04/02 07:39:37
Showing 19 changed files
... ...
@@ -286,10 +286,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
286 286
 
287 287
 	v.Set("dockerfile", *dockerfileName)
288 288
 
289
-	cli.LoadConfigFile()
290
-
291 289
 	headers := http.Header(make(map[string][]string))
292
-	buf, err := json.Marshal(cli.configFile)
290
+	buf, err := json.Marshal(cli.configFile.AuthConfigs)
293 291
 	if err != nil {
294 292
 		return err
295 293
 	}
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"net"
10 10
 	"net/http"
11 11
 	"os"
12
+	"path/filepath"
12 13
 	"reflect"
13 14
 	"strings"
14 15
 	"text/template"
... ...
@@ -120,14 +121,6 @@ func (cli *DockerCli) Subcmd(name, signature, description string, exitOnError bo
120 120
 	return flags
121 121
 }
122 122
 
123
-func (cli *DockerCli) LoadConfigFile() (err error) {
124
-	cli.configFile, err = registry.LoadConfig(homedir.Get())
125
-	if err != nil {
126
-		fmt.Fprintf(cli.err, "WARNING: %s\n", err)
127
-	}
128
-	return err
129
-}
130
-
131 123
 func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
132 124
 	// In order to attach to a container tty, input stream for the client must
133 125
 	// be a tty itself: redirecting or piping the client standard input is
... ...
@@ -184,9 +177,15 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a
184 184
 		tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
185 185
 	}
186 186
 
187
+	configFile, e := registry.LoadConfig(filepath.Join(homedir.Get(), ".docker"))
188
+	if e != nil {
189
+		fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
190
+	}
191
+
187 192
 	return &DockerCli{
188 193
 		proto:         proto,
189 194
 		addr:          addr,
195
+		configFile:    configFile,
190 196
 		in:            in,
191 197
 		out:           out,
192 198
 		err:           err,
... ...
@@ -37,9 +37,6 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
37 37
 		return err
38 38
 	}
39 39
 
40
-	// Load the auth config file, to be able to pull the image
41
-	cli.LoadConfigFile()
42
-
43 40
 	// Resolve the Auth config relevant for this server
44 41
 	authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index)
45 42
 	buf, err := json.Marshal(authConfig)
... ...
@@ -142,6 +142,13 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
142 142
 	if err != nil {
143 143
 		return err
144 144
 	}
145
+
146
+	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
147
+	// then the user can't change OUR headers
148
+	for k, v := range cli.configFile.HttpHeaders {
149
+		req.Header.Set(k, v)
150
+	}
151
+
145 152
 	req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
146 153
 	req.Header.Set("Content-Type", "text/plain")
147 154
 	req.Header.Set("Connection", "Upgrade")
... ...
@@ -68,8 +68,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
68 68
 	}
69 69
 
70 70
 	if info.IndexServerAddress != "" {
71
-		cli.LoadConfigFile()
72
-		u := cli.configFile.Configs[info.IndexServerAddress].Username
71
+		u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
73 72
 		if len(u) > 0 {
74 73
 			fmt.Fprintf(cli.out, "Username: %v\n", u)
75 74
 			fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
... ...
@@ -6,11 +6,9 @@ import (
6 6
 	"fmt"
7 7
 	"io"
8 8
 	"os"
9
-	"path"
10 9
 	"strings"
11 10
 
12 11
 	"github.com/docker/docker/api/types"
13
-	"github.com/docker/docker/pkg/homedir"
14 12
 	flag "github.com/docker/docker/pkg/mflag"
15 13
 	"github.com/docker/docker/pkg/term"
16 14
 	"github.com/docker/docker/registry"
... ...
@@ -56,8 +54,7 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
56 56
 		return string(line)
57 57
 	}
58 58
 
59
-	cli.LoadConfigFile()
60
-	authconfig, ok := cli.configFile.Configs[serverAddress]
59
+	authconfig, ok := cli.configFile.AuthConfigs[serverAddress]
61 60
 	if !ok {
62 61
 		authconfig = registry.AuthConfig{}
63 62
 	}
... ...
@@ -113,12 +110,14 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
113 113
 	authconfig.Password = password
114 114
 	authconfig.Email = email
115 115
 	authconfig.ServerAddress = serverAddress
116
-	cli.configFile.Configs[serverAddress] = authconfig
116
+	cli.configFile.AuthConfigs[serverAddress] = authconfig
117 117
 
118
-	stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress], nil)
118
+	stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.AuthConfigs[serverAddress], nil)
119 119
 	if statusCode == 401 {
120
-		delete(cli.configFile.Configs, serverAddress)
121
-		registry.SaveConfig(cli.configFile)
120
+		delete(cli.configFile.AuthConfigs, serverAddress)
121
+		if err2 := cli.configFile.Save(); err2 != nil {
122
+			fmt.Fprintf(cli.out, "WARNING: could not save config file: %v\n", err2)
123
+		}
122 124
 		return err
123 125
 	}
124 126
 	if err != nil {
... ...
@@ -127,12 +126,15 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
127 127
 
128 128
 	var response types.AuthResponse
129 129
 	if err := json.NewDecoder(stream).Decode(&response); err != nil {
130
-		cli.configFile, _ = registry.LoadConfig(homedir.Get())
130
+		// Upon error, remove entry
131
+		delete(cli.configFile.AuthConfigs, serverAddress)
131 132
 		return err
132 133
 	}
133 134
 
134
-	registry.SaveConfig(cli.configFile)
135
-	fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s.\n", path.Join(homedir.Get(), registry.CONFIGFILE))
135
+	if err := cli.configFile.Save(); err != nil {
136
+		return fmt.Errorf("Error saving config file: %v", err)
137
+	}
138
+	fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s\n", cli.configFile.Filename())
136 139
 
137 140
 	if response.Status != "" {
138 141
 		fmt.Fprintf(cli.out, "%s\n", response.Status)
... ...
@@ -22,14 +22,13 @@ func (cli *DockerCli) CmdLogout(args ...string) error {
22 22
 		serverAddress = cmd.Arg(0)
23 23
 	}
24 24
 
25
-	cli.LoadConfigFile()
26
-	if _, ok := cli.configFile.Configs[serverAddress]; !ok {
25
+	if _, ok := cli.configFile.AuthConfigs[serverAddress]; !ok {
27 26
 		fmt.Fprintf(cli.out, "Not logged in to %s\n", serverAddress)
28 27
 	} else {
29 28
 		fmt.Fprintf(cli.out, "Remove login credentials for %s\n", serverAddress)
30
-		delete(cli.configFile.Configs, serverAddress)
29
+		delete(cli.configFile.AuthConfigs, serverAddress)
31 30
 
32
-		if err := registry.SaveConfig(cli.configFile); err != nil {
31
+		if err := cli.configFile.Save(); err != nil {
33 32
 			return fmt.Errorf("Failed to save docker config: %v", err)
34 33
 		}
35 34
 	}
... ...
@@ -42,8 +42,6 @@ func (cli *DockerCli) CmdPull(args ...string) error {
42 42
 		return err
43 43
 	}
44 44
 
45
-	cli.LoadConfigFile()
46
-
47 45
 	_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
48 46
 	return err
49 47
 }
... ...
@@ -20,8 +20,6 @@ func (cli *DockerCli) CmdPush(args ...string) error {
20 20
 
21 21
 	name := cmd.Arg(0)
22 22
 
23
-	cli.LoadConfigFile()
24
-
25 23
 	remote, tag := parsers.ParseRepositoryTag(name)
26 24
 
27 25
 	// Resolve the Repository name from fqn to RepositoryInfo
... ...
@@ -44,8 +44,6 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
44 44
 		return err
45 45
 	}
46 46
 
47
-	cli.LoadConfigFile()
48
-
49 47
 	rdr, _, err := cli.clientRequestAttemptLogin("GET", "/images/search?"+v.Encode(), nil, nil, repoInfo.Index, "search")
50 48
 	if err != nil {
51 49
 		return err
... ...
@@ -65,6 +65,13 @@ func (cli *DockerCli) clientRequest(method, path string, in io.Reader, headers m
65 65
 	if err != nil {
66 66
 		return nil, "", -1, err
67 67
 	}
68
+
69
+	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
70
+	// then the user can't change OUR headers
71
+	for k, v := range cli.configFile.HttpHeaders {
72
+		req.Header.Set(k, v)
73
+	}
74
+
68 75
 	req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
69 76
 	req.URL.Host = cli.addr
70 77
 	req.URL.Scheme = cli.scheme
... ...
@@ -299,7 +306,7 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
299 299
 		sigchan := make(chan os.Signal, 1)
300 300
 		gosignal.Notify(sigchan, signal.SIGWINCH)
301 301
 		go func() {
302
-			for _ = range sigchan {
302
+			for range sigchan {
303 303
 				cli.resizeTty(id, isExec)
304 304
 			}
305 305
 		}()
... ...
@@ -101,8 +101,8 @@ type Builder struct {
101 101
 	// the final configs of the Dockerfile but dont want the layers
102 102
 	disableCommit bool
103 103
 
104
-	AuthConfig     *registry.AuthConfig
105
-	AuthConfigFile *registry.ConfigFile
104
+	AuthConfig *registry.AuthConfig
105
+	ConfigFile *registry.ConfigFile
106 106
 
107 107
 	// Deprecated, original writer used for ImagePull. To be removed.
108 108
 	OutOld          io.Writer
... ...
@@ -437,13 +437,13 @@ func (b *Builder) pullImage(name string) (*imagepkg.Image, error) {
437 437
 	}
438 438
 
439 439
 	pullRegistryAuth := b.AuthConfig
440
-	if len(b.AuthConfigFile.Configs) > 0 {
440
+	if len(b.ConfigFile.AuthConfigs) > 0 {
441 441
 		// The request came with a full auth config file, we prefer to use that
442 442
 		repoInfo, err := b.Daemon.RegistryService.ResolveRepository(remote)
443 443
 		if err != nil {
444 444
 			return nil, err
445 445
 		}
446
-		resolvedAuth := b.AuthConfigFile.ResolveAuthConfig(repoInfo.Index)
446
+		resolvedAuth := b.ConfigFile.ResolveAuthConfig(repoInfo.Index)
447 447
 		pullRegistryAuth = &resolvedAuth
448 448
 	}
449 449
 
... ...
@@ -150,7 +150,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error {
150 150
 		OutOld:          job.Stdout,
151 151
 		StreamFormatter: sf,
152 152
 		AuthConfig:      authConfig,
153
-		AuthConfigFile:  configFile,
153
+		ConfigFile:      configFile,
154 154
 		dockerfileName:  dockerfileName,
155 155
 		cpuShares:       cpuShares,
156 156
 		cpuSetCpus:      cpuSetCpus,
... ...
@@ -48,6 +48,35 @@ These Go environment variables are case-insensitive. See the
48 48
 [Go specification](http://golang.org/pkg/net/http/) for details on these
49 49
 variables.
50 50
 
51
+## Configuration Files
52
+
53
+The Docker command line stores its configuration files in a directory called
54
+`.docker` within your `HOME` directory. Docker manages most of the files in
55
+`.docker` and you should not modify them. However, you *can modify* the
56
+`.docker/config.json` file to control certain aspects of how the `docker`
57
+command behaves.
58
+
59
+Currently, you can modify the `docker` command behavior using environment 
60
+variables or command-line options. You can also use options within 
61
+`config.json` to modify some of the same behavior.  When using these 
62
+mechanisms, you must keep in mind the order of precedence among them. Command 
63
+line options override environment variables and environment variables override 
64
+properties you specify in a `config.json` file.
65
+
66
+The `config.json` file stores a JSON encoding of a single `HttpHeaders`
67
+property. The property specifies a set of headers to include in all
68
+messages sent from the Docker client to the daemon. Docker does not try to
69
+interpret or understand these header; it simply puts them into the messages.
70
+Docker does not allow these headers to change any headers it sets for itself.
71
+
72
+Following is a sample `config.json` file:
73
+
74
+    {
75
+      "HttpHeaders: {
76
+        "MyHeader": "MyValue"
77
+      }
78
+    }
79
+
51 80
 ## Help
52 81
 To list the help on any command just execute the command, followed by the `--help` option.
53 82
 
54 83
new file mode 100644
... ...
@@ -0,0 +1,58 @@
0
+package main
1
+
2
+import (
3
+	"io/ioutil"
4
+	"net/http"
5
+	"net/http/httptest"
6
+	"os"
7
+	"os/exec"
8
+	"path/filepath"
9
+	"testing"
10
+
11
+	"github.com/docker/docker/pkg/homedir"
12
+)
13
+
14
+func TestConfigHttpHeader(t *testing.T) {
15
+	testRequires(t, UnixCli) // Can't set/unset HOME on windows right now
16
+	// We either need a level of Go that supports Unsetenv (for cases
17
+	// when HOME/USERPROFILE isn't set), or we need to be able to use
18
+	// os/user but user.Current() only works if we aren't statically compiling
19
+
20
+	var headers map[string][]string
21
+
22
+	server := httptest.NewServer(http.HandlerFunc(
23
+		func(w http.ResponseWriter, r *http.Request) {
24
+			headers = r.Header
25
+		}))
26
+	defer server.Close()
27
+
28
+	homeKey := homedir.Key()
29
+	homeVal := homedir.Get()
30
+	tmpDir, _ := ioutil.TempDir("", "fake-home")
31
+	defer os.RemoveAll(tmpDir)
32
+
33
+	dotDocker := filepath.Join(tmpDir, ".docker")
34
+	os.Mkdir(dotDocker, 0600)
35
+	tmpCfg := filepath.Join(dotDocker, "config.json")
36
+
37
+	defer func() { os.Setenv(homeKey, homeVal) }()
38
+	os.Setenv(homeKey, tmpDir)
39
+
40
+	data := `{
41
+		"HttpHeaders": { "MyHeader": "MyValue" }
42
+	}`
43
+
44
+	err := ioutil.WriteFile(tmpCfg, []byte(data), 0600)
45
+	if err != nil {
46
+		t.Fatalf("Err creating file(%s): %v", tmpCfg, err)
47
+	}
48
+
49
+	cmd := exec.Command(dockerBinary, "-H="+server.URL[7:], "ps")
50
+	out, _, _ := runCommandWithOutput(cmd)
51
+
52
+	if headers["Myheader"] == nil || headers["Myheader"][0] != "MyValue" {
53
+		t.Fatalf("Missing/bad header: %q\nout:%v", headers, out)
54
+	}
55
+
56
+	logDone("config - add new http headers")
57
+}
... ...
@@ -8,24 +8,27 @@ import (
8 8
 	"io/ioutil"
9 9
 	"net/http"
10 10
 	"os"
11
-	"path"
11
+	"path/filepath"
12 12
 	"strings"
13 13
 	"sync"
14 14
 	"time"
15 15
 
16 16
 	"github.com/Sirupsen/logrus"
17
+	"github.com/docker/docker/pkg/homedir"
17 18
 	"github.com/docker/docker/pkg/requestdecorator"
18 19
 )
19 20
 
20 21
 const (
21 22
 	// Where we store the config file
22
-	CONFIGFILE = ".dockercfg"
23
+	CONFIGFILE     = "config.json"
24
+	OLD_CONFIGFILE = ".dockercfg"
23 25
 )
24 26
 
25 27
 var (
26 28
 	ErrConfigFileMissing = errors.New("The Auth config file is missing")
27 29
 )
28 30
 
31
+// Registry Auth Info
29 32
 type AuthConfig struct {
30 33
 	Username      string `json:"username,omitempty"`
31 34
 	Password      string `json:"password,omitempty"`
... ...
@@ -34,9 +37,11 @@ type AuthConfig struct {
34 34
 	ServerAddress string `json:"serveraddress,omitempty"`
35 35
 }
36 36
 
37
+// ~/.docker/config.json file info
37 38
 type ConfigFile struct {
38
-	Configs  map[string]AuthConfig `json:"configs,omitempty"`
39
-	rootPath string
39
+	AuthConfigs map[string]AuthConfig `json:"auths"`
40
+	HttpHeaders map[string]string     `json:"HttpHeaders,omitempty"`
41
+	filename    string                // Note: not serialized - for internal use only
40 42
 }
41 43
 
42 44
 type RequestAuthorization struct {
... ...
@@ -147,18 +152,58 @@ func decodeAuth(authStr string) (string, string, error) {
147 147
 
148 148
 // load up the auth config information and return values
149 149
 // FIXME: use the internal golang config parser
150
-func LoadConfig(rootPath string) (*ConfigFile, error) {
151
-	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
152
-	confFile := path.Join(rootPath, CONFIGFILE)
150
+func LoadConfig(configDir string) (*ConfigFile, error) {
151
+	if configDir == "" {
152
+		configDir = filepath.Join(homedir.Get(), ".docker")
153
+	}
154
+
155
+	configFile := ConfigFile{
156
+		AuthConfigs: make(map[string]AuthConfig),
157
+		filename:    filepath.Join(configDir, CONFIGFILE),
158
+	}
159
+
160
+	// Try happy path first - latest config file
161
+	if _, err := os.Stat(configFile.filename); err == nil {
162
+		file, err := os.Open(configFile.filename)
163
+		if err != nil {
164
+			return &configFile, err
165
+		}
166
+		defer file.Close()
167
+
168
+		if err := json.NewDecoder(file).Decode(&configFile); err != nil {
169
+			return &configFile, err
170
+		}
171
+
172
+		for addr, ac := range configFile.AuthConfigs {
173
+			ac.Username, ac.Password, err = decodeAuth(ac.Auth)
174
+			if err != nil {
175
+				return &configFile, err
176
+			}
177
+			ac.Auth = ""
178
+			ac.ServerAddress = addr
179
+			configFile.AuthConfigs[addr] = ac
180
+		}
181
+
182
+		return &configFile, nil
183
+	} else if !os.IsNotExist(err) {
184
+		// if file is there but we can't stat it for any reason other
185
+		// than it doesn't exist then stop
186
+		return &configFile, err
187
+	}
188
+
189
+	// Can't find latest config file so check for the old one
190
+	confFile := filepath.Join(homedir.Get(), OLD_CONFIGFILE)
191
+
153 192
 	if _, err := os.Stat(confFile); err != nil {
154 193
 		return &configFile, nil //missing file is not an error
155 194
 	}
195
+
156 196
 	b, err := ioutil.ReadFile(confFile)
157 197
 	if err != nil {
158 198
 		return &configFile, err
159 199
 	}
160 200
 
161
-	if err := json.Unmarshal(b, &configFile.Configs); err != nil {
201
+	if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil {
162 202
 		arr := strings.Split(string(b), "\n")
163 203
 		if len(arr) < 2 {
164 204
 			return &configFile, fmt.Errorf("The Auth config file is empty")
... ...
@@ -179,48 +224,52 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
179 179
 		authConfig.Email = origEmail[1]
180 180
 		authConfig.ServerAddress = IndexServerAddress()
181 181
 		// *TODO: Switch to using IndexServerName() instead?
182
-		configFile.Configs[IndexServerAddress()] = authConfig
182
+		configFile.AuthConfigs[IndexServerAddress()] = authConfig
183 183
 	} else {
184
-		for k, authConfig := range configFile.Configs {
184
+		for k, authConfig := range configFile.AuthConfigs {
185 185
 			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
186 186
 			if err != nil {
187 187
 				return &configFile, err
188 188
 			}
189 189
 			authConfig.Auth = ""
190 190
 			authConfig.ServerAddress = k
191
-			configFile.Configs[k] = authConfig
191
+			configFile.AuthConfigs[k] = authConfig
192 192
 		}
193 193
 	}
194 194
 	return &configFile, nil
195 195
 }
196 196
 
197
-// save the auth config
198
-func SaveConfig(configFile *ConfigFile) error {
199
-	confFile := path.Join(configFile.rootPath, CONFIGFILE)
200
-	if len(configFile.Configs) == 0 {
201
-		os.Remove(confFile)
202
-		return nil
203
-	}
204
-
205
-	configs := make(map[string]AuthConfig, len(configFile.Configs))
206
-	for k, authConfig := range configFile.Configs {
197
+func (configFile *ConfigFile) Save() error {
198
+	// Encode sensitive data into a new/temp struct
199
+	tmpAuthConfigs := make(map[string]AuthConfig, len(configFile.AuthConfigs))
200
+	for k, authConfig := range configFile.AuthConfigs {
207 201
 		authCopy := authConfig
208 202
 
209 203
 		authCopy.Auth = encodeAuth(&authCopy)
210 204
 		authCopy.Username = ""
211 205
 		authCopy.Password = ""
212 206
 		authCopy.ServerAddress = ""
213
-		configs[k] = authCopy
207
+		tmpAuthConfigs[k] = authCopy
214 208
 	}
215 209
 
216
-	b, err := json.MarshalIndent(configs, "", "\t")
210
+	saveAuthConfigs := configFile.AuthConfigs
211
+	configFile.AuthConfigs = tmpAuthConfigs
212
+	defer func() { configFile.AuthConfigs = saveAuthConfigs }()
213
+
214
+	data, err := json.MarshalIndent(configFile, "", "\t")
217 215
 	if err != nil {
218 216
 		return err
219 217
 	}
220
-	err = ioutil.WriteFile(confFile, b, 0600)
218
+
219
+	if err := os.MkdirAll(filepath.Dir(configFile.filename), 0600); err != nil {
220
+		return err
221
+	}
222
+
223
+	err = ioutil.WriteFile(configFile.filename, data, 0600)
221 224
 	if err != nil {
222 225
 		return err
223 226
 	}
227
+
224 228
 	return nil
225 229
 }
226 230
 
... ...
@@ -431,7 +480,7 @@ func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, regis
431 431
 func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
432 432
 	configKey := index.GetAuthConfigKey()
433 433
 	// First try the happy case
434
-	if c, found := config.Configs[configKey]; found || index.Official {
434
+	if c, found := config.AuthConfigs[configKey]; found || index.Official {
435 435
 		return c
436 436
 	}
437 437
 
... ...
@@ -450,7 +499,7 @@ func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
450 450
 
451 451
 	// Maybe they have a legacy config file, we will iterate the keys converting
452 452
 	// them to the new format and testing
453
-	for registry, config := range config.Configs {
453
+	for registry, config := range config.AuthConfigs {
454 454
 		if configKey == convertToHostname(registry) {
455 455
 			return config
456 456
 		}
... ...
@@ -459,3 +508,7 @@ func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
459 459
 	// When all else fails, return an empty auth config
460 460
 	return AuthConfig{}
461 461
 }
462
+
463
+func (config *ConfigFile) Filename() string {
464
+	return config.filename
465
+}
... ...
@@ -3,6 +3,7 @@ package registry
3 3
 import (
4 4
 	"io/ioutil"
5 5
 	"os"
6
+	"path/filepath"
6 7
 	"testing"
7 8
 )
8 9
 
... ...
@@ -31,13 +32,14 @@ func setupTempConfigFile() (*ConfigFile, error) {
31 31
 	if err != nil {
32 32
 		return nil, err
33 33
 	}
34
+	root = filepath.Join(root, CONFIGFILE)
34 35
 	configFile := &ConfigFile{
35
-		rootPath: root,
36
-		Configs:  make(map[string]AuthConfig),
36
+		AuthConfigs: make(map[string]AuthConfig),
37
+		filename:    root,
37 38
 	}
38 39
 
39 40
 	for _, registry := range []string{"testIndex", IndexServerAddress()} {
40
-		configFile.Configs[registry] = AuthConfig{
41
+		configFile.AuthConfigs[registry] = AuthConfig{
41 42
 			Username: "docker-user",
42 43
 			Password: "docker-pass",
43 44
 			Email:    "docker@docker.io",
... ...
@@ -52,14 +54,14 @@ func TestSameAuthDataPostSave(t *testing.T) {
52 52
 	if err != nil {
53 53
 		t.Fatal(err)
54 54
 	}
55
-	defer os.RemoveAll(configFile.rootPath)
55
+	defer os.RemoveAll(configFile.filename)
56 56
 
57
-	err = SaveConfig(configFile)
57
+	err = configFile.Save()
58 58
 	if err != nil {
59 59
 		t.Fatal(err)
60 60
 	}
61 61
 
62
-	authConfig := configFile.Configs["testIndex"]
62
+	authConfig := configFile.AuthConfigs["testIndex"]
63 63
 	if authConfig.Username != "docker-user" {
64 64
 		t.Fail()
65 65
 	}
... ...
@@ -79,9 +81,9 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
79 79
 	if err != nil {
80 80
 		t.Fatal(err)
81 81
 	}
82
-	defer os.RemoveAll(configFile.rootPath)
82
+	defer os.RemoveAll(configFile.filename)
83 83
 
84
-	indexConfig := configFile.Configs[IndexServerAddress()]
84
+	indexConfig := configFile.AuthConfigs[IndexServerAddress()]
85 85
 
86 86
 	officialIndex := &IndexInfo{
87 87
 		Official: true,
... ...
@@ -102,7 +104,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
102 102
 	if err != nil {
103 103
 		t.Fatal(err)
104 104
 	}
105
-	defer os.RemoveAll(configFile.rootPath)
105
+	defer os.RemoveAll(configFile.filename)
106 106
 
107 107
 	registryAuth := AuthConfig{
108 108
 		Username: "foo-user",
... ...
@@ -119,7 +121,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
119 119
 		Password: "baz-pass",
120 120
 		Email:    "baz@example.com",
121 121
 	}
122
-	configFile.Configs[IndexServerAddress()] = officialAuth
122
+	configFile.AuthConfigs[IndexServerAddress()] = officialAuth
123 123
 
124 124
 	expectedAuths := map[string]AuthConfig{
125 125
 		"registry.example.com": registryAuth,
... ...
@@ -157,12 +159,12 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
157 157
 			Name: configKey,
158 158
 		}
159 159
 		for _, registry := range registries {
160
-			configFile.Configs[registry] = configured
160
+			configFile.AuthConfigs[registry] = configured
161 161
 			resolved := configFile.ResolveAuthConfig(index)
162 162
 			if resolved.Email != configured.Email {
163 163
 				t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email)
164 164
 			}
165
-			delete(configFile.Configs, registry)
165
+			delete(configFile.AuthConfigs, registry)
166 166
 			resolved = configFile.ResolveAuthConfig(index)
167 167
 			if resolved.Email == configured.Email {
168 168
 				t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email)
169 169
new file mode 100644
... ...
@@ -0,0 +1,135 @@
0
+package registry
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"path/filepath"
6
+	"runtime"
7
+	"strings"
8
+	"testing"
9
+
10
+	"github.com/docker/docker/pkg/homedir"
11
+)
12
+
13
+func TestMissingFile(t *testing.T) {
14
+	tmpHome, _ := ioutil.TempDir("", "config-test")
15
+
16
+	config, err := LoadConfig(tmpHome)
17
+	if err != nil {
18
+		t.Fatalf("Failed loading on missing file: %q", err)
19
+	}
20
+
21
+	// Now save it and make sure it shows up in new form
22
+	err = config.Save()
23
+	if err != nil {
24
+		t.Fatalf("Failed to save: %q", err)
25
+	}
26
+
27
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
28
+	if !strings.Contains(string(buf), `"auths":`) {
29
+		t.Fatalf("Should have save in new form: %s", string(buf))
30
+	}
31
+}
32
+
33
+func TestEmptyFile(t *testing.T) {
34
+	tmpHome, _ := ioutil.TempDir("", "config-test")
35
+	fn := filepath.Join(tmpHome, CONFIGFILE)
36
+	ioutil.WriteFile(fn, []byte(""), 0600)
37
+
38
+	_, err := LoadConfig(tmpHome)
39
+	if err == nil {
40
+		t.Fatalf("Was supposed to fail")
41
+	}
42
+}
43
+
44
+func TestEmptyJson(t *testing.T) {
45
+	tmpHome, _ := ioutil.TempDir("", "config-test")
46
+	fn := filepath.Join(tmpHome, CONFIGFILE)
47
+	ioutil.WriteFile(fn, []byte("{}"), 0600)
48
+
49
+	config, err := LoadConfig(tmpHome)
50
+	if err != nil {
51
+		t.Fatalf("Failed loading on empty json file: %q", err)
52
+	}
53
+
54
+	// Now save it and make sure it shows up in new form
55
+	err = config.Save()
56
+	if err != nil {
57
+		t.Fatalf("Failed to save: %q", err)
58
+	}
59
+
60
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
61
+	if !strings.Contains(string(buf), `"auths":`) {
62
+		t.Fatalf("Should have save in new form: %s", string(buf))
63
+	}
64
+}
65
+
66
+func TestOldJson(t *testing.T) {
67
+	if runtime.GOOS == "windows" {
68
+		return
69
+	}
70
+
71
+	tmpHome, _ := ioutil.TempDir("", "config-test")
72
+	defer os.RemoveAll(tmpHome)
73
+
74
+	homeKey := homedir.Key()
75
+	homeVal := homedir.Get()
76
+
77
+	defer func() { os.Setenv(homeKey, homeVal) }()
78
+	os.Setenv(homeKey, tmpHome)
79
+
80
+	fn := filepath.Join(tmpHome, OLD_CONFIGFILE)
81
+	js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
82
+	ioutil.WriteFile(fn, []byte(js), 0600)
83
+
84
+	config, err := LoadConfig(tmpHome)
85
+	if err != nil {
86
+		t.Fatalf("Failed loading on empty json file: %q", err)
87
+	}
88
+
89
+	ac := config.AuthConfigs["https://index.docker.io/v1/"]
90
+	if ac.Email != "user@example.com" || ac.Username != "joejoe" || ac.Password != "hello" {
91
+		t.Fatalf("Missing data from parsing:\n%q", config)
92
+	}
93
+
94
+	// Now save it and make sure it shows up in new form
95
+	err = config.Save()
96
+	if err != nil {
97
+		t.Fatalf("Failed to save: %q", err)
98
+	}
99
+
100
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
101
+	if !strings.Contains(string(buf), `"auths":`) ||
102
+		!strings.Contains(string(buf), "user@example.com") {
103
+		t.Fatalf("Should have save in new form: %s", string(buf))
104
+	}
105
+}
106
+
107
+func TestNewJson(t *testing.T) {
108
+	tmpHome, _ := ioutil.TempDir("", "config-test")
109
+	fn := filepath.Join(tmpHome, CONFIGFILE)
110
+	js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
111
+	ioutil.WriteFile(fn, []byte(js), 0600)
112
+
113
+	config, err := LoadConfig(tmpHome)
114
+	if err != nil {
115
+		t.Fatalf("Failed loading on empty json file: %q", err)
116
+	}
117
+
118
+	ac := config.AuthConfigs["https://index.docker.io/v1/"]
119
+	if ac.Email != "user@example.com" || ac.Username != "joejoe" || ac.Password != "hello" {
120
+		t.Fatalf("Missing data from parsing:\n%q", config)
121
+	}
122
+
123
+	// Now save it and make sure it shows up in new form
124
+	err = config.Save()
125
+	if err != nil {
126
+		t.Fatalf("Failed to save: %q", err)
127
+	}
128
+
129
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
130
+	if !strings.Contains(string(buf), `"auths":`) ||
131
+		!strings.Contains(string(buf), "user@example.com") {
132
+		t.Fatalf("Should have save in new form: %s", string(buf))
133
+	}
134
+}