Add registry-specific credential helper support

Signed-off-by: Jake Sanders <jsand@google.com>
(cherry picked from commit 07c4b4124b)
Signed-off-by: Victor Vieux <vieux@docker.com>
This commit is contained in:
Jake Sanders 2016-08-18 14:23:10 -07:00 committed by Victor Vieux
parent b18516e6ac
commit 4fba52a2f7
8 changed files with 139 additions and 24 deletions

View file

@ -10,6 +10,7 @@ import (
"runtime"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
cliflags "github.com/docker/docker/cli/flags"
"github.com/docker/docker/cliconfig"
@ -86,15 +87,55 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
return cli.configFile
}
// 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.
func (cli *DockerCli) CredentialsStore() credentials.Store {
if cli.configFile.CredentialsStore != "" {
return credentials.NewNativeStore(cli.configFile)
// 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
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {

View file

@ -280,7 +280,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
}
}
authConfig, _ := dockerCli.CredentialsStore().GetAll()
authConfigs, _ := dockerCli.GetAllCredentials()
buildOptions := types.ImageBuildOptions{
Memory: memory,
MemorySwap: memorySwap,
@ -301,7 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
ShmSize: shmSize,
Ulimits: options.ulimits.GetList(),
BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()),
AuthConfigs: authConfig,
AuthConfigs: authConfigs,
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom,
SecurityOpt: options.securityOpt,

View file

@ -67,7 +67,7 @@ func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes
configKey = ElectAuthServer(ctx, cli)
}
a, _ := cli.CredentialsStore().Get(configKey)
a, _ := cli.CredentialsStore(configKey).Get(configKey)
return a
}
@ -82,7 +82,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD
serverAddress = registry.ConvertToHostname(serverAddress)
}
authconfig, err := cli.CredentialsStore().Get(serverAddress)
authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress)
if err != nil {
return authconfig, err
}

View file

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

View file

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

View file

@ -86,7 +86,7 @@ func TestEmptyFile(t *testing.T) {
}
}
func TestEmptyJson(t *testing.T) {
func TestEmptyJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
@ -193,7 +193,7 @@ func TestOldValidAuth(t *testing.T) {
}
}
func TestOldJsonInvalid(t *testing.T) {
func TestOldJSONInvalid(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
@ -219,7 +219,7 @@ func TestOldJsonInvalid(t *testing.T) {
}
}
func TestOldJson(t *testing.T) {
func TestOldJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
@ -265,7 +265,7 @@ func TestOldJson(t *testing.T) {
}
}
func TestNewJson(t *testing.T) {
func TestNewJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
@ -304,7 +304,7 @@ func TestNewJson(t *testing.T) {
}
}
func TestNewJsonNoEmail(t *testing.T) {
func TestNewJSONNoEmail(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
@ -343,7 +343,7 @@ func TestNewJsonNoEmail(t *testing.T) {
}
}
func TestJsonWithPsFormat(t *testing.T) {
func TestJSONWithPsFormat(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
@ -376,6 +376,78 @@ func TestJsonWithPsFormat(t *testing.T) {
}
}
func TestJSONWithCredentialStore(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
js := `{
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
"credsStore": "crazy-secure-storage"
}`
if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil {
t.Fatal(err)
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.CredentialsStore != "crazy-secure-storage" {
t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore)
}
// Now save it and make sure it shows up in new form
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
if !strings.Contains(configStr, `"credsStore":`) ||
!strings.Contains(configStr, "crazy-secure-storage") {
t.Fatalf("Should have save in new form: %s", configStr)
}
}
func TestJSONWithCredentialHelpers(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
js := `{
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
"credHelpers": { "images.io": "images-io", "containers.com": "crazy-secure-storage" }
}`
if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil {
t.Fatal(err)
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
if config.CredentialHelpers == nil {
t.Fatal("config.CredentialHelpers was nil")
} else if config.CredentialHelpers["images.io"] != "images-io" ||
config.CredentialHelpers["containers.com"] != "crazy-secure-storage" {
t.Fatalf("Credential helpers not deserialized properly: %v\n", config.CredentialHelpers)
}
// Now save it and make sure it shows up in new form
configStr := saveConfigAndValidateNewFormat(t, config, tmpHome)
if !strings.Contains(configStr, `"credHelpers":`) ||
!strings.Contains(configStr, "images.io") ||
!strings.Contains(configStr, "images-io") ||
!strings.Contains(configStr, "containers.com") ||
!strings.Contains(configStr, "crazy-secure-storage") {
t.Fatalf("Should have save in new form: %s", configStr)
}
}
// Save it and make sure it shows up in new form
func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string {
if err := config.Save(); err != nil {
@ -420,7 +492,7 @@ func TestConfigFile(t *testing.T) {
}
}
func TestJsonReaderNoFile(t *testing.T) {
func TestJSONReaderNoFile(t *testing.T) {
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
config, err := LoadFromReader(strings.NewReader(js))
@ -435,7 +507,7 @@ func TestJsonReaderNoFile(t *testing.T) {
}
func TestOldJsonReaderNoFile(t *testing.T) {
func TestOldJSONReaderNoFile(t *testing.T) {
js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}`
config, err := LegacyLoadFromReader(strings.NewReader(js))
@ -449,7 +521,7 @@ func TestOldJsonReaderNoFile(t *testing.T) {
}
}
func TestJsonWithPsFormatNoFile(t *testing.T) {
func TestJSONWithPsFormatNoFile(t *testing.T) {
js := `{
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } },
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
@ -465,7 +537,7 @@ func TestJsonWithPsFormatNoFile(t *testing.T) {
}
func TestJsonSaveWithNoFile(t *testing.T) {
func TestJSONSaveWithNoFile(t *testing.T) {
js := `{
"auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } },
"psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}"
@ -507,7 +579,7 @@ 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"}}`
config, err := LegacyLoadFromReader(strings.NewReader(js))

View file

@ -31,6 +31,7 @@ type ConfigFile struct {
StatsFormat string `json:"statsFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
Filename string `json:"-"` // Note: for internal use only
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
}
@ -96,7 +97,8 @@ func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
// in this file or not.
func (configFile *ConfigFile) ContainsAuth() bool {
return configFile.CredentialsStore != "" ||
(configFile.AuthConfigs != nil && len(configFile.AuthConfigs) > 0)
len(configFile.CredentialHelpers) > 0 ||
len(configFile.AuthConfigs) > 0
}
// SaveToWriter encodes and writes out all the authorization information to

View file

@ -22,8 +22,8 @@ type nativeStore struct {
// NewNativeStore creates a new native store that
// uses a remote helper program to manage credentials.
func NewNativeStore(file *configfile.ConfigFile) Store {
name := remoteCredentialsPrefix + file.CredentialsStore
func NewNativeStore(file *configfile.ConfigFile, helperSuffix string) Store {
name := remoteCredentialsPrefix + helperSuffix
return &nativeStore{
programFunc: client.NewShellProgramFunc(name),
fileStore: NewFileStore(file),