mirror of https://github.com/docker/cli.git
Add support for configs
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
parent
15b5dda768
commit
db5620026d
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/checkpoint"
|
"github.com/docker/cli/cli/command/checkpoint"
|
||||||
|
"github.com/docker/cli/cli/command/config"
|
||||||
"github.com/docker/cli/cli/command/container"
|
"github.com/docker/cli/cli/command/container"
|
||||||
"github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
"github.com/docker/cli/cli/command/network"
|
"github.com/docker/cli/cli/command/network"
|
||||||
|
@ -26,6 +27,9 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||||
// checkpoint
|
// checkpoint
|
||||||
checkpoint.NewCheckpointCommand(dockerCli),
|
checkpoint.NewCheckpointCommand(dockerCli),
|
||||||
|
|
||||||
|
// config
|
||||||
|
config.NewConfigCommand(dockerCli),
|
||||||
|
|
||||||
// container
|
// container
|
||||||
container.NewContainerCommand(dockerCli),
|
container.NewContainerCommand(dockerCli),
|
||||||
container.NewRunCommand(dockerCli),
|
container.NewRunCommand(dockerCli),
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
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.Client
|
||||||
|
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||||
|
configInspectFunc func(string) (swarm.Config, []byte, error)
|
||||||
|
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||||
|
configRemoveFunc func(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
|
if c.configCreateFunc != nil {
|
||||||
|
return c.configCreateFunc(spec)
|
||||||
|
}
|
||||||
|
return types.ConfigCreateResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||||
|
if c.configInspectFunc != nil {
|
||||||
|
return c.configInspectFunc(id)
|
||||||
|
}
|
||||||
|
return swarm.Config{}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
if c.configListFunc != nil {
|
||||||
|
return c.configListFunc(options)
|
||||||
|
}
|
||||||
|
return []swarm.Config{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) ConfigRemove(ctx context.Context, name string) error {
|
||||||
|
if c.configRemoveFunc != nil {
|
||||||
|
return c.configRemoveFunc(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||||
|
// nolint: interfacer
|
||||||
|
func NewConfigCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage Docker configs",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
|
Tags: map[string]string{"version": "1.30"},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
newConfigListCommand(dockerCli),
|
||||||
|
newConfigCreateCommand(dockerCli),
|
||||||
|
newConfigInspectCommand(dockerCli),
|
||||||
|
newConfigRemoveCommand(dockerCli),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/docker/docker/pkg/system"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createOptions struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
labels opts.ListOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
createOpts := createOptions{
|
||||||
|
labels: opts.NewListOpts(opts.ValidateEnv),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [OPTIONS] CONFIG file|-",
|
||||||
|
Short: "Create a configuration file from a file or STDIN as content",
|
||||||
|
Args: cli.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
createOpts.name = args[0]
|
||||||
|
createOpts.file = args[1]
|
||||||
|
return runConfigCreate(dockerCli, createOpts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigCreate(dockerCli command.Cli, options createOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var in io.Reader = dockerCli.In()
|
||||||
|
if options.file != "-" {
|
||||||
|
file, err := system.OpenSequential(options.file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
in = file
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := ioutil.ReadAll(in)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("Error reading content from %q: %v", options.file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := swarm.ConfigSpec{
|
||||||
|
Annotations: swarm.Annotations{
|
||||||
|
Name: options.name,
|
||||||
|
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||||
|
},
|
||||||
|
Data: configData,
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := client.ConfigCreate(ctx, spec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/internal/test"
|
||||||
|
"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/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const configDataFile = "config-create-with-name.golden"
|
||||||
|
|
||||||
|
func TestConfigCreateErrors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
args []string
|
||||||
|
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{"too_few"},
|
||||||
|
expectedError: "requires exactly 2 argument(s)",
|
||||||
|
},
|
||||||
|
{args: []string{"too", "many", "arguments"},
|
||||||
|
expectedError: "requires exactly 2 argument(s)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||||
|
configCreateFunc: func(configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
|
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
|
||||||
|
},
|
||||||
|
expectedError: "error creating config",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd := newConfigCreateCommand(
|
||||||
|
test.NewFakeCli(&fakeClient{
|
||||||
|
configCreateFunc: tc.configCreateFunc,
|
||||||
|
}, buf),
|
||||||
|
)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
cmd.SetOutput(ioutil.Discard)
|
||||||
|
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigCreateWithName(t *testing.T) {
|
||||||
|
name := "foo"
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
var actual []byte
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
|
if spec.Name != name {
|
||||||
|
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual = spec.Data
|
||||||
|
|
||||||
|
return types.ConfigCreateResponse{
|
||||||
|
ID: "ID-" + spec.Name,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
|
||||||
|
cmd := newConfigCreateCommand(cli)
|
||||||
|
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
expected := golden.Get(t, actual, configDataFile)
|
||||||
|
assert.Equal(t, string(expected), string(actual))
|
||||||
|
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigCreateWithLabels(t *testing.T) {
|
||||||
|
expectedLabels := map[string]string{
|
||||||
|
"lbl1": "Label-foo",
|
||||||
|
"lbl2": "Label-bar",
|
||||||
|
}
|
||||||
|
name := "foo"
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
|
if spec.Name != name {
|
||||||
|
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(spec.Labels, expectedLabels) {
|
||||||
|
return types.ConfigCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ConfigCreateResponse{
|
||||||
|
ID: "ID-" + spec.Name,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
|
||||||
|
cmd := newConfigCreateCommand(cli)
|
||||||
|
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||||
|
cmd.Flags().Set("label", "lbl1=Label-foo")
|
||||||
|
cmd.Flags().Set("label", "lbl2=Label-bar")
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/inspect"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
names []string
|
||||||
|
format string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
opts := inspectOptions{}
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||||
|
Short: "Display detailed information on one or more configuration files",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.names = args
|
||||||
|
return runConfigInspect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
getRef := func(id string) (interface{}, []byte, error) {
|
||||||
|
return client.ConfigInspectWithRaw(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef)
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/internal/test"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
// Import builders to get the builder function as package function
|
||||||
|
. "github.com/docker/cli/cli/internal/test/builders"
|
||||||
|
"github.com/docker/docker/pkg/testutil"
|
||||||
|
"github.com/docker/docker/pkg/testutil/golden"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigInspectErrors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
args []string
|
||||||
|
flags map[string]string
|
||||||
|
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expectedError: "requires at least 1 argument",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"foo"},
|
||||||
|
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||||
|
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||||
|
},
|
||||||
|
expectedError: "error while inspecting the config",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"foo"},
|
||||||
|
flags: map[string]string{
|
||||||
|
"format": "{{invalid format}}",
|
||||||
|
},
|
||||||
|
expectedError: "Template parsing error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"foo", "bar"},
|
||||||
|
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||||
|
if configID == "foo" {
|
||||||
|
return *Config(ConfigName("foo")), nil, nil
|
||||||
|
}
|
||||||
|
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||||
|
},
|
||||||
|
expectedError: "error while inspecting the config",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd := newConfigInspectCommand(
|
||||||
|
test.NewFakeCli(&fakeClient{
|
||||||
|
configInspectFunc: tc.configInspectFunc,
|
||||||
|
}, buf),
|
||||||
|
)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
for key, value := range tc.flags {
|
||||||
|
cmd.Flags().Set(key, value)
|
||||||
|
}
|
||||||
|
cmd.SetOutput(ioutil.Discard)
|
||||||
|
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single-config",
|
||||||
|
args: []string{"foo"},
|
||||||
|
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||||
|
if name != "foo" {
|
||||||
|
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
|
||||||
|
}
|
||||||
|
return *Config(ConfigID("ID-foo"), ConfigName("foo")), nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple-configs-with-labels",
|
||||||
|
args: []string{"foo", "bar"},
|
||||||
|
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||||
|
return *Config(ConfigID("ID-"+name), ConfigName(name), ConfigLabels(map[string]string{
|
||||||
|
"label1": "label-foo",
|
||||||
|
})), nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd := newConfigInspectCommand(
|
||||||
|
test.NewFakeCli(&fakeClient{
|
||||||
|
configInspectFunc: tc.configInspectFunc,
|
||||||
|
}, buf),
|
||||||
|
)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigInspectWithFormat(t *testing.T) {
|
||||||
|
configInspectFunc := func(name string) (swarm.Config, []byte, error) {
|
||||||
|
return *Config(ConfigName("foo"), ConfigLabels(map[string]string{
|
||||||
|
"label1": "label-foo",
|
||||||
|
})), nil, nil
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
format string
|
||||||
|
args []string
|
||||||
|
configInspectFunc func(name string) (swarm.Config, []byte, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple-template",
|
||||||
|
format: "{{.Spec.Name}}",
|
||||||
|
args: []string{"foo"},
|
||||||
|
configInspectFunc: configInspectFunc,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "json-template",
|
||||||
|
format: "{{json .Spec.Labels}}",
|
||||||
|
args: []string{"foo"},
|
||||||
|
configInspectFunc: configInspectFunc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd := newConfigInspectCommand(
|
||||||
|
test.NewFakeCli(&fakeClient{
|
||||||
|
configInspectFunc: tc.configInspectFunc,
|
||||||
|
}, buf),
|
||||||
|
)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
cmd.Flags().Set("format", tc.format)
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
quiet bool
|
||||||
|
format string
|
||||||
|
filter opts.FilterOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls [OPTIONS]",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List configs",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runConfigList(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||||
|
flags.StringVarP(&opts.format, "format", "", "", "Pretty-print configs using a Go template")
|
||||||
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigList(dockerCli command.Cli, opts listOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: opts.filter.Value()})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
format := opts.format
|
||||||
|
if len(format) == 0 {
|
||||||
|
if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !opts.quiet {
|
||||||
|
format = dockerCli.ConfigFile().ConfigFormat
|
||||||
|
} else {
|
||||||
|
format = formatter.TableFormatKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configCtx := formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: formatter.NewConfigFormat(format, opts.quiet),
|
||||||
|
}
|
||||||
|
return formatter.ConfigWrite(configCtx, configs)
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
|
"github.com/docker/cli/cli/internal/test"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
// Import builders to get the builder function as package function
|
||||||
|
. "github.com/docker/cli/cli/internal/test/builders"
|
||||||
|
"github.com/docker/docker/pkg/testutil"
|
||||||
|
"github.com/docker/docker/pkg/testutil/golden"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigListErrors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
args []string
|
||||||
|
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{"foo"},
|
||||||
|
expectedError: "accepts no argument",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||||
|
},
|
||||||
|
expectedError: "error listing configs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd := newConfigListCommand(
|
||||||
|
test.NewFakeCli(&fakeClient{
|
||||||
|
configListFunc: tc.configListFunc,
|
||||||
|
}, buf),
|
||||||
|
)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
cmd.SetOutput(ioutil.Discard)
|
||||||
|
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigList(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
return []swarm.Config{
|
||||||
|
*Config(ConfigID("ID-foo"),
|
||||||
|
ConfigName("foo"),
|
||||||
|
ConfigVersion(swarm.Version{Index: 10}),
|
||||||
|
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
|
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
|
),
|
||||||
|
*Config(ConfigID("ID-bar"),
|
||||||
|
ConfigName("bar"),
|
||||||
|
ConfigVersion(swarm.Version{Index: 11}),
|
||||||
|
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
|
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newConfigListCommand(cli)
|
||||||
|
cmd.SetOutput(buf)
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), "config-list.golden")
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigListWithQuietOption(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
return []swarm.Config{
|
||||||
|
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||||
|
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||||
|
"label": "label-bar",
|
||||||
|
})),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newConfigListCommand(cli)
|
||||||
|
cmd.Flags().Set("quiet", "true")
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), "config-list-with-quiet-option.golden")
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
return []swarm.Config{
|
||||||
|
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||||
|
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||||
|
"label": "label-bar",
|
||||||
|
})),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{
|
||||||
|
ConfigFormat: "{{ .Name }} {{ .Labels }}",
|
||||||
|
})
|
||||||
|
cmd := newConfigListCommand(cli)
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), "config-list-with-config-format.golden")
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigListWithFormat(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
return []swarm.Config{
|
||||||
|
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||||
|
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||||
|
"label": "label-bar",
|
||||||
|
})),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cmd := newConfigListCommand(cli)
|
||||||
|
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), "config-list-with-format.golden")
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigListWithFilter(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
assert.Equal(t, "foo", options.Filters.Get("name")[0])
|
||||||
|
assert.Equal(t, "lbl1=Label-bar", options.Filters.Get("label")[0])
|
||||||
|
return []swarm.Config{
|
||||||
|
*Config(ConfigID("ID-foo"),
|
||||||
|
ConfigName("foo"),
|
||||||
|
ConfigVersion(swarm.Version{Index: 10}),
|
||||||
|
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
|
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
|
),
|
||||||
|
*Config(ConfigID("ID-bar"),
|
||||||
|
ConfigName("bar"),
|
||||||
|
ConfigVersion(swarm.Version{Index: 11}),
|
||||||
|
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
|
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newConfigListCommand(cli)
|
||||||
|
cmd.Flags().Set("filter", "name=foo")
|
||||||
|
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
actual := buf.String()
|
||||||
|
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
|
||||||
|
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "rm CONFIG [CONFIG...]",
|
||||||
|
Aliases: []string{"remove"},
|
||||||
|
Short: "Remove one or more configuration files",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts := removeOptions{
|
||||||
|
names: args,
|
||||||
|
}
|
||||||
|
return runConfigRemove(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigRemove(dockerCli command.Cli, opts removeOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
|
||||||
|
for _, name := range opts.names {
|
||||||
|
if err := client.ConfigRemove(ctx, name); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(dockerCli.Out(), name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errors.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/internal/test"
|
||||||
|
"github.com/docker/docker/pkg/testutil"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigRemoveErrors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
args []string
|
||||||
|
configRemoveFunc func(string) error
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{},
|
||||||
|
expectedError: "requires at least 1 argument(s).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"foo"},
|
||||||
|
configRemoveFunc: func(name string) error {
|
||||||
|
return errors.Errorf("error removing config")
|
||||||
|
},
|
||||||
|
expectedError: "error removing config",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd := newConfigRemoveCommand(
|
||||||
|
test.NewFakeCli(&fakeClient{
|
||||||
|
configRemoveFunc: tc.configRemoveFunc,
|
||||||
|
}, buf),
|
||||||
|
)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
cmd.SetOutput(ioutil.Discard)
|
||||||
|
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRemoveWithName(t *testing.T) {
|
||||||
|
names := []string{"foo", "bar"}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
var removedConfigs []string
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configRemoveFunc: func(name string) error {
|
||||||
|
removedConfigs = append(removedConfigs, name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cmd := newConfigRemoveCommand(cli)
|
||||||
|
cmd.SetArgs(names)
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n"))
|
||||||
|
assert.Equal(t, names, removedConfigs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||||
|
names := []string{"foo", "bar"}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
var removedConfigs []string
|
||||||
|
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
configRemoveFunc: func(name string) error {
|
||||||
|
removedConfigs = append(removedConfigs, name)
|
||||||
|
if name == "foo" {
|
||||||
|
return errors.Errorf("error removing config: %s", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
|
||||||
|
cmd := newConfigRemoveCommand(cli)
|
||||||
|
cmd.SetArgs(names)
|
||||||
|
assert.EqualError(t, cmd.Execute(), "error removing config: foo")
|
||||||
|
assert.Equal(t, names, removedConfigs)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
config_foo_bar
|
|
@ -0,0 +1 @@
|
||||||
|
{"label1":"label-foo"}
|
1
cli/command/config/testdata/config-inspect-with-format.simple-template.golden
vendored
Normal file
1
cli/command/config/testdata/config-inspect-with-format.simple-template.golden
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
foo
|
26
cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden
vendored
Normal file
26
cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ID": "ID-foo",
|
||||||
|
"Version": {},
|
||||||
|
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"Spec": {
|
||||||
|
"Name": "foo",
|
||||||
|
"Labels": {
|
||||||
|
"label1": "label-foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "ID-bar",
|
||||||
|
"Version": {},
|
||||||
|
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"Spec": {
|
||||||
|
"Name": "bar",
|
||||||
|
"Labels": {
|
||||||
|
"label1": "label-foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
12
cli/command/config/testdata/config-inspect-without-format.single-config.golden
vendored
Normal file
12
cli/command/config/testdata/config-inspect-without-format.single-config.golden
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ID": "ID-foo",
|
||||||
|
"Version": {},
|
||||||
|
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"Spec": {
|
||||||
|
"Name": "foo",
|
||||||
|
"Labels": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,2 @@
|
||||||
|
foo
|
||||||
|
bar label=label-bar
|
|
@ -0,0 +1,3 @@
|
||||||
|
ID NAME CREATED UPDATED
|
||||||
|
ID-foo foo 2 hours ago About an hour ago
|
||||||
|
ID-bar bar 2 hours ago About an hour ago
|
|
@ -0,0 +1,2 @@
|
||||||
|
foo
|
||||||
|
bar label=label-bar
|
|
@ -0,0 +1,2 @@
|
||||||
|
ID-foo
|
||||||
|
ID-bar
|
|
@ -0,0 +1,3 @@
|
||||||
|
ID NAME CREATED UPDATED
|
||||||
|
ID-foo foo 2 hours ago About an hour ago
|
||||||
|
ID-bar bar 2 hours ago About an hour ago
|
|
@ -0,0 +1,100 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
|
||||||
|
configIDHeader = "ID"
|
||||||
|
configCreatedHeader = "CREATED"
|
||||||
|
configUpdatedHeader = "UPDATED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewConfigFormat returns a Format for rendering using a config Context
|
||||||
|
func NewConfigFormat(source string, quiet bool) Format {
|
||||||
|
switch source {
|
||||||
|
case TableFormatKey:
|
||||||
|
if quiet {
|
||||||
|
return defaultQuietFormat
|
||||||
|
}
|
||||||
|
return defaultConfigTableFormat
|
||||||
|
}
|
||||||
|
return Format(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigWrite writes the context
|
||||||
|
func ConfigWrite(ctx Context, configs []swarm.Config) error {
|
||||||
|
render := func(format func(subContext subContext) error) error {
|
||||||
|
for _, config := range configs {
|
||||||
|
configCtx := &configContext{c: config}
|
||||||
|
if err := format(configCtx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ctx.Write(newConfigContext(), render)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigContext() *configContext {
|
||||||
|
cCtx := &configContext{}
|
||||||
|
|
||||||
|
cCtx.header = map[string]string{
|
||||||
|
"ID": configIDHeader,
|
||||||
|
"Name": nameHeader,
|
||||||
|
"CreatedAt": configCreatedHeader,
|
||||||
|
"UpdatedAt": configUpdatedHeader,
|
||||||
|
"Labels": labelsHeader,
|
||||||
|
}
|
||||||
|
return cCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
type configContext struct {
|
||||||
|
HeaderContext
|
||||||
|
c swarm.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshalJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) ID() string {
|
||||||
|
return c.c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) Name() string {
|
||||||
|
return c.c.Spec.Annotations.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) CreatedAt() string {
|
||||||
|
return units.HumanDuration(time.Now().UTC().Sub(c.c.Meta.CreatedAt)) + " ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) UpdatedAt() string {
|
||||||
|
return units.HumanDuration(time.Now().UTC().Sub(c.c.Meta.UpdatedAt)) + " ago"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) Labels() string {
|
||||||
|
mapLabels := c.c.Spec.Annotations.Labels
|
||||||
|
if mapLabels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var joinLabels []string
|
||||||
|
for k, v := range mapLabels {
|
||||||
|
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
return strings.Join(joinLabels, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configContext) Label(name string) string {
|
||||||
|
if c.c.Spec.Annotations.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.c.Spec.Annotations.Labels[name]
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigContextFormatWrite(t *testing.T) {
|
||||||
|
// Check default output format (verbose and non-verbose mode) for table headers
|
||||||
|
cases := []struct {
|
||||||
|
context Context
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Errors
|
||||||
|
{
|
||||||
|
Context{Format: "{{InvalidFunction}}"},
|
||||||
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Context{Format: "{{nil}}"},
|
||||||
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Table format
|
||||||
|
{Context{Format: NewConfigFormat("table", false)},
|
||||||
|
`ID NAME CREATED UPDATED
|
||||||
|
1 passwords Less than a second ago Less than a second ago
|
||||||
|
2 id_rsa Less than a second ago Less than a second ago
|
||||||
|
`},
|
||||||
|
{Context{Format: NewConfigFormat("table {{.Name}}", true)},
|
||||||
|
`NAME
|
||||||
|
passwords
|
||||||
|
id_rsa
|
||||||
|
`},
|
||||||
|
{Context{Format: NewConfigFormat("{{.ID}}-{{.Name}}", false)},
|
||||||
|
`1-passwords
|
||||||
|
2-id_rsa
|
||||||
|
`},
|
||||||
|
}
|
||||||
|
|
||||||
|
configs := []swarm.Config{
|
||||||
|
{ID: "1",
|
||||||
|
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||||
|
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}},
|
||||||
|
{ID: "2",
|
||||||
|
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||||
|
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
|
||||||
|
}
|
||||||
|
for _, testcase := range cases {
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
testcase.context.Output = out
|
||||||
|
if err := ConfigWrite(testcase.context, configs); err != nil {
|
||||||
|
assert.Error(t, err, testcase.expected)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, out.String(), testcase.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ const (
|
||||||
secretUpdatedHeader = "UPDATED"
|
secretUpdatedHeader = "UPDATED"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSecretFormat returns a Format for rendering using a network Context
|
// NewSecretFormat returns a Format for rendering using a secret Context
|
||||||
func NewSecretFormat(source string, quiet bool) Format {
|
func NewSecretFormat(source string, quiet bool) Format {
|
||||||
switch source {
|
switch source {
|
||||||
case TableFormatKey:
|
case TableFormatKey:
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -92,7 +93,7 @@ func TestSecretCreateWithLabels(t *testing.T) {
|
||||||
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !compareMap(spec.Labels, expectedLabels) {
|
if !reflect.DeepEqual(spec.Labels, expectedLabels) {
|
||||||
return types.SecretCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels)
|
return types.SecretCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,19 +110,3 @@ func TestSecretCreateWithLabels(t *testing.T) {
|
||||||
assert.NoError(t, cmd.Execute())
|
assert.NoError(t, cmd.Execute())
|
||||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareMap(actual map[string]string, expected map[string]string) bool {
|
|
||||||
if len(actual) != len(expected) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for key, value := range actual {
|
|
||||||
if expectedValue, ok := expected[key]; ok {
|
|
||||||
if expectedValue != value {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
|
@ -43,6 +43,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
flags.Var(&opts.networks, flagNetwork, "Network attachments")
|
flags.Var(&opts.networks, flagNetwork, "Network attachments")
|
||||||
flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
|
flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
|
||||||
flags.SetAnnotation(flagSecret, "version", []string{"1.25"})
|
flags.SetAnnotation(flagSecret, "version", []string{"1.25"})
|
||||||
|
flags.Var(&opts.configs, flagConfig, "Specify configurations to expose to the service")
|
||||||
|
flags.SetAnnotation(flagConfig, "version", []string{"1.30"})
|
||||||
flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port")
|
flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port")
|
||||||
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
|
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
|
||||||
flags.SetAnnotation(flagGroup, "version", []string{"1.25"})
|
flags.SetAnnotation(flagGroup, "version", []string{"1.25"})
|
||||||
|
@ -78,7 +80,16 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
service.TaskTemplate.ContainerSpec.Secrets = secrets
|
service.TaskTemplate.ContainerSpec.Secrets = secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
specifiedConfigs := opts.configs.Value()
|
||||||
|
if len(specifiedConfigs) > 0 {
|
||||||
|
// parse and validate configs
|
||||||
|
configs, err := ParseConfigs(apiClient, specifiedConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.TaskTemplate.ContainerSpec.Configs = configs
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
|
if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
|
||||||
|
|
|
@ -548,6 +548,7 @@ type serviceOptions struct {
|
||||||
|
|
||||||
healthcheck healthCheckOptions
|
healthcheck healthCheckOptions
|
||||||
secrets opts.SecretOpt
|
secrets opts.SecretOpt
|
||||||
|
configs opts.ConfigOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServiceOptions() *serviceOptions {
|
func newServiceOptions() *serviceOptions {
|
||||||
|
@ -657,7 +658,6 @@ func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.Netw
|
||||||
},
|
},
|
||||||
Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()),
|
Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()),
|
||||||
StopGracePeriod: opts.ToStopGracePeriod(flags),
|
StopGracePeriod: opts.ToStopGracePeriod(flags),
|
||||||
Secrets: nil,
|
|
||||||
Healthcheck: healthConfig,
|
Healthcheck: healthConfig,
|
||||||
},
|
},
|
||||||
Networks: networks,
|
Networks: networks,
|
||||||
|
@ -910,4 +910,7 @@ const (
|
||||||
flagSecret = "secret"
|
flagSecret = "secret"
|
||||||
flagSecretAdd = "secret-add"
|
flagSecretAdd = "secret-add"
|
||||||
flagSecretRemove = "secret-rm"
|
flagSecretRemove = "secret-rm"
|
||||||
|
flagConfig = "config"
|
||||||
|
flagConfigAdd = "config-add"
|
||||||
|
flagConfigRemove = "config-rm"
|
||||||
)
|
)
|
||||||
|
|
|
@ -57,3 +57,53 @@ func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.
|
||||||
|
|
||||||
return addedSecrets, nil
|
return addedSecrets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseConfigs retrieves the configs from the requested names and converts
|
||||||
|
// them to config references to use with the spec
|
||||||
|
func ParseConfigs(client client.ConfigAPIClient, requestedConfigs []*swarmtypes.ConfigReference) ([]*swarmtypes.ConfigReference, error) {
|
||||||
|
configRefs := make(map[string]*swarmtypes.ConfigReference)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, config := range requestedConfigs {
|
||||||
|
if _, exists := configRefs[config.File.Name]; exists {
|
||||||
|
return nil, errors.Errorf("duplicate config target for %s not allowed", config.ConfigName)
|
||||||
|
}
|
||||||
|
|
||||||
|
configRef := new(swarmtypes.ConfigReference)
|
||||||
|
*configRef = *config
|
||||||
|
configRefs[config.File.Name] = configRef
|
||||||
|
}
|
||||||
|
|
||||||
|
args := filters.NewArgs()
|
||||||
|
for _, s := range configRefs {
|
||||||
|
args.Add("name", s.ConfigName)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := client.ConfigList(ctx, types.ConfigListOptions{
|
||||||
|
Filters: args,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
foundConfigs := make(map[string]string)
|
||||||
|
for _, config := range configs {
|
||||||
|
foundConfigs[config.Spec.Annotations.Name] = config.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
addedConfigs := []*swarmtypes.ConfigReference{}
|
||||||
|
|
||||||
|
for _, ref := range configRefs {
|
||||||
|
id, ok := foundConfigs[ref.ConfigName]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("config not found: %s", ref.ConfigName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the id for the ref to properly assign in swarm
|
||||||
|
// since swarm needs the ID instead of the name
|
||||||
|
ref.ConfigID = id
|
||||||
|
addedConfigs = append(addedConfigs, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addedConfigs, nil
|
||||||
|
}
|
||||||
|
|
|
@ -68,6 +68,12 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"})
|
flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"})
|
||||||
flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service")
|
flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service")
|
||||||
flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"})
|
flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"})
|
||||||
|
|
||||||
|
flags.Var(newListOptsVar(), flagConfigRemove, "Remove a configuration file")
|
||||||
|
flags.SetAnnotation(flagConfigRemove, "version", []string{"1.30"})
|
||||||
|
flags.Var(&serviceOpts.configs, flagConfigAdd, "Add or update a config file on a service")
|
||||||
|
flags.SetAnnotation(flagConfigAdd, "version", []string{"1.30"})
|
||||||
|
|
||||||
flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service")
|
flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service")
|
||||||
flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint")
|
flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint")
|
||||||
flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference")
|
flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference")
|
||||||
|
@ -170,6 +176,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
|
||||||
|
|
||||||
spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets
|
spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets
|
||||||
|
|
||||||
|
updatedConfigs, err := getUpdatedConfigs(apiClient, flags, spec.TaskTemplate.ContainerSpec.Configs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.TaskTemplate.ContainerSpec.Configs = updatedConfigs
|
||||||
|
|
||||||
// only send auth if flag was set
|
// only send auth if flag was set
|
||||||
sendAuth, err := flags.GetBool(flagRegistryAuth)
|
sendAuth, err := flags.GetBool(flagRegistryAuth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -581,6 +594,29 @@ func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, s
|
||||||
return newSecrets, nil
|
return newSecrets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUpdatedConfigs(apiClient client.ConfigAPIClient, flags *pflag.FlagSet, configs []*swarm.ConfigReference) ([]*swarm.ConfigReference, error) {
|
||||||
|
newConfigs := []*swarm.ConfigReference{}
|
||||||
|
|
||||||
|
toRemove := buildToRemoveSet(flags, flagConfigRemove)
|
||||||
|
for _, config := range configs {
|
||||||
|
if _, exists := toRemove[config.ConfigName]; !exists {
|
||||||
|
newConfigs = append(newConfigs, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.Changed(flagConfigAdd) {
|
||||||
|
values := flags.Lookup(flagConfigAdd).Value.(*opts.ConfigOpt).Value()
|
||||||
|
|
||||||
|
addConfigs, err := ParseConfigs(apiClient, values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newConfigs = append(newConfigs, addConfigs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConfigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func envKey(value string) string {
|
func envKey(value string) string {
|
||||||
kv := strings.SplitN(value, "=", 2)
|
kv := strings.SplitN(value, "=", 2)
|
||||||
return kv[0]
|
return kv[0]
|
||||||
|
|
|
@ -3,6 +3,7 @@ package volume
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -104,10 +105,10 @@ func TestVolumeCreateWithFlags(t *testing.T) {
|
||||||
if body.Driver != expectedDriver {
|
if body.Driver != expectedDriver {
|
||||||
return types.Volume{}, errors.Errorf("expected driver %q, got %q", expectedDriver, body.Driver)
|
return types.Volume{}, errors.Errorf("expected driver %q, got %q", expectedDriver, body.Driver)
|
||||||
}
|
}
|
||||||
if !compareMap(body.DriverOpts, expectedOpts) {
|
if !reflect.DeepEqual(body.DriverOpts, expectedOpts) {
|
||||||
return types.Volume{}, errors.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts)
|
return types.Volume{}, errors.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts)
|
||||||
}
|
}
|
||||||
if !compareMap(body.Labels, expectedLabels) {
|
if !reflect.DeepEqual(body.Labels, expectedLabels) {
|
||||||
return types.Volume{}, errors.Errorf("expected labels %v, got %v", expectedLabels, body.Labels)
|
return types.Volume{}, errors.Errorf("expected labels %v, got %v", expectedLabels, body.Labels)
|
||||||
}
|
}
|
||||||
return types.Volume{
|
return types.Volume{
|
||||||
|
@ -125,19 +126,3 @@ func TestVolumeCreateWithFlags(t *testing.T) {
|
||||||
assert.NoError(t, cmd.Execute())
|
assert.NoError(t, cmd.Execute())
|
||||||
assert.Equal(t, name, strings.TrimSpace(buf.String()))
|
assert.Equal(t, name, strings.TrimSpace(buf.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareMap(actual map[string]string, expected map[string]string) bool {
|
|
||||||
if len(actual) != len(expected) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for key, value := range actual {
|
|
||||||
if expectedValue, ok := expected[key]; ok {
|
|
||||||
if expectedValue != value {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ type ConfigFile struct {
|
||||||
ServicesFormat string `json:"servicesFormat,omitempty"`
|
ServicesFormat string `json:"servicesFormat,omitempty"`
|
||||||
TasksFormat string `json:"tasksFormat,omitempty"`
|
TasksFormat string `json:"tasksFormat,omitempty"`
|
||||||
SecretFormat string `json:"secretFormat,omitempty"`
|
SecretFormat string `json:"secretFormat,omitempty"`
|
||||||
|
ConfigFormat string `json:"configFormat,omitempty"`
|
||||||
NodesFormat string `json:"nodesFormat,omitempty"`
|
NodesFormat string `json:"nodesFormat,omitempty"`
|
||||||
PruneFilters []string `json:"pruneFilters,omitempty"`
|
PruneFilters []string `json:"pruneFilters,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package builders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config creates a config with default values.
|
||||||
|
// Any number of config builder functions can be passed to augment it.
|
||||||
|
func Config(builders ...func(config *swarm.Config)) *swarm.Config {
|
||||||
|
config := &swarm.Config{}
|
||||||
|
|
||||||
|
for _, builder := range builders {
|
||||||
|
builder(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigLabels sets the config's labels
|
||||||
|
func ConfigLabels(labels map[string]string) func(config *swarm.Config) {
|
||||||
|
return func(config *swarm.Config) {
|
||||||
|
config.Spec.Labels = labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigName sets the config's name
|
||||||
|
func ConfigName(name string) func(config *swarm.Config) {
|
||||||
|
return func(config *swarm.Config) {
|
||||||
|
config.Spec.Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigID sets the config's ID
|
||||||
|
func ConfigID(ID string) func(config *swarm.Config) {
|
||||||
|
return func(config *swarm.Config) {
|
||||||
|
config.ID = ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigVersion sets the version for the config
|
||||||
|
func ConfigVersion(v swarm.Version) func(*swarm.Config) {
|
||||||
|
return func(config *swarm.Config) {
|
||||||
|
config.Version = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigCreatedAt sets the creation time for the config
|
||||||
|
func ConfigCreatedAt(t time.Time) func(*swarm.Config) {
|
||||||
|
return func(config *swarm.Config) {
|
||||||
|
config.CreatedAt = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigUpdatedAt sets the update time for the config
|
||||||
|
func ConfigUpdatedAt(t time.Time) func(*swarm.Config) {
|
||||||
|
return func(config *swarm.Config) {
|
||||||
|
config.UpdatedAt = t
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue