Merge pull request #227 from dnephin/expose-config

expose config credentials without needing the Cli
This commit is contained in:
Vincent Demeester 2017-06-29 19:11:37 +02:00 committed by GitHub
commit 74af31be7f
19 changed files with 339 additions and 484 deletions

View File

@ -1,7 +1,6 @@
package command package command
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@ -10,11 +9,9 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
cliconfig "github.com/docker/cli/cli/config" cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/sockets" "github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig" "github.com/docker/go-connections/tlsconfig"
@ -39,7 +36,7 @@ type Cli interface {
In() *InStream In() *InStream
SetIn(in *InStream) SetIn(in *InStream)
ConfigFile() *configfile.ConfigFile ConfigFile() *configfile.ConfigFile
CredentialsStore(serverAddress string) credentials.Store ServerInfo() ServerInfo
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
@ -104,59 +101,10 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
return cli.server return cli.server
} }
// GetAllCredentials returns all of the credentials stored in all of the
// configured credential stores.
func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) {
auths := make(map[string]types.AuthConfig)
for registry := range cli.configFile.CredentialHelpers {
helper := cli.CredentialsStore(registry)
newAuths, err := helper.GetAll()
if err != nil {
return nil, err
}
addAll(auths, newAuths)
}
defaultStore := cli.CredentialsStore("")
newAuths, err := defaultStore.GetAll()
if err != nil {
return nil, err
}
addAll(auths, newAuths)
return auths, nil
}
func addAll(to, from map[string]types.AuthConfig) {
for reg, ac := range from {
to[reg] = ac
}
}
// CredentialsStore returns a new credentials store based
// on the settings provided in the configuration file. Empty string returns
// the default credential store.
func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store {
if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" {
return credentials.NewNativeStore(cli.configFile, helper)
}
return credentials.NewFileStore(cli.configFile)
}
// getConfiguredCredentialStore returns the credential helper configured for the
// given registry, the default credsStore, or the empty string if neither are
// configured.
func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string {
if c.CredentialHelpers != nil && serverAddress != "" {
if helper, exists := c.CredentialHelpers[serverAddress]; exists {
return helper
}
}
return c.CredentialsStore
}
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = LoadDefaultConfigFile(cli.err) cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
var err error var err error
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
@ -213,19 +161,6 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err} return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
} }
// LoadDefaultConfigFile attempts to load the default config file and returns
// an initialized ConfigFile struct if none is found.
func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile {
configFile, e := cliconfig.Load(cliconfig.Dir())
if e != nil {
fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
}
if !configFile.ContainsAuth() {
credentials.DetectDefaultStore(configFile)
}
return configFile
}
// NewAPIClientFromFlags creates a new APIClient from command line flags // NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions) host, err := getServerHost(opts.Hosts, opts.TLSOptions)

View File

@ -78,7 +78,7 @@ func (o buildOptions) contextFromStdin() bool {
} }
// NewBuildCommand creates a new `docker build` command // NewBuildCommand creates a new `docker build` command
func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
ulimits := make(map[string]*units.Ulimit) ulimits := make(map[string]*units.Ulimit)
options := buildOptions{ options := buildOptions{
tags: opts.NewListOpts(validateTag), tags: opts.NewListOpts(validateTag),
@ -159,7 +159,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
} }
// nolint: gocyclo // nolint: gocyclo
func runBuild(dockerCli *command.DockerCli, options buildOptions) error { func runBuild(dockerCli command.Cli, options buildOptions) error {
var ( var (
buildCtx io.ReadCloser buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser dockerfileCtx io.ReadCloser
@ -336,7 +336,8 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
body = buildCtx body = buildCtx
} }
authConfigs, _ := dockerCli.GetAllCredentials() configFile := dockerCli.ConfigFile()
authConfigs, _ := configFile.GetAllCredentials()
buildOptions := types.ImageBuildOptions{ buildOptions := types.ImageBuildOptions{
Memory: options.memory.Value(), Memory: options.memory.Value(),
MemorySwap: options.memorySwap.Value(), MemorySwap: options.memorySwap.Value(),
@ -356,7 +357,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
Dockerfile: relDockerfile, Dockerfile: relDockerfile,
ShmSize: options.shmSize.Value(), ShmSize: options.shmSize.Value(),
Ulimits: options.ulimits.GetList(), Ulimits: options.ulimits.GetList(),
BuildArgs: dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()), BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
AuthConfigs: authConfigs, AuthConfigs: authConfigs,
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()), Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom, CacheFrom: options.cacheFrom,

View File

@ -26,11 +26,11 @@ import (
const clientSessionRemote = "client-session" const clientSessionRemote = "client-session"
func isSessionSupported(dockerCli *command.DockerCli) bool { func isSessionSupported(dockerCli command.Cli) bool {
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31") return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
} }
func trySession(dockerCli *command.DockerCli, contextDir string) (*session.Session, error) { func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
var s *session.Session var s *session.Session
if isSessionSupported(dockerCli) { if isSessionSupported(dockerCli) {
sharedKey, err := getBuildSharedKey(contextDir) sharedKey, err := getBuildSharedKey(contextDir)

View File

@ -8,8 +8,7 @@ import (
) )
// NewImageCommand returns a cobra command for `image` subcommands // NewImageCommand returns a cobra command for `image` subcommands
// nolint: interfacer func NewImageCommand(dockerCli command.Cli) *cobra.Command {
func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "image", Use: "image",
Short: "Manage images", Short: "Manage images",

View File

@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"testing" "testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/internal/test" "github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil" "github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden" "github.com/docker/docker/pkg/testutil/golden"
@ -41,7 +42,9 @@ func TestNewPullCommandErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) cli := test.NewFakeCli(&fakeClient{}, buf)
cli.SetConfigfile(configfile.New("filename"))
cmd := NewPullCommand(cli)
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -64,7 +67,9 @@ func TestNewPullCommandSuccess(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf)) cli := test.NewFakeCli(&fakeClient{}, buf)
cli.SetConfigfile(configfile.New("filename"))
cmd := NewPullCommand(cli)
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
err := cmd.Execute() err := cmd.Execute()

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/internal/test" "github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil" "github.com/docker/docker/pkg/testutil"
@ -47,7 +48,9 @@ func TestNewPushCommandErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf)) cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf)
cli.SetConfigfile(configfile.New("filename"))
cmd := NewPushCommand(cli)
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -66,11 +69,13 @@ func TestNewPushCommandSuccess(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
cmd := NewPushCommand(test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) { imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil return ioutil.NopCloser(strings.NewReader("")), nil
}, },
}, buf)) }, buf)
cli.SetConfigfile(configfile.New("filename"))
cmd := NewPushCommand(cli)
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute()) assert.NoError(t, cmd.Execute())

View File

@ -70,7 +70,7 @@ func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexI
configKey = ElectAuthServer(ctx, cli) configKey = ElectAuthServer(ctx, cli)
} }
a, _ := cli.CredentialsStore(configKey).Get(configKey) a, _ := cli.ConfigFile().GetAuthConfig(configKey)
return a return a
} }
@ -85,7 +85,7 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR
serverAddress = registry.ConvertToHostname(serverAddress) serverAddress = registry.ConvertToHostname(serverAddress)
} }
authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) authconfig, err := cli.ConfigFile().GetAuthConfig(serverAddress)
if err != nil { if err != nil {
return authconfig, err return authconfig, err
} }

View File

@ -71,7 +71,7 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
authConfig.Password = "" authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken authConfig.IdentityToken = response.IdentityToken
} }
if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(authConfig); err != nil {
return errors.Errorf("Error saving credentials: %v", err) return errors.Errorf("Error saving credentials: %v", err)
} }

View File

@ -68,7 +68,7 @@ func runLogout(dockerCli command.Cli, serverAddress string) error {
fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress)
for _, r := range regsToLogout { for _, r := range regsToLogout {
if err := dockerCli.CredentialsStore(r).Erase(r); err != nil { if err := dockerCli.ConfigFile().GetCredentialsStore(r).Erase(r); err != nil {
fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err)
} }
} }

View File

@ -1,11 +1,13 @@
package config package config
import ( import (
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/homedir"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -38,15 +40,6 @@ func SetDir(dir string) {
configDir = dir configDir = dir
} }
// NewConfigFile initializes an empty configuration file for the given filename 'fn'
func NewConfigFile(fn string) *configfile.ConfigFile {
return &configfile.ConfigFile{
AuthConfigs: make(map[string]types.AuthConfig),
HTTPHeaders: make(map[string]string),
Filename: fn,
}
}
// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from // LegacyLoadFromReader is a convenience function that creates a ConfigFile object from
// a non-nested reader // a non-nested reader
func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) { func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) {
@ -75,46 +68,53 @@ func Load(configDir string) (*configfile.ConfigFile, error) {
configDir = Dir() configDir = Dir()
} }
configFile := configfile.ConfigFile{ filename := filepath.Join(configDir, ConfigFileName)
AuthConfigs: make(map[string]types.AuthConfig), configFile := configfile.New(filename)
Filename: filepath.Join(configDir, ConfigFileName),
}
// Try happy path first - latest config file // Try happy path first - latest config file
if _, err := os.Stat(configFile.Filename); err == nil { if _, err := os.Stat(filename); err == nil {
file, err := os.Open(configFile.Filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return &configFile, errors.Errorf("%s - %v", configFile.Filename, err) return configFile, errors.Errorf("%s - %v", filename, err)
} }
defer file.Close() defer file.Close()
err = configFile.LoadFromReader(file) err = configFile.LoadFromReader(file)
if err != nil { if err != nil {
err = errors.Errorf("%s - %v", configFile.Filename, err) err = errors.Errorf("%s - %v", filename, err)
} }
return &configFile, err return configFile, err
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
// if file is there but we can't stat it for any reason other // if file is there but we can't stat it for any reason other
// than it doesn't exist then stop // than it doesn't exist then stop
return &configFile, errors.Errorf("%s - %v", configFile.Filename, err) return configFile, errors.Errorf("%s - %v", filename, err)
} }
// Can't find latest config file so check for the old one // Can't find latest config file so check for the old one
confFile := filepath.Join(homedir.Get(), oldConfigfile) confFile := filepath.Join(homedir.Get(), oldConfigfile)
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
} }
file, err := os.Open(confFile) file, err := os.Open(confFile)
if err != nil { if err != nil {
return &configFile, errors.Errorf("%s - %v", confFile, err) return configFile, errors.Errorf("%s - %v", confFile, err)
} }
defer file.Close() defer file.Close()
err = configFile.LegacyLoadFromReader(file) err = configFile.LegacyLoadFromReader(file)
if err != nil { if err != nil {
return &configFile, errors.Errorf("%s - %v", confFile, err) return configFile, errors.Errorf("%s - %v", confFile, err)
} }
return configFile, nil
if configFile.HTTPHeaders == nil { }
configFile.HTTPHeaders = map[string]string{}
} // LoadDefaultConfigFile attempts to load the default config file and returns
return &configFile, nil // an initialized ConfigFile struct if none is found.
func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile {
configFile, err := Load(Dir())
if err != nil {
fmt.Fprintf(stderr, "WARNING: Error loading config file: %v\n", err)
}
if !configFile.ContainsAuth() {
configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore)
}
return configFile
} }

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -8,28 +9,34 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestEmptyConfigDir(t *testing.T) { func setupConfigDir(t *testing.T) (string, func()) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpdir, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err) oldDir := Dir()
} SetDir(tmpdir)
defer os.RemoveAll(tmpHome)
SetDir(tmpHome) return tmpdir, func() {
SetDir(oldDir)
os.RemoveAll(tmpdir)
}
}
func TestEmptyConfigDir(t *testing.T) {
tmpHome, cleanup := setupConfigDir(t)
defer cleanup()
config, err := Load("") config, err := Load("")
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty config dir: %q", err)
}
expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName) expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName)
if config.Filename != expectedConfigFilename { assert.Equal(t, expectedConfigFilename, config.Filename)
t.Fatalf("Expected config filename %s, got %s", expectedConfigFilename, config.Filename)
}
// Now save it and make sure it shows up in new form // Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome) saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -37,15 +44,11 @@ func TestEmptyConfigDir(t *testing.T) {
func TestMissingFile(t *testing.T) { func TestMissingFile(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on missing file: %q", err)
}
// Now save it and make sure it shows up in new form // Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome) saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -53,17 +56,13 @@ func TestMissingFile(t *testing.T) {
func TestSaveFileToDirs(t *testing.T) { func TestSaveFileToDirs(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
tmpHome += "/.docker" tmpHome += "/.docker"
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on missing file: %q", err)
}
// Now save it and make sure it shows up in new form // Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome) saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -71,38 +70,28 @@ func TestSaveFileToDirs(t *testing.T) {
func TestEmptyFile(t *testing.T) { func TestEmptyFile(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
if err := ioutil.WriteFile(fn, []byte(""), 0600); err != nil { err = ioutil.WriteFile(fn, []byte(""), 0600)
t.Fatal(err) require.NoError(t, err)
}
_, err = Load(tmpHome) _, err = Load(tmpHome)
if err == nil { testutil.ErrorContains(t, err, "EOF")
t.Fatalf("Was supposed to fail")
}
} }
func TestEmptyJSON(t *testing.T) { func TestEmptyJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
if err := ioutil.WriteFile(fn, []byte("{}"), 0600); err != nil { err = ioutil.WriteFile(fn, []byte("{}"), 0600)
t.Fatal(err) require.NoError(t, err)
}
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
// Now save it and make sure it shows up in new form // Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome) saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -118,9 +107,7 @@ email`: "Invalid auth configuration file",
} }
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
homeKey := homedir.Key() homeKey := homedir.Key()
@ -131,27 +118,17 @@ email`: "Invalid auth configuration file",
for content, expectedError := range invalids { for content, expectedError := range invalids {
fn := filepath.Join(tmpHome, oldConfigfile) fn := filepath.Join(tmpHome, oldConfigfile)
if err := ioutil.WriteFile(fn, []byte(content), 0600); err != nil { err := ioutil.WriteFile(fn, []byte(content), 0600)
t.Fatal(err) require.NoError(t, err)
}
config, err := Load(tmpHome)
// Use Contains instead of == since the file name will change each time
if err == nil || !strings.Contains(err.Error(), expectedError) {
t.Fatalf("Should have failed\nConfig: %v\nGot: %v\nExpected: %v", config, err, expectedError)
}
_, err = Load(tmpHome)
testutil.ErrorContains(t, err, expectedError)
} }
} }
func TestOldValidAuth(t *testing.T) { func TestOldValidAuth(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
homeKey := homedir.Key() homeKey := homedir.Key()
@ -163,14 +140,11 @@ func TestOldValidAuth(t *testing.T) {
fn := filepath.Join(tmpHome, oldConfigfile) fn := filepath.Join(tmpHome, oldConfigfile)
js := `username = am9lam9lOmhlbGxv js := `username = am9lam9lOmhlbGxv
email = user@example.com` email = user@example.com`
if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { err = ioutil.WriteFile(fn, []byte(js), 0600)
t.Fatal(err) require.NoError(t, err)
}
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
// defaultIndexserver is https://index.docker.io/v1/ // defaultIndexserver is https://index.docker.io/v1/
ac := config.AuthConfigs["https://index.docker.io/v1/"] ac := config.AuthConfigs["https://index.docker.io/v1/"]
@ -189,16 +163,12 @@ func TestOldValidAuth(t *testing.T) {
} }
}` }`
if configStr != expConfStr { assert.Equal(t, expConfStr, configStr)
t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr)
}
} }
func TestOldJSONInvalid(t *testing.T) { func TestOldJSONInvalid(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
homeKey := homedir.Key() homeKey := homedir.Key()
@ -222,9 +192,7 @@ func TestOldJSONInvalid(t *testing.T) {
func TestOldJSON(t *testing.T) { func TestOldJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
homeKey := homedir.Key() homeKey := homedir.Key()
@ -240,9 +208,7 @@ func TestOldJSON(t *testing.T) {
} }
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
ac := config.AuthConfigs["https://index.docker.io/v1/"] ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" { if ac.Username != "joejoe" || ac.Password != "hello" {
@ -268,9 +234,7 @@ func TestOldJSON(t *testing.T) {
func TestNewJSON(t *testing.T) { func TestNewJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
@ -280,9 +244,7 @@ func TestNewJSON(t *testing.T) {
} }
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
ac := config.AuthConfigs["https://index.docker.io/v1/"] ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" { if ac.Username != "joejoe" || ac.Password != "hello" {
@ -307,9 +269,7 @@ func TestNewJSON(t *testing.T) {
func TestNewJSONNoEmail(t *testing.T) { func TestNewJSONNoEmail(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
@ -319,9 +279,7 @@ func TestNewJSONNoEmail(t *testing.T) {
} }
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
ac := config.AuthConfigs["https://index.docker.io/v1/"] ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" { if ac.Username != "joejoe" || ac.Password != "hello" {
@ -346,9 +304,7 @@ func TestNewJSONNoEmail(t *testing.T) {
func TestJSONWithPsFormat(t *testing.T) { func TestJSONWithPsFormat(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
@ -361,9 +317,7 @@ func TestJSONWithPsFormat(t *testing.T) {
} }
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
t.Fatalf("Unknown ps format: %s\n", config.PsFormat) t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
@ -379,9 +333,7 @@ func TestJSONWithPsFormat(t *testing.T) {
func TestJSONWithCredentialStore(t *testing.T) { func TestJSONWithCredentialStore(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
@ -394,9 +346,7 @@ func TestJSONWithCredentialStore(t *testing.T) {
} }
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.CredentialsStore != "crazy-secure-storage" { if config.CredentialsStore != "crazy-secure-storage" {
t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore) t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore)
@ -412,9 +362,7 @@ func TestJSONWithCredentialStore(t *testing.T) {
func TestJSONWithCredentialHelpers(t *testing.T) { func TestJSONWithCredentialHelpers(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
@ -427,9 +375,7 @@ func TestJSONWithCredentialHelpers(t *testing.T) {
} }
config, err := Load(tmpHome) config, err := Load(tmpHome)
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.CredentialHelpers == nil { if config.CredentialHelpers == nil {
t.Fatal("config.CredentialHelpers was nil") t.Fatal("config.CredentialHelpers was nil")
@ -450,26 +396,18 @@ func TestJSONWithCredentialHelpers(t *testing.T) {
} }
// Save it and make sure it shows up in new form // Save it and make sure it shows up in new form
func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string { func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, configDir string) string {
if err := config.Save(); err != nil { require.NoError(t, config.Save())
t.Fatalf("Failed to save: %q", err)
}
buf, err := ioutil.ReadFile(filepath.Join(homeFolder, ConfigFileName)) buf, err := ioutil.ReadFile(filepath.Join(configDir, ConfigFileName))
if err != nil { require.NoError(t, err)
t.Fatal(err) assert.Contains(t, string(buf), `"auths":`)
}
if !strings.Contains(string(buf), `"auths":`) {
t.Fatalf("Should have save in new form: %s", string(buf))
}
return string(buf) return string(buf)
} }
func TestConfigDir(t *testing.T) { func TestConfigDir(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
if Dir() == tmpHome { if Dir() == tmpHome {
@ -484,22 +422,11 @@ func TestConfigDir(t *testing.T) {
} }
} }
func TestConfigFile(t *testing.T) {
configFilename := "configFilename"
configFile := NewConfigFile(configFilename)
if configFile.Filename != configFilename {
t.Fatalf("Expected %s, got %s", configFilename, configFile.Filename)
}
}
func TestJSONReaderNoFile(t *testing.T) { func TestJSONReaderNoFile(t *testing.T) {
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }` js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
config, err := LoadFromReader(strings.NewReader(js)) config, err := LoadFromReader(strings.NewReader(js))
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
ac := config.AuthConfigs["https://index.docker.io/v1/"] ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" { if ac.Username != "joejoe" || ac.Password != "hello" {
@ -512,9 +439,7 @@ func TestOldJSONReaderNoFile(t *testing.T) {
js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
config, err := LegacyLoadFromReader(strings.NewReader(js)) config, err := LegacyLoadFromReader(strings.NewReader(js))
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
ac := config.AuthConfigs["https://index.docker.io/v1/"] ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" { if ac.Username != "joejoe" || ac.Password != "hello" {
@ -528,9 +453,7 @@ func TestJSONWithPsFormatNoFile(t *testing.T) {
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
}` }`
config, err := LoadFromReader(strings.NewReader(js)) config, err := LoadFromReader(strings.NewReader(js))
if err != nil { require.NoError(t, err)
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
t.Fatalf("Unknown ps format: %s\n", config.PsFormat) t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
@ -546,28 +469,19 @@ func TestJSONSaveWithNoFile(t *testing.T) {
config, err := LoadFromReader(strings.NewReader(js)) config, err := LoadFromReader(strings.NewReader(js))
require.NoError(t, err) require.NoError(t, err)
err = config.Save() err = config.Save()
if err == nil { testutil.ErrorContains(t, err, "with empty filename")
t.Fatalf("Expected error. File should not have been able to save with no file name.")
}
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatalf("Failed to create a temp dir: %q", err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
defer f.Close() defer f.Close()
err = config.SaveToWriter(f) require.NoError(t, config.SaveToWriter(f))
if err != nil {
t.Fatalf("Failed saving to file: %q", err)
}
buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName))
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
expConfStr := `{ expConfStr := `{
"auths": { "auths": {
"https://index.docker.io/v1/": { "https://index.docker.io/v1/": {
@ -582,32 +496,23 @@ func TestJSONSaveWithNoFile(t *testing.T) {
} }
func TestLegacyJSONSaveWithNoFile(t *testing.T) { func TestLegacyJSONSaveWithNoFile(t *testing.T) {
js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
config, err := LegacyLoadFromReader(strings.NewReader(js)) config, err := LegacyLoadFromReader(strings.NewReader(js))
require.NoError(t, err) require.NoError(t, err)
err = config.Save() err = config.Save()
if err == nil { testutil.ErrorContains(t, err, "with empty filename")
t.Fatalf("Expected error. File should not have been able to save with no file name.")
}
tmpHome, err := ioutil.TempDir("", "config-test") tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil { require.NoError(t, err)
t.Fatalf("Failed to create a temp dir: %q", err)
}
defer os.RemoveAll(tmpHome) defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName) fn := filepath.Join(tmpHome, ConfigFileName)
f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
defer f.Close() defer f.Close()
if err = config.SaveToWriter(f); err != nil { require.NoError(t, config.SaveToWriter(f))
t.Fatalf("Failed saving to file: %q", err)
}
buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName))
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
expConfStr := `{ expConfStr := `{
"auths": { "auths": {
@ -622,3 +527,22 @@ func TestLegacyJSONSaveWithNoFile(t *testing.T) {
t.Fatalf("Should have save in new form: \n%s\n not \n%s", string(buf), expConfStr) t.Fatalf("Should have save in new form: \n%s\n not \n%s", string(buf), expConfStr)
} }
} }
func TestLoadDefaultConfigFile(t *testing.T) {
dir, cleanup := setupConfigDir(t)
defer cleanup()
buffer := new(bytes.Buffer)
filename := filepath.Join(dir, ConfigFileName)
content := []byte(`{"PsFormat": "format"}`)
err := ioutil.WriteFile(filename, content, 0644)
require.NoError(t, err)
configFile := LoadDefaultConfigFile(buffer)
credStore := credentials.DetectDefaultStore("")
expected := configfile.New(filename)
expected.CredentialsStore = credStore
expected.PsFormat = "format"
assert.Equal(t, expected, configFile)
}

View File

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -53,6 +54,15 @@ type ProxyConfig struct {
FTPProxy string `json:"ftpProxy,omitempty"` FTPProxy string `json:"ftpProxy,omitempty"`
} }
// New initializes an empty configuration file for the given filename 'fn'
func New(fn string) *ConfigFile {
return &ConfigFile{
AuthConfigs: make(map[string]types.AuthConfig),
HTTPHeaders: make(map[string]string),
Filename: fn,
}
}
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the // LegacyLoadFromReader reads the non-nested configuration data given and sets up the
// auth config information with given directory and populates the receiver object // auth config information with given directory and populates the receiver object
func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error {
@ -118,6 +128,11 @@ func (configFile *ConfigFile) ContainsAuth() bool {
len(configFile.AuthConfigs) > 0 len(configFile.AuthConfigs) > 0
} }
// GetAuthConfigs returns the mapping of repo to auth configuration
func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig {
return configFile.AuthConfigs
}
// SaveToWriter encodes and writes out all the authorization information to // SaveToWriter encodes and writes out all the authorization information to
// the given writer // the given writer
func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error {
@ -231,3 +246,56 @@ func decodeAuth(authStr string) (string, string, error) {
password := strings.Trim(arr[1], "\x00") password := strings.Trim(arr[1], "\x00")
return arr[0], password, nil return arr[0], password, nil
} }
// GetCredentialsStore returns a new credentials store from the settings in the
// configuration file
func (configFile *ConfigFile) GetCredentialsStore(serverAddress string) credentials.Store {
if helper := getConfiguredCredentialStore(configFile, serverAddress); helper != "" {
return credentials.NewNativeStore(configFile, helper)
}
return credentials.NewFileStore(configFile)
}
// GetAuthConfig for a repository from the credential store
func (configFile *ConfigFile) GetAuthConfig(serverAddress string) (types.AuthConfig, error) {
return configFile.GetCredentialsStore(serverAddress).Get(serverAddress)
}
// getConfiguredCredentialStore returns the credential helper configured for the
// given registry, the default credsStore, or the empty string if neither are
// configured.
func getConfiguredCredentialStore(c *ConfigFile, serverAddress string) string {
if c.CredentialHelpers != nil && serverAddress != "" {
if helper, exists := c.CredentialHelpers[serverAddress]; exists {
return helper
}
}
return c.CredentialsStore
}
// GetAllCredentials returns all of the credentials stored in all of the
// configured credential stores.
func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) {
auths := make(map[string]types.AuthConfig)
addAll := func(from map[string]types.AuthConfig) {
for reg, ac := range from {
auths[reg] = ac
}
}
for registry := range configFile.CredentialHelpers {
helper := configFile.GetCredentialsStore(registry)
newAuths, err := helper.GetAll()
if err != nil {
return nil, err
}
addAll(newAuths)
}
defaultStore := configFile.GetCredentialsStore("")
newAuths, err := defaultStore.GetAll()
if err != nil {
return nil, err
}
addAll(newAuths)
return auths, nil
}

View File

@ -6,26 +6,18 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestEncodeAuth(t *testing.T) { func TestEncodeAuth(t *testing.T) {
newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"} newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"}
authStr := encodeAuth(newAuthConfig) authStr := encodeAuth(newAuthConfig)
decAuthConfig := &types.AuthConfig{}
expected := &types.AuthConfig{}
var err error var err error
decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr) expected.Username, expected.Password, err = decodeAuth(authStr)
if err != nil { require.NoError(t, err)
t.Fatal(err) assert.Equal(t, expected, newAuthConfig)
}
if newAuthConfig.Username != decAuthConfig.Username {
t.Fatal("Encode Username doesn't match decoded Username")
}
if newAuthConfig.Password != decAuthConfig.Password {
t.Fatal("Encode Password doesn't match decoded Password")
}
if authStr != "a2VuOnRlc3Q=" {
t.Fatal("AuthString encoding isn't correct.")
}
} }
func TestProxyConfig(t *testing.T) { func TestProxyConfig(t *testing.T) {
@ -142,3 +134,26 @@ func TestProxyConfigPerHost(t *testing.T) {
} }
assert.Equal(t, expected, proxyConfig) assert.Equal(t, expected, proxyConfig)
} }
func TestConfigFile(t *testing.T) {
configFilename := "configFilename"
configFile := New(configFilename)
assert.Equal(t, configFilename, configFile.Filename)
}
func TestGetAllCredentials(t *testing.T) {
configFile := New("filename")
exampleAuth := types.AuthConfig{
Username: "user",
Password: "pass",
}
configFile.AuthConfigs["example.com/foo"] = exampleAuth
authConfigs, err := configFile.GetAllCredentials()
require.NoError(t, err)
expected := make(map[string]types.AuthConfig)
expected["example.com/foo"] = exampleAuth
assert.Equal(t, expected, authConfigs)
}

View File

@ -2,21 +2,18 @@ package credentials
import ( import (
"os/exec" "os/exec"
"github.com/docker/cli/cli/config/configfile"
) )
// DetectDefaultStore sets the default credentials store // DetectDefaultStore return the default credentials store for the platform if
// if the host includes the default store helper program. // the store executable is available.
func DetectDefaultStore(c *configfile.ConfigFile) { func DetectDefaultStore(store string) string {
if c.CredentialsStore != "" { // user defined or no default for platform
// user defined if store != "" || defaultCredentialsStore == "" {
return return store
} }
if defaultCredentialsStore != "" { if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil {
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil { return defaultCredentialsStore
c.CredentialsStore = defaultCredentialsStore
}
} }
return ""
} }

View File

@ -1,37 +1,39 @@
package credentials package credentials
import ( import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
) )
type store interface {
Save() error
GetAuthConfigs() map[string]types.AuthConfig
}
// fileStore implements a credentials store using // fileStore implements a credentials store using
// the docker configuration file to keep the credentials in plain text. // the docker configuration file to keep the credentials in plain text.
type fileStore struct { type fileStore struct {
file *configfile.ConfigFile file store
} }
// NewFileStore creates a new file credentials store. // NewFileStore creates a new file credentials store.
func NewFileStore(file *configfile.ConfigFile) Store { func NewFileStore(file store) Store {
return &fileStore{ return &fileStore{file: file}
file: file,
}
} }
// Erase removes the given credentials from the file store. // Erase removes the given credentials from the file store.
func (c *fileStore) Erase(serverAddress string) error { func (c *fileStore) Erase(serverAddress string) error {
delete(c.file.AuthConfigs, serverAddress) delete(c.file.GetAuthConfigs(), serverAddress)
return c.file.Save() return c.file.Save()
} }
// Get retrieves credentials for a specific server from the file store. // Get retrieves credentials for a specific server from the file store.
func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) { func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) {
authConfig, ok := c.file.AuthConfigs[serverAddress] authConfig, ok := c.file.GetAuthConfigs()[serverAddress]
if !ok { if !ok {
// 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 r, ac := range c.file.AuthConfigs { for r, ac := range c.file.GetAuthConfigs() {
if serverAddress == registry.ConvertToHostname(r) { if serverAddress == registry.ConvertToHostname(r) {
return ac, nil return ac, nil
} }
@ -43,11 +45,11 @@ func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) {
} }
func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) { func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) {
return c.file.AuthConfigs, nil return c.file.GetAuthConfigs(), nil
} }
// Store saves the given credentials in the file store. // Store saves the given credentials in the file store.
func (c *fileStore) Store(authConfig types.AuthConfig) error { func (c *fileStore) Store(authConfig types.AuthConfig) error {
c.file.AuthConfigs[authConfig.ServerAddress] = authConfig c.file.GetAuthConfigs()[authConfig.ServerAddress] = authConfig
return c.file.Save() return c.file.Save()
} }

View File

@ -1,56 +1,49 @@
package credentials package credentials
import ( import (
"io/ioutil"
"testing" "testing"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func newConfigFile(auths map[string]types.AuthConfig) *configfile.ConfigFile { type fakeStore struct {
tmp, _ := ioutil.TempFile("", "docker-test") configs map[string]types.AuthConfig
name := tmp.Name() }
tmp.Close()
c := cliconfig.NewConfigFile(name) func (f *fakeStore) Save() error {
c.AuthConfigs = auths return nil
return c }
func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
return f.configs
}
func newStore(auths map[string]types.AuthConfig) store {
return &fakeStore{configs: auths}
} }
func TestFileStoreAddCredentials(t *testing.T) { func TestFileStoreAddCredentials(t *testing.T) {
f := newConfigFile(make(map[string]types.AuthConfig)) f := newStore(make(map[string]types.AuthConfig))
s := NewFileStore(f) s := NewFileStore(f)
err := s.Store(types.AuthConfig{ auth := types.AuthConfig{
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",
ServerAddress: "https://example.com", ServerAddress: "https://example.com",
}) }
err := s.Store(auth)
require.NoError(t, err)
assert.Len(t, f.GetAuthConfigs(), 1)
if err != nil { actual, ok := f.GetAuthConfigs()["https://example.com"]
t.Fatal(err) assert.True(t, ok)
} assert.Equal(t, auth, actual)
if len(f.AuthConfigs) != 1 {
t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs))
}
a, ok := f.AuthConfigs["https://example.com"]
if !ok {
t.Fatalf("expected auth for https://example.com, got %v", f.AuthConfigs)
}
if a.Auth != "super_secret_token" {
t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth)
}
if a.Email != "foo@example.com" {
t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
}
} }
func TestFileStoreGet(t *testing.T) { func TestFileStoreGet(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
"https://example.com": { "https://example.com": {
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",
@ -74,7 +67,7 @@ func TestFileStoreGet(t *testing.T) {
func TestFileStoreGetAll(t *testing.T) { func TestFileStoreGetAll(t *testing.T) {
s1 := "https://example.com" s1 := "https://example.com"
s2 := "https://example2.com" s2 := "https://example2.com"
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
s1: { s1: {
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",
@ -110,7 +103,7 @@ func TestFileStoreGetAll(t *testing.T) {
} }
func TestFileStoreErase(t *testing.T) { func TestFileStoreErase(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
"https://example.com": { "https://example.com": {
Auth: "super_secret_token", Auth: "super_secret_token",
Email: "foo@example.com", Email: "foo@example.com",

View File

@ -1,7 +1,6 @@
package credentials package credentials
import ( import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/docker-credential-helpers/client" "github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -22,7 +21,7 @@ type nativeStore struct {
// NewNativeStore creates a new native store that // NewNativeStore creates a new native store that
// uses a remote helper program to manage credentials. // uses a remote helper program to manage credentials.
func NewNativeStore(file *configfile.ConfigFile, helperSuffix string) Store { func NewNativeStore(file store, helperSuffix string) Store {
name := remoteCredentialsPrefix + helperSuffix name := remoteCredentialsPrefix + helperSuffix
return &nativeStore{ return &nativeStore{
programFunc: client.NewShellProgramFunc(name), programFunc: client.NewShellProgramFunc(name),

View File

@ -11,7 +11,10 @@ import (
"github.com/docker/docker-credential-helpers/client" "github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const ( const (
@ -90,53 +93,32 @@ func mockCommandFn(args ...string) client.Program {
} }
func TestNativeStoreAddCredentials(t *testing.T) { func TestNativeStoreAddCredentials(t *testing.T) {
f := newConfigFile(make(map[string]types.AuthConfig)) f := newStore(make(map[string]types.AuthConfig))
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
err := s.Store(types.AuthConfig{ auth := types.AuthConfig{
Username: "foo", Username: "foo",
Password: "bar", Password: "bar",
Email: "foo@example.com", Email: "foo@example.com",
ServerAddress: validServerAddress, ServerAddress: validServerAddress,
}) }
err := s.Store(auth)
require.NoError(t, err)
assert.Len(t, f.GetAuthConfigs(), 1)
if err != nil { actual, ok := f.GetAuthConfigs()[validServerAddress]
t.Fatal(err) assert.True(t, ok)
} expected := types.AuthConfig{
Email: auth.Email,
if len(f.AuthConfigs) != 1 { ServerAddress: auth.ServerAddress,
t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs))
}
a, ok := f.AuthConfigs[validServerAddress]
if !ok {
t.Fatalf("expected auth for %s, got %v", validServerAddress, f.AuthConfigs)
}
if a.Auth != "" {
t.Fatalf("expected auth to be empty, got %s", a.Auth)
}
if a.Username != "" {
t.Fatalf("expected username to be empty, got %s", a.Username)
}
if a.Password != "" {
t.Fatalf("expected password to be empty, got %s", a.Password)
}
if a.IdentityToken != "" {
t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
}
if a.Email != "foo@example.com" {
t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
} }
assert.Equal(t, expected, actual)
} }
func TestNativeStoreAddInvalidCredentials(t *testing.T) { func TestNativeStoreAddInvalidCredentials(t *testing.T) {
f := newConfigFile(make(map[string]types.AuthConfig)) f := newStore(make(map[string]types.AuthConfig))
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
@ -147,102 +129,66 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
Email: "foo@example.com", Email: "foo@example.com",
ServerAddress: invalidServerAddress, ServerAddress: invalidServerAddress,
}) })
testutil.ErrorContains(t, err, "program failed")
if err == nil { assert.Len(t, f.GetAuthConfigs(), 0)
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "program failed") {
t.Fatalf("expected `program failed`, got %v", err)
}
if len(f.AuthConfigs) != 0 {
t.Fatalf("expected 0 auth config, got %d", len(f.AuthConfigs))
}
} }
func TestNativeStoreGet(t *testing.T) { func TestNativeStoreGet(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
a, err := s.Get(validServerAddress) actual, err := s.Get(validServerAddress)
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
if a.Username != "foo" { expected := types.AuthConfig{
t.Fatalf("expected username `foo`, got %s", a.Username) Username: "foo",
} Password: "bar",
if a.Password != "bar" { Email: "foo@example.com",
t.Fatalf("expected password `bar`, got %s", a.Password)
}
if a.IdentityToken != "" {
t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
}
if a.Email != "foo@example.com" {
t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
} }
assert.Equal(t, expected, actual)
} }
func TestNativeStoreGetIdentityToken(t *testing.T) { func TestNativeStoreGetIdentityToken(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress2: { validServerAddress2: {
Email: "foo@example2.com", Email: "foo@example2.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
a, err := s.Get(validServerAddress2) actual, err := s.Get(validServerAddress2)
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
if a.Username != "" { expected := types.AuthConfig{
t.Fatalf("expected username to be empty, got %s", a.Username) IdentityToken: "abcd1234",
} Email: "foo@example2.com",
if a.Password != "" {
t.Fatalf("expected password to be empty, got %s", a.Password)
}
if a.IdentityToken != "abcd1234" {
t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken)
}
if a.Email != "foo@example2.com" {
t.Fatalf("expected email `foo@example2.com`, got %s", a.Email)
} }
assert.Equal(t, expected, actual)
} }
func TestNativeStoreGetAll(t *testing.T) { func TestNativeStoreGetAll(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
as, err := s.GetAll() as, err := s.GetAll()
if err != nil { require.NoError(t, err)
t.Fatal(err) assert.Len(t, as, 2)
}
if len(as) != 2 {
t.Fatalf("wanted 2, got %d", len(as))
}
if as[validServerAddress].Username != "foo" { if as[validServerAddress].Username != "foo" {
t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username)
@ -271,86 +217,62 @@ func TestNativeStoreGetAll(t *testing.T) {
} }
func TestNativeStoreGetMissingCredentials(t *testing.T) { func TestNativeStoreGetMissingCredentials(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
_, err := s.Get(missingCredsAddress) _, err := s.Get(missingCredsAddress)
if err != nil { assert.NoError(t, err)
// missing credentials do not produce an error
t.Fatal(err)
}
} }
func TestNativeStoreGetInvalidAddress(t *testing.T) { func TestNativeStoreGetInvalidAddress(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
_, err := s.Get(invalidServerAddress) _, err := s.Get(invalidServerAddress)
if err == nil { testutil.ErrorContains(t, err, "program failed")
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "program failed") {
t.Fatalf("expected `program failed`, got %v", err)
}
} }
func TestNativeStoreErase(t *testing.T) { func TestNativeStoreErase(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
err := s.Erase(validServerAddress) err := s.Erase(validServerAddress)
if err != nil { require.NoError(t, err)
t.Fatal(err) assert.Len(t, f.GetAuthConfigs(), 0)
}
if len(f.AuthConfigs) != 0 {
t.Fatalf("expected 0 auth configs, got %d", len(f.AuthConfigs))
}
} }
func TestNativeStoreEraseInvalidAddress(t *testing.T) { func TestNativeStoreEraseInvalidAddress(t *testing.T) {
f := newConfigFile(map[string]types.AuthConfig{ f := newStore(map[string]types.AuthConfig{
validServerAddress: { validServerAddress: {
Email: "foo@example.com", Email: "foo@example.com",
}, },
}) })
f.CredentialsStore = "mock"
s := &nativeStore{ s := &nativeStore{
programFunc: mockCommandFn, programFunc: mockCommandFn,
fileStore: NewFileStore(f), fileStore: NewFileStore(f),
} }
err := s.Erase(invalidServerAddress) err := s.Erase(invalidServerAddress)
if err == nil { testutil.ErrorContains(t, err, "program failed")
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "program failed") {
t.Fatalf("expected `program failed`, got %v", err)
}
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
@ -19,7 +18,6 @@ type FakeCli struct {
out *command.OutStream out *command.OutStream
err io.Writer err io.Writer
in *command.InStream in *command.InStream
store credentials.Store
} }
// NewFakeCli returns a Cli backed by the fakeCli // NewFakeCli returns a Cli backed by the fakeCli
@ -71,11 +69,3 @@ func (c *FakeCli) In() *command.InStream {
func (c *FakeCli) ConfigFile() *configfile.ConfigFile { func (c *FakeCli) ConfigFile() *configfile.ConfigFile {
return c.configfile return c.configfile
} }
// CredentialsStore returns the fake store the cli will use
func (c *FakeCli) CredentialsStore(serverAddress string) credentials.Store {
if c.store == nil {
c.store = NewFakeStore()
}
return c.store
}