mirror of https://github.com/docker/cli.git
Merge pull request #45 from aaronlehmann/configs
Add support for configs
This commit is contained in:
commit
05267be05b
|
@ -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),
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||
configInspectFunc func(string) (swarm.Config, []byte, error)
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
configRemoveFunc func(string) error
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if c.configCreateFunc != nil {
|
||||
return c.configCreateFunc(spec)
|
||||
}
|
||||
return types.ConfigCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
if c.configInspectFunc != nil {
|
||||
return c.configInspectFunc(id)
|
||||
}
|
||||
return swarm.Config{}, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(options)
|
||||
}
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string) error {
|
||||
if c.configRemoveFunc != nil {
|
||||
return c.configRemoveFunc(name)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
)
|
||||
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
// nolint: interfacer
|
||||
func NewConfigCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Tags: map[string]string{"version": "1.30"},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCli),
|
||||
newConfigCreateCommand(dockerCli),
|
||||
newConfigInspectCommand(dockerCli),
|
||||
newConfigRemoveCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
file string
|
||||
labels opts.ListOpts
|
||||
}
|
||||
|
||||
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
createOpts := createOptions{
|
||||
labels: opts.NewListOpts(opts.ValidateEnv),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] CONFIG file|-",
|
||||
Short: "Create a configuration file from a file or STDIN as content",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
createOpts.name = args[0]
|
||||
createOpts.file = args[1]
|
||||
return runConfigCreate(dockerCli, createOpts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConfigCreate(dockerCli command.Cli, options createOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var in io.Reader = dockerCli.In()
|
||||
if options.file != "-" {
|
||||
file, err := system.OpenSequential(options.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in = file
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
configData, err := ioutil.ReadAll(in)
|
||||
if err != nil {
|
||||
return errors.Errorf("Error reading content from %q: %v", options.file, err)
|
||||
}
|
||||
|
||||
spec := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
|
||||
r, err := client.ConfigCreate(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const configDataFile = "config-create-with-name.golden"
|
||||
|
||||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too_few"},
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
configCreateFunc: func(configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCreateWithName(t *testing.T) {
|
||||
name := "foo"
|
||||
buf := new(bytes.Buffer)
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
actual = spec.Data
|
||||
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
expected := golden.Get(t, actual, configDataFile)
|
||||
assert.Equal(t, string(expected), string(actual))
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||
}
|
||||
|
||||
func TestConfigCreateWithLabels(t *testing.T) {
|
||||
expectedLabels := map[string]string{
|
||||
"lbl1": "Label-foo",
|
||||
"lbl2": "Label-bar",
|
||||
}
|
||||
name := "foo"
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(spec.Labels, expectedLabels) {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels)
|
||||
}
|
||||
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
cmd.Flags().Set("label", "lbl1=Label-foo")
|
||||
cmd.Flags().Set("label", "lbl2=Label-bar")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
names []string
|
||||
format string
|
||||
}
|
||||
|
||||
func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||
Short: "Display detailed information on one or more configuration files",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.names = args
|
||||
return runConfigInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConfigInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
getRef := func(id string) (interface{}, []byte, error) {
|
||||
return client.ConfigInspectWithRaw(ctx, id)
|
||||
}
|
||||
|
||||
return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef)
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigInspectErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
expectedError: "requires at least 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
flags: map[string]string{
|
||||
"format": "{{invalid format}}",
|
||||
},
|
||||
expectedError: "Template parsing error",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||
if configID == "foo" {
|
||||
return *Config(ConfigName("foo")), nil, nil
|
||||
}
|
||||
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "single-config",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||
if name != "foo" {
|
||||
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return *Config(ConfigID("ID-foo"), ConfigName("foo")), nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-configs-with-labels",
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||
return *Config(ConfigID("ID-"+name), ConfigName(name), ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspectWithFormat(t *testing.T) {
|
||||
configInspectFunc := func(name string) (swarm.Config, []byte, error) {
|
||||
return *Config(ConfigName("foo"), ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
format string
|
||||
args []string
|
||||
configInspectFunc func(name string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple-template",
|
||||
format: "{{.Spec.Name}}",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: configInspectFunc,
|
||||
},
|
||||
{
|
||||
name: "json-template",
|
||||
format: "{{json .Spec.Labels}}",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: configInspectFunc,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.Flags().Set("format", tc.format)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS]",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runConfigList(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVarP(&opts.format, "format", "", "", "Pretty-print configs using a Go template")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConfigList(dockerCli command.Cli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: opts.filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().ConfigFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewConfigFormat(format, opts.quiet),
|
||||
}
|
||||
return formatter.ConfigWrite(configCtx, configs)
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"foo"},
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigListCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configListFunc: tc.configListFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"),
|
||||
ConfigName("foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-bar"),
|
||||
ConfigName("bar"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.SetOutput(buf)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-quiet-option.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{
|
||||
ConfigFormat: "{{ .Name }} {{ .Labels }}",
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-config-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithFilter(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Equal(t, "foo", options.Filters.Get("name")[0])
|
||||
assert.Equal(t, "lbl1=Label-bar", options.Filters.Get("label")[0])
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"),
|
||||
ConfigName("foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-bar"),
|
||||
ConfigName("bar"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("filter", "name=foo")
|
||||
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
names []string
|
||||
}
|
||||
|
||||
func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more configuration files",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := removeOptions{
|
||||
names: args,
|
||||
}
|
||||
return runConfigRemove(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runConfigRemove(dockerCli command.Cli, opts removeOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
|
||||
for _, name := range opts.names {
|
||||
if err := client.ConfigRemove(ctx, name); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), name)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configRemoveFunc func(string) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires at least 1 argument(s).",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.Errorf("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigRemoveCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: tc.configRemoveFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
buf := new(bytes.Buffer)
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return nil
|
||||
},
|
||||
}, buf)
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n"))
|
||||
assert.Equal(t, names, removedConfigs)
|
||||
}
|
||||
|
||||
func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
buf := new(bytes.Buffer)
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return errors.Errorf("error removing config: %s", name)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}, buf)
|
||||
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
assert.EqualError(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Equal(t, names, removedConfigs)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
config_foo_bar
|
|
@ -0,0 +1 @@
|
|||
{"label1":"label-foo"}
|
1
cli/command/config/testdata/config-inspect-with-format.simple-template.golden
vendored
Normal file
1
cli/command/config/testdata/config-inspect-with-format.simple-template.golden
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
foo
|
26
cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden
vendored
Normal file
26
cli/command/config/testdata/config-inspect-without-format.multiple-configs-with-labels.golden
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"ID": "ID-foo",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "foo",
|
||||
"Labels": {
|
||||
"label1": "label-foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ID-bar",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "bar",
|
||||
"Labels": {
|
||||
"label1": "label-foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
12
cli/command/config/testdata/config-inspect-without-format.single-config.golden
vendored
Normal file
12
cli/command/config/testdata/config-inspect-without-format.single-config.golden
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"ID": "ID-foo",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "foo",
|
||||
"Labels": null
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,2 @@
|
|||
foo
|
||||
bar label=label-bar
|
|
@ -0,0 +1,3 @@
|
|||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
|
@ -0,0 +1,2 @@
|
|||
foo
|
||||
bar label=label-bar
|
|
@ -0,0 +1,2 @@
|
|||
ID-foo
|
||||
ID-bar
|
|
@ -0,0 +1,3 @@
|
|||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
|
@ -0,0 +1,100 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
|
||||
configIDHeader = "ID"
|
||||
configCreatedHeader = "CREATED"
|
||||
configUpdatedHeader = "UPDATED"
|
||||
)
|
||||
|
||||
// NewConfigFormat returns a Format for rendering using a config Context
|
||||
func NewConfigFormat(source string, quiet bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
if quiet {
|
||||
return defaultQuietFormat
|
||||
}
|
||||
return defaultConfigTableFormat
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
// ConfigWrite writes the context
|
||||
func ConfigWrite(ctx Context, configs []swarm.Config) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, config := range configs {
|
||||
configCtx := &configContext{c: config}
|
||||
if err := format(configCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newConfigContext(), render)
|
||||
}
|
||||
|
||||
func newConfigContext() *configContext {
|
||||
cCtx := &configContext{}
|
||||
|
||||
cCtx.header = map[string]string{
|
||||
"ID": configIDHeader,
|
||||
"Name": nameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": labelsHeader,
|
||||
}
|
||||
return cCtx
|
||||
}
|
||||
|
||||
type configContext struct {
|
||||
HeaderContext
|
||||
c swarm.Config
|
||||
}
|
||||
|
||||
func (c *configContext) MarshalJSON() ([]byte, error) {
|
||||
return marshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *configContext) ID() string {
|
||||
return c.c.ID
|
||||
}
|
||||
|
||||
func (c *configContext) Name() string {
|
||||
return c.c.Spec.Annotations.Name
|
||||
}
|
||||
|
||||
func (c *configContext) CreatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.c.Meta.CreatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *configContext) UpdatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.c.Meta.UpdatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *configContext) Labels() string {
|
||||
mapLabels := c.c.Spec.Annotations.Labels
|
||||
if mapLabels == nil {
|
||||
return ""
|
||||
}
|
||||
var joinLabels []string
|
||||
for k, v := range mapLabels {
|
||||
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
func (c *configContext) Label(name string) string {
|
||||
if c.c.Spec.Annotations.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
return c.c.Spec.Annotations.Labels[name]
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfigContextFormatWrite(t *testing.T) {
|
||||
// Check default output format (verbose and non-verbose mode) for table headers
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
// Errors
|
||||
{
|
||||
Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table format
|
||||
{Context{Format: NewConfigFormat("table", false)},
|
||||
`ID NAME CREATED UPDATED
|
||||
1 passwords Less than a second ago Less than a second ago
|
||||
2 id_rsa Less than a second ago Less than a second ago
|
||||
`},
|
||||
{Context{Format: NewConfigFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
passwords
|
||||
id_rsa
|
||||
`},
|
||||
{Context{Format: NewConfigFormat("{{.ID}}-{{.Name}}", false)},
|
||||
`1-passwords
|
||||
2-id_rsa
|
||||
`},
|
||||
}
|
||||
|
||||
configs := []swarm.Config{
|
||||
{ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}},
|
||||
{ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
|
||||
}
|
||||
for _, testcase := range cases {
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
if err := ConfigWrite(testcase.context, configs); err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ const (
|
|||
secretUpdatedHeader = "UPDATED"
|
||||
)
|
||||
|
||||
// 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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -68,4 +68,5 @@ type ContainerSpec struct {
|
|||
Hosts []string `json:",omitempty"`
|
||||
DNSConfig *DNSConfig `json:",omitempty"`
|
||||
Secrets []*SecretReference `json:",omitempty"`
|
||||
Configs []*ConfigReference `json:",omitempty"`
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// +build freebsd,cgo
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
#include <sys/mount.h>
|
||||
*/
|
||||
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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package mount
|
||||
|
||||
/*
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/_iovec.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
*/
|
||||
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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// +build solaris,cgo
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// #include <stdlib.h>
|
||||
// #include <stdio.h>
|
||||
// #include <sys/mount.h>
|
||||
// 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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package mount
|
||||
|
||||
// Info reveals information about a particular mounted filesystem. This
|
||||
// struct is populated from the content in the /proc/<pid>/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]
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package mount
|
||||
|
||||
/*
|
||||
#include <sys/param.h>
|
||||
#include <sys/ucred.h>
|
||||
#include <sys/mount.h>
|
||||
*/
|
||||
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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// +build solaris,cgo
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
#include <stdio.h>
|
||||
#include <sys/mnttab.h>
|
||||
*/
|
||||
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
|
||||
}
|
12
vendor/github.com/docker/docker/pkg/mount/mountinfo_unsupported.go
generated
vendored
Normal file
12
vendor/github.com/docker/docker/pkg/mount/mountinfo_unsupported.go
generated
vendored
Normal file
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package mount
|
||||
|
||||
func parseMountTable() ([]*Info, error) {
|
||||
// Do NOT return an error!
|
||||
return nil, nil
|
||||
}
|
|
@ -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)
|
||||
}
|
58
vendor/github.com/docker/docker/pkg/mount/sharedsubtree_solaris.go
generated
vendored
Normal file
58
vendor/github.com/docker/docker/pkg/mount/sharedsubtree_solaris.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue