Merge component 'cli' from git@github.com:docker/cli master

This commit is contained in:
Andrew Hsu
2017-07-03 18:51:34 +00:00
48 changed files with 928 additions and 599 deletions

View File

@ -1,7 +1,6 @@
package command
import (
"fmt"
"io"
"net/http"
"os"
@ -10,11 +9,9 @@ import (
"github.com/docker/cli/cli"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
cliflags "github.com/docker/cli/cli/flags"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
@ -39,7 +36,7 @@ type Cli interface {
In() *InStream
SetIn(in *InStream)
ConfigFile() *configfile.ConfigFile
CredentialsStore(serverAddress string) credentials.Store
ServerInfo() ServerInfo
}
// DockerCli is an instance the docker command line client.
@ -104,59 +101,10 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
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
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
cli.configFile = LoadDefaultConfigFile(cli.err)
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
var err error
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}
}
// 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
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)

View File

@ -77,16 +77,20 @@ func (o buildOptions) contextFromStdin() bool {
return o.context == "-"
}
// NewBuildCommand creates a new `docker build` command
func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
func newBuildOptions() buildOptions {
ulimits := make(map[string]*units.Ulimit)
options := buildOptions{
return buildOptions{
tags: opts.NewListOpts(validateTag),
buildArgs: opts.NewListOpts(opts.ValidateEnv),
ulimits: opts.NewUlimitOpt(&ulimits),
labels: opts.NewListOpts(opts.ValidateEnv),
extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
}
}
// NewBuildCommand creates a new `docker build` command
func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
options := newBuildOptions()
cmd := &cobra.Command{
Use: "build [OPTIONS] PATH | URL | -",
@ -159,7 +163,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
}
// nolint: gocyclo
func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
func runBuild(dockerCli command.Cli, options buildOptions) error {
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
@ -237,13 +241,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
}
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
compression := archive.Uncompressed
if options.compress {
compression = archive.Gzip
}
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
Compression: compression,
ExcludePatterns: excludes,
})
if err != nil {
@ -292,6 +290,13 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
}
}
if options.compress {
buildCtx, err = build.Compress(buildCtx)
if err != nil {
return err
}
}
// Setup an upload progress bar
progressOutput := streamformatter.NewProgressOutput(progBuff)
if !dockerCli.Out().IsTerminal() {
@ -336,7 +341,8 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
body = buildCtx
}
authConfigs, _ := dockerCli.GetAllCredentials()
configFile := dockerCli.ConfigFile()
authConfigs, _ := configFile.GetAllCredentials()
buildOptions := types.ImageBuildOptions{
Memory: options.memory.Value(),
MemorySwap: options.memorySwap.Value(),
@ -356,7 +362,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
Dockerfile: relDockerfile,
ShmSize: options.shmSize.Value(),
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,
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom,

View File

@ -19,6 +19,7 @@ import (
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/pools"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
@ -375,3 +376,27 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
})
return buildCtx, randomName, nil
}
// Compress the build context for sending to the API
func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
pipeReader, pipeWriter := io.Pipe()
go func() {
compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip)
if err != nil {
pipeWriter.CloseWithError(err)
}
defer buildCtx.Close()
if _, err := pools.Copy(compressWriter, buildCtx); err != nil {
pipeWriter.CloseWithError(
errors.Wrap(err, "failed to compress context"))
compressWriter.Close()
return
}
compressWriter.Close()
pipeWriter.Close()
}()
return pipeReader, nil
}

View File

@ -26,11 +26,11 @@ import (
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")
}
func trySession(dockerCli *command.DockerCli, contextDir string) (*session.Session, error) {
func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
var s *session.Session
if isSessionSupported(dockerCli) {
sharedKey, err := getBuildSharedKey(contextDir)

View File

@ -0,0 +1,70 @@
package image
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
dest, err := ioutil.TempDir("", "test-build-compress-dest")
require.NoError(t, err)
defer os.RemoveAll(dest)
var dockerfileName string
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
buffer := new(bytes.Buffer)
tee := io.TeeReader(context, buffer)
assert.NoError(t, archive.Untar(tee, dest, nil))
dockerfileName = options.Dockerfile
header := buffer.Bytes()[:10]
assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild}, ioutil.Discard)
dockerfile := bytes.NewBufferString(`
FROM alpine:3.6
COPY foo /
`)
cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile)))
dir, err := ioutil.TempDir("", "test-build-compress")
require.NoError(t, err)
defer os.RemoveAll(dir)
ioutil.WriteFile(filepath.Join(dir, "foo"), []byte("some content"), 0644)
options := newBuildOptions()
options.compress = true
options.dockerfileName = "-"
options.context = dir
err = runBuild(cli, options)
require.NoError(t, err)
files, err := ioutil.ReadDir(dest)
require.NoError(t, err)
actual := []string{}
for _, fileInfo := range files {
actual = append(actual, fileInfo.Name())
}
sort.Strings(actual)
assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual)
}

View File

@ -27,6 +27,7 @@ type fakeClient struct {
imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error)
imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error)
}
func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error {
@ -114,3 +115,10 @@ func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.Hist
}
return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil
}
func (cli *fakeClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
if cli.imageBuildFunc != nil {
return cli.imageBuildFunc(ctx, context, options)
}
return types.ImageBuildResponse{Body: ioutil.NopCloser(strings.NewReader(""))}, nil
}

View File

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

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
@ -41,7 +42,9 @@ func TestNewPullCommandErrors(t *testing.T) {
}
for _, tc := range testCases {
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.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -64,7 +67,9 @@ func TestNewPullCommandSuccess(t *testing.T) {
}
for _, tc := range testCases {
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.SetArgs(tc.args)
err := cmd.Execute()

View File

@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
@ -47,7 +48,9 @@ func TestNewPushCommandErrors(t *testing.T) {
}
for _, tc := range testCases {
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.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -66,11 +69,13 @@ func TestNewPushCommandSuccess(t *testing.T) {
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := NewPushCommand(test.NewFakeCli(&fakeClient{
cli := test.NewFakeCli(&fakeClient{
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
}, buf))
}, buf)
cli.SetConfigfile(configfile.New("filename"))
cmd := NewPushCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
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)
}
a, _ := cli.CredentialsStore(configKey).Get(configKey)
a, _ := cli.ConfigFile().GetAuthConfig(configKey)
return a
}
@ -85,7 +85,7 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR
serverAddress = registry.ConvertToHostname(serverAddress)
}
authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress)
authconfig, err := cli.ConfigFile().GetAuthConfig(serverAddress)
if err != nil {
return authconfig, err
}

View File

@ -71,7 +71,7 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
authConfig.Password = ""
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)
}

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

View File

@ -129,6 +129,11 @@ func ServiceProgress(ctx context.Context, client client.APIClient, serviceID str
}
}
if converged && time.Since(convergedAt) >= monitor {
progressOut.WriteProgress(progress.Progress{
ID: "verify",
Action: "Service converged",
})
return nil
}

View File

@ -3,23 +3,22 @@ package swarm
import (
"fmt"
"io"
"strings"
"golang.org/x/net/context"
"io/ioutil"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/swarm/progress"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/context"
)
type caOptions struct {
swarmOptions
swarmCAOptions
rootCACert PEMFile
rootCAKey PEMFile
rotate bool
@ -27,21 +26,21 @@ type caOptions struct {
quiet bool
}
func newRotateCACommand(dockerCli command.Cli) *cobra.Command {
func newCACommand(dockerCli command.Cli) *cobra.Command {
opts := caOptions{}
cmd := &cobra.Command{
Use: "ca [OPTIONS]",
Short: "Manage root CA",
Short: "Display and rotate the root CA",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runRotateCA(dockerCli, cmd.Flags(), opts)
return runCA(dockerCli, cmd.Flags(), opts)
},
Tags: map[string]string{"version": "1.30"},
}
flags := cmd.Flags()
addSwarmCAFlags(flags, &opts.swarmOptions)
addSwarmCAFlags(flags, &opts.swarmCAOptions)
flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate the swarm CA - if no certificate or key are provided, new ones will be generated")
flags.Var(&opts.rootCACert, flagCACert, "Path to the PEM-formatted root CA certificate to use for the new cluster")
flags.Var(&opts.rootCAKey, flagCAKey, "Path to the PEM-formatted root CA key to use for the new cluster")
@ -51,7 +50,7 @@ func newRotateCACommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error {
func runCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error {
client := dockerCli.Client()
ctx := context.Background()
@ -66,31 +65,10 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
return fmt.Errorf("`--%s` flag requires the `--rotate` flag to update the CA", f)
}
}
if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" {
fmt.Fprintln(dockerCli.Out(), "No CA information available")
} else {
fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot))
}
return nil
}
genRootCA := true
spec := &swarmInspect.Spec
opts.mergeSwarmSpec(spec, flags) // updates the spec given the cert expiry or external CA flag
if flags.Changed(flagCACert) {
spec.CAConfig.SigningCACert = opts.rootCACert.Contents()
genRootCA = false
}
if flags.Changed(flagCAKey) {
spec.CAConfig.SigningCAKey = opts.rootCAKey.Contents()
genRootCA = false
}
if genRootCA {
spec.CAConfig.ForceRotate++
spec.CAConfig.SigningCACert = ""
spec.CAConfig.SigningCAKey = ""
return displayTrustRoot(dockerCli.Out(), swarmInspect)
}
updateSwarmSpec(&swarmInspect.Spec, flags, opts)
if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil {
return err
}
@ -98,7 +76,29 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
if opts.detach {
return nil
}
return attach(ctx, dockerCli, opts)
}
func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) {
opts.mergeSwarmSpecCAFlags(spec, flags)
caCert := opts.rootCACert.Contents()
caKey := opts.rootCAKey.Contents()
if caCert != "" {
spec.CAConfig.SigningCACert = caCert
}
if caKey != "" {
spec.CAConfig.SigningCAKey = caKey
}
if caKey == "" && caCert == "" {
spec.CAConfig.ForceRotate++
spec.CAConfig.SigningCACert = ""
spec.CAConfig.SigningCAKey = ""
}
}
func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error {
client := dockerCli.Client()
errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe()
@ -111,7 +111,7 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
return <-errChan
}
err = jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
if err == nil {
err = <-errChan
}
@ -119,15 +119,17 @@ func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) er
return err
}
swarmInspect, err = client.SwarmInspect(ctx)
swarmInspect, err := client.SwarmInspect(ctx)
if err != nil {
return err
}
return displayTrustRoot(dockerCli.Out(), swarmInspect)
}
if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" {
fmt.Fprintln(dockerCli.Out(), "No CA information available")
} else {
fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot))
func displayTrustRoot(out io.Writer, info swarm.Swarm) error {
if info.ClusterInfo.TLSInfo.TrustRoot == "" {
return errors.New("No CA information available")
}
fmt.Fprintln(out, strings.TrimSpace(info.ClusterInfo.TLSInfo.TrustRoot))
return nil
}

View File

@ -0,0 +1,88 @@
package swarm
import (
"bytes"
"testing"
"time"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func swarmSpecWithFullCAConfig() *swarm.Spec {
return &swarm.Spec{
CAConfig: swarm.CAConfig{
SigningCACert: "cacert",
SigningCAKey: "cakey",
ForceRotate: 1,
NodeCertExpiry: time.Duration(200),
ExternalCAs: []*swarm.ExternalCA{
{
URL: "https://example.com/ca",
Protocol: swarm.ExternalCAProtocolCFSSL,
CACert: "excacert",
},
},
},
}
}
func TestDisplayTrustRootNoRoot(t *testing.T) {
buffer := new(bytes.Buffer)
err := displayTrustRoot(buffer, swarm.Swarm{})
assert.EqualError(t, err, "No CA information available")
}
func TestDisplayTrustRoot(t *testing.T) {
buffer := new(bytes.Buffer)
trustRoot := "trustme"
err := displayTrustRoot(buffer, swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{
TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot},
},
})
require.NoError(t, err)
assert.Equal(t, trustRoot+"\n", buffer.String())
}
func TestUpdateSwarmSpecDefaultRotate(t *testing.T) {
spec := swarmSpecWithFullCAConfig()
flags := newCACommand(nil).Flags()
updateSwarmSpec(spec, flags, caOptions{})
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.ForceRotate = 2
expected.CAConfig.SigningCACert = ""
expected.CAConfig.SigningCAKey = ""
assert.Equal(t, expected, spec)
}
func TestUpdateSwarmSpecPartial(t *testing.T) {
spec := swarmSpecWithFullCAConfig()
flags := newCACommand(nil).Flags()
updateSwarmSpec(spec, flags, caOptions{
rootCACert: PEMFile{contents: "cacert"},
})
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = "cacert"
assert.Equal(t, expected, spec)
}
func TestUpdateSwarmSpecFullFlags(t *testing.T) {
flags := newCACommand(nil).Flags()
flags.Lookup(flagCertExpiry).Changed = true
spec := swarmSpecWithFullCAConfig()
updateSwarmSpec(spec, flags, caOptions{
rootCACert: PEMFile{contents: "cacert"},
rootCAKey: PEMFile{contents: "cakey"},
swarmCAOptions: swarmCAOptions{nodeCertExpiry: 3 * time.Minute},
})
expected := swarmSpecWithFullCAConfig()
expected.CAConfig.SigningCACert = "cacert"
expected.CAConfig.SigningCAKey = "cakey"
expected.CAConfig.NodeCertExpiry = 3 * time.Minute
assert.Equal(t, expected, spec)
}

View File

@ -25,7 +25,7 @@ func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
newUpdateCommand(dockerCli),
newLeaveCommand(dockerCli),
newUnlockCommand(dockerCli),
newRotateCACommand(dockerCli),
newCACommand(dockerCli),
)
return cmd
}

View File

@ -36,10 +36,9 @@ const (
)
type swarmOptions struct {
swarmCAOptions
taskHistoryLimit int64
dispatcherHeartbeat time.Duration
nodeCertExpiry time.Duration
externalCA ExternalCAOption
maxSnapshots uint64
snapshotInterval uint64
autolock bool
@ -216,7 +215,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
return &externalCA, nil
}
func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmOptions) {
func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmCAOptions) {
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, 90*24*time.Hour, "Validity period for node certificates (ns|us|ms|s|m|h)")
flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
}
@ -228,7 +227,7 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"})
flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots")
flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"})
addSwarmCAFlags(flags, opts)
addSwarmCAFlags(flags, &opts.swarmCAOptions)
}
func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) {
@ -238,12 +237,6 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet)
if flags.Changed(flagDispatcherHeartbeat) {
spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat
}
if flags.Changed(flagCertExpiry) {
spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
}
if flags.Changed(flagExternalCA) {
spec.CAConfig.ExternalCAs = opts.externalCA.Value()
}
if flags.Changed(flagMaxSnapshots) {
spec.Raft.KeepOldSnapshots = &opts.maxSnapshots
}
@ -253,6 +246,21 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet)
if flags.Changed(flagAutolock) {
spec.EncryptionConfig.AutoLockManagers = opts.autolock
}
opts.mergeSwarmSpecCAFlags(spec, flags)
}
type swarmCAOptions struct {
nodeCertExpiry time.Duration
externalCA ExternalCAOption
}
func (opts *swarmCAOptions) mergeSwarmSpecCAFlags(spec *swarm.Spec, flags *pflag.FlagSet) {
if flags.Changed(flagCertExpiry) {
spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
}
if flags.Changed(flagExternalCA) {
spec.CAConfig.ExternalCAs = opts.externalCA.Value()
}
}
func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec {

View File

@ -1,7 +1,9 @@
package system
import (
"bytes"
"fmt"
"text/template"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -44,29 +46,14 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
const (
warning = `WARNING! This will remove:
- all stopped containers
- all volumes not used by at least one container
- all networks not used by at least one container
%s
- all build cache
const confirmationTemplate = `WARNING! This will remove:
{{- range $_, $warning := . }}
- {{ $warning }}
{{- end }}
Are you sure you want to continue?`
danglingImageDesc = "- all dangling images"
allImageDesc = `- all images without at least one container associated to them`
)
func runPrune(dockerCli command.Cli, options pruneOptions) error {
var message string
if options.all {
message = fmt.Sprintf(warning, allImageDesc)
} else {
message = fmt.Sprintf(warning, danglingImageDesc)
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) {
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), confirmationMessage(options)) {
return nil
}
@ -109,3 +96,26 @@ func runPrune(dockerCli command.Cli, options pruneOptions) error {
return nil
}
// confirmationMessage constructs a confirmation message that depends on the cli options.
func confirmationMessage(options pruneOptions) string {
t := template.Must(template.New("confirmation message").Parse(confirmationTemplate))
warnings := []string{
"all stopped containers",
"all networks not used by at least one container",
}
if options.pruneVolumes {
warnings = append(warnings, "all volumes not used by at least one container")
}
if options.all {
warnings = append(warnings, "all images without at least one container associated to them")
} else {
warnings = append(warnings, "all dangling images")
}
warnings = append(warnings, "all build cache")
var buffer bytes.Buffer
t.Execute(&buffer, &warnings)
return buffer.String()
}

View File

@ -0,0 +1,28 @@
package task
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.APIClient
nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error)
serviceInspectWithRaw func(ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error)
}
func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) {
if cli.nodeInspectWithRaw != nil {
return cli.nodeInspectWithRaw(ref)
}
return swarm.Node{}, nil, nil
}
func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) {
if cli.serviceInspectWithRaw != nil {
return cli.serviceInspectWithRaw(ref, options)
}
return swarm.Service{}, nil, nil
}

View File

@ -0,0 +1,150 @@
package task
import (
"bytes"
"testing"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/cli/cli/internal/test"
"golang.org/x/net/context"
// Import builders to get the builder function as package function
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
func TestTaskPrintWithQuietOption(t *testing.T) {
quiet := true
trunc := false
noResolve := true
buf := new(bytes.Buffer)
apiClient := &fakeClient{}
cli := test.NewFakeCli(apiClient, buf)
tasks := []swarm.Task{
*Task(TaskID("id-foo")),
}
err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, formatter.TableFormatKey)
assert.NoError(t, err)
actual := buf.String()
expected := golden.Get(t, []byte(actual), "task-print-with-quiet-option.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestTaskPrintWithNoTruncOption(t *testing.T) {
quiet := false
trunc := false
noResolve := true
buf := new(bytes.Buffer)
apiClient := &fakeClient{}
cli := test.NewFakeCli(apiClient, buf)
tasks := []swarm.Task{
*Task(TaskID("id-foo-yov6omdek8fg3k5stosyp2m50")),
}
err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .ID }}")
assert.NoError(t, err)
actual := buf.String()
expected := golden.Get(t, []byte(actual), "task-print-with-no-trunc-option.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestTaskPrintWithGlobalService(t *testing.T) {
quiet := false
trunc := false
noResolve := true
buf := new(bytes.Buffer)
apiClient := &fakeClient{}
cli := test.NewFakeCli(apiClient, buf)
tasks := []swarm.Task{
*Task(TaskServiceID("service-id-foo"), TaskNodeID("node-id-bar"), TaskSlot(0)),
}
err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }}")
assert.NoError(t, err)
actual := buf.String()
expected := golden.Get(t, []byte(actual), "task-print-with-global-service.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestTaskPrintWithReplicatedService(t *testing.T) {
quiet := false
trunc := false
noResolve := true
buf := new(bytes.Buffer)
apiClient := &fakeClient{}
cli := test.NewFakeCli(apiClient, buf)
tasks := []swarm.Task{
*Task(TaskServiceID("service-id-foo"), TaskSlot(1)),
}
err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }}")
assert.NoError(t, err)
actual := buf.String()
expected := golden.Get(t, []byte(actual), "task-print-with-replicated-service.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestTaskPrintWithIndentation(t *testing.T) {
quiet := false
trunc := false
noResolve := false
buf := new(bytes.Buffer)
apiClient := &fakeClient{
serviceInspectWithRaw: func(ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) {
return *Service(ServiceName("service-name-foo")), nil, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
}
cli := test.NewFakeCli(apiClient, buf)
tasks := []swarm.Task{
*Task(
TaskID("id-foo"),
TaskServiceID("service-id-foo"),
TaskNodeID("id-node"),
WithTaskSpec(TaskImage("myimage:mytag")),
TaskDesiredState(swarm.TaskStateReady),
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
),
*Task(
TaskID("id-bar"),
TaskServiceID("service-id-foo"),
TaskNodeID("id-node"),
WithTaskSpec(TaskImage("myimage:mytag")),
TaskDesiredState(swarm.TaskStateReady),
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
),
}
err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, formatter.TableFormatKey)
assert.NoError(t, err)
actual := buf.String()
expected := golden.Get(t, []byte(actual), "task-print-with-indentation.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestTaskPrintWithResolution(t *testing.T) {
quiet := false
trunc := false
noResolve := false
buf := new(bytes.Buffer)
apiClient := &fakeClient{
serviceInspectWithRaw: func(ref string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) {
return *Service(ServiceName("service-name-foo")), nil, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
}
cli := test.NewFakeCli(apiClient, buf)
tasks := []swarm.Task{
*Task(TaskServiceID("service-id-foo"), TaskSlot(1)),
}
err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }} {{ .Node }}")
assert.NoError(t, err)
actual := buf.String()
expected := golden.Get(t, []byte(actual), "task-print-with-resolution.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -0,0 +1 @@
service-id-foo.node-id-bar

View File

@ -0,0 +1,3 @@
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
id-foo service-name-foo.1 myimage:mytag node-name-bar Ready Failed 2 hours ago
id-bar \_ service-name-foo.1 myimage:mytag node-name-bar Ready Failed 2 hours ago

View File

@ -0,0 +1 @@
id-foo-yov6omdek8fg3k5stosyp2m50

View File

@ -0,0 +1 @@
id-foo

View File

@ -0,0 +1 @@
service-id-foo.1

View File

@ -0,0 +1 @@
service-name-foo.1 node-name-bar

View File

@ -39,7 +39,7 @@ services:
cpus: '0.0001'
memory: 20M
restart_policy:
condition: on_failure
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s

View File

@ -683,7 +683,7 @@ func TestFullExample(t *testing.T) {
},
},
RestartPolicy: &types.RestartPolicy{
Condition: "on_failure",
Condition: "on-failure",
Delay: durationPtr(5 * time.Second),
MaxAttempts: uint64Ptr(3),
Window: durationPtr(2 * time.Minute),

View File

@ -1,11 +1,13 @@
package config
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/homedir"
"github.com/pkg/errors"
@ -38,15 +40,6 @@ func SetDir(dir string) {
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
// a non-nested reader
func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) {
@ -75,46 +68,53 @@ func Load(configDir string) (*configfile.ConfigFile, error) {
configDir = Dir()
}
configFile := configfile.ConfigFile{
AuthConfigs: make(map[string]types.AuthConfig),
Filename: filepath.Join(configDir, ConfigFileName),
}
filename := filepath.Join(configDir, ConfigFileName)
configFile := configfile.New(filename)
// Try happy path first - latest config file
if _, err := os.Stat(configFile.Filename); err == nil {
file, err := os.Open(configFile.Filename)
if _, err := os.Stat(filename); err == nil {
file, err := os.Open(filename)
if err != nil {
return &configFile, errors.Errorf("%s - %v", configFile.Filename, err)
return configFile, errors.Errorf("%s - %v", filename, err)
}
defer file.Close()
err = configFile.LoadFromReader(file)
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) {
// if file is there but we can't stat it for any reason other
// 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
confFile := filepath.Join(homedir.Get(), oldConfigfile)
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)
if err != nil {
return &configFile, errors.Errorf("%s - %v", confFile, err)
return configFile, errors.Errorf("%s - %v", confFile, err)
}
defer file.Close()
err = configFile.LegacyLoadFromReader(file)
if err != nil {
return &configFile, errors.Errorf("%s - %v", confFile, err)
return configFile, errors.Errorf("%s - %v", confFile, err)
}
if configFile.HTTPHeaders == nil {
configFile.HTTPHeaders = map[string]string{}
}
return &configFile, nil
return configFile, nil
}
// LoadDefaultConfigFile attempts to load the default config file and returns
// 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
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
@ -8,28 +9,34 @@ import (
"testing"
"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/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmptyConfigDir(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpHome)
func setupConfigDir(t *testing.T) (string, func()) {
tmpdir, err := ioutil.TempDir("", "config-test")
require.NoError(t, err)
oldDir := Dir()
SetDir(tmpdir)
SetDir(tmpHome)
return tmpdir, func() {
SetDir(oldDir)
os.RemoveAll(tmpdir)
}
}
func TestEmptyConfigDir(t *testing.T) {
tmpHome, cleanup := setupConfigDir(t)
defer cleanup()
config, err := Load("")
if err != nil {
t.Fatalf("Failed loading on empty config dir: %q", err)
}
require.NoError(t, err)
expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName)
if config.Filename != expectedConfigFilename {
t.Fatalf("Expected config filename %s, got %s", expectedConfigFilename, config.Filename)
}
assert.Equal(t, expectedConfigFilename, config.Filename)
// Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -37,15 +44,11 @@ func TestEmptyConfigDir(t *testing.T) {
func TestMissingFile(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on missing file: %q", err)
}
require.NoError(t, err)
// Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -53,17 +56,13 @@ func TestMissingFile(t *testing.T) {
func TestSaveFileToDirs(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
tmpHome += "/.docker"
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on missing file: %q", err)
}
require.NoError(t, err)
// Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -71,38 +70,28 @@ func TestSaveFileToDirs(t *testing.T) {
func TestEmptyFile(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
if err := ioutil.WriteFile(fn, []byte(""), 0600); err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fn, []byte(""), 0600)
require.NoError(t, err)
_, err = Load(tmpHome)
if err == nil {
t.Fatalf("Was supposed to fail")
}
testutil.ErrorContains(t, err, "EOF")
}
func TestEmptyJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
if err := ioutil.WriteFile(fn, []byte("{}"), 0600); err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fn, []byte("{}"), 0600)
require.NoError(t, err)
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
// Now save it and make sure it shows up in new form
saveConfigAndValidateNewFormat(t, config, tmpHome)
@ -118,9 +107,7 @@ email`: "Invalid auth configuration file",
}
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
homeKey := homedir.Key()
@ -131,27 +118,17 @@ email`: "Invalid auth configuration file",
for content, expectedError := range invalids {
fn := filepath.Join(tmpHome, oldConfigfile)
if err := ioutil.WriteFile(fn, []byte(content), 0600); err != nil {
t.Fatal(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 := ioutil.WriteFile(fn, []byte(content), 0600)
require.NoError(t, err)
_, err = Load(tmpHome)
testutil.ErrorContains(t, err, expectedError)
}
}
func TestOldValidAuth(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
homeKey := homedir.Key()
@ -163,14 +140,11 @@ func TestOldValidAuth(t *testing.T) {
fn := filepath.Join(tmpHome, oldConfigfile)
js := `username = am9lam9lOmhlbGxv
email = user@example.com`
if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fn, []byte(js), 0600)
require.NoError(t, err)
config, err := Load(tmpHome)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// defaultIndexserver is 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 {
t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr)
}
assert.Equal(t, expConfStr, configStr)
}
func TestOldJSONInvalid(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
homeKey := homedir.Key()
@ -222,9 +192,7 @@ func TestOldJSONInvalid(t *testing.T) {
func TestOldJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
homeKey := homedir.Key()
@ -240,9 +208,7 @@ func TestOldJSON(t *testing.T) {
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" {
@ -268,9 +234,7 @@ func TestOldJSON(t *testing.T) {
func TestNewJSON(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
@ -280,9 +244,7 @@ func TestNewJSON(t *testing.T) {
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" {
@ -307,9 +269,7 @@ func TestNewJSON(t *testing.T) {
func TestNewJSONNoEmail(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
@ -319,9 +279,7 @@ func TestNewJSONNoEmail(t *testing.T) {
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
ac := config.AuthConfigs["https://index.docker.io/v1/"]
if ac.Username != "joejoe" || ac.Password != "hello" {
@ -346,9 +304,7 @@ func TestNewJSONNoEmail(t *testing.T) {
func TestJSONWithPsFormat(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
@ -361,9 +317,7 @@ func TestJSONWithPsFormat(t *testing.T) {
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
@ -379,9 +333,7 @@ func TestJSONWithPsFormat(t *testing.T) {
func TestJSONWithCredentialStore(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
@ -394,9 +346,7 @@ func TestJSONWithCredentialStore(t *testing.T) {
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
if config.CredentialsStore != "crazy-secure-storage" {
t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore)
@ -412,9 +362,7 @@ func TestJSONWithCredentialStore(t *testing.T) {
func TestJSONWithCredentialHelpers(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
@ -427,9 +375,7 @@ func TestJSONWithCredentialHelpers(t *testing.T) {
}
config, err := Load(tmpHome)
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
if config.CredentialHelpers == 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
func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string {
if err := config.Save(); err != nil {
t.Fatalf("Failed to save: %q", err)
}
func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, configDir string) string {
require.NoError(t, config.Save())
buf, err := ioutil.ReadFile(filepath.Join(homeFolder, ConfigFileName))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(buf), `"auths":`) {
t.Fatalf("Should have save in new form: %s", string(buf))
}
buf, err := ioutil.ReadFile(filepath.Join(configDir, ConfigFileName))
require.NoError(t, err)
assert.Contains(t, string(buf), `"auths":`)
return string(buf)
}
func TestConfigDir(t *testing.T) {
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer os.RemoveAll(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) {
js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }`
config, err := LoadFromReader(strings.NewReader(js))
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
ac := config.AuthConfigs["https://index.docker.io/v1/"]
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"}}`
config, err := LegacyLoadFromReader(strings.NewReader(js))
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
ac := config.AuthConfigs["https://index.docker.io/v1/"]
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\"}}"
}`
config, err := LoadFromReader(strings.NewReader(js))
if err != nil {
t.Fatalf("Failed loading on empty json file: %q", err)
}
require.NoError(t, err)
if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` {
t.Fatalf("Unknown ps format: %s\n", config.PsFormat)
@ -546,28 +469,19 @@ func TestJSONSaveWithNoFile(t *testing.T) {
config, err := LoadFromReader(strings.NewReader(js))
require.NoError(t, err)
err = config.Save()
if err == nil {
t.Fatalf("Expected error. File should not have been able to save with no file name.")
}
testutil.ErrorContains(t, err, "with empty filename")
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatalf("Failed to create a temp dir: %q", err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
defer f.Close()
err = config.SaveToWriter(f)
if err != nil {
t.Fatalf("Failed saving to file: %q", err)
}
require.NoError(t, config.SaveToWriter(f))
buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expConfStr := `{
"auths": {
"https://index.docker.io/v1/": {
@ -582,32 +496,23 @@ func TestJSONSaveWithNoFile(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))
require.NoError(t, err)
err = config.Save()
if err == nil {
t.Fatalf("Expected error. File should not have been able to save with no file name.")
}
testutil.ErrorContains(t, err, "with empty filename")
tmpHome, err := ioutil.TempDir("", "config-test")
if err != nil {
t.Fatalf("Failed to create a temp dir: %q", err)
}
require.NoError(t, err)
defer os.RemoveAll(tmpHome)
fn := filepath.Join(tmpHome, ConfigFileName)
f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
defer f.Close()
if err = config.SaveToWriter(f); err != nil {
t.Fatalf("Failed saving to file: %q", err)
}
require.NoError(t, config.SaveToWriter(f))
buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expConfStr := `{
"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)
}
}
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"
"strings"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
@ -53,6 +54,15 @@ type ProxyConfig struct {
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
// auth config information with given directory and populates the receiver object
func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error {
@ -118,6 +128,11 @@ func (configFile *ConfigFile) ContainsAuth() bool {
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
// the given writer
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")
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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeAuth(t *testing.T) {
newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"}
authStr := encodeAuth(newAuthConfig)
decAuthConfig := &types.AuthConfig{}
expected := &types.AuthConfig{}
var err error
decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr)
if err != nil {
t.Fatal(err)
}
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.")
}
expected.Username, expected.Password, err = decodeAuth(authStr)
require.NoError(t, err)
assert.Equal(t, expected, newAuthConfig)
}
func TestProxyConfig(t *testing.T) {
@ -142,3 +134,26 @@ func TestProxyConfigPerHost(t *testing.T) {
}
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 (
"os/exec"
"github.com/docker/cli/cli/config/configfile"
)
// DetectDefaultStore sets the default credentials store
// if the host includes the default store helper program.
func DetectDefaultStore(c *configfile.ConfigFile) {
if c.CredentialsStore != "" {
// user defined
return
// DetectDefaultStore return the default credentials store for the platform if
// the store executable is available.
func DetectDefaultStore(store string) string {
// user defined or no default for platform
if store != "" || defaultCredentialsStore == "" {
return store
}
if defaultCredentialsStore != "" {
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil {
c.CredentialsStore = defaultCredentialsStore
}
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil {
return defaultCredentialsStore
}
return ""
}

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package credentials
import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker/api/types"
@ -22,7 +21,7 @@ type nativeStore struct {
// NewNativeStore creates a new native store that
// 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
return &nativeStore{
programFunc: client.NewShellProgramFunc(name),

View File

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

View File

@ -70,6 +70,13 @@ func TaskDesiredState(state swarm.TaskState) func(*swarm.Task) {
}
}
// TaskSlot sets the task's slot
func TaskSlot(slot int) func(*swarm.Task) {
return func(task *swarm.Task) {
task.Slot = slot
}
}
// WithStatus sets the task status
func WithStatus(statusBuilders ...func(*swarm.TaskStatus)) func(*swarm.Task) {
return func(task *swarm.Task) {

View File

@ -7,7 +7,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/client"
)
@ -19,16 +18,17 @@ type FakeCli struct {
out *command.OutStream
err io.Writer
in *command.InStream
store credentials.Store
server command.ServerInfo
}
// NewFakeCli returns a Cli backed by the fakeCli
func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli {
return &FakeCli{
client: client,
out: command.NewOutStream(out),
err: ioutil.Discard,
in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))),
client: client,
out: command.NewOutStream(out),
err: ioutil.Discard,
in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))),
configfile: configfile.New("configfile"),
}
}
@ -72,10 +72,7 @@ func (c *FakeCli) ConfigFile() *configfile.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
// ServerInfo returns API server information for the server used by this client
func (c *FakeCli) ServerInfo() command.ServerInfo {
return c.server
}

View File

@ -3,6 +3,7 @@
package main
import (
"io/ioutil"
"testing"
"github.com/spf13/cobra"
@ -17,6 +18,7 @@ func TestDaemonCommandHelp(t *testing.T) {
cmd := newDaemonCommand()
cmd.RunE = stubRun
cmd.SetArgs([]string{"--help"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.NoError(t, err)
}
@ -25,6 +27,7 @@ func TestDaemonCommand(t *testing.T) {
cmd := newDaemonCommand()
cmd.RunE = stubRun
cmd.SetArgs([]string{"--containerd", "/foo"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.NoError(t, err)
}

View File

@ -2704,7 +2704,7 @@ _docker_network_connect() {
_docker_network_create() {
case "$prev" in
--aux-address|--gateway|--internal|--ip-range|--ipam-opt|--ipv6|--opt|-o|--subnet)
--aux-address|--gateway|--ip-range|--ipam-opt|--ipv6|--opt|-o|--subnet)
return
;;
--ipam-driver)
@ -2723,7 +2723,7 @@ _docker_network_create() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--attachable --aux-address --driver -d --gateway --help --internal --ip-range --ipam-driver --ipam-opt --ipv6 --label --opt -o --subnet" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--attachable --aux-address --driver -d --gateway --help --ingress --internal --ip-range --ipam-driver --ipam-opt --ipv6 --label --opt -o --subnet" -- "$cur" ) )
;;
esac
}
@ -3039,6 +3039,7 @@ _docker_service_update() {
_docker_service_update_and_create() {
local options_with_args="
--endpoint-mode
--entrypoint
--env -e
--force
--health-cmd
@ -3065,6 +3066,7 @@ _docker_service_update_and_create() {
--rollback-failure-action
--rollback-max-failure-ratio
--rollback-monitor
--rollback-order
--rollback-parallelism
--stop-grace-period
--stop-signal
@ -3072,12 +3074,14 @@ _docker_service_update_and_create() {
--update-failure-action
--update-max-failure-ratio
--update-monitor
--update-order
--update-parallelism
--user -u
--workdir -w
"
local boolean_options="
--detach -d
--help
--no-healthcheck
--read-only
@ -3138,7 +3142,7 @@ _docker_service_update_and_create() {
fi
if [ "$subcommand" = "update" ] ; then
options_with_args="$options_with_args
--arg
--args
--constraint-add
--constraint-rm
--container-label-add
@ -3154,6 +3158,8 @@ _docker_service_update_and_create() {
--host-add
--host-rm
--image
--network-add
--network-rm
--placement-pref-add
--placement-pref-rm
--publish-add
@ -3180,6 +3186,10 @@ _docker_service_update_and_create() {
__docker_complete_image_repos_and_tags
return
;;
--network-add|--network-rm)
__docker_complete_networks
return
;;
--placement-pref-add|--placement-pref-rm)
COMPREPLY=( $( compgen -W "spread" -S = -- "$cur" ) )
__docker_nospace
@ -3240,6 +3250,10 @@ _docker_service_update_and_create() {
COMPREPLY=( $( compgen -W "continue pause rollback" -- "$cur" ) )
return
;;
--update-order|--rollback-order)
COMPREPLY=( $( compgen -W "start-first stop-first" -- "$cur" ) )
return
;;
--user|-u)
__docker_complete_user_group
return
@ -3270,6 +3284,7 @@ _docker_service_update_and_create() {
_docker_swarm() {
local subcommands="
ca
init
join
join-token
@ -3290,6 +3305,24 @@ _docker_swarm() {
esac
}
_docker_swarm_ca() {
case "$prev" in
--ca-cert|--ca-key)
_filedir
return
;;
--cert-expiry|--external-ca)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--ca-cert --ca-key --cert-expiry --detach -d --external-ca --help --quiet -q --rotate" -- "$cur" ) )
;;
esac
}
_docker_swarm_init() {
case "$prev" in
--advertise-addr)
@ -3308,6 +3341,10 @@ _docker_swarm_init() {
--cert-expiry|--dispatcher-heartbeat|--external-ca|--max-snapshots|--snapshot-interval|--task-history-limit)
return
;;
--data-path-addr)
__docker_complete_local_interfaces
return
;;
--listen-addr)
if [[ $cur == *: ]] ; then
COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) )
@ -3321,7 +3358,7 @@ _docker_swarm_init() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--advertise-addr --data-path-addr --autolock --availability --cert-expiry --dispatcher-heartbeat --external-ca --force-new-cluster --help --listen-addr --max-snapshots --snapshot-interval --task-history-limit" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--advertise-addr --autolock --availability --cert-expiry --data-path-addr --dispatcher-heartbeat --external-ca --force-new-cluster --help --listen-addr --max-snapshots --snapshot-interval --task-history-limit" -- "$cur" ) )
;;
esac
}
@ -3337,6 +3374,14 @@ _docker_swarm_join() {
fi
return
;;
--availability)
COMPREPLY=( $( compgen -W "active drain pause" -- "$cur" ) )
return
;;
--data-path-addr)
__docker_complete_local_interfaces
return
;;
--listen-addr)
if [[ $cur == *: ]] ; then
COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) )
@ -3346,10 +3391,6 @@ _docker_swarm_join() {
fi
return
;;
--availability)
COMPREPLY=( $( compgen -W "active drain pause" -- "$cur" ) )
return
;;
--token)
return
;;
@ -3357,7 +3398,7 @@ _docker_swarm_join() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--advertise-addr --data-path-addr --availability --help --listen-addr --token" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--advertise-addr --availability --data-path-addr --help --listen-addr --token" -- "$cur" ) )
;;
*:)
COMPREPLY=( $( compgen -W "2377" -- "${cur##*:}" ) )
@ -4230,13 +4271,16 @@ _docker_system_events() {
destroy
detach
die
disable
disconnect
enable
exec_create
exec_detach
exec_start
export
health_status
import
install
kill
load
mount
@ -4245,6 +4289,7 @@ _docker_system_events() {
pull
push
reload
remove
rename
resize
restart
@ -4270,7 +4315,7 @@ _docker_system_events() {
return
;;
type)
COMPREPLY=( $( compgen -W "container daemon image network volume" -- "${cur##*=}" ) )
COMPREPLY=( $( compgen -W "container daemon image network plugin volume" -- "${cur##*=}" ) )
return
;;
volume)
@ -4322,7 +4367,7 @@ _docker_system_prune() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--all -a --force -f --filter --help" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--all -a --force -f --filter --help --volumes" -- "$cur" ) )
;;
esac
}

View File

@ -30,13 +30,13 @@ Practices](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-pr
## Usage
The [`docker build`](commandline/build.md) command builds an image from
a `Dockerfile` and a *context*. The build's context is the files at a specified
location `PATH` or `URL`. The `PATH` is a directory on your local filesystem.
The `URL` is a Git repository location.
a `Dockerfile` and a *context*. The build's context is the set of files at a
specified location `PATH` or `URL`. The `PATH` is a directory on your local
filesystem. The `URL` is a Git repository location.
A context is processed recursively. So, a `PATH` includes any subdirectories and
the `URL` includes the repository and its submodules. A simple build command
that uses the current directory as context:
the `URL` includes the repository and its submodules. This example shows a
build command that uses the current directory as context:
$ docker build .
Sending build context to Docker daemon 6.51 MB
@ -1328,9 +1328,9 @@ The `WORKDIR` instruction sets the working directory for any `RUN`, `CMD`,
If the `WORKDIR` doesn't exist, it will be created even if it's not used in any
subsequent `Dockerfile` instruction.
It can be used multiple times in the one `Dockerfile`. If a relative path
is provided, it will be relative to the path of the previous `WORKDIR`
instruction. For example:
The `WORKDIR` instruction can be used multiple times in a `Dockerfile`. If a
relative path is provided, it will be relative to the path of the previous
`WORKDIR` instruction. For example:
WORKDIR /a
WORKDIR b

View File

@ -63,11 +63,11 @@ Options:
## Description
Builds Docker images from a Dockerfile and a "context". A build's context is
the files located in the specified `PATH` or `URL`. The build process can refer
to any of the files in the context. For example, your build can use an
[*ADD*](../builder.md#add) instruction to reference a file in the
context.
The `docker build` command builds Docker images from a Dockerfile and a
"context". A build's context is the set of files located in the specified
`PATH` or `URL`. The build process can refer to any of the files in the
context. For example, your build can use a [*COPY*](../builder.md#copy)
instruction to reference a file in the context.
The `URL` parameter can refer to three kinds of resources: Git repositories,
pre-packaged tarball contexts and plain text files.
@ -88,7 +88,7 @@ user credentials, VPN's, and so forth.
Git URLs accept context configuration in their fragment section, separated by a
colon `:`. The first part represents the reference that Git will check out,
this can be either a branch, a tag, or a remote reference. The second part
and can be either a branch, a tag, or a remote reference. The second part
represents a subdirectory inside the repository that will be used as a build
context.

View File

@ -740,6 +740,18 @@ to add multiple lower directory support for OverlayFS. This option should
only be used after verifying this support exists in the kernel. Applying
this option on a kernel without this support will cause failures on mount.
##### `overlay2.size`
Sets the default max size of the container. It is supported only when the
backing fs is `xfs` and mounted with `pquota` mount option. Under these
conditions the user can pass any size less then the backing fs size.
###### Example
```bash
$ sudo dockerd -s overlay2 --storage-opt overlay2.size=1G
```
### Docker runtime execution options
The Docker daemon relies on a

View File

@ -10,7 +10,7 @@ github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621
github.com/docker/docker 050c1bb17bd033e909cb653f5449b683608293d6
github.com/docker/docker-credential-helpers v0.5.1
github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06 #?
github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5
github.com/docker/go-connections 3ede32e2033de7505e6500d6c868c2b9ed9f169d
github.com/docker/go-events 18b43f1bc85d9cdd42c05a6cd2d444c7a200a894
github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1
github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a

View File

@ -5,8 +5,6 @@ package tlsconfig
import (
"crypto/x509"
"runtime"
"github.com/Sirupsen/logrus"
)
// SystemCertPool returns a copy of the system cert pool,
@ -14,7 +12,6 @@ import (
func SystemCertPool() (*x509.CertPool, error) {
certpool, err := x509.SystemCertPool()
if err != nil && runtime.GOOS == "windows" {
logrus.Infof("Unable to use system certificate pool: %v", err)
return x509.NewCertPool(), nil
}
return certpool, err

View File

@ -5,12 +5,10 @@ package tlsconfig
import (
"crypto/x509"
"github.com/Sirupsen/logrus"
)
// SystemCertPool returns an new empty cert pool,
// accessing system cert pool is supported in go 1.7
func SystemCertPool() (*x509.CertPool, error) {
logrus.Warn("Unable to use system certificate pool: requires building with go 1.7 or later")
return x509.NewCertPool(), nil
}

View File

@ -13,7 +13,6 @@ import (
"io/ioutil"
"os"
"github.com/Sirupsen/logrus"
"github.com/pkg/errors"
)
@ -106,7 +105,6 @@ func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) {
if !certPool.AppendCertsFromPEM(pem) {
return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile)
}
logrus.Debugf("Trusting %d certs", len(certPool.Subjects()))
return certPool, nil
}