Merge component 'cli' from git@github.com:docker/cli master
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
70
components/cli/cli/command/image/build_test.go
Normal file
70
components/cli/cli/command/image/build_test.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
88
components/cli/cli/command/swarm/ca_test.go
Normal file
88
components/cli/cli/command/swarm/ca_test.go
Normal 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)
|
||||
}
|
||||
@ -25,7 +25,7 @@ func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
newUpdateCommand(dockerCli),
|
||||
newLeaveCommand(dockerCli),
|
||||
newUnlockCommand(dockerCli),
|
||||
newRotateCACommand(dockerCli),
|
||||
newCACommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
28
components/cli/cli/command/task/client_test.go
Normal file
28
components/cli/cli/command/task/client_test.go
Normal 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
|
||||
}
|
||||
150
components/cli/cli/command/task/print_test.go
Normal file
150
components/cli/cli/command/task/print_test.go
Normal 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))
|
||||
}
|
||||
1
components/cli/cli/command/task/testdata/task-print-with-global-service.golden
vendored
Normal file
1
components/cli/cli/command/task/testdata/task-print-with-global-service.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-id-foo.node-id-bar
|
||||
3
components/cli/cli/command/task/testdata/task-print-with-indentation.golden
vendored
Normal file
3
components/cli/cli/command/task/testdata/task-print-with-indentation.golden
vendored
Normal 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
|
||||
1
components/cli/cli/command/task/testdata/task-print-with-no-trunc-option.golden
vendored
Normal file
1
components/cli/cli/command/task/testdata/task-print-with-no-trunc-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
id-foo-yov6omdek8fg3k5stosyp2m50
|
||||
1
components/cli/cli/command/task/testdata/task-print-with-quiet-option.golden
vendored
Normal file
1
components/cli/cli/command/task/testdata/task-print-with-quiet-option.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
id-foo
|
||||
1
components/cli/cli/command/task/testdata/task-print-with-replicated-service.golden
vendored
Normal file
1
components/cli/cli/command/task/testdata/task-print-with-replicated-service.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-id-foo.1
|
||||
1
components/cli/cli/command/task/testdata/task-print-with-resolution.golden
vendored
Normal file
1
components/cli/cli/command/task/testdata/task-print-with-resolution.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
service-name-foo.1 node-name-bar
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 ""
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
3
components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go
generated
vendored
3
components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_go17.go
generated
vendored
@ -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
|
||||
|
||||
2
components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_other.go
generated
vendored
2
components/cli/vendor/github.com/docker/go-connections/tlsconfig/certpool_other.go
generated
vendored
@ -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
|
||||
}
|
||||
|
||||
2
components/cli/vendor/github.com/docker/go-connections/tlsconfig/config.go
generated
vendored
2
components/cli/vendor/github.com/docker/go-connections/tlsconfig/config.go
generated
vendored
@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user