Procházet zdrojové kódy

Merge pull request #22988 from calavera/use_client_credentials_library

Move native credentials lookup to the client library.
Vincent Demeester před 9 roky
rodič
revize
0f13b69fe2

+ 18 - 88
cliconfig/credentials/native_store.go

@@ -1,14 +1,8 @@
 package credentials
 
 import (
-	"bytes"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"strings"
-
-	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker-credential-helpers/client"
+	"github.com/docker/docker-credential-helpers/credentials"
 	"github.com/docker/docker/cliconfig/configfile"
 	"github.com/docker/engine-api/types"
 )
@@ -18,50 +12,27 @@ const (
 	tokenUsername           = "<token>"
 )
 
-// Standarize the not found error, so every helper returns
-// the same message and docker can handle it properly.
-var errCredentialsNotFound = errors.New("credentials not found in native keychain")
-
-// command is an interface that remote executed commands implement.
-type command interface {
-	Output() ([]byte, error)
-	Input(in io.Reader)
-}
-
-// credentialsRequest holds information shared between docker and a remote credential store.
-type credentialsRequest struct {
-	ServerURL string
-	Username  string
-	Secret    string
-}
-
-// credentialsGetResponse is the information serialized from a remote store
-// when the plugin sends requests to get the user credentials.
-type credentialsGetResponse struct {
-	Username string
-	Secret   string
-}
-
 // nativeStore implements a credentials store
 // using native keychain to keep credentials secure.
 // It piggybacks into a file store to keep users' emails.
 type nativeStore struct {
-	commandFn func(args ...string) command
-	fileStore Store
+	programFunc client.ProgramFunc
+	fileStore   Store
 }
 
 // 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
 	return &nativeStore{
-		commandFn: shellCommandFn(file.CredentialsStore),
-		fileStore: NewFileStore(file),
+		programFunc: client.NewShellProgramFunc(name),
+		fileStore:   NewFileStore(file),
 	}
 }
 
 // Erase removes the given credentials from the native store.
 func (c *nativeStore) Erase(serverAddress string) error {
-	if err := c.eraseCredentialsFromStore(serverAddress); err != nil {
+	if err := client.Erase(c.programFunc, serverAddress); err != nil {
 		return err
 	}
 
@@ -115,8 +86,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
 
 // storeCredentialsInStore executes the command to store the credentials in the native store.
 func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
-	cmd := c.commandFn("store")
-	creds := &credentialsRequest{
+	creds := &credentials.Credentials{
 		ServerURL: config.ServerAddress,
 		Username:  config.Username,
 		Secret:    config.Password,
@@ -127,70 +97,30 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
 		creds.Secret = config.IdentityToken
 	}
 
-	buffer := new(bytes.Buffer)
-	if err := json.NewEncoder(buffer).Encode(creds); err != nil {
-		return err
-	}
-	cmd.Input(buffer)
-
-	out, err := cmd.Output()
-	if err != nil {
-		t := strings.TrimSpace(string(out))
-		logrus.Debugf("error adding credentials - err: %v, out: `%s`", err, t)
-		return fmt.Errorf(t)
-	}
-
-	return nil
+	return client.Store(c.programFunc, creds)
 }
 
 // getCredentialsFromStore executes the command to get the credentials from the native store.
 func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) {
 	var ret types.AuthConfig
 
-	cmd := c.commandFn("get")
-	cmd.Input(strings.NewReader(serverAddress))
-
-	out, err := cmd.Output()
+	creds, err := client.Get(c.programFunc, serverAddress)
 	if err != nil {
-		t := strings.TrimSpace(string(out))
-
-		// do not return an error if the credentials are not
-		// in the keyckain. Let docker ask for new credentials.
-		if t == errCredentialsNotFound.Error() {
+		if credentials.IsErrCredentialsNotFound(err) {
+			// do not return an error if the credentials are not
+			// in the keyckain. Let docker ask for new credentials.
 			return ret, nil
 		}
-
-		logrus.Debugf("error getting credentials - err: %v, out: `%s`", err, t)
-		return ret, fmt.Errorf(t)
-	}
-
-	var resp credentialsGetResponse
-	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
 		return ret, err
 	}
 
-	if resp.Username == tokenUsername {
-		ret.IdentityToken = resp.Secret
+	if creds.Username == tokenUsername {
+		ret.IdentityToken = creds.Secret
 	} else {
-		ret.Password = resp.Secret
-		ret.Username = resp.Username
+		ret.Password = creds.Secret
+		ret.Username = creds.Username
 	}
 
 	ret.ServerAddress = serverAddress
 	return ret, nil
 }
-
-// eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
-func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
-	cmd := c.commandFn("erase")
-	cmd.Input(strings.NewReader(serverURL))
-
-	out, err := cmd.Output()
-	if err != nil {
-		t := strings.TrimSpace(string(out))
-		logrus.Debugf("error erasing credentials - err: %v, out: `%s`", err, t)
-		return fmt.Errorf(t)
-	}
-
-	return nil
-}

+ 33 - 31
cliconfig/credentials/native_store_test.go

@@ -8,6 +8,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/docker/docker-credential-helpers/client"
+	"github.com/docker/docker-credential-helpers/credentials"
 	"github.com/docker/engine-api/types"
 )
 
@@ -43,7 +45,7 @@ func (m *mockCommand) Output() ([]byte, error) {
 		case validServerAddress:
 			return nil, nil
 		default:
-			return []byte("error erasing credentials"), errCommandExited
+			return []byte("program failed"), errCommandExited
 		}
 	case "get":
 		switch inS {
@@ -52,21 +54,21 @@ func (m *mockCommand) Output() ([]byte, error) {
 		case validServerAddress2:
 			return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
 		case missingCredsAddress:
-			return []byte(errCredentialsNotFound.Error()), errCommandExited
+			return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited
 		case invalidServerAddress:
-			return []byte("error getting credentials"), errCommandExited
+			return []byte("program failed"), errCommandExited
 		}
 	case "store":
-		var c credentialsRequest
+		var c credentials.Credentials
 		err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
 		if err != nil {
-			return []byte("error storing credentials"), errCommandExited
+			return []byte("program failed"), errCommandExited
 		}
 		switch c.ServerURL {
 		case validServerAddress:
 			return nil, nil
 		default:
-			return []byte("error storing credentials"), errCommandExited
+			return []byte("program failed"), errCommandExited
 		}
 	}
 
@@ -78,7 +80,7 @@ func (m *mockCommand) Input(in io.Reader) {
 	m.input = in
 }
 
-func mockCommandFn(args ...string) command {
+func mockCommandFn(args ...string) client.Program {
 	return &mockCommand{
 		arg: args[0],
 	}
@@ -89,8 +91,8 @@ func TestNativeStoreAddCredentials(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	err := s.Store(types.AuthConfig{
 		Username:      "foo",
@@ -133,8 +135,8 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	err := s.Store(types.AuthConfig{
 		Username:      "foo",
@@ -147,8 +149,8 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
 		t.Fatal("expected error, got nil")
 	}
 
-	if err.Error() != "error storing credentials" {
-		t.Fatalf("expected `error storing credentials`, got %v", err)
+	if !strings.Contains(err.Error(), "program failed") {
+		t.Fatalf("expected `program failed`, got %v", err)
 	}
 
 	if len(f.AuthConfigs) != 0 {
@@ -165,8 +167,8 @@ func TestNativeStoreGet(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	a, err := s.Get(validServerAddress)
 	if err != nil {
@@ -196,8 +198,8 @@ func TestNativeStoreGetIdentityToken(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	a, err := s.Get(validServerAddress2)
 	if err != nil {
@@ -230,8 +232,8 @@ func TestNativeStoreGetAll(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	as, err := s.GetAll()
 	if err != nil {
@@ -277,8 +279,8 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	_, err := s.Get(missingCredsAddress)
 	if err != nil {
@@ -296,16 +298,16 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	_, err := s.Get(invalidServerAddress)
 	if err == nil {
 		t.Fatal("expected error, got nil")
 	}
 
-	if err.Error() != "error getting credentials" {
-		t.Fatalf("expected `error getting credentials`, got %v", err)
+	if !strings.Contains(err.Error(), "program failed") {
+		t.Fatalf("expected `program failed`, got %v", err)
 	}
 }
 
@@ -318,8 +320,8 @@ func TestNativeStoreErase(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	err := s.Erase(validServerAddress)
 	if err != nil {
@@ -340,15 +342,15 @@ func TestNativeStoreEraseInvalidAddress(t *testing.T) {
 	f.CredentialsStore = "mock"
 
 	s := &nativeStore{
-		commandFn: mockCommandFn,
-		fileStore: NewFileStore(f),
+		programFunc: mockCommandFn,
+		fileStore:   NewFileStore(f),
 	}
 	err := s.Erase(invalidServerAddress)
 	if err == nil {
 		t.Fatal("expected error, got nil")
 	}
 
-	if err.Error() != "error erasing credentials" {
-		t.Fatalf("expected `error erasing credentials`, got %v", err)
+	if !strings.Contains(err.Error(), "program failed") {
+		t.Fatalf("expected `program failed`, got %v", err)
 	}
 }

+ 0 - 28
cliconfig/credentials/shell_command.go

@@ -1,28 +0,0 @@
-package credentials
-
-import (
-	"io"
-	"os/exec"
-)
-
-func shellCommandFn(storeName string) func(args ...string) command {
-	name := remoteCredentialsPrefix + storeName
-	return func(args ...string) command {
-		return &shell{cmd: exec.Command(name, args...)}
-	}
-}
-
-// shell invokes shell commands to talk with a remote credentials helper.
-type shell struct {
-	cmd *exec.Cmd
-}
-
-// Output returns responses from the remote credentials helper.
-func (s *shell) Output() ([]byte, error) {
-	return s.cmd.Output()
-}
-
-// Input sets the input to send to a remote credentials helper.
-func (s *shell) Input(in io.Reader) {
-	s.cmd.Stdin = in
-}

+ 1 - 1
hack/.vendor-helpers.sh

@@ -108,7 +108,7 @@ clean() {
 				go list -e -tags "$buildTags" -f '{{join .Deps "\n"}}' "${packages[@]}"
 				go list -e -tags "$buildTags" -f '{{join .TestImports "\n"}}' "${packages[@]}"
 			done
-		done | grep -vE "^${PROJECT}" | sort -u
+		done | grep -vE "^${PROJECT}/" | sort -u
 	) )
 	imports=( $(go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' "${imports[@]}") )
 	unset IFS

+ 3 - 0
hack/vendor.sh

@@ -131,6 +131,9 @@ clone git golang.org/x/oauth2 2baa8a1b9338cf13d9eeb27696d761155fa480be https://g
 clone git google.golang.org/api dc6d2353af16e2a2b0ff6986af051d473a4ed468 https://code.googlesource.com/google-api-go-client
 clone git google.golang.org/cloud dae7e3d993bc3812a2185af60552bb6b847e52a0 https://code.googlesource.com/gocloud
 
+# native credentials
+clone git github.com/docker/docker-credential-helpers v0.3.0
+
 # containerd
 clone git github.com/docker/containerd 57b7c3da915ebe943bd304c00890959b191e5264
 

+ 20 - 0
vendor/src/github.com/docker/docker-credential-helpers/LICENSE

@@ -0,0 +1,20 @@
+Copyright (c) 2016 David Calavera
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 70 - 0
vendor/src/github.com/docker/docker-credential-helpers/client/client.go

@@ -0,0 +1,70 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/docker/docker-credential-helpers/credentials"
+)
+
+// Store uses an external program to save credentials.
+func Store(program ProgramFunc, credentials *credentials.Credentials) error {
+	cmd := program("store")
+
+	buffer := new(bytes.Buffer)
+	if err := json.NewEncoder(buffer).Encode(credentials); err != nil {
+		return err
+	}
+	cmd.Input(buffer)
+
+	out, err := cmd.Output()
+	if err != nil {
+		t := strings.TrimSpace(string(out))
+		return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
+	}
+
+	return nil
+}
+
+// Get executes an external program to get the credentials from a native store.
+func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
+	cmd := program("get")
+	cmd.Input(strings.NewReader(serverURL))
+
+	out, err := cmd.Output()
+	if err != nil {
+		t := strings.TrimSpace(string(out))
+
+		if credentials.IsErrCredentialsNotFoundMessage(t) {
+			return nil, credentials.NewErrCredentialsNotFound()
+		}
+
+		return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
+	}
+
+	resp := &credentials.Credentials{
+		ServerURL: serverURL,
+	}
+
+	if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
+// Erase executes a program to remove the server credentails from the native store.
+func Erase(program ProgramFunc, serverURL string) error {
+	cmd := program("erase")
+	cmd.Input(strings.NewReader(serverURL))
+
+	out, err := cmd.Output()
+	if err != nil {
+		t := strings.TrimSpace(string(out))
+		return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
+	}
+
+	return nil
+}

+ 37 - 0
vendor/src/github.com/docker/docker-credential-helpers/client/command.go

@@ -0,0 +1,37 @@
+package client
+
+import (
+	"io"
+	"os/exec"
+)
+
+// Program is an interface to execute external programs.
+type Program interface {
+	Output() ([]byte, error)
+	Input(in io.Reader)
+}
+
+// ProgramFunc is a type of function that initializes programs based on arguments.
+type ProgramFunc func(args ...string) Program
+
+// NewShellProgramFunc creates programs that are executed in a Shell.
+func NewShellProgramFunc(name string) ProgramFunc {
+	return func(args ...string) Program {
+		return &Shell{cmd: exec.Command(name, args...)}
+	}
+}
+
+// Shell invokes shell commands to talk with a remote credentials helper.
+type Shell struct {
+	cmd *exec.Cmd
+}
+
+// Output returns responses from the remote credentials helper.
+func (s *Shell) Output() ([]byte, error) {
+	return s.cmd.Output()
+}
+
+// Input sets the input to send to a remote credentials helper.
+func (s *Shell) Input(in io.Reader) {
+	s.cmd.Stdin = in
+}

+ 129 - 0
vendor/src/github.com/docker/docker-credential-helpers/credentials/credentials.go

@@ -0,0 +1,129 @@
+package credentials
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+)
+
+// Credentials holds the information shared between docker and the credentials store.
+type Credentials struct {
+	ServerURL string
+	Username  string
+	Secret    string
+}
+
+// Serve initializes the credentials helper and parses the action argument.
+// This function is designed to be called from a command line interface.
+// It uses os.Args[1] as the key for the action.
+// It uses os.Stdin as input and os.Stdout as output.
+// This function terminates the program with os.Exit(1) if there is an error.
+func Serve(helper Helper) {
+	var err error
+	if len(os.Args) != 2 {
+		err = fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
+	}
+
+	if err == nil {
+		err = HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout)
+	}
+
+	if err != nil {
+		fmt.Fprintf(os.Stdout, "%v\n", err)
+		os.Exit(1)
+	}
+}
+
+// HandleCommand uses a helper and a key to run a credential action.
+func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error {
+	switch key {
+	case "store":
+		return Store(helper, in)
+	case "get":
+		return Get(helper, in, out)
+	case "erase":
+		return Erase(helper, in)
+	}
+	return fmt.Errorf("Unknown credential action `%s`", key)
+}
+
+// Store uses a helper and an input reader to save credentials.
+// The reader must contain the JSON serialization of a Credentials struct.
+func Store(helper Helper, reader io.Reader) error {
+	scanner := bufio.NewScanner(reader)
+
+	buffer := new(bytes.Buffer)
+	for scanner.Scan() {
+		buffer.Write(scanner.Bytes())
+	}
+
+	if err := scanner.Err(); err != nil && err != io.EOF {
+		return err
+	}
+
+	var creds Credentials
+	if err := json.NewDecoder(buffer).Decode(&creds); err != nil {
+		return err
+	}
+
+	return helper.Add(&creds)
+}
+
+// Get retrieves the credentials for a given server url.
+// The reader must contain the server URL to search.
+// The writer is used to write the JSON serialization of the credentials.
+func Get(helper Helper, reader io.Reader, writer io.Writer) error {
+	scanner := bufio.NewScanner(reader)
+
+	buffer := new(bytes.Buffer)
+	for scanner.Scan() {
+		buffer.Write(scanner.Bytes())
+	}
+
+	if err := scanner.Err(); err != nil && err != io.EOF {
+		return err
+	}
+
+	serverURL := strings.TrimSpace(buffer.String())
+
+	username, secret, err := helper.Get(serverURL)
+	if err != nil {
+		return err
+	}
+
+	resp := Credentials{
+		Username: username,
+		Secret:   secret,
+	}
+
+	buffer.Reset()
+	if err := json.NewEncoder(buffer).Encode(resp); err != nil {
+		return err
+	}
+
+	fmt.Fprint(writer, buffer.String())
+	return nil
+}
+
+// Erase removes credentials from the store.
+// The reader must contain the server URL to remove.
+func Erase(helper Helper, reader io.Reader) error {
+	scanner := bufio.NewScanner(reader)
+
+	buffer := new(bytes.Buffer)
+	for scanner.Scan() {
+		buffer.Write(scanner.Bytes())
+	}
+
+	if err := scanner.Err(); err != nil && err != io.EOF {
+		return err
+	}
+
+	serverURL := strings.TrimSpace(buffer.String())
+
+	return helper.Delete(serverURL)
+}

+ 37 - 0
vendor/src/github.com/docker/docker-credential-helpers/credentials/error.go

@@ -0,0 +1,37 @@
+package credentials
+
+// ErrCredentialsNotFound standarizes the not found error, so every helper returns
+// the same message and docker can handle it properly.
+const errCredentialsNotFoundMessage = "credentials not found in native keychain"
+
+// errCredentialsNotFound represents an error
+// raised when credentials are not in the store.
+type errCredentialsNotFound struct{}
+
+// Error returns the standard error message
+// for when the credentials are not in the store.
+func (errCredentialsNotFound) Error() string {
+	return errCredentialsNotFoundMessage
+}
+
+// NewErrCredentialsNotFound creates a new error
+// for when the credentials are not in the store.
+func NewErrCredentialsNotFound() error {
+	return errCredentialsNotFound{}
+}
+
+// IsErrCredentialsNotFound returns true if the error
+// was caused by not having a set of credentials in a store.
+func IsErrCredentialsNotFound(err error) bool {
+	_, ok := err.(errCredentialsNotFound)
+	return ok
+}
+
+// IsErrCredentialsNotFoundMessage returns true if the error
+// was caused by not having a set of credentials in a store.
+//
+// This function helps to check messages returned by an
+// external program via its standard output.
+func IsErrCredentialsNotFoundMessage(err string) bool {
+	return err == errCredentialsNotFoundMessage
+}

+ 12 - 0
vendor/src/github.com/docker/docker-credential-helpers/credentials/helper.go

@@ -0,0 +1,12 @@
+package credentials
+
+// Helper is the interface a credentials store helper must implement.
+type Helper interface {
+	// Add appends credentials to the store.
+	Add(*Credentials) error
+	// Delete removes credentials from the store.
+	Delete(serverURL string) error
+	// Get retrieves credentials from the store.
+	// It returns username and secret as strings.
+	Get(serverURL string) (string, string, error)
+}