diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go index a4c00eee24..c6fc8d6ace 100644 --- a/cli/command/commands/commands.go +++ b/cli/command/commands/commands.go @@ -5,6 +5,7 @@ import ( "github.com/docker/cli/cli/command" "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/image" "github.com/docker/cli/cli/command/network" @@ -26,6 +27,9 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { // checkpoint checkpoint.NewCheckpointCommand(dockerCli), + // config + config.NewConfigCommand(dockerCli), + // container container.NewContainerCommand(dockerCli), container.NewRunCommand(dockerCli), diff --git a/cli/command/config/client_test.go b/cli/command/config/client_test.go new file mode 100644 index 0000000000..fdb1321847 --- /dev/null +++ b/cli/command/config/client_test.go @@ -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 +} diff --git a/cli/command/config/cmd.go b/cli/command/config/cmd.go new file mode 100644 index 0000000000..1f762596c2 --- /dev/null +++ b/cli/command/config/cmd.go @@ -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 +} diff --git a/cli/command/config/create.go b/cli/command/config/create.go new file mode 100644 index 0000000000..fed2f207bf --- /dev/null +++ b/cli/command/config/create.go @@ -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 +} diff --git a/cli/command/config/create_test.go b/cli/command/config/create_test.go new file mode 100644 index 0000000000..25b133836f --- /dev/null +++ b/cli/command/config/create_test.go @@ -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())) +} diff --git a/cli/command/config/inspect.go b/cli/command/config/inspect.go new file mode 100644 index 0000000000..fdf3bc5e7e --- /dev/null +++ b/cli/command/config/inspect.go @@ -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) +} diff --git a/cli/command/config/inspect_test.go b/cli/command/config/inspect_test.go new file mode 100644 index 0000000000..13ef40549c --- /dev/null +++ b/cli/command/config/inspect_test.go @@ -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)) + } +} diff --git a/cli/command/config/ls.go b/cli/command/config/ls.go new file mode 100644 index 0000000000..e3ca82ad50 --- /dev/null +++ b/cli/command/config/ls.go @@ -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) +} diff --git a/cli/command/config/ls_test.go b/cli/command/config/ls_test.go new file mode 100644 index 0000000000..b43c764a4d --- /dev/null +++ b/cli/command/config/ls_test.go @@ -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)) +} diff --git a/cli/command/config/remove.go b/cli/command/config/remove.go new file mode 100644 index 0000000000..5512986d90 --- /dev/null +++ b/cli/command/config/remove.go @@ -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 +} diff --git a/cli/command/config/remove_test.go b/cli/command/config/remove_test.go new file mode 100644 index 0000000000..84423c0254 --- /dev/null +++ b/cli/command/config/remove_test.go @@ -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) +} diff --git a/cli/command/config/testdata/config-create-with-name.golden b/cli/command/config/testdata/config-create-with-name.golden new file mode 100644 index 0000000000..7b28bb3f30 --- /dev/null +++ b/cli/command/config/testdata/config-create-with-name.golden @@ -0,0 +1 @@ +config_foo_bar diff --git a/cli/command/config/testdata/config-inspect-with-format.json-template.golden b/cli/command/config/testdata/config-inspect-with-format.json-template.golden new file mode 100644 index 0000000000..aab678f85d --- /dev/null +++ b/cli/command/config/testdata/config-inspect-with-format.json-template.golden @@ -0,0 +1 @@ +{"label1":"label-foo"} diff --git a/cli/command/config/testdata/config-inspect-with-format.simple-template.golden b/cli/command/config/testdata/config-inspect-with-format.simple-template.golden new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/cli/command/config/testdata/config-inspect-with-format.simple-template.golden @@ -0,0 +1 @@ +foo diff --git a/cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden b/cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden new file mode 100644 index 0000000000..6887c185f1 --- /dev/null +++ b/cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden @@ -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" + } + } + } +] diff --git a/cli/command/config/testdata/config-inspect-without-format.single-config.golden b/cli/command/config/testdata/config-inspect-without-format.single-config.golden new file mode 100644 index 0000000000..ea42ec6f4f --- /dev/null +++ b/cli/command/config/testdata/config-inspect-without-format.single-config.golden @@ -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 + } + } +] diff --git a/cli/command/config/testdata/config-list-with-config-format.golden b/cli/command/config/testdata/config-list-with-config-format.golden new file mode 100644 index 0000000000..9a47538804 --- /dev/null +++ b/cli/command/config/testdata/config-list-with-config-format.golden @@ -0,0 +1,2 @@ +foo +bar label=label-bar diff --git a/cli/command/config/testdata/config-list-with-filter.golden b/cli/command/config/testdata/config-list-with-filter.golden new file mode 100644 index 0000000000..29983de8e9 --- /dev/null +++ b/cli/command/config/testdata/config-list-with-filter.golden @@ -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 diff --git a/cli/command/config/testdata/config-list-with-format.golden b/cli/command/config/testdata/config-list-with-format.golden new file mode 100644 index 0000000000..9a47538804 --- /dev/null +++ b/cli/command/config/testdata/config-list-with-format.golden @@ -0,0 +1,2 @@ +foo +bar label=label-bar diff --git a/cli/command/config/testdata/config-list-with-quiet-option.golden b/cli/command/config/testdata/config-list-with-quiet-option.golden new file mode 100644 index 0000000000..83fb6e8979 --- /dev/null +++ b/cli/command/config/testdata/config-list-with-quiet-option.golden @@ -0,0 +1,2 @@ +ID-foo +ID-bar diff --git a/cli/command/config/testdata/config-list.golden b/cli/command/config/testdata/config-list.golden new file mode 100644 index 0000000000..29983de8e9 --- /dev/null +++ b/cli/command/config/testdata/config-list.golden @@ -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 diff --git a/cli/command/formatter/config.go b/cli/command/formatter/config.go new file mode 100644 index 0000000000..69b30e9010 --- /dev/null +++ b/cli/command/formatter/config.go @@ -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] +} diff --git a/cli/command/formatter/config_test.go b/cli/command/formatter/config_test.go new file mode 100644 index 0000000000..227f454ffa --- /dev/null +++ b/cli/command/formatter/config_test.go @@ -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 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) + } + } +} diff --git a/cli/command/formatter/secret.go b/cli/command/formatter/secret.go index 46c97e2393..b3e53e8f78 100644 --- a/cli/command/formatter/secret.go +++ b/cli/command/formatter/secret.go @@ -16,7 +16,7 @@ const ( 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 { switch source { case TableFormatKey: diff --git a/cli/command/secret/create_test.go b/cli/command/secret/create_test.go index cc77fd7fac..197483cf70 100644 --- a/cli/command/secret/create_test.go +++ b/cli/command/secret/create_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "path/filepath" + "reflect" "strings" "testing" @@ -92,7 +93,7 @@ func TestSecretCreateWithLabels(t *testing.T) { 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) } @@ -109,19 +110,3 @@ func TestSecretCreateWithLabels(t *testing.T) { assert.NoError(t, cmd.Execute()) 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 -} diff --git a/cli/command/service/create.go b/cli/command/service/create.go index a9400ad38d..cfc2830f55 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -43,6 +43,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") 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.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.SetAnnotation(flagGroup, "version", []string{"1.25"}) @@ -78,7 +80,16 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service return err } 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 { diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 027c2081f6..d1742fea41 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -548,6 +548,7 @@ type serviceOptions struct { healthcheck healthCheckOptions secrets opts.SecretOpt + configs opts.ConfigOpt } func newServiceOptions() *serviceOptions { @@ -657,7 +658,6 @@ func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.Netw }, Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), StopGracePeriod: opts.ToStopGracePeriod(flags), - Secrets: nil, Healthcheck: healthConfig, }, Networks: networks, @@ -910,4 +910,7 @@ const ( flagSecret = "secret" flagSecretAdd = "secret-add" flagSecretRemove = "secret-rm" + flagConfig = "config" + flagConfigAdd = "config-add" + flagConfigRemove = "config-rm" ) diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go index acee08761f..f3c9306881 100644 --- a/cli/command/service/parse.go +++ b/cli/command/service/parse.go @@ -57,3 +57,53 @@ func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes. 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 +} diff --git a/cli/command/service/update.go b/cli/command/service/update.go index cb12d04262..6dba8c8bae 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -68,6 +68,12 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"}) flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service") 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.constraints, flagConstraintAdd, "Add or update a placement constraint") 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 + 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 sendAuth, err := flags.GetBool(flagRegistryAuth) if err != nil { @@ -581,6 +594,29 @@ func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, s 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 { kv := strings.SplitN(value, "=", 2) return kv[0] diff --git a/cli/command/volume/create_test.go b/cli/command/volume/create_test.go index a0ef71ffb0..74c81aba1f 100644 --- a/cli/command/volume/create_test.go +++ b/cli/command/volume/create_test.go @@ -3,6 +3,7 @@ package volume import ( "bytes" "io/ioutil" + "reflect" "strings" "testing" @@ -104,10 +105,10 @@ func TestVolumeCreateWithFlags(t *testing.T) { if body.Driver != expectedDriver { 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) } - 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{ @@ -125,19 +126,3 @@ func TestVolumeCreateWithFlags(t *testing.T) { assert.NoError(t, cmd.Execute()) 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 -} diff --git a/cli/config/configfile/file.go b/cli/config/configfile/file.go index 44bf71f388..7214325d87 100644 --- a/cli/config/configfile/file.go +++ b/cli/config/configfile/file.go @@ -38,6 +38,7 @@ type ConfigFile struct { ServicesFormat string `json:"servicesFormat,omitempty"` TasksFormat string `json:"tasksFormat,omitempty"` SecretFormat string `json:"secretFormat,omitempty"` + ConfigFormat string `json:"configFormat,omitempty"` NodesFormat string `json:"nodesFormat,omitempty"` PruneFilters []string `json:"pruneFilters,omitempty"` } diff --git a/cli/internal/test/builders/config.go b/cli/internal/test/builders/config.go new file mode 100644 index 0000000000..dee6e90a82 --- /dev/null +++ b/cli/internal/test/builders/config.go @@ -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 + } +} diff --git a/vendor.conf b/vendor.conf index 4cfab3bdc4..658f3d365b 100644 --- a/vendor.conf +++ b/vendor.conf @@ -6,7 +6,7 @@ github.com/agl/ed25519 d2b94fd789ea21d12fac1a4443dd3a3f79cda72c github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621 -github.com/docker/docker f02a5b50c407bdb087388e18e1ac619f2788dd8d +github.com/docker/docker 69c35dad8e7ec21de32d42b9dd606d3416ae1566 github.com/docker/docker-credential-helpers v0.5.0 github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06 github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5 diff --git a/vendor/github.com/docker/docker/api/types/swarm/config.go b/vendor/github.com/docker/docker/api/types/swarm/config.go new file mode 100644 index 0000000000..0fb021ce92 --- /dev/null +++ b/vendor/github.com/docker/docker/api/types/swarm/config.go @@ -0,0 +1,31 @@ +package swarm + +import "os" + +// Config represents a config. +type Config struct { + ID string + Meta + Spec ConfigSpec +} + +// ConfigSpec represents a config specification from a config in swarm +type ConfigSpec struct { + Annotations + Data []byte `json:",omitempty"` +} + +// ConfigReferenceFileTarget is a file target in a config reference +type ConfigReferenceFileTarget struct { + Name string + UID string + GID string + Mode os.FileMode +} + +// ConfigReference is a reference to a config in swarm +type ConfigReference struct { + File *ConfigReferenceFileTarget + ConfigID string + ConfigName string +} diff --git a/vendor/github.com/docker/docker/api/types/swarm/container.go b/vendor/github.com/docker/docker/api/types/swarm/container.go index 135f7cbbfc..6f8b45f6bb 100644 --- a/vendor/github.com/docker/docker/api/types/swarm/container.go +++ b/vendor/github.com/docker/docker/api/types/swarm/container.go @@ -68,4 +68,5 @@ type ContainerSpec struct { Hosts []string `json:",omitempty"` DNSConfig *DNSConfig `json:",omitempty"` Secrets []*SecretReference `json:",omitempty"` + Configs []*ConfigReference `json:",omitempty"` } diff --git a/vendor/github.com/docker/docker/api/types/types.go b/vendor/github.com/docker/docker/api/types/types.go index 75aaab157d..efba16bc97 100644 --- a/vendor/github.com/docker/docker/api/types/types.go +++ b/vendor/github.com/docker/docker/api/types/types.go @@ -522,6 +522,18 @@ type SecretListOptions struct { Filters filters.Args } +// ConfigCreateResponse contains the information returned to a client +// on the creation of a new config. +type ConfigCreateResponse struct { + // ID is the id of the created config. + ID string +} + +// ConfigListOptions holds parameters to list configs +type ConfigListOptions struct { + Filters filters.Args +} + // PushResult contains the tag, manifest digest, and manifest size from the // push. It's used to signal this information to the trust code in the client // so it can sign the manifest if necessary. diff --git a/vendor/github.com/docker/docker/client/client.go b/vendor/github.com/docker/docker/client/client.go index df3698adc6..f8f2fc6ad5 100644 --- a/vendor/github.com/docker/docker/client/client.go +++ b/vendor/github.com/docker/docker/client/client.go @@ -46,6 +46,7 @@ For example, to list running containers (the equivalent of "docker ps"): package client import ( + "errors" "fmt" "net/http" "net/url" @@ -58,6 +59,9 @@ import ( "github.com/docker/go-connections/tlsconfig" ) +// ErrRedirect is the error returned by checkRedirect when the request is non-GET. +var ErrRedirect = errors.New("unexpected redirect in response") + // Client is the API client that performs all operations // against a docker server. type Client struct { @@ -81,6 +85,23 @@ type Client struct { manualOverride bool } +// CheckRedirect specifies the policy for dealing with redirect responses: +// If the request is non-GET return `ErrRedirect`. Otherwise use the last response. +// +// Go 1.8 changes behavior for HTTP redirects (specificlaly 301, 307, and 308) in the client . +// The Docker client (and by extension docker API client) can be made to to send a request +// like POST /containers//start where what would normally be in the name section of the URL is empty. +// This triggers an HTTP 301 from the daemon. +// In go 1.8 this 301 will be converted to a GET request, and ends up getting a 404 from the daemon. +// This behavior change manifests in the client in that before the 301 was not followed and +// the client did not generate an error, but now results in a message like Error response from daemon: page not found. +func CheckRedirect(req *http.Request, via []*http.Request) error { + if via[0].Method == http.MethodGet { + return http.ErrUseLastResponse + } + return ErrRedirect +} + // NewEnvClient initializes a new API client based on environment variables. // Use DOCKER_HOST to set the url to the docker server. // Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest. @@ -104,6 +125,7 @@ func NewEnvClient() (*Client, error) { Transport: &http.Transport{ TLSClientConfig: tlsc, }, + CheckRedirect: CheckRedirect, } } @@ -147,7 +169,8 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map transport := new(http.Transport) sockets.ConfigureTransport(transport, proto, addr) client = &http.Client{ - Transport: transport, + Transport: transport, + CheckRedirect: CheckRedirect, } } diff --git a/vendor/github.com/docker/docker/client/config_create.go b/vendor/github.com/docker/docker/client/config_create.go new file mode 100644 index 0000000000..7ddeaf4b43 --- /dev/null +++ b/vendor/github.com/docker/docker/client/config_create.go @@ -0,0 +1,22 @@ +package client + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ConfigCreate creates a new Config. +func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) { + var response types.ConfigCreateResponse + resp, err := cli.post(ctx, "/configs/create", nil, config, nil) + if err != nil { + return response, err + } + + err = json.NewDecoder(resp.body).Decode(&response) + ensureReaderClosed(resp) + return response, err +} diff --git a/vendor/github.com/docker/docker/client/config_inspect.go b/vendor/github.com/docker/docker/client/config_inspect.go new file mode 100644 index 0000000000..1917b181fa --- /dev/null +++ b/vendor/github.com/docker/docker/client/config_inspect.go @@ -0,0 +1,34 @@ +package client + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ConfigInspectWithRaw returns the config information with raw data +func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) { + resp, err := cli.get(ctx, "/configs/"+id, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return swarm.Config{}, nil, configNotFoundError{id} + } + return swarm.Config{}, nil, err + } + defer ensureReaderClosed(resp) + + body, err := ioutil.ReadAll(resp.body) + if err != nil { + return swarm.Config{}, nil, err + } + + var config swarm.Config + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&config) + + return config, body, err +} diff --git a/vendor/github.com/docker/docker/client/config_list.go b/vendor/github.com/docker/docker/client/config_list.go new file mode 100644 index 0000000000..cea11708a7 --- /dev/null +++ b/vendor/github.com/docker/docker/client/config_list.go @@ -0,0 +1,35 @@ +package client + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ConfigList returns the list of configs. +func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { + query := url.Values{} + + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get(ctx, "/configs", query, nil) + if err != nil { + return nil, err + } + + var configs []swarm.Config + err = json.NewDecoder(resp.body).Decode(&configs) + ensureReaderClosed(resp) + return configs, err +} diff --git a/vendor/github.com/docker/docker/client/config_remove.go b/vendor/github.com/docker/docker/client/config_remove.go new file mode 100644 index 0000000000..f04b8df17a --- /dev/null +++ b/vendor/github.com/docker/docker/client/config_remove.go @@ -0,0 +1,10 @@ +package client + +import "golang.org/x/net/context" + +// ConfigRemove removes a Config. +func (cli *Client) ConfigRemove(ctx context.Context, id string) error { + resp, err := cli.delete(ctx, "/configs/"+id, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/github.com/docker/docker/client/config_update.go b/vendor/github.com/docker/docker/client/config_update.go new file mode 100644 index 0000000000..06f711517d --- /dev/null +++ b/vendor/github.com/docker/docker/client/config_update.go @@ -0,0 +1,18 @@ +package client + +import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types/swarm" + "golang.org/x/net/context" +) + +// ConfigUpdate attempts to updates a Config +func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error { + query := url.Values{} + query.Set("version", strconv.FormatUint(version.Index, 10)) + resp, err := cli.post(ctx, "/configs/"+id+"/update", query, config, nil) + ensureReaderClosed(resp) + return err +} diff --git a/vendor/github.com/docker/docker/client/errors.go b/vendor/github.com/docker/docker/client/errors.go index 4f767bd8d3..e0effafc0a 100644 --- a/vendor/github.com/docker/docker/client/errors.go +++ b/vendor/github.com/docker/docker/client/errors.go @@ -256,6 +256,28 @@ func IsErrSecretNotFound(err error) bool { return ok } +// configNotFoundError implements an error returned when a config is not found. +type configNotFoundError struct { + name string +} + +// Error returns a string representation of a configNotFoundError +func (e configNotFoundError) Error() string { + return fmt.Sprintf("Error: no such config: %s", e.name) +} + +// NotFound indicates that this error type is of NotFound +func (e configNotFoundError) NotFound() bool { + return true +} + +// IsErrConfigNotFound returns true if the error is caused +// when a config is not found. +func IsErrConfigNotFound(err error) bool { + _, ok := err.(configNotFoundError) + return ok +} + // pluginNotFoundError implements an error returned when a plugin is not in the docker host. type pluginNotFoundError struct { name string diff --git a/vendor/github.com/docker/docker/client/interface.go b/vendor/github.com/docker/docker/client/interface.go index 8dbe4300dc..bccfc7b2ca 100644 --- a/vendor/github.com/docker/docker/client/interface.go +++ b/vendor/github.com/docker/docker/client/interface.go @@ -18,6 +18,7 @@ import ( // CommonAPIClient is the common methods between stable and experimental versions of APIClient. type CommonAPIClient interface { + ConfigAPIClient ContainerAPIClient ImageAPIClient NodeAPIClient @@ -171,3 +172,12 @@ type SecretAPIClient interface { SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error } + +// ConfigAPIClient defines API client methods for configs +type ConfigAPIClient interface { + ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) + ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (types.ConfigCreateResponse, error) + ConfigRemove(ctx context.Context, id string) error + ConfigInspectWithRaw(ctx context.Context, name string) (swarm.Config, []byte, error) + ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error +} diff --git a/vendor/github.com/docker/docker/opts/config.go b/vendor/github.com/docker/docker/opts/config.go new file mode 100644 index 0000000000..82fd2bce4e --- /dev/null +++ b/vendor/github.com/docker/docker/opts/config.go @@ -0,0 +1,98 @@ +package opts + +import ( + "encoding/csv" + "fmt" + "os" + "strconv" + "strings" + + swarmtypes "github.com/docker/docker/api/types/swarm" +) + +// ConfigOpt is a Value type for parsing configs +type ConfigOpt struct { + values []*swarmtypes.ConfigReference +} + +// Set a new config value +func (o *ConfigOpt) Set(value string) error { + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return err + } + + options := &swarmtypes.ConfigReference{ + File: &swarmtypes.ConfigReferenceFileTarget{ + UID: "0", + GID: "0", + Mode: 0444, + }, + } + + // support a simple syntax of --config foo + if len(fields) == 1 { + options.File.Name = fields[0] + options.ConfigName = fields[0] + o.values = append(o.values, options) + return nil + } + + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + key := strings.ToLower(parts[0]) + + if len(parts) != 2 { + return fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + value := parts[1] + switch key { + case "source", "src": + options.ConfigName = value + case "target": + options.File.Name = value + case "uid": + options.File.UID = value + case "gid": + options.File.GID = value + case "mode": + m, err := strconv.ParseUint(value, 0, 32) + if err != nil { + return fmt.Errorf("invalid mode specified: %v", err) + } + + options.File.Mode = os.FileMode(m) + default: + return fmt.Errorf("invalid field in config request: %s", key) + } + } + + if options.ConfigName == "" { + return fmt.Errorf("source is required") + } + + o.values = append(o.values, options) + return nil +} + +// Type returns the type of this option +func (o *ConfigOpt) Type() string { + return "config" +} + +// String returns a string repr of this option +func (o *ConfigOpt) String() string { + configs := []string{} + for _, config := range o.values { + repr := fmt.Sprintf("%s -> %s", config.ConfigName, config.File.Name) + configs = append(configs, repr) + } + return strings.Join(configs, ", ") +} + +// Value returns the config requests +func (o *ConfigOpt) Value() []*swarmtypes.ConfigReference { + return o.values +} diff --git a/vendor/github.com/docker/docker/pkg/mount/flags.go b/vendor/github.com/docker/docker/pkg/mount/flags.go new file mode 100644 index 0000000000..607dbed43a --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/flags.go @@ -0,0 +1,149 @@ +package mount + +import ( + "fmt" + "strings" +) + +var flags = map[string]struct { + clear bool + flag int +}{ + "defaults": {false, 0}, + "ro": {false, RDONLY}, + "rw": {true, RDONLY}, + "suid": {true, NOSUID}, + "nosuid": {false, NOSUID}, + "dev": {true, NODEV}, + "nodev": {false, NODEV}, + "exec": {true, NOEXEC}, + "noexec": {false, NOEXEC}, + "sync": {false, SYNCHRONOUS}, + "async": {true, SYNCHRONOUS}, + "dirsync": {false, DIRSYNC}, + "remount": {false, REMOUNT}, + "mand": {false, MANDLOCK}, + "nomand": {true, MANDLOCK}, + "atime": {true, NOATIME}, + "noatime": {false, NOATIME}, + "diratime": {true, NODIRATIME}, + "nodiratime": {false, NODIRATIME}, + "bind": {false, BIND}, + "rbind": {false, RBIND}, + "unbindable": {false, UNBINDABLE}, + "runbindable": {false, RUNBINDABLE}, + "private": {false, PRIVATE}, + "rprivate": {false, RPRIVATE}, + "shared": {false, SHARED}, + "rshared": {false, RSHARED}, + "slave": {false, SLAVE}, + "rslave": {false, RSLAVE}, + "relatime": {false, RELATIME}, + "norelatime": {true, RELATIME}, + "strictatime": {false, STRICTATIME}, + "nostrictatime": {true, STRICTATIME}, +} + +var validFlags = map[string]bool{ + "": true, + "size": true, + "mode": true, + "uid": true, + "gid": true, + "nr_inodes": true, + "nr_blocks": true, + "mpol": true, +} + +var propagationFlags = map[string]bool{ + "bind": true, + "rbind": true, + "unbindable": true, + "runbindable": true, + "private": true, + "rprivate": true, + "shared": true, + "rshared": true, + "slave": true, + "rslave": true, +} + +// MergeTmpfsOptions merge mount options to make sure there is no duplicate. +func MergeTmpfsOptions(options []string) ([]string, error) { + // We use collisions maps to remove duplicates. + // For flag, the key is the flag value (the key for propagation flag is -1) + // For data=value, the key is the data + flagCollisions := map[int]bool{} + dataCollisions := map[string]bool{} + + var newOptions []string + // We process in reverse order + for i := len(options) - 1; i >= 0; i-- { + option := options[i] + if option == "defaults" { + continue + } + if f, ok := flags[option]; ok && f.flag != 0 { + // There is only one propagation mode + key := f.flag + if propagationFlags[option] { + key = -1 + } + // Check to see if there is collision for flag + if !flagCollisions[key] { + // We prepend the option and add to collision map + newOptions = append([]string{option}, newOptions...) + flagCollisions[key] = true + } + continue + } + opt := strings.SplitN(option, "=", 2) + if len(opt) != 2 || !validFlags[opt[0]] { + return nil, fmt.Errorf("Invalid tmpfs option %q", opt) + } + if !dataCollisions[opt[0]] { + // We prepend the option and add to collision map + newOptions = append([]string{option}, newOptions...) + dataCollisions[opt[0]] = true + } + } + + return newOptions, nil +} + +// Parse fstab type mount options into mount() flags +// and device specific data +func parseOptions(options string) (int, string) { + var ( + flag int + data []string + ) + + for _, o := range strings.Split(options, ",") { + // If the option does not exist in the flags table or the flag + // is not supported on the platform, + // then it is a data value for a specific fs type + if f, exists := flags[o]; exists && f.flag != 0 { + if f.clear { + flag &= ^f.flag + } else { + flag |= f.flag + } + } else { + data = append(data, o) + } + } + return flag, strings.Join(data, ",") +} + +// ParseTmpfsOptions parse fstab type mount options into flags and data +func ParseTmpfsOptions(options string) (int, string, error) { + flags, data := parseOptions(options) + for _, o := range strings.Split(data, ",") { + opt := strings.SplitN(o, "=", 2) + if !validFlags[opt[0]] { + return 0, "", fmt.Errorf("Invalid tmpfs option %q", opt) + } + } + return flags, data, nil +} diff --git a/vendor/github.com/docker/docker/pkg/mount/flags_freebsd.go b/vendor/github.com/docker/docker/pkg/mount/flags_freebsd.go new file mode 100644 index 0000000000..5f76f331b6 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/flags_freebsd.go @@ -0,0 +1,49 @@ +// +build freebsd,cgo + +package mount + +/* +#include +*/ +import "C" + +const ( + // RDONLY will mount the filesystem as read-only. + RDONLY = C.MNT_RDONLY + + // NOSUID will not allow set-user-identifier or set-group-identifier bits to + // take effect. + NOSUID = C.MNT_NOSUID + + // NOEXEC will not allow execution of any binaries on the mounted file system. + NOEXEC = C.MNT_NOEXEC + + // SYNCHRONOUS will allow any I/O to the file system to be done synchronously. + SYNCHRONOUS = C.MNT_SYNCHRONOUS + + // NOATIME will not update the file access time when reading from a file. + NOATIME = C.MNT_NOATIME +) + +// These flags are unsupported. +const ( + BIND = 0 + DIRSYNC = 0 + MANDLOCK = 0 + NODEV = 0 + NODIRATIME = 0 + UNBINDABLE = 0 + RUNBINDABLE = 0 + PRIVATE = 0 + RPRIVATE = 0 + SHARED = 0 + RSHARED = 0 + SLAVE = 0 + RSLAVE = 0 + RBIND = 0 + RELATIVE = 0 + RELATIME = 0 + REMOUNT = 0 + STRICTATIME = 0 + mntDetach = 0 +) diff --git a/vendor/github.com/docker/docker/pkg/mount/flags_linux.go b/vendor/github.com/docker/docker/pkg/mount/flags_linux.go new file mode 100644 index 0000000000..25f466183e --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/flags_linux.go @@ -0,0 +1,87 @@ +package mount + +import ( + "syscall" +) + +const ( + // RDONLY will mount the file system read-only. + RDONLY = syscall.MS_RDONLY + + // NOSUID will not allow set-user-identifier or set-group-identifier bits to + // take effect. + NOSUID = syscall.MS_NOSUID + + // NODEV will not interpret character or block special devices on the file + // system. + NODEV = syscall.MS_NODEV + + // NOEXEC will not allow execution of any binaries on the mounted file system. + NOEXEC = syscall.MS_NOEXEC + + // SYNCHRONOUS will allow I/O to the file system to be done synchronously. + SYNCHRONOUS = syscall.MS_SYNCHRONOUS + + // DIRSYNC will force all directory updates within the file system to be done + // synchronously. This affects the following system calls: create, link, + // unlink, symlink, mkdir, rmdir, mknod and rename. + DIRSYNC = syscall.MS_DIRSYNC + + // REMOUNT will attempt to remount an already-mounted file system. This is + // commonly used to change the mount flags for a file system, especially to + // make a readonly file system writeable. It does not change device or mount + // point. + REMOUNT = syscall.MS_REMOUNT + + // MANDLOCK will force mandatory locks on a filesystem. + MANDLOCK = syscall.MS_MANDLOCK + + // NOATIME will not update the file access time when reading from a file. + NOATIME = syscall.MS_NOATIME + + // NODIRATIME will not update the directory access time. + NODIRATIME = syscall.MS_NODIRATIME + + // BIND remounts a subtree somewhere else. + BIND = syscall.MS_BIND + + // RBIND remounts a subtree and all possible submounts somewhere else. + RBIND = syscall.MS_BIND | syscall.MS_REC + + // UNBINDABLE creates a mount which cannot be cloned through a bind operation. + UNBINDABLE = syscall.MS_UNBINDABLE + + // RUNBINDABLE marks the entire mount tree as UNBINDABLE. + RUNBINDABLE = syscall.MS_UNBINDABLE | syscall.MS_REC + + // PRIVATE creates a mount which carries no propagation abilities. + PRIVATE = syscall.MS_PRIVATE + + // RPRIVATE marks the entire mount tree as PRIVATE. + RPRIVATE = syscall.MS_PRIVATE | syscall.MS_REC + + // SLAVE creates a mount which receives propagation from its master, but not + // vice versa. + SLAVE = syscall.MS_SLAVE + + // RSLAVE marks the entire mount tree as SLAVE. + RSLAVE = syscall.MS_SLAVE | syscall.MS_REC + + // SHARED creates a mount which provides the ability to create mirrors of + // that mount such that mounts and unmounts within any of the mirrors + // propagate to the other mirrors. + SHARED = syscall.MS_SHARED + + // RSHARED marks the entire mount tree as SHARED. + RSHARED = syscall.MS_SHARED | syscall.MS_REC + + // RELATIME updates inode access times relative to modify or change time. + RELATIME = syscall.MS_RELATIME + + // STRICTATIME allows to explicitly request full atime updates. This makes + // it possible for the kernel to default to relatime or noatime but still + // allow userspace to override it. + STRICTATIME = syscall.MS_STRICTATIME + + mntDetach = syscall.MNT_DETACH +) diff --git a/vendor/github.com/docker/docker/pkg/mount/flags_unsupported.go b/vendor/github.com/docker/docker/pkg/mount/flags_unsupported.go new file mode 100644 index 0000000000..9ed741e3ff --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/flags_unsupported.go @@ -0,0 +1,31 @@ +// +build !linux,!freebsd freebsd,!cgo solaris,!cgo + +package mount + +// These flags are unsupported. +const ( + BIND = 0 + DIRSYNC = 0 + MANDLOCK = 0 + NOATIME = 0 + NODEV = 0 + NODIRATIME = 0 + NOEXEC = 0 + NOSUID = 0 + UNBINDABLE = 0 + RUNBINDABLE = 0 + PRIVATE = 0 + RPRIVATE = 0 + SHARED = 0 + RSHARED = 0 + SLAVE = 0 + RSLAVE = 0 + RBIND = 0 + RELATIME = 0 + RELATIVE = 0 + REMOUNT = 0 + STRICTATIME = 0 + SYNCHRONOUS = 0 + RDONLY = 0 + mntDetach = 0 +) diff --git a/vendor/github.com/docker/docker/pkg/mount/mount.go b/vendor/github.com/docker/docker/pkg/mount/mount.go new file mode 100644 index 0000000000..c9fdfd6942 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mount.go @@ -0,0 +1,86 @@ +package mount + +import ( + "sort" + "strings" +) + +// GetMounts retrieves a list of mounts for the current running process. +func GetMounts() ([]*Info, error) { + return parseMountTable() +} + +// Mounted determines if a specified mountpoint has been mounted. +// On Linux it looks at /proc/self/mountinfo and on Solaris at mnttab. +func Mounted(mountpoint string) (bool, error) { + entries, err := parseMountTable() + if err != nil { + return false, err + } + + // Search the table for the mountpoint + for _, e := range entries { + if e.Mountpoint == mountpoint { + return true, nil + } + } + return false, nil +} + +// Mount will mount filesystem according to the specified configuration, on the +// condition that the target path is *not* already mounted. Options must be +// specified like the mount or fstab unix commands: "opt1=val1,opt2=val2". See +// flags.go for supported option flags. +func Mount(device, target, mType, options string) error { + flag, _ := parseOptions(options) + if flag&REMOUNT != REMOUNT { + if mounted, err := Mounted(target); err != nil || mounted { + return err + } + } + return ForceMount(device, target, mType, options) +} + +// ForceMount will mount a filesystem according to the specified configuration, +// *regardless* if the target path is not already mounted. Options must be +// specified like the mount or fstab unix commands: "opt1=val1,opt2=val2". See +// flags.go for supported option flags. +func ForceMount(device, target, mType, options string) error { + flag, data := parseOptions(options) + return mount(device, target, mType, uintptr(flag), data) +} + +// Unmount lazily unmounts a filesystem on supported platforms, otherwise +// does a normal unmount. +func Unmount(target string) error { + if mounted, err := Mounted(target); err != nil || !mounted { + return err + } + return unmount(target, mntDetach) +} + +// RecursiveUnmount unmounts the target and all mounts underneath, starting with +// the deepsest mount first. +func RecursiveUnmount(target string) error { + mounts, err := GetMounts() + if err != nil { + return err + } + + // Make the deepest mount be first + sort.Sort(sort.Reverse(byMountpoint(mounts))) + + for i, m := range mounts { + if !strings.HasPrefix(m.Mountpoint, target) { + continue + } + if err := Unmount(m.Mountpoint); err != nil && i == len(mounts)-1 { + if mounted, err := Mounted(m.Mountpoint); err != nil || mounted { + return err + } + // Ignore errors for submounts and continue trying to unmount others + // The final unmount should fail if there ane any submounts remaining + } + } + return nil +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mounter_freebsd.go b/vendor/github.com/docker/docker/pkg/mount/mounter_freebsd.go new file mode 100644 index 0000000000..bb870e6f59 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mounter_freebsd.go @@ -0,0 +1,59 @@ +package mount + +/* +#include +#include +#include +#include +#include +#include +*/ +import "C" + +import ( + "fmt" + "strings" + "syscall" + "unsafe" +) + +func allocateIOVecs(options []string) []C.struct_iovec { + out := make([]C.struct_iovec, len(options)) + for i, option := range options { + out[i].iov_base = unsafe.Pointer(C.CString(option)) + out[i].iov_len = C.size_t(len(option) + 1) + } + return out +} + +func mount(device, target, mType string, flag uintptr, data string) error { + isNullFS := false + + xs := strings.Split(data, ",") + for _, x := range xs { + if x == "bind" { + isNullFS = true + } + } + + options := []string{"fspath", target} + if isNullFS { + options = append(options, "fstype", "nullfs", "target", device) + } else { + options = append(options, "fstype", mType, "from", device) + } + rawOptions := allocateIOVecs(options) + for _, rawOption := range rawOptions { + defer C.free(rawOption.iov_base) + } + + if errno := C.nmount(&rawOptions[0], C.uint(len(options)), C.int(flag)); errno != 0 { + reason := C.GoString(C.strerror(*C.__error())) + return fmt.Errorf("Failed to call nmount: %s", reason) + } + return nil +} + +func unmount(target string, flag int) error { + return syscall.Unmount(target, flag) +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mounter_linux.go b/vendor/github.com/docker/docker/pkg/mount/mounter_linux.go new file mode 100644 index 0000000000..3ef2ce6f0d --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mounter_linux.go @@ -0,0 +1,56 @@ +package mount + +import ( + "syscall" +) + +const ( + // ptypes is the set propagation types. + ptypes = syscall.MS_SHARED | syscall.MS_PRIVATE | syscall.MS_SLAVE | syscall.MS_UNBINDABLE + + // pflags is the full set valid flags for a change propagation call. + pflags = ptypes | syscall.MS_REC | syscall.MS_SILENT + + // broflags is the combination of bind and read only + broflags = syscall.MS_BIND | syscall.MS_RDONLY +) + +// isremount returns true if either device name or flags identify a remount request, false otherwise. +func isremount(device string, flags uintptr) bool { + switch { + // We treat device "" and "none" as a remount request to provide compatibility with + // requests that don't explicitly set MS_REMOUNT such as those manipulating bind mounts. + case flags&syscall.MS_REMOUNT != 0, device == "", device == "none": + return true + default: + return false + } +} + +func mount(device, target, mType string, flags uintptr, data string) error { + oflags := flags &^ ptypes + if !isremount(device, flags) { + // Initial call applying all non-propagation flags. + if err := syscall.Mount(device, target, mType, oflags, data); err != nil { + return err + } + } + + if flags&ptypes != 0 { + // Change the propagation type. + if err := syscall.Mount("", target, "", flags&pflags, ""); err != nil { + return err + } + } + + if oflags&broflags == broflags { + // Remount the bind to apply read only. + return syscall.Mount("", target, "", oflags|syscall.MS_REMOUNT, "") + } + + return nil +} + +func unmount(target string, flag int) error { + return syscall.Unmount(target, flag) +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mounter_solaris.go b/vendor/github.com/docker/docker/pkg/mount/mounter_solaris.go new file mode 100644 index 0000000000..c684aa81fc --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mounter_solaris.go @@ -0,0 +1,33 @@ +// +build solaris,cgo + +package mount + +import ( + "golang.org/x/sys/unix" + "unsafe" +) + +// #include +// #include +// #include +// int Mount(const char *spec, const char *dir, int mflag, +// char *fstype, char *dataptr, int datalen, char *optptr, int optlen) { +// return mount(spec, dir, mflag, fstype, dataptr, datalen, optptr, optlen); +// } +import "C" + +func mount(device, target, mType string, flag uintptr, data string) error { + spec := C.CString(device) + dir := C.CString(target) + fstype := C.CString(mType) + _, err := C.Mount(spec, dir, C.int(flag), fstype, nil, 0, nil, 0) + C.free(unsafe.Pointer(spec)) + C.free(unsafe.Pointer(dir)) + C.free(unsafe.Pointer(fstype)) + return err +} + +func unmount(target string, flag int) error { + err := unix.Unmount(target, flag) + return err +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mounter_unsupported.go b/vendor/github.com/docker/docker/pkg/mount/mounter_unsupported.go new file mode 100644 index 0000000000..a2a3bb457f --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mounter_unsupported.go @@ -0,0 +1,11 @@ +// +build !linux,!freebsd,!solaris freebsd,!cgo solaris,!cgo + +package mount + +func mount(device, target, mType string, flag uintptr, data string) error { + panic("Not implemented") +} + +func unmount(target string, flag int) error { + panic("Not implemented") +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mountinfo.go b/vendor/github.com/docker/docker/pkg/mount/mountinfo.go new file mode 100644 index 0000000000..ff4cc1d86b --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mountinfo.go @@ -0,0 +1,54 @@ +package mount + +// Info reveals information about a particular mounted filesystem. This +// struct is populated from the content in the /proc//mountinfo file. +type Info struct { + // ID is a unique identifier of the mount (may be reused after umount). + ID int + + // Parent indicates the ID of the mount parent (or of self for the top of the + // mount tree). + Parent int + + // Major indicates one half of the device ID which identifies the device class. + Major int + + // Minor indicates one half of the device ID which identifies a specific + // instance of device. + Minor int + + // Root of the mount within the filesystem. + Root string + + // Mountpoint indicates the mount point relative to the process's root. + Mountpoint string + + // Opts represents mount-specific options. + Opts string + + // Optional represents optional fields. + Optional string + + // Fstype indicates the type of filesystem, such as EXT3. + Fstype string + + // Source indicates filesystem specific information or "none". + Source string + + // VfsOpts represents per super block options. + VfsOpts string +} + +type byMountpoint []*Info + +func (by byMountpoint) Len() int { + return len(by) +} + +func (by byMountpoint) Less(i, j int) bool { + return by[i].Mountpoint < by[j].Mountpoint +} + +func (by byMountpoint) Swap(i, j int) { + by[i], by[j] = by[j], by[i] +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mountinfo_freebsd.go b/vendor/github.com/docker/docker/pkg/mount/mountinfo_freebsd.go new file mode 100644 index 0000000000..4f32edcd90 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mountinfo_freebsd.go @@ -0,0 +1,41 @@ +package mount + +/* +#include +#include +#include +*/ +import "C" + +import ( + "fmt" + "reflect" + "unsafe" +) + +// Parse /proc/self/mountinfo because comparing Dev and ino does not work from +// bind mounts. +func parseMountTable() ([]*Info, error) { + var rawEntries *C.struct_statfs + + count := int(C.getmntinfo(&rawEntries, C.MNT_WAIT)) + if count == 0 { + return nil, fmt.Errorf("Failed to call getmntinfo") + } + + var entries []C.struct_statfs + header := (*reflect.SliceHeader)(unsafe.Pointer(&entries)) + header.Cap = count + header.Len = count + header.Data = uintptr(unsafe.Pointer(rawEntries)) + + var out []*Info + for _, entry := range entries { + var mountinfo Info + mountinfo.Mountpoint = C.GoString(&entry.f_mntonname[0]) + mountinfo.Source = C.GoString(&entry.f_mntfromname[0]) + mountinfo.Fstype = C.GoString(&entry.f_fstypename[0]) + out = append(out, &mountinfo) + } + return out, nil +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mountinfo_linux.go b/vendor/github.com/docker/docker/pkg/mount/mountinfo_linux.go new file mode 100644 index 0000000000..be69fee1d7 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mountinfo_linux.go @@ -0,0 +1,95 @@ +// +build linux + +package mount + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" +) + +const ( + /* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) + + (1) mount ID: unique identifier of the mount (may be reused after umount) + (2) parent ID: ID of parent (or of self for the top of the mount tree) + (3) major:minor: value of st_dev for files on filesystem + (4) root: root of the mount within the filesystem + (5) mount point: mount point relative to the process's root + (6) mount options: per mount options + (7) optional fields: zero or more fields of the form "tag[:value]" + (8) separator: marks the end of the optional fields + (9) filesystem type: name of filesystem of the form "type[.subtype]" + (10) mount source: filesystem specific information or "none" + (11) super options: per super block options*/ + mountinfoFormat = "%d %d %d:%d %s %s %s %s" +) + +// Parse /proc/self/mountinfo because comparing Dev and ino does not work from +// bind mounts +func parseMountTable() ([]*Info, error) { + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return nil, err + } + defer f.Close() + + return parseInfoFile(f) +} + +func parseInfoFile(r io.Reader) ([]*Info, error) { + var ( + s = bufio.NewScanner(r) + out = []*Info{} + ) + + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + var ( + p = &Info{} + text = s.Text() + optionalFields string + ) + + if _, err := fmt.Sscanf(text, mountinfoFormat, + &p.ID, &p.Parent, &p.Major, &p.Minor, + &p.Root, &p.Mountpoint, &p.Opts, &optionalFields); err != nil { + return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err) + } + // Safe as mountinfo encodes mountpoints with spaces as \040. + index := strings.Index(text, " - ") + postSeparatorFields := strings.Fields(text[index+3:]) + if len(postSeparatorFields) < 3 { + return nil, fmt.Errorf("Error found less than 3 fields post '-' in %q", text) + } + + if optionalFields != "-" { + p.Optional = optionalFields + } + + p.Fstype = postSeparatorFields[0] + p.Source = postSeparatorFields[1] + p.VfsOpts = strings.Join(postSeparatorFields[2:], " ") + out = append(out, p) + } + return out, nil +} + +// PidMountInfo collects the mounts for a specific process ID. If the process +// ID is unknown, it is better to use `GetMounts` which will inspect +// "/proc/self/mountinfo" instead. +func PidMountInfo(pid int) ([]*Info, error) { + f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid)) + if err != nil { + return nil, err + } + defer f.Close() + + return parseInfoFile(f) +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mountinfo_solaris.go b/vendor/github.com/docker/docker/pkg/mount/mountinfo_solaris.go new file mode 100644 index 0000000000..ad9ab57f8b --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mountinfo_solaris.go @@ -0,0 +1,37 @@ +// +build solaris,cgo + +package mount + +/* +#include +#include +*/ +import "C" + +import ( + "fmt" +) + +func parseMountTable() ([]*Info, error) { + mnttab := C.fopen(C.CString(C.MNTTAB), C.CString("r")) + if mnttab == nil { + return nil, fmt.Errorf("Failed to open %s", C.MNTTAB) + } + + var out []*Info + var mp C.struct_mnttab + + ret := C.getmntent(mnttab, &mp) + for ret == 0 { + var mountinfo Info + mountinfo.Mountpoint = C.GoString(mp.mnt_mountp) + mountinfo.Source = C.GoString(mp.mnt_special) + mountinfo.Fstype = C.GoString(mp.mnt_fstype) + mountinfo.Opts = C.GoString(mp.mnt_mntopts) + out = append(out, &mountinfo) + ret = C.getmntent(mnttab, &mp) + } + + C.fclose(mnttab) + return out, nil +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mountinfo_unsupported.go b/vendor/github.com/docker/docker/pkg/mount/mountinfo_unsupported.go new file mode 100644 index 0000000000..7fbcf19214 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mountinfo_unsupported.go @@ -0,0 +1,12 @@ +// +build !windows,!linux,!freebsd,!solaris freebsd,!cgo solaris,!cgo + +package mount + +import ( + "fmt" + "runtime" +) + +func parseMountTable() ([]*Info, error) { + return nil, fmt.Errorf("mount.parseMountTable is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} diff --git a/vendor/github.com/docker/docker/pkg/mount/mountinfo_windows.go b/vendor/github.com/docker/docker/pkg/mount/mountinfo_windows.go new file mode 100644 index 0000000000..dab8a37ed0 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/mountinfo_windows.go @@ -0,0 +1,6 @@ +package mount + +func parseMountTable() ([]*Info, error) { + // Do NOT return an error! + return nil, nil +} diff --git a/vendor/github.com/docker/docker/pkg/mount/sharedsubtree_linux.go b/vendor/github.com/docker/docker/pkg/mount/sharedsubtree_linux.go new file mode 100644 index 0000000000..8ceec84bc6 --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/sharedsubtree_linux.go @@ -0,0 +1,69 @@ +// +build linux + +package mount + +// MakeShared ensures a mounted filesystem has the SHARED mount option enabled. +// See the supported options in flags.go for further reference. +func MakeShared(mountPoint string) error { + return ensureMountedAs(mountPoint, "shared") +} + +// MakeRShared ensures a mounted filesystem has the RSHARED mount option enabled. +// See the supported options in flags.go for further reference. +func MakeRShared(mountPoint string) error { + return ensureMountedAs(mountPoint, "rshared") +} + +// MakePrivate ensures a mounted filesystem has the PRIVATE mount option enabled. +// See the supported options in flags.go for further reference. +func MakePrivate(mountPoint string) error { + return ensureMountedAs(mountPoint, "private") +} + +// MakeRPrivate ensures a mounted filesystem has the RPRIVATE mount option +// enabled. See the supported options in flags.go for further reference. +func MakeRPrivate(mountPoint string) error { + return ensureMountedAs(mountPoint, "rprivate") +} + +// MakeSlave ensures a mounted filesystem has the SLAVE mount option enabled. +// See the supported options in flags.go for further reference. +func MakeSlave(mountPoint string) error { + return ensureMountedAs(mountPoint, "slave") +} + +// MakeRSlave ensures a mounted filesystem has the RSLAVE mount option enabled. +// See the supported options in flags.go for further reference. +func MakeRSlave(mountPoint string) error { + return ensureMountedAs(mountPoint, "rslave") +} + +// MakeUnbindable ensures a mounted filesystem has the UNBINDABLE mount option +// enabled. See the supported options in flags.go for further reference. +func MakeUnbindable(mountPoint string) error { + return ensureMountedAs(mountPoint, "unbindable") +} + +// MakeRUnbindable ensures a mounted filesystem has the RUNBINDABLE mount +// option enabled. See the supported options in flags.go for further reference. +func MakeRUnbindable(mountPoint string) error { + return ensureMountedAs(mountPoint, "runbindable") +} + +func ensureMountedAs(mountPoint, options string) error { + mounted, err := Mounted(mountPoint) + if err != nil { + return err + } + + if !mounted { + if err := Mount(mountPoint, mountPoint, "none", "bind,rw"); err != nil { + return err + } + } + if _, err = Mounted(mountPoint); err != nil { + return err + } + + return ForceMount("", mountPoint, "none", options) +} diff --git a/vendor/github.com/docker/docker/pkg/mount/sharedsubtree_solaris.go b/vendor/github.com/docker/docker/pkg/mount/sharedsubtree_solaris.go new file mode 100644 index 0000000000..09f6b03cbc --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/mount/sharedsubtree_solaris.go @@ -0,0 +1,58 @@ +// +build solaris + +package mount + +// MakeShared ensures a mounted filesystem has the SHARED mount option enabled. +// See the supported options in flags.go for further reference. +func MakeShared(mountPoint string) error { + return ensureMountedAs(mountPoint, "shared") +} + +// MakeRShared ensures a mounted filesystem has the RSHARED mount option enabled. +// See the supported options in flags.go for further reference. +func MakeRShared(mountPoint string) error { + return ensureMountedAs(mountPoint, "rshared") +} + +// MakePrivate ensures a mounted filesystem has the PRIVATE mount option enabled. +// See the supported options in flags.go for further reference. +func MakePrivate(mountPoint string) error { + return ensureMountedAs(mountPoint, "private") +} + +// MakeRPrivate ensures a mounted filesystem has the RPRIVATE mount option +// enabled. See the supported options in flags.go for further reference. +func MakeRPrivate(mountPoint string) error { + return ensureMountedAs(mountPoint, "rprivate") +} + +// MakeSlave ensures a mounted filesystem has the SLAVE mount option enabled. +// See the supported options in flags.go for further reference. +func MakeSlave(mountPoint string) error { + return ensureMountedAs(mountPoint, "slave") +} + +// MakeRSlave ensures a mounted filesystem has the RSLAVE mount option enabled. +// See the supported options in flags.go for further reference. +func MakeRSlave(mountPoint string) error { + return ensureMountedAs(mountPoint, "rslave") +} + +// MakeUnbindable ensures a mounted filesystem has the UNBINDABLE mount option +// enabled. See the supported options in flags.go for further reference. +func MakeUnbindable(mountPoint string) error { + return ensureMountedAs(mountPoint, "unbindable") +} + +// MakeRUnbindable ensures a mounted filesystem has the RUNBINDABLE mount +// option enabled. See the supported options in flags.go for further reference. +func MakeRUnbindable(mountPoint string) error { + return ensureMountedAs(mountPoint, "runbindable") +} + +func ensureMountedAs(mountPoint, options string) error { + // TODO: Solaris does not support bind mounts. + // Evaluate lofs and also look at the relevant + // mount flags to be supported. + return nil +} diff --git a/vendor/github.com/docker/docker/pkg/system/rm.go b/vendor/github.com/docker/docker/pkg/system/rm.go new file mode 100644 index 0000000000..ca0621f04f --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/system/rm.go @@ -0,0 +1,80 @@ +package system + +import ( + "os" + "syscall" + "time" + + "github.com/docker/docker/pkg/mount" + "github.com/pkg/errors" +) + +// EnsureRemoveAll wraps `os.RemoveAll` to check for specific errors that can +// often be remedied. +// Only use `EnsureRemoveAll` if you really want to make every effort to remove +// a directory. +// +// Because of the way `os.Remove` (and by extension `os.RemoveAll`) works, there +// can be a race between reading directory entries and then actually attempting +// to remove everything in the directory. +// These types of errors do not need to be returned since it's ok for the dir to +// be gone we can just retry the remove operation. +// +// This should not return a `os.ErrNotExist` kind of error under any cirucmstances +func EnsureRemoveAll(dir string) error { + notExistErr := make(map[string]bool) + + // track retries + exitOnErr := make(map[string]int) + maxRetry := 5 + + // Attempt to unmount anything beneath this dir first + mount.RecursiveUnmount(dir) + + for { + err := os.RemoveAll(dir) + if err == nil { + return err + } + + pe, ok := err.(*os.PathError) + if !ok { + return err + } + + if os.IsNotExist(err) { + if notExistErr[pe.Path] { + return err + } + notExistErr[pe.Path] = true + + // There is a race where some subdir can be removed but after the parent + // dir entries have been read. + // So the path could be from `os.Remove(subdir)` + // If the reported non-existent path is not the passed in `dir` we + // should just retry, but otherwise return with no error. + if pe.Path == dir { + return nil + } + continue + } + + if pe.Err != syscall.EBUSY { + return err + } + + if mounted, _ := mount.Mounted(pe.Path); mounted { + if e := mount.Unmount(pe.Path); e != nil { + if mounted, _ := mount.Mounted(pe.Path); mounted { + return errors.Wrapf(e, "error while removing %s", dir) + } + } + } + + if exitOnErr[pe.Path] == maxRetry { + return err + } + exitOnErr[pe.Path]++ + time.Sleep(100 * time.Millisecond) + } +} diff --git a/vendor/github.com/docker/docker/vendor.conf b/vendor/github.com/docker/docker/vendor.conf index b5b7d336a0..adfa5982cd 100644 --- a/vendor/github.com/docker/docker/vendor.conf +++ b/vendor/github.com/docker/docker/vendor.conf @@ -25,7 +25,7 @@ github.com/imdario/mergo 0.2.1 golang.org/x/sync de49d9dcd27d4f764488181bea099dfe6179bcf0 #get libnetwork packages -github.com/docker/libnetwork cace103704768d39bd88a23d0df76df125a0e39a +github.com/docker/libnetwork 6786135bf7de08ec26a72a6f7e4291d27d113a3f github.com/docker/go-events 18b43f1bc85d9cdd42c05a6cd2d444c7a200a894 github.com/armon/go-radix e39d623f12e8e41c7b5529e9a9dd67a1e2261f80 github.com/armon/go-metrics eb0af217e5e9747e41dd5303755356b62d28e3ec @@ -60,7 +60,7 @@ google.golang.org/grpc v1.0.4 github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f # When updating, also update RUNC_COMMIT in hack/dockerfile/binaries-commits accordingly -github.com/opencontainers/runc b6b70e53451794e8333e9b602cc096b47a20bd0f +github.com/opencontainers/runc b6b70e53451794e8333e9b602cc096b47a20bd0f github.com/opencontainers/runtime-spec v1.0.0-rc5 # specs github.com/seccomp/libseccomp-golang 32f571b70023028bd57d9288c20efbcb237f3ce0 @@ -134,13 +134,4 @@ github.com/Nvveen/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c https://github. # metrics github.com/docker/go-metrics 8fd5772bf1584597834c6f7961a530f06cbfbb87 -<<<<<<< 0f6f1eafe31c4beceba31490017878b80b609331 -# composefile -github.com/mitchellh/mapstructure f3009df150dadf309fdee4a54ed65c124afad715 -github.com/xeipuuv/gojsonpointer e0fe6f68307607d540ed8eac07a342c33fa1b54a -github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45 -github.com/xeipuuv/gojsonschema 93e72a773fade158921402d6a24c819b48aba29d -gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6 -======= ->>>>>>> Update docker dependency github.com/opencontainers/selinux v1.0.0-rc1