Browse Source

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 10 years ago
parent
commit
18c9b6c645

+ 1 - 3
api/client/build.go

@@ -286,10 +286,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 
 
 	v.Set("dockerfile", *dockerfileName)
 	v.Set("dockerfile", *dockerfileName)
 
 
-	cli.LoadConfigFile()
-
 	headers := http.Header(make(map[string][]string))
 	headers := http.Header(make(map[string][]string))
-	buf, err := json.Marshal(cli.configFile)
+	buf, err := json.Marshal(cli.configFile.AuthConfigs)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 7 - 8
api/client/cli.go

@@ -9,6 +9,7 @@ import (
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
+	"path/filepath"
 	"reflect"
 	"reflect"
 	"strings"
 	"strings"
 	"text/template"
 	"text/template"
@@ -120,14 +121,6 @@ func (cli *DockerCli) Subcmd(name, signature, description string, exitOnError bo
 	return flags
 	return flags
 }
 }
 
 
-func (cli *DockerCli) LoadConfigFile() (err error) {
-	cli.configFile, err = registry.LoadConfig(homedir.Get())
-	if err != nil {
-		fmt.Fprintf(cli.err, "WARNING: %s\n", err)
-	}
-	return err
-}
-
 func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
 func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
 	// In order to attach to a container tty, input stream for the client must
 	// In order to attach to a container tty, input stream for the client must
 	// be a tty itself: redirecting or piping the client standard input is
 	// 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
 		tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
 		tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
 	}
 	}
 
 
+	configFile, e := registry.LoadConfig(filepath.Join(homedir.Get(), ".docker"))
+	if e != nil {
+		fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
+	}
+
 	return &DockerCli{
 	return &DockerCli{
 		proto:         proto,
 		proto:         proto,
 		addr:          addr,
 		addr:          addr,
+		configFile:    configFile,
 		in:            in,
 		in:            in,
 		out:           out,
 		out:           out,
 		err:           err,
 		err:           err,

+ 0 - 3
api/client/create.go

@@ -37,9 +37,6 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
 		return err
 		return err
 	}
 	}
 
 
-	// Load the auth config file, to be able to pull the image
-	cli.LoadConfigFile()
-
 	// Resolve the Auth config relevant for this server
 	// Resolve the Auth config relevant for this server
 	authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index)
 	authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index)
 	buf, err := json.Marshal(authConfig)
 	buf, err := json.Marshal(authConfig)

+ 7 - 0
api/client/hijack.go

@@ -142,6 +142,13 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+
+	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
+	// then the user can't change OUR headers
+	for k, v := range cli.configFile.HttpHeaders {
+		req.Header.Set(k, v)
+	}
+
 	req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
 	req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
 	req.Header.Set("Content-Type", "text/plain")
 	req.Header.Set("Content-Type", "text/plain")
 	req.Header.Set("Connection", "Upgrade")
 	req.Header.Set("Connection", "Upgrade")

+ 1 - 2
api/client/info.go

@@ -68,8 +68,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
 	}
 	}
 
 
 	if info.IndexServerAddress != "" {
 	if info.IndexServerAddress != "" {
-		cli.LoadConfigFile()
-		u := cli.configFile.Configs[info.IndexServerAddress].Username
+		u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
 		if len(u) > 0 {
 		if len(u) > 0 {
 			fmt.Fprintf(cli.out, "Username: %v\n", u)
 			fmt.Fprintf(cli.out, "Username: %v\n", u)
 			fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
 			fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)

+ 13 - 11
api/client/login.go

@@ -6,11 +6,9 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"os"
 	"os"
-	"path"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/pkg/homedir"
 	flag "github.com/docker/docker/pkg/mflag"
 	flag "github.com/docker/docker/pkg/mflag"
 	"github.com/docker/docker/pkg/term"
 	"github.com/docker/docker/pkg/term"
 	"github.com/docker/docker/registry"
 	"github.com/docker/docker/registry"
@@ -56,8 +54,7 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 		return string(line)
 		return string(line)
 	}
 	}
 
 
-	cli.LoadConfigFile()
-	authconfig, ok := cli.configFile.Configs[serverAddress]
+	authconfig, ok := cli.configFile.AuthConfigs[serverAddress]
 	if !ok {
 	if !ok {
 		authconfig = registry.AuthConfig{}
 		authconfig = registry.AuthConfig{}
 	}
 	}
@@ -113,12 +110,14 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 	authconfig.Password = password
 	authconfig.Password = password
 	authconfig.Email = email
 	authconfig.Email = email
 	authconfig.ServerAddress = serverAddress
 	authconfig.ServerAddress = serverAddress
-	cli.configFile.Configs[serverAddress] = authconfig
+	cli.configFile.AuthConfigs[serverAddress] = authconfig
 
 
-	stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress], nil)
+	stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.AuthConfigs[serverAddress], nil)
 	if statusCode == 401 {
 	if statusCode == 401 {
-		delete(cli.configFile.Configs, serverAddress)
-		registry.SaveConfig(cli.configFile)
+		delete(cli.configFile.AuthConfigs, serverAddress)
+		if err2 := cli.configFile.Save(); err2 != nil {
+			fmt.Fprintf(cli.out, "WARNING: could not save config file: %v\n", err2)
+		}
 		return err
 		return err
 	}
 	}
 	if err != nil {
 	if err != nil {
@@ -127,12 +126,15 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 
 
 	var response types.AuthResponse
 	var response types.AuthResponse
 	if err := json.NewDecoder(stream).Decode(&response); err != nil {
 	if err := json.NewDecoder(stream).Decode(&response); err != nil {
-		cli.configFile, _ = registry.LoadConfig(homedir.Get())
+		// Upon error, remove entry
+		delete(cli.configFile.AuthConfigs, serverAddress)
 		return err
 		return err
 	}
 	}
 
 
-	registry.SaveConfig(cli.configFile)
-	fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s.\n", path.Join(homedir.Get(), registry.CONFIGFILE))
+	if err := cli.configFile.Save(); err != nil {
+		return fmt.Errorf("Error saving config file: %v", err)
+	}
+	fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s\n", cli.configFile.Filename())
 
 
 	if response.Status != "" {
 	if response.Status != "" {
 		fmt.Fprintf(cli.out, "%s\n", response.Status)
 		fmt.Fprintf(cli.out, "%s\n", response.Status)

+ 3 - 4
api/client/logout.go

@@ -22,14 +22,13 @@ func (cli *DockerCli) CmdLogout(args ...string) error {
 		serverAddress = cmd.Arg(0)
 		serverAddress = cmd.Arg(0)
 	}
 	}
 
 
-	cli.LoadConfigFile()
-	if _, ok := cli.configFile.Configs[serverAddress]; !ok {
+	if _, ok := cli.configFile.AuthConfigs[serverAddress]; !ok {
 		fmt.Fprintf(cli.out, "Not logged in to %s\n", serverAddress)
 		fmt.Fprintf(cli.out, "Not logged in to %s\n", serverAddress)
 	} else {
 	} else {
 		fmt.Fprintf(cli.out, "Remove login credentials for %s\n", serverAddress)
 		fmt.Fprintf(cli.out, "Remove login credentials for %s\n", serverAddress)
-		delete(cli.configFile.Configs, serverAddress)
+		delete(cli.configFile.AuthConfigs, serverAddress)
 
 
-		if err := registry.SaveConfig(cli.configFile); err != nil {
+		if err := cli.configFile.Save(); err != nil {
 			return fmt.Errorf("Failed to save docker config: %v", err)
 			return fmt.Errorf("Failed to save docker config: %v", err)
 		}
 		}
 	}
 	}

+ 0 - 2
api/client/pull.go

@@ -42,8 +42,6 @@ func (cli *DockerCli) CmdPull(args ...string) error {
 		return err
 		return err
 	}
 	}
 
 
-	cli.LoadConfigFile()
-
 	_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
 	_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
 	return err
 	return err
 }
 }

+ 0 - 2
api/client/push.go

@@ -20,8 +20,6 @@ func (cli *DockerCli) CmdPush(args ...string) error {
 
 
 	name := cmd.Arg(0)
 	name := cmd.Arg(0)
 
 
-	cli.LoadConfigFile()
-
 	remote, tag := parsers.ParseRepositoryTag(name)
 	remote, tag := parsers.ParseRepositoryTag(name)
 
 
 	// Resolve the Repository name from fqn to RepositoryInfo
 	// Resolve the Repository name from fqn to RepositoryInfo

+ 0 - 2
api/client/search.go

@@ -44,8 +44,6 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
 		return err
 		return err
 	}
 	}
 
 
-	cli.LoadConfigFile()
-
 	rdr, _, err := cli.clientRequestAttemptLogin("GET", "/images/search?"+v.Encode(), nil, nil, repoInfo.Index, "search")
 	rdr, _, err := cli.clientRequestAttemptLogin("GET", "/images/search?"+v.Encode(), nil, nil, repoInfo.Index, "search")
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 8 - 1
api/client/utils.go

@@ -65,6 +65,13 @@ func (cli *DockerCli) clientRequest(method, path string, in io.Reader, headers m
 	if err != nil {
 	if err != nil {
 		return nil, "", -1, err
 		return nil, "", -1, err
 	}
 	}
+
+	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
+	// then the user can't change OUR headers
+	for k, v := range cli.configFile.HttpHeaders {
+		req.Header.Set(k, v)
+	}
+
 	req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
 	req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
 	req.URL.Host = cli.addr
 	req.URL.Host = cli.addr
 	req.URL.Scheme = cli.scheme
 	req.URL.Scheme = cli.scheme
@@ -299,7 +306,7 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
 		sigchan := make(chan os.Signal, 1)
 		sigchan := make(chan os.Signal, 1)
 		gosignal.Notify(sigchan, signal.SIGWINCH)
 		gosignal.Notify(sigchan, signal.SIGWINCH)
 		go func() {
 		go func() {
-			for _ = range sigchan {
+			for range sigchan {
 				cli.resizeTty(id, isExec)
 				cli.resizeTty(id, isExec)
 			}
 			}
 		}()
 		}()

+ 2 - 2
builder/evaluator.go

@@ -101,8 +101,8 @@ type Builder struct {
 	// the final configs of the Dockerfile but dont want the layers
 	// the final configs of the Dockerfile but dont want the layers
 	disableCommit bool
 	disableCommit bool
 
 
-	AuthConfig     *registry.AuthConfig
-	AuthConfigFile *registry.ConfigFile
+	AuthConfig *registry.AuthConfig
+	ConfigFile *registry.ConfigFile
 
 
 	// Deprecated, original writer used for ImagePull. To be removed.
 	// Deprecated, original writer used for ImagePull. To be removed.
 	OutOld          io.Writer
 	OutOld          io.Writer

+ 2 - 2
builder/internals.go

@@ -437,13 +437,13 @@ func (b *Builder) pullImage(name string) (*imagepkg.Image, error) {
 	}
 	}
 
 
 	pullRegistryAuth := b.AuthConfig
 	pullRegistryAuth := b.AuthConfig
-	if len(b.AuthConfigFile.Configs) > 0 {
+	if len(b.ConfigFile.AuthConfigs) > 0 {
 		// The request came with a full auth config file, we prefer to use that
 		// The request came with a full auth config file, we prefer to use that
 		repoInfo, err := b.Daemon.RegistryService.ResolveRepository(remote)
 		repoInfo, err := b.Daemon.RegistryService.ResolveRepository(remote)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
-		resolvedAuth := b.AuthConfigFile.ResolveAuthConfig(repoInfo.Index)
+		resolvedAuth := b.ConfigFile.ResolveAuthConfig(repoInfo.Index)
 		pullRegistryAuth = &resolvedAuth
 		pullRegistryAuth = &resolvedAuth
 	}
 	}
 
 

+ 1 - 1
builder/job.go

@@ -150,7 +150,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error {
 		OutOld:          job.Stdout,
 		OutOld:          job.Stdout,
 		StreamFormatter: sf,
 		StreamFormatter: sf,
 		AuthConfig:      authConfig,
 		AuthConfig:      authConfig,
-		AuthConfigFile:  configFile,
+		ConfigFile:      configFile,
 		dockerfileName:  dockerfileName,
 		dockerfileName:  dockerfileName,
 		cpuShares:       cpuShares,
 		cpuShares:       cpuShares,
 		cpuSetCpus:      cpuSetCpus,
 		cpuSetCpus:      cpuSetCpus,

+ 29 - 0
docs/sources/reference/commandline/cli.md

@@ -48,6 +48,35 @@ These Go environment variables are case-insensitive. See the
 [Go specification](http://golang.org/pkg/net/http/) for details on these
 [Go specification](http://golang.org/pkg/net/http/) for details on these
 variables.
 variables.
 
 
+## Configuration Files
+
+The Docker command line stores its configuration files in a directory called
+`.docker` within your `HOME` directory. Docker manages most of the files in
+`.docker` and you should not modify them. However, you *can modify* the
+`.docker/config.json` file to control certain aspects of how the `docker`
+command behaves.
+
+Currently, you can modify the `docker` command behavior using environment 
+variables or command-line options. You can also use options within 
+`config.json` to modify some of the same behavior.  When using these 
+mechanisms, you must keep in mind the order of precedence among them. Command 
+line options override environment variables and environment variables override 
+properties you specify in a `config.json` file.
+
+The `config.json` file stores a JSON encoding of a single `HttpHeaders`
+property. The property specifies a set of headers to include in all
+messages sent from the Docker client to the daemon. Docker does not try to
+interpret or understand these header; it simply puts them into the messages.
+Docker does not allow these headers to change any headers it sets for itself.
+
+Following is a sample `config.json` file:
+
+    {
+      "HttpHeaders: {
+        "MyHeader": "MyValue"
+      }
+    }
+
 ## Help
 ## Help
 To list the help on any command just execute the command, followed by the `--help` option.
 To list the help on any command just execute the command, followed by the `--help` option.
 
 

+ 58 - 0
integration-cli/docker_cli_config_test.go

@@ -0,0 +1,58 @@
+package main
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"testing"
+
+	"github.com/docker/docker/pkg/homedir"
+)
+
+func TestConfigHttpHeader(t *testing.T) {
+	testRequires(t, UnixCli) // Can't set/unset HOME on windows right now
+	// We either need a level of Go that supports Unsetenv (for cases
+	// when HOME/USERPROFILE isn't set), or we need to be able to use
+	// os/user but user.Current() only works if we aren't statically compiling
+
+	var headers map[string][]string
+
+	server := httptest.NewServer(http.HandlerFunc(
+		func(w http.ResponseWriter, r *http.Request) {
+			headers = r.Header
+		}))
+	defer server.Close()
+
+	homeKey := homedir.Key()
+	homeVal := homedir.Get()
+	tmpDir, _ := ioutil.TempDir("", "fake-home")
+	defer os.RemoveAll(tmpDir)
+
+	dotDocker := filepath.Join(tmpDir, ".docker")
+	os.Mkdir(dotDocker, 0600)
+	tmpCfg := filepath.Join(dotDocker, "config.json")
+
+	defer func() { os.Setenv(homeKey, homeVal) }()
+	os.Setenv(homeKey, tmpDir)
+
+	data := `{
+		"HttpHeaders": { "MyHeader": "MyValue" }
+	}`
+
+	err := ioutil.WriteFile(tmpCfg, []byte(data), 0600)
+	if err != nil {
+		t.Fatalf("Err creating file(%s): %v", tmpCfg, err)
+	}
+
+	cmd := exec.Command(dockerBinary, "-H="+server.URL[7:], "ps")
+	out, _, _ := runCommandWithOutput(cmd)
+
+	if headers["Myheader"] == nil || headers["Myheader"][0] != "MyValue" {
+		t.Fatalf("Missing/bad header: %q\nout:%v", headers, out)
+	}
+
+	logDone("config - add new http headers")
+}

+ 79 - 26
registry/auth.go

@@ -8,24 +8,27 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
-	"path"
+	"path/filepath"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/pkg/homedir"
 	"github.com/docker/docker/pkg/requestdecorator"
 	"github.com/docker/docker/pkg/requestdecorator"
 )
 )
 
 
 const (
 const (
 	// Where we store the config file
 	// Where we store the config file
-	CONFIGFILE = ".dockercfg"
+	CONFIGFILE     = "config.json"
+	OLD_CONFIGFILE = ".dockercfg"
 )
 )
 
 
 var (
 var (
 	ErrConfigFileMissing = errors.New("The Auth config file is missing")
 	ErrConfigFileMissing = errors.New("The Auth config file is missing")
 )
 )
 
 
+// Registry Auth Info
 type AuthConfig struct {
 type AuthConfig struct {
 	Username      string `json:"username,omitempty"`
 	Username      string `json:"username,omitempty"`
 	Password      string `json:"password,omitempty"`
 	Password      string `json:"password,omitempty"`
@@ -34,9 +37,11 @@ type AuthConfig struct {
 	ServerAddress string `json:"serveraddress,omitempty"`
 	ServerAddress string `json:"serveraddress,omitempty"`
 }
 }
 
 
+// ~/.docker/config.json file info
 type ConfigFile struct {
 type ConfigFile struct {
-	Configs  map[string]AuthConfig `json:"configs,omitempty"`
-	rootPath string
+	AuthConfigs map[string]AuthConfig `json:"auths"`
+	HttpHeaders map[string]string     `json:"HttpHeaders,omitempty"`
+	filename    string                // Note: not serialized - for internal use only
 }
 }
 
 
 type RequestAuthorization struct {
 type RequestAuthorization struct {
@@ -147,18 +152,58 @@ func decodeAuth(authStr string) (string, string, error) {
 
 
 // load up the auth config information and return values
 // load up the auth config information and return values
 // FIXME: use the internal golang config parser
 // FIXME: use the internal golang config parser
-func LoadConfig(rootPath string) (*ConfigFile, error) {
-	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
-	confFile := path.Join(rootPath, CONFIGFILE)
+func LoadConfig(configDir string) (*ConfigFile, error) {
+	if configDir == "" {
+		configDir = filepath.Join(homedir.Get(), ".docker")
+	}
+
+	configFile := ConfigFile{
+		AuthConfigs: make(map[string]AuthConfig),
+		filename:    filepath.Join(configDir, CONFIGFILE),
+	}
+
+	// Try happy path first - latest config file
+	if _, err := os.Stat(configFile.filename); err == nil {
+		file, err := os.Open(configFile.filename)
+		if err != nil {
+			return &configFile, err
+		}
+		defer file.Close()
+
+		if err := json.NewDecoder(file).Decode(&configFile); err != nil {
+			return &configFile, err
+		}
+
+		for addr, ac := range configFile.AuthConfigs {
+			ac.Username, ac.Password, err = decodeAuth(ac.Auth)
+			if err != nil {
+				return &configFile, err
+			}
+			ac.Auth = ""
+			ac.ServerAddress = addr
+			configFile.AuthConfigs[addr] = ac
+		}
+
+		return &configFile, nil
+	} else if !os.IsNotExist(err) {
+		// if file is there but we can't stat it for any reason other
+		// than it doesn't exist then stop
+		return &configFile, err
+	}
+
+	// Can't find latest config file so check for the old one
+	confFile := filepath.Join(homedir.Get(), OLD_CONFIGFILE)
+
 	if _, err := os.Stat(confFile); err != nil {
 	if _, err := os.Stat(confFile); err != nil {
 		return &configFile, nil //missing file is not an error
 		return &configFile, nil //missing file is not an error
 	}
 	}
+
 	b, err := ioutil.ReadFile(confFile)
 	b, err := ioutil.ReadFile(confFile)
 	if err != nil {
 	if err != nil {
 		return &configFile, err
 		return &configFile, err
 	}
 	}
 
 
-	if err := json.Unmarshal(b, &configFile.Configs); err != nil {
+	if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil {
 		arr := strings.Split(string(b), "\n")
 		arr := strings.Split(string(b), "\n")
 		if len(arr) < 2 {
 		if len(arr) < 2 {
 			return &configFile, fmt.Errorf("The Auth config file is empty")
 			return &configFile, fmt.Errorf("The Auth config file is empty")
@@ -179,48 +224,52 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
 		authConfig.Email = origEmail[1]
 		authConfig.Email = origEmail[1]
 		authConfig.ServerAddress = IndexServerAddress()
 		authConfig.ServerAddress = IndexServerAddress()
 		// *TODO: Switch to using IndexServerName() instead?
 		// *TODO: Switch to using IndexServerName() instead?
-		configFile.Configs[IndexServerAddress()] = authConfig
+		configFile.AuthConfigs[IndexServerAddress()] = authConfig
 	} else {
 	} else {
-		for k, authConfig := range configFile.Configs {
+		for k, authConfig := range configFile.AuthConfigs {
 			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
 			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
 			if err != nil {
 			if err != nil {
 				return &configFile, err
 				return &configFile, err
 			}
 			}
 			authConfig.Auth = ""
 			authConfig.Auth = ""
 			authConfig.ServerAddress = k
 			authConfig.ServerAddress = k
-			configFile.Configs[k] = authConfig
+			configFile.AuthConfigs[k] = authConfig
 		}
 		}
 	}
 	}
 	return &configFile, nil
 	return &configFile, nil
 }
 }
 
 
-// save the auth config
-func SaveConfig(configFile *ConfigFile) error {
-	confFile := path.Join(configFile.rootPath, CONFIGFILE)
-	if len(configFile.Configs) == 0 {
-		os.Remove(confFile)
-		return nil
-	}
-
-	configs := make(map[string]AuthConfig, len(configFile.Configs))
-	for k, authConfig := range configFile.Configs {
+func (configFile *ConfigFile) Save() error {
+	// Encode sensitive data into a new/temp struct
+	tmpAuthConfigs := make(map[string]AuthConfig, len(configFile.AuthConfigs))
+	for k, authConfig := range configFile.AuthConfigs {
 		authCopy := authConfig
 		authCopy := authConfig
 
 
 		authCopy.Auth = encodeAuth(&authCopy)
 		authCopy.Auth = encodeAuth(&authCopy)
 		authCopy.Username = ""
 		authCopy.Username = ""
 		authCopy.Password = ""
 		authCopy.Password = ""
 		authCopy.ServerAddress = ""
 		authCopy.ServerAddress = ""
-		configs[k] = authCopy
+		tmpAuthConfigs[k] = authCopy
 	}
 	}
 
 
-	b, err := json.MarshalIndent(configs, "", "\t")
+	saveAuthConfigs := configFile.AuthConfigs
+	configFile.AuthConfigs = tmpAuthConfigs
+	defer func() { configFile.AuthConfigs = saveAuthConfigs }()
+
+	data, err := json.MarshalIndent(configFile, "", "\t")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	err = ioutil.WriteFile(confFile, b, 0600)
+
+	if err := os.MkdirAll(filepath.Dir(configFile.filename), 0600); err != nil {
+		return err
+	}
+
+	err = ioutil.WriteFile(configFile.filename, data, 0600)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+
 	return nil
 	return nil
 }
 }
 
 
@@ -431,7 +480,7 @@ func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, regis
 func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
 func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
 	configKey := index.GetAuthConfigKey()
 	configKey := index.GetAuthConfigKey()
 	// First try the happy case
 	// First try the happy case
-	if c, found := config.Configs[configKey]; found || index.Official {
+	if c, found := config.AuthConfigs[configKey]; found || index.Official {
 		return c
 		return c
 	}
 	}
 
 
@@ -450,7 +499,7 @@ func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
 
 
 	// Maybe they have a legacy config file, we will iterate the keys converting
 	// Maybe they have a legacy config file, we will iterate the keys converting
 	// them to the new format and testing
 	// them to the new format and testing
-	for registry, config := range config.Configs {
+	for registry, config := range config.AuthConfigs {
 		if configKey == convertToHostname(registry) {
 		if configKey == convertToHostname(registry) {
 			return config
 			return config
 		}
 		}
@@ -459,3 +508,7 @@ func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
 	// When all else fails, return an empty auth config
 	// When all else fails, return an empty auth config
 	return AuthConfig{}
 	return AuthConfig{}
 }
 }
+
+func (config *ConfigFile) Filename() string {
+	return config.filename
+}

+ 14 - 12
registry/auth_test.go

@@ -3,6 +3,7 @@ package registry
 import (
 import (
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
+	"path/filepath"
 	"testing"
 	"testing"
 )
 )
 
 
@@ -31,13 +32,14 @@ func setupTempConfigFile() (*ConfigFile, error) {
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+	root = filepath.Join(root, CONFIGFILE)
 	configFile := &ConfigFile{
 	configFile := &ConfigFile{
-		rootPath: root,
-		Configs:  make(map[string]AuthConfig),
+		AuthConfigs: make(map[string]AuthConfig),
+		filename:    root,
 	}
 	}
 
 
 	for _, registry := range []string{"testIndex", IndexServerAddress()} {
 	for _, registry := range []string{"testIndex", IndexServerAddress()} {
-		configFile.Configs[registry] = AuthConfig{
+		configFile.AuthConfigs[registry] = AuthConfig{
 			Username: "docker-user",
 			Username: "docker-user",
 			Password: "docker-pass",
 			Password: "docker-pass",
 			Email:    "docker@docker.io",
 			Email:    "docker@docker.io",
@@ -52,14 +54,14 @@ func TestSameAuthDataPostSave(t *testing.T) {
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
-	defer os.RemoveAll(configFile.rootPath)
+	defer os.RemoveAll(configFile.filename)
 
 
-	err = SaveConfig(configFile)
+	err = configFile.Save()
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 
 
-	authConfig := configFile.Configs["testIndex"]
+	authConfig := configFile.AuthConfigs["testIndex"]
 	if authConfig.Username != "docker-user" {
 	if authConfig.Username != "docker-user" {
 		t.Fail()
 		t.Fail()
 	}
 	}
@@ -79,9 +81,9 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
-	defer os.RemoveAll(configFile.rootPath)
+	defer os.RemoveAll(configFile.filename)
 
 
-	indexConfig := configFile.Configs[IndexServerAddress()]
+	indexConfig := configFile.AuthConfigs[IndexServerAddress()]
 
 
 	officialIndex := &IndexInfo{
 	officialIndex := &IndexInfo{
 		Official: true,
 		Official: true,
@@ -102,7 +104,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
-	defer os.RemoveAll(configFile.rootPath)
+	defer os.RemoveAll(configFile.filename)
 
 
 	registryAuth := AuthConfig{
 	registryAuth := AuthConfig{
 		Username: "foo-user",
 		Username: "foo-user",
@@ -119,7 +121,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
 		Password: "baz-pass",
 		Password: "baz-pass",
 		Email:    "baz@example.com",
 		Email:    "baz@example.com",
 	}
 	}
-	configFile.Configs[IndexServerAddress()] = officialAuth
+	configFile.AuthConfigs[IndexServerAddress()] = officialAuth
 
 
 	expectedAuths := map[string]AuthConfig{
 	expectedAuths := map[string]AuthConfig{
 		"registry.example.com": registryAuth,
 		"registry.example.com": registryAuth,
@@ -157,12 +159,12 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
 			Name: configKey,
 			Name: configKey,
 		}
 		}
 		for _, registry := range registries {
 		for _, registry := range registries {
-			configFile.Configs[registry] = configured
+			configFile.AuthConfigs[registry] = configured
 			resolved := configFile.ResolveAuthConfig(index)
 			resolved := configFile.ResolveAuthConfig(index)
 			if resolved.Email != configured.Email {
 			if resolved.Email != configured.Email {
 				t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email)
 				t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email)
 			}
 			}
-			delete(configFile.Configs, registry)
+			delete(configFile.AuthConfigs, registry)
 			resolved = configFile.ResolveAuthConfig(index)
 			resolved = configFile.ResolveAuthConfig(index)
 			if resolved.Email == configured.Email {
 			if resolved.Email == configured.Email {
 				t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email)
 				t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email)

+ 135 - 0
registry/config_file_test.go

@@ -0,0 +1,135 @@
+package registry
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/pkg/homedir"
+)
+
+func TestMissingFile(t *testing.T) {
+	tmpHome, _ := ioutil.TempDir("", "config-test")
+
+	config, err := LoadConfig(tmpHome)
+	if err != nil {
+		t.Fatalf("Failed loading on missing file: %q", err)
+	}
+
+	// Now save it and make sure it shows up in new form
+	err = config.Save()
+	if err != nil {
+		t.Fatalf("Failed to save: %q", err)
+	}
+
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
+	if !strings.Contains(string(buf), `"auths":`) {
+		t.Fatalf("Should have save in new form: %s", string(buf))
+	}
+}
+
+func TestEmptyFile(t *testing.T) {
+	tmpHome, _ := ioutil.TempDir("", "config-test")
+	fn := filepath.Join(tmpHome, CONFIGFILE)
+	ioutil.WriteFile(fn, []byte(""), 0600)
+
+	_, err := LoadConfig(tmpHome)
+	if err == nil {
+		t.Fatalf("Was supposed to fail")
+	}
+}
+
+func TestEmptyJson(t *testing.T) {
+	tmpHome, _ := ioutil.TempDir("", "config-test")
+	fn := filepath.Join(tmpHome, CONFIGFILE)
+	ioutil.WriteFile(fn, []byte("{}"), 0600)
+
+	config, err := LoadConfig(tmpHome)
+	if err != nil {
+		t.Fatalf("Failed loading on empty json file: %q", err)
+	}
+
+	// Now save it and make sure it shows up in new form
+	err = config.Save()
+	if err != nil {
+		t.Fatalf("Failed to save: %q", err)
+	}
+
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
+	if !strings.Contains(string(buf), `"auths":`) {
+		t.Fatalf("Should have save in new form: %s", string(buf))
+	}
+}
+
+func TestOldJson(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		return
+	}
+
+	tmpHome, _ := ioutil.TempDir("", "config-test")
+	defer os.RemoveAll(tmpHome)
+
+	homeKey := homedir.Key()
+	homeVal := homedir.Get()
+
+	defer func() { os.Setenv(homeKey, homeVal) }()
+	os.Setenv(homeKey, tmpHome)
+
+	fn := filepath.Join(tmpHome, OLD_CONFIGFILE)
+	js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
+	ioutil.WriteFile(fn, []byte(js), 0600)
+
+	config, err := LoadConfig(tmpHome)
+	if err != nil {
+		t.Fatalf("Failed loading on empty json file: %q", err)
+	}
+
+	ac := config.AuthConfigs["https://index.docker.io/v1/"]
+	if ac.Email != "user@example.com" || ac.Username != "joejoe" || ac.Password != "hello" {
+		t.Fatalf("Missing data from parsing:\n%q", config)
+	}
+
+	// Now save it and make sure it shows up in new form
+	err = config.Save()
+	if err != nil {
+		t.Fatalf("Failed to save: %q", err)
+	}
+
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
+	if !strings.Contains(string(buf), `"auths":`) ||
+		!strings.Contains(string(buf), "user@example.com") {
+		t.Fatalf("Should have save in new form: %s", string(buf))
+	}
+}
+
+func TestNewJson(t *testing.T) {
+	tmpHome, _ := ioutil.TempDir("", "config-test")
+	fn := filepath.Join(tmpHome, CONFIGFILE)
+	js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
+	ioutil.WriteFile(fn, []byte(js), 0600)
+
+	config, err := LoadConfig(tmpHome)
+	if err != nil {
+		t.Fatalf("Failed loading on empty json file: %q", err)
+	}
+
+	ac := config.AuthConfigs["https://index.docker.io/v1/"]
+	if ac.Email != "user@example.com" || ac.Username != "joejoe" || ac.Password != "hello" {
+		t.Fatalf("Missing data from parsing:\n%q", config)
+	}
+
+	// Now save it and make sure it shows up in new form
+	err = config.Save()
+	if err != nil {
+		t.Fatalf("Failed to save: %q", err)
+	}
+
+	buf, err := ioutil.ReadFile(filepath.Join(tmpHome, CONFIGFILE))
+	if !strings.Contains(string(buf), `"auths":`) ||
+		!strings.Contains(string(buf), "user@example.com") {
+		t.Fatalf("Should have save in new form: %s", string(buf))
+	}
+}