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>
This commit is contained in:
Doug Davis 2015-04-01 15:39:37 -07:00
parent 94e2413ec0
commit 7b8b61bda1
3 changed files with 228 additions and 38 deletions

View file

@ -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"` AuthConfigs map[string]AuthConfig `json:"auths"`
rootPath string 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) { func LoadConfig(configDir string) (*ConfigFile, error) {
configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath} if configDir == "" {
confFile := path.Join(rootPath, CONFIGFILE) 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 (configFile *ConfigFile) Save() error {
func SaveConfig(configFile *ConfigFile) error { // Encode sensitive data into a new/temp struct
confFile := path.Join(configFile.rootPath, CONFIGFILE) tmpAuthConfigs := make(map[string]AuthConfig, len(configFile.AuthConfigs))
if len(configFile.Configs) == 0 { for k, authConfig := range configFile.AuthConfigs {
os.Remove(confFile)
return nil
}
configs := make(map[string]AuthConfig, len(configFile.Configs))
for k, authConfig := range configFile.Configs {
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
}

View file

@ -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, AuthConfigs: make(map[string]AuthConfig),
Configs: 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
docs/config_file_test.go Normal file
View file

@ -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))
}
}