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:
parent
94e2413ec0
commit
7b8b61bda1
3 changed files with 228 additions and 38 deletions
105
docs/auth.go
105
docs/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"`
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
135
docs/config_file_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue