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

This commit is contained in:
Andrew Hsu
2017-11-28 17:01:27 -08:00
301 changed files with 9640 additions and 5108 deletions

View File

@ -5,5 +5,5 @@ cli/command/stack/** @dnephin @vdemeester
cli/compose/** @dnephin @vdemeester
contrib/completion/bash/** @albers
contrib/completion/zsh/** @sdurrheimer
docs/** @mstanleyjones @vdemeester @thaJeztah
docs/** @mistyhacks @vdemeester @thaJeztah
scripts/** @dnephin

View File

@ -133,7 +133,7 @@
[people.misty]
Name = "Misty Stanley-Jones"
Email = "misty@docker.com"
GitHub = "mstanleyjones"
GitHub = "mistyhacks"
[people.mlaventure]
Name = "Kenfe-Mickaël Laventure"

View File

@ -1 +1 @@
17.11.0-dev
17.12.0-dev

View File

@ -12,12 +12,15 @@ import (
type fakeClient struct {
client.Client
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
@ -71,3 +74,24 @@ func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
}
return types.Info{}, nil
}
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
if f.containerStatPathFunc != nil {
return f.containerStatPathFunc(container, path)
}
return types.ContainerPathStat{}, nil
}
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
if f.containerCopyFromFunc != nil {
return f.containerCopyFromFunc(container, srcPath)
}
return nil, types.ContainerPathStat{}, nil
}
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
if f.logFunc != nil {
return f.logFunc(container, options)
}
return nil, nil
}

View File

@ -26,13 +26,17 @@ type copyOptions struct {
type copyDirection int
const (
fromContainer copyDirection = (1 << iota)
fromContainer copyDirection = 1 << iota
toContainer
acrossContainers = fromContainer | toContainer
)
type cpConfig struct {
followLink bool
copyUIDGID bool
sourcePath string
destPath string
container string
}
// NewCopyCommand creates a new `docker cp` command
@ -65,58 +69,57 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
return cmd
}
func runCopy(dockerCli command.Cli, opts copyOptions) error {
srcContainer, srcPath := splitCpArg(opts.source)
dstContainer, dstPath := splitCpArg(opts.destination)
destContainer, destPath := splitCpArg(opts.destination)
copyConfig := cpConfig{
followLink: opts.followLink,
copyUIDGID: opts.copyUIDGID,
sourcePath: srcPath,
destPath: destPath,
}
var direction copyDirection
if srcContainer != "" {
direction |= fromContainer
copyConfig.container = srcContainer
}
if dstContainer != "" {
if destContainer != "" {
direction |= toContainer
}
cpParam := &cpConfig{
followLink: opts.followLink,
copyConfig.container = destContainer
}
ctx := context.Background()
switch direction {
case fromContainer:
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam)
return copyFromContainer(ctx, dockerCli, copyConfig)
case toContainer:
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID)
return copyToContainer(ctx, dockerCli, copyConfig)
case acrossContainers:
// Copying between containers isn't supported.
return errors.New("copying between containers is not supported")
default:
// User didn't specify any container.
return errors.New("must specify at least one container source")
}
}
func statContainerPath(ctx context.Context, dockerCli command.Cli, containerName, path string) (types.ContainerPathStat, error) {
return dockerCli.Client().ContainerStatPath(ctx, containerName, path)
}
func resolveLocalPath(localPath string) (absPath string, err error) {
if absPath, err = filepath.Abs(localPath); err != nil {
return
}
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
}
func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
dstPath := copyConfig.destPath
srcPath := copyConfig.sourcePath
if dstPath != "-" {
// Get an absolute destination path.
dstPath, err = resolveLocalPath(dstPath)
@ -125,10 +128,11 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
}
}
client := dockerCli.Client()
// if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string
if cpParam.followLink {
srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath)
if copyConfig.followLink {
srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
// If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
@ -145,20 +149,17 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
}
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath)
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
if err != nil {
return err
}
defer content.Close()
if dstPath == "-" {
// Send the response to STDOUT.
_, err = io.Copy(os.Stdout, content)
_, err = io.Copy(dockerCli.Out(), content)
return err
}
// Prepare source copy info.
srcInfo := archive.CopyInfo{
Path: srcPath,
Exists: true,
@ -171,13 +172,17 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
}
// See comments in the implementation of `archive.CopyTo` for exactly what
// goes into deciding how and whether the source archive needs to be
// altered for the correct copy behavior.
return archive.CopyTo(preArchive, srcInfo, dstPath)
}
func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) {
// In order to get the copy behavior right, we need to know information
// about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is.
func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
srcPath := copyConfig.sourcePath
dstPath := copyConfig.destPath
if srcPath != "-" {
// Get an absolute source path.
srcPath, err = resolveLocalPath(srcPath)
@ -186,14 +191,10 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
}
}
// In order to get the copy behavior right, we need to know information
// about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is.
client := dockerCli.Client()
// Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath}
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath)
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
@ -205,7 +206,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
}
dstInfo.Path = linkTarget
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget)
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
}
// Ignore any error and assume that the parent directory of the destination
@ -224,15 +225,14 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
)
if srcPath == "-" {
// Use STDIN.
content = os.Stdin
resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir {
return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath)
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
}
} else {
// Prepare source copy info.
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
if err != nil {
return err
}
@ -267,10 +267,9 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
options := types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyUIDGID,
CopyUIDGID: copyConfig.copyUIDGID,
}
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
}
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be

View File

@ -0,0 +1,160 @@
package container
import (
"io"
"io/ioutil"
"runtime"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunCopyWithInvalidArguments(t *testing.T) {
var testcases = []struct {
doc string
options copyOptions
expectedErr string
}{
{
doc: "copy between container",
options: copyOptions{
source: "first:/path",
destination: "second:/path",
},
expectedErr: "copying between containers is not supported",
},
{
doc: "copy without a container",
options: copyOptions{
source: "./source",
destination: "./dest",
},
expectedErr: "must specify at least one container source",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
err := runCopy(test.NewFakeCli(nil), testcase.options)
assert.EqualError(t, err, testcase.expectedErr)
})
}
}
func TestRunCopyFromContainerToStdout(t *testing.T) {
tarContent := "the tar content"
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Equal(t, "container", container)
return ioutil.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
},
}
options := copyOptions{source: "container:/path", destination: "-"}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
require.NoError(t, err)
assert.Equal(t, tarContent, cli.OutBuffer().String())
assert.Equal(t, "", cli.ErrBuffer().String())
}
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
destDir := fs.NewDir(t, "cp-test",
fs.WithFile("file1", "content\n"))
defer destDir.Remove()
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Equal(t, "container", container)
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, types.ContainerPathStat{}, err
},
}
options := copyOptions{source: "container:/path", destination: destDir.Path()}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
require.NoError(t, err)
assert.Equal(t, "", cli.OutBuffer().String())
assert.Equal(t, "", cli.ErrBuffer().String())
content, err := ioutil.ReadFile(destDir.Join("file1"))
require.NoError(t, err)
assert.Equal(t, "content\n", string(content))
}
func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.T) {
destDir := fs.NewDir(t, "cp-test",
fs.WithFile("file1", "content\n"))
defer destDir.Remove()
fakeClient := &fakeClient{
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
assert.Equal(t, "container", container)
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
return readCloser, types.ContainerPathStat{}, err
},
}
options := copyOptions{
source: "container:/path",
destination: destDir.Join("missing", "foo"),
}
cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options)
testutil.ErrorContains(t, err, destDir.Join("missing"))
}
func TestSplitCpArg(t *testing.T) {
var testcases = []struct {
doc string
path string
os string
expectedContainer string
expectedPath string
}{
{
doc: "absolute path with colon",
os: "linux",
path: "/abs/path:withcolon",
expectedPath: "/abs/path:withcolon",
},
{
doc: "relative path with colon",
path: "./relative:path",
expectedPath: "./relative:path",
},
{
doc: "absolute path with drive",
os: "windows",
path: `d:\abs\path`,
expectedPath: `d:\abs\path`,
},
{
doc: "no separator",
path: "relative/path",
expectedPath: "relative/path",
},
{
doc: "with separator",
path: "container:/opt/foo",
expectedPath: "/opt/foo",
expectedContainer: "container",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
skip.IfCondition(t, testcase.os != "" && testcase.os != runtime.GOOS)
container, path := splitCpArg(testcase.path)
assert.Equal(t, testcase.expectedContainer, container)
assert.Equal(t, testcase.expectedPath, path)
})
}
}

View File

@ -21,7 +21,8 @@ import (
)
type createOptions struct {
name string
name string
platform string
}
// NewCreateCommand creates a new cobra.Command for `docker create`
@ -51,6 +52,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
// with hostname
flags.Bool("help", false, "Print usage")
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags)
copts = addFlags(flags)
return cmd
@ -62,7 +64,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name)
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name, opts.platform)
if err != nil {
return err
}
@ -70,7 +72,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
return nil
}
func pullImage(ctx context.Context, dockerCli command.Cli, image string, out io.Writer) error {
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
@ -90,6 +92,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, image string, out io.
options := types.ImageCreateOptions{
RegistryAuth: encodedAuth,
Platform: platform,
}
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
@ -155,7 +158,7 @@ func newCIDFile(path string) (*cidFile, error) {
return &cidFile{path: path, file: f}, nil
}
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) {
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string, platform string) (*container.ContainerCreateCreatedBody, error) {
config := containerConfig.Config
hostConfig := containerConfig.HostConfig
networkingConfig := containerConfig.NetworkingConfig
@ -198,7 +201,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
if err := pullImage(ctx, dockerCli, config.Image, platform, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {

View File

@ -5,6 +5,7 @@ import (
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
@ -106,7 +107,7 @@ func TestCreateContainerPullsImageIfMissing(t *testing.T) {
},
HostConfig: &container.HostConfig{},
}
body, err := createContainer(context.Background(), cli, config, "name")
body, err := createContainer(context.Background(), cli, config, "name", runtime.GOOS)
require.NoError(t, err)
expected := container.ContainerCreateCreatedBody{ID: containerID}
assert.Equal(t, expected, *body)

View File

@ -14,6 +14,7 @@ import (
type logsOptions struct {
follow bool
since string
until string
timestamps bool
details bool
tail string
@ -38,6 +39,8 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
flags.SetAnnotation("until", "version", []string{"1.35"})
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
@ -51,6 +54,7 @@ func runLogs(dockerCli command.Cli, opts *logsOptions) error {
ShowStdout: true,
ShowStderr: true,
Since: opts.since,
Until: opts.until,
Timestamps: opts.timestamps,
Follow: opts.follow,
Tail: opts.tail,

View File

@ -0,0 +1,62 @@
package container
import (
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
)
var logFn = func(expectedOut string) func(string, types.ContainerLogsOptions) (io.ReadCloser, error) {
return func(container string, opts types.ContainerLogsOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader(expectedOut)), nil
}
}
func TestRunLogs(t *testing.T) {
inspectFn := func(containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{
Config: &container.Config{Tty: true},
ContainerJSONBase: &types.ContainerJSONBase{State: &types.ContainerState{Running: false}},
}, nil
}
var testcases = []struct {
doc string
options *logsOptions
client fakeClient
expectedError string
expectedOut string
expectedErr string
}{
{
doc: "successful logs",
expectedOut: "foo",
options: &logsOptions{},
client: fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client)
err := runLogs(cli, testcase.options)
if testcase.expectedError != "" {
testutil.ErrorContains(t, err, testcase.expectedError)
} else {
if !assert.NoError(t, err) {
return
}
}
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
})
}
}

View File

@ -29,6 +29,7 @@ type runOptions struct {
sigProxy bool
name string
detachKeys string
platform string
}
// NewRunCommand create a new `docker run` command
@ -62,6 +63,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
// with hostname
flags.Bool("help", false, "Print usage")
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags)
copts = addFlags(flags)
return cmd
@ -160,7 +162,7 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
ctx, cancelFun := context.WithCancel(context.Background())
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name)
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name, opts.platform)
if err != nil {
reportError(stderr, cmdPath, err.Error(), true)
return runStartContainerErr(err)

View File

@ -64,6 +64,7 @@ type buildOptions struct {
target string
imageIDFile string
stream bool
platform string
}
// dockerfileFromStdin returns true when the user specified that the Dockerfile
@ -135,6 +136,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
command.AddTrustVerificationFlags(flags)
command.AddPlatformFlag(flags, &options.platform)
flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
flags.SetAnnotation("squash", "experimental", nil)
@ -305,8 +307,8 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
progressOutput = &lastProgressOutput{output: progressOutput}
}
// if up to this point nothing has set the context then we must have have
// another way for sending it(streaming) and set the context to the Dockerfile
// if up to this point nothing has set the context then we must have another
// way for sending it(streaming) and set the context to the Dockerfile
if dockerfileCtx != nil && buildCtx == nil {
buildCtx = dockerfileCtx
}
@ -374,6 +376,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
ExtraHosts: options.extraHosts.GetAll(),
Target: options.target,
RemoteContext: remote,
Platform: options.platform,
}
if s != nil {

View File

@ -14,8 +14,9 @@ import (
)
type pullOptions struct {
remote string
all bool
remote string
all bool
platform string
}
// NewPullCommand creates a new `docker pull` command
@ -35,6 +36,8 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags)
return cmd
@ -63,9 +66,9 @@ func runPull(cli command.Cli, opts pullOptions) error {
// Check if reference has a digest
_, isCanonical := distributionRef.(reference.Canonical)
if command.IsTrusted() && !isCanonical {
err = trustedPull(ctx, cli, imgRefAndAuth)
err = trustedPull(ctx, cli, imgRefAndAuth, opts.platform)
} else {
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all)
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all, opts.platform)
}
if err != nil {
if strings.Contains(err.Error(), "when fetching 'plugin'") {

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -56,9 +57,13 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
}
var errs []string
var fatalErr = false
for _, img := range images {
dels, err := client.ImageRemove(ctx, img, options)
if err != nil {
if !apiclient.IsErrNotFound(err) {
fatalErr = true
}
errs = append(errs, err.Error())
} else {
for _, del := range dels {
@ -73,7 +78,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
if len(errs) > 0 {
msg := strings.Join(errs, "\n")
if !opts.force {
if !opts.force || fatalErr {
return errors.New(msg)
}
fmt.Fprintf(dockerCli.Err(), msg)

View File

@ -13,6 +13,18 @@ import (
"github.com/stretchr/testify/assert"
)
type notFound struct {
imageID string
}
func (n notFound) Error() string {
return fmt.Sprintf("Error: No such image: %s", n.imageID)
}
func (n notFound) NotFound() bool {
return true
}
func TestNewRemoveCommandAlias(t *testing.T) {
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
assert.True(t, cmd.HasAlias("rmi"))
@ -31,6 +43,15 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name: "wrong args",
expectedError: "requires at least 1 argument.",
},
{
name: "ImageRemove fail with force option",
args: []string{"-f", "image1"},
expectedError: "error removing image",
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
assert.Equal(t, "image1", image)
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
},
},
{
name: "ImageRemove fail",
args: []string{"arg1"},
@ -43,12 +64,14 @@ func TestNewRemoveCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
imageRemoveFunc: tc.imageRemoveFunc,
}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
t.Run(tc.name, func(t *testing.T) {
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
imageRemoveFunc: tc.imageRemoveFunc,
}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
}
}
@ -57,7 +80,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
name string
args []string
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
expectedErrMsg string
expectedStderr string
}{
{
name: "Image Deleted",
@ -68,14 +91,16 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
},
},
{
name: "Image Deleted with force option",
name: "Image not found with force option",
args: []string{"-f", "image1"},
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
assert.Equal(t, "image1", image)
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
assert.Equal(t, true, options.Force)
return []types.ImageDeleteResponseItem{}, notFound{"image1"}
},
expectedErrMsg: "error removing image",
expectedStderr: "Error: No such image: image1",
},
{
name: "Image Untagged",
args: []string{"image1"},
@ -96,14 +121,14 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, cli.ErrBuffer().String())
}
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
assert.Equal(t, tc.expectedStderr, cli.ErrBuffer().String())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
})
}
}

View File

@ -180,7 +180,7 @@ func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.
}
// trustedPull handles content trust pulling of an image
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) error {
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, platform string) error {
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
if err != nil {
return err
@ -202,7 +202,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
if err != nil {
return err
}
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false); err != nil {
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false, platform); err != nil {
return err
}
@ -268,7 +268,7 @@ func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth)
}
// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool) error {
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool, platform string) error {
ref := reference.FamiliarString(imgRefAndAuth.Reference())
encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
@ -280,8 +280,8 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: all,
Platform: platform,
}
responseBody, err := cli.Client().ImagePull(ctx, ref, options)
if err != nil {
return err

View File

@ -505,6 +505,8 @@ type serviceOptions struct {
healthcheck healthCheckOptions
secrets opts.SecretOpt
configs opts.ConfigOpt
isolation string
}
func newServiceOptions() *serviceOptions {
@ -614,6 +616,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
Hosts: convertExtraHostsToSwarmHosts(options.hosts.GetAll()),
StopGracePeriod: options.ToStopGracePeriod(flags),
Healthcheck: healthConfig,
Isolation: container.Isolation(options.isolation),
},
Networks: networks,
Resources: options.resources.ToResourceRequirements(),
@ -784,6 +787,8 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu
flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container")
flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"})
flags.StringVar(&opts.isolation, flagIsolation, "", "Service container isolation mode")
flags.SetAnnotation(flagIsolation, "version", []string{"1.35"})
}
const (
@ -879,4 +884,5 @@ const (
flagConfig = "config"
flagConfigAdd = "config-add"
flagConfigRemove = "config-rm"
flagIsolation = "isolation"
)

View File

@ -269,6 +269,14 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
}
}
updateIsolation := func(flag string, field *container.Isolation) error {
if flags.Changed(flag) {
val, _ := flags.GetString(flag)
*field = container.Isolation(val)
}
return nil
}
cspec := spec.TaskTemplate.ContainerSpec
task := &spec.TaskTemplate
@ -288,6 +296,9 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
updateString(flagWorkdir, &cspec.Dir)
updateString(flagUser, &cspec.User)
updateString(flagHostname, &cspec.Hostname)
if err := updateIsolation(flagIsolation, &cspec.Isolation); err != nil {
return err
}
if err := updateMounts(flags, &cspec.Mounts); err != nil {
return err
}

View File

@ -518,3 +518,32 @@ func TestUpdateStopSignal(t *testing.T) {
updateService(nil, nil, flags, spec)
assert.Equal(t, "SIGWINCH", cspec.StopSignal)
}
func TestUpdateIsolationValid(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
err := flags.Set("isolation", "process")
require.NoError(t, err)
spec := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
err = updateService(context.Background(), nil, flags, &spec)
require.NoError(t, err)
assert.Equal(t, container.IsolationProcess, spec.TaskTemplate.ContainerSpec.Isolation)
}
func TestUpdateIsolationInvalid(t *testing.T) {
// validation depends on daemon os / version so validation should be done on the daemon side
flags := newUpdateCommand(nil).Flags()
err := flags.Set("isolation", "test")
require.NoError(t, err)
spec := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
err = updateService(context.Background(), nil, flags, &spec)
require.NoError(t, err)
assert.Equal(t, container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation)
}

View File

@ -18,19 +18,29 @@ import (
"github.com/theupdateframework/notary/tuf/data"
)
type signOptions struct {
local bool
imageName string
}
func newSignCommand(dockerCli command.Cli) *cobra.Command {
options := signOptions{}
cmd := &cobra.Command{
Use: "sign IMAGE:TAG",
Short: "Sign an image",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runSignImage(dockerCli, args[0])
options.imageName = args[0]
return runSignImage(dockerCli, options)
},
}
flags := cmd.Flags()
flags.BoolVar(&options.local, "local", false, "Sign a locally tagged image")
return cmd
}
func runSignImage(cli command.Cli, imageName string) error {
func runSignImage(cli command.Cli, options signOptions) error {
imageName := options.imageName
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), imageName)
if err != nil {
@ -71,13 +81,15 @@ func runSignImage(cli command.Cli, imageName string) error {
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push")
target, err := createTarget(notaryRepo, imgRefAndAuth.Tag())
if err != nil {
if err != nil || options.local {
switch err := err.(type) {
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist:
// If the error is nil then the local flag is set
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist, nil:
// Fail fast if the image doesn't exist locally
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
return err
}
fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName)
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), requestPrivilege)
default:
return err

View File

@ -1,13 +1,12 @@
package trust
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"testing"
"bytes"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
@ -296,3 +295,13 @@ func TestSignCommandChangeListIsCleanedOnError(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, len(cl.List()), 0)
}
func TestSignCommandLocalFlag(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newSignCommand(cli)
cmd.SetArgs([]string{"--local", "reg-name.io/image:red"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "error during connect: Get /images/reg-name.io/image:red/json: unsupported protocol scheme")
}

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/system"
"github.com/spf13/pflag"
)
// CopyToFile writes the content of the reader to the specified file
@ -117,3 +118,10 @@ func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
return pruneFilters
}
// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later.
func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
flags.SetAnnotation("platform", "version", []string{"1.32"})
flags.SetAnnotation("platform", "experimental", nil)
}

View File

@ -101,18 +101,11 @@ func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig)
continue
}
data, err := ioutil.ReadFile(secret.File)
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
if err != nil {
return nil, err
}
result = append(result, swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: namespace.Scope(name),
Labels: AddStackLabel(namespace, secret.Labels),
},
Data: data,
})
result = append(result, swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data})
}
return result, nil
}
@ -125,18 +118,37 @@ func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfi
continue
}
data, err := ioutil.ReadFile(config.File)
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
if err != nil {
return nil, err
}
result = append(result, swarm.ConfigSpec{
Annotations: swarm.Annotations{
Name: namespace.Scope(name),
Labels: AddStackLabel(namespace, config.Labels),
},
Data: data,
})
result = append(result, swarm.ConfigSpec{Annotations: obj.Annotations, Data: obj.Data})
}
return result, nil
}
type swarmFileObject struct {
Annotations swarm.Annotations
Data []byte
}
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
data, err := ioutil.ReadFile(obj.File)
if err != nil {
return swarmFileObject{}, err
}
if obj.Name != "" {
name = obj.Name
} else {
name = namespace.Scope(name)
}
return swarmFileObject{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, obj.Labels),
},
Data: data,
}, nil
}

View File

@ -149,6 +149,7 @@ func Service(
Configs: configs,
ReadOnly: service.ReadOnly,
Privileges: &privileges,
Isolation: container.Isolation(service.Isolation),
},
LogDriver: logDriver,
Resources: resources,
@ -255,43 +256,24 @@ func convertServiceSecrets(
secretSpecs map[string]composetypes.SecretConfig,
) ([]*swarm.SecretReference, error) {
refs := []*swarm.SecretReference{}
for _, secret := range secrets {
target := secret.Target
if target == "" {
target = secret.Source
}
secretSpec, exists := secretSpecs[secret.Source]
lookup := func(key string) (composetypes.FileObjectConfig, error) {
secretSpec, exists := secretSpecs[key]
if !exists {
return nil, errors.Errorf("undefined secret %q", secret.Source)
}
source := namespace.Scope(secret.Source)
if secretSpec.External.External {
source = secretSpec.External.Name
}
uid := secret.UID
gid := secret.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := secret.Mode
if mode == nil {
mode = uint32Ptr(0444)
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
}
return composetypes.FileObjectConfig(secretSpec), nil
}
for _, secret := range secrets {
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
if err != nil {
return nil, err
}
file := swarm.SecretReferenceFileTarget(obj.File)
refs = append(refs, &swarm.SecretReference{
File: &swarm.SecretReferenceFileTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
SecretName: source,
File: &file,
SecretName: obj.Name,
})
}
@ -312,43 +294,24 @@ func convertServiceConfigObjs(
configSpecs map[string]composetypes.ConfigObjConfig,
) ([]*swarm.ConfigReference, error) {
refs := []*swarm.ConfigReference{}
for _, config := range configs {
target := config.Target
if target == "" {
target = config.Source
}
configSpec, exists := configSpecs[config.Source]
lookup := func(key string) (composetypes.FileObjectConfig, error) {
configSpec, exists := configSpecs[key]
if !exists {
return nil, errors.Errorf("undefined config %q", config.Source)
}
source := namespace.Scope(config.Source)
if configSpec.External.External {
source = configSpec.External.Name
}
uid := config.UID
gid := config.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := config.Mode
if mode == nil {
mode = uint32Ptr(0444)
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
}
return composetypes.FileObjectConfig(configSpec), nil
}
for _, config := range configs {
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
if err != nil {
return nil, err
}
file := swarm.ConfigReferenceFileTarget(obj.File)
refs = append(refs, &swarm.ConfigReference{
File: &swarm.ConfigReferenceFileTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
ConfigName: source,
File: &file,
ConfigName: obj.Name,
})
}
@ -361,6 +324,63 @@ func convertServiceConfigObjs(
return confs, err
}
type swarmReferenceTarget struct {
Name string
UID string
GID string
Mode os.FileMode
}
type swarmReferenceObject struct {
File swarmReferenceTarget
ID string
Name string
}
func convertFileObject(
namespace Namespace,
config composetypes.FileReferenceConfig,
lookup func(key string) (composetypes.FileObjectConfig, error),
) (swarmReferenceObject, error) {
target := config.Target
if target == "" {
target = config.Source
}
obj, err := lookup(config.Source)
if err != nil {
return swarmReferenceObject{}, err
}
source := namespace.Scope(config.Source)
if obj.Name != "" {
source = obj.Name
}
uid := config.UID
gid := config.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := config.Mode
if mode == nil {
mode = uint32Ptr(0444)
}
return swarmReferenceObject{
File: swarmReferenceTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
Name: source,
}, nil
}
func uint32Ptr(value uint32) *uint32 {
return &value
}

View File

@ -1,6 +1,7 @@
package convert
import (
"os"
"sort"
"strings"
"testing"
@ -9,7 +10,9 @@ import (
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvertRestartPolicyFromNone(t *testing.T) {
@ -372,3 +375,61 @@ func TestConvertUpdateConfigOrder(t *testing.T) {
})
assert.Equal(t, updateConfig.Order, "stop-first")
}
func TestConvertFileObject(t *testing.T) {
namespace := NewNamespace("testing")
config := composetypes.FileReferenceConfig{
Source: "source",
Target: "target",
UID: "user",
GID: "group",
Mode: uint32Ptr(0644),
}
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
require.NoError(t, err)
expected := swarmReferenceObject{
Name: "testing_source",
File: swarmReferenceTarget{
Name: config.Target,
UID: config.UID,
GID: config.GID,
Mode: os.FileMode(0644),
},
}
assert.Equal(t, expected, swarmRef)
}
func lookupConfig(key string) (composetypes.FileObjectConfig, error) {
if key != "source" {
return composetypes.FileObjectConfig{}, errors.New("bad key")
}
return composetypes.FileObjectConfig{}, nil
}
func TestConvertFileObjectDefaults(t *testing.T) {
namespace := NewNamespace("testing")
config := composetypes.FileReferenceConfig{Source: "source"}
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
require.NoError(t, err)
expected := swarmReferenceObject{
Name: "testing_source",
File: swarmReferenceTarget{
Name: config.Source,
UID: "0",
GID: "0",
Mode: os.FileMode(0444),
},
}
assert.Equal(t, expected, swarmRef)
}
func TestServiceConvertsIsolation(t *testing.T) {
src := composetypes.ServiceConfig{
Isolation: "hyperv",
}
result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation)
}

View File

@ -68,16 +68,15 @@ func convertVolumeToMount(
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
}
// External named volumes
if stackVolume.External.External {
result.Source = stackVolume.External.Name
return result, nil
}
if stackVolume.Name != "" {
result.Source = stackVolume.Name
}
// External named volumes
if stackVolume.External.External {
return result, nil
}
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
result.VolumeOptions.DriverConfig = &mount.Driver{

View File

@ -148,20 +148,16 @@ func TestConvertVolumeToMountNamedVolumeWithNameCustomizd(t *testing.T) {
func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
stackVolumes := volumes{
"outside": composetypes.VolumeConfig{
External: composetypes.External{
External: true,
Name: "special",
},
Name: "special",
External: composetypes.External{External: true},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "special",
Target: "/foo",
VolumeOptions: &mount.VolumeOptions{
NoCopy: false,
},
Type: mount.TypeVolume,
Source: "special",
Target: "/foo",
VolumeOptions: &mount.VolumeOptions{NoCopy: false},
}
config := composetypes.ServiceVolumeConfig{
Type: "volume",
@ -176,10 +172,8 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
stackVolumes := volumes{
"outside": composetypes.VolumeConfig{
External: composetypes.External{
External: true,
Name: "special",
},
Name: "special",
External: composetypes.External{External: true},
},
}
namespace := NewNamespace("foo")

View File

@ -12,6 +12,7 @@ import (
"github.com/docker/cli/cli/compose/template"
"github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
shellwords "github.com/mattn/go-shellwords"
@ -49,6 +50,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
}
configDict := getConfigDict(configDetails)
configDetails.Version = schema.Version(configDict)
if err := validateForbidden(configDict); err != nil {
return nil, err
@ -60,7 +62,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
return nil, err
}
if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
if err := schema.Validate(configDict, configDetails.Version); err != nil {
return nil, err
}
return loadSections(configDict, configDetails)
@ -103,21 +105,21 @@ func loadSections(config map[string]interface{}, configDetails types.ConfigDetai
{
key: "volumes",
fnc: func(config map[string]interface{}) error {
cfg.Volumes, err = LoadVolumes(config)
cfg.Volumes, err = LoadVolumes(config, configDetails.Version)
return err
},
},
{
key: "secrets",
fnc: func(config map[string]interface{}) error {
cfg.Secrets, err = LoadSecrets(config, configDetails.WorkingDir)
cfg.Secrets, err = LoadSecrets(config, configDetails)
return err
},
},
{
key: "configs",
fnc: func(config map[string]interface{}) error {
cfg.Configs, err = LoadConfigObjs(config, configDetails.WorkingDir)
cfg.Configs, err = LoadConfigObjs(config, configDetails)
return err
},
},
@ -446,76 +448,100 @@ func externalVolumeError(volume, key string) error {
// LoadVolumes produces a VolumeConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig, error) {
func LoadVolumes(source map[string]interface{}, version string) (map[string]types.VolumeConfig, error) {
volumes := make(map[string]types.VolumeConfig)
err := transform(source, &volumes)
if err != nil {
if err := transform(source, &volumes); err != nil {
return volumes, err
}
for name, volume := range volumes {
if volume.External.External {
if volume.Driver != "" {
return nil, externalVolumeError(name, "driver")
}
if len(volume.DriverOpts) > 0 {
return nil, externalVolumeError(name, "driver_opts")
}
if len(volume.Labels) > 0 {
return nil, externalVolumeError(name, "labels")
}
if volume.External.Name == "" {
volume.External.Name = name
volumes[name] = volume
} else {
logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
if volume.Name != "" {
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
}
}
for name, volume := range volumes {
if !volume.External.External {
continue
}
switch {
case volume.Driver != "":
return nil, externalVolumeError(name, "driver")
case len(volume.DriverOpts) > 0:
return nil, externalVolumeError(name, "driver_opts")
case len(volume.Labels) > 0:
return nil, externalVolumeError(name, "labels")
case volume.External.Name != "":
if volume.Name != "" {
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
}
if versions.GreaterThanOrEqualTo(version, "3.4") {
logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
}
volume.Name = volume.External.Name
volume.External.Name = ""
case volume.Name == "":
volume.Name = name
}
volumes[name] = volume
}
return volumes, nil
}
// LoadSecrets produces a SecretConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]types.SecretConfig, error) {
func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
secrets := make(map[string]types.SecretConfig)
if err := transform(source, &secrets); err != nil {
return secrets, err
}
for name, secret := range secrets {
if secret.External.External && secret.External.Name == "" {
secret.External.Name = name
secrets[name] = secret
}
if secret.File != "" {
secret.File = absPath(workingDir, secret.File)
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
if err != nil {
return nil, err
}
secrets[name] = types.SecretConfig(obj)
}
return secrets, nil
}
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadConfigObjs(source map[string]interface{}, workingDir string) (map[string]types.ConfigObjConfig, error) {
func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
configs := make(map[string]types.ConfigObjConfig)
if err := transform(source, &configs); err != nil {
return configs, err
}
for name, config := range configs {
if config.External.External && config.External.Name == "" {
config.External.Name = name
configs[name] = config
}
if config.File != "" {
config.File = absPath(workingDir, config.File)
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
if err != nil {
return nil, err
}
configs[name] = types.ConfigObjConfig(obj)
}
return configs, nil
}
func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
// if "external: true"
if obj.External.External {
// handle deprecated external.name
if obj.External.Name != "" {
if obj.Name != "" {
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
}
if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
}
obj.Name = obj.External.Name
obj.External.Name = ""
} else {
if obj.Name == "" {
obj.Name = name
}
}
// if not "external: true"
} else {
obj.File = absPath(details.WorkingDir, obj.File)
}
return obj, nil
}
func absPath(workingDir string, filePath string) string {
if filepath.IsAbs(filePath) {
return filePath

View File

@ -1,6 +1,7 @@
package loader
import (
"bytes"
"fmt"
"io/ioutil"
"os"
@ -9,6 +10,7 @@ import (
"time"
"github.com/docker/cli/cli/compose/types"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -624,13 +626,13 @@ networks:
},
},
Configs: map[string]types.ConfigObjConfig{
"appconfig": {External: types.External{External: true, Name: "appconfig"}},
"appconfig": {External: types.External{External: true}, Name: "appconfig"},
},
Secrets: map[string]types.SecretConfig{
"super": {External: types.External{External: true, Name: "super"}},
"super": {External: types.External{External: true}, Name: "super"},
},
Volumes: map[string]types.VolumeConfig{
"data": {External: types.External{External: true, Name: "data"}},
"data": {External: types.External{External: true}, Name: "data"},
},
Networks: map[string]types.NetworkConfig{
"front": {
@ -1190,23 +1192,16 @@ func TestFullExample(t *testing.T) {
},
},
"external-volume": {
External: types.External{
Name: "external-volume",
External: true,
},
Name: "external-volume",
External: types.External{External: true},
},
"other-external-volume": {
External: types.External{
Name: "my-cool-volume",
External: true,
},
Name: "my-cool-volume",
External: types.External{External: true},
},
"external-volume3": {
Name: "this-is-volume3",
External: types.External{
Name: "external-volume3",
External: true,
},
Name: "this-is-volume3",
External: types.External{External: true},
},
}
@ -1406,3 +1401,144 @@ services:
require.Len(t, config.Services, 1)
assert.Equal(t, expected, config.Services[0].ExtraHosts)
}
func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
buf, cleanup := patchLogrus()
defer cleanup()
source := map[string]interface{}{
"foo": map[string]interface{}{
"external": map[string]interface{}{
"name": "oops",
},
},
}
volumes, err := LoadVolumes(source, "3.4")
require.NoError(t, err)
expected := map[string]types.VolumeConfig{
"foo": {
Name: "oops",
External: types.External{External: true},
},
}
assert.Equal(t, expected, volumes)
assert.Contains(t, buf.String(), "volume.external.name is deprecated")
}
func patchLogrus() (*bytes.Buffer, func()) {
buf := new(bytes.Buffer)
out := logrus.StandardLogger().Out
logrus.SetOutput(buf)
return buf, func() { logrus.SetOutput(out) }
}
func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) {
buf, cleanup := patchLogrus()
defer cleanup()
source := map[string]interface{}{
"foo": map[string]interface{}{
"external": map[string]interface{}{
"name": "oops",
},
},
}
volumes, err := LoadVolumes(source, "3.3")
require.NoError(t, err)
expected := map[string]types.VolumeConfig{
"foo": {
Name: "oops",
External: types.External{External: true},
},
}
assert.Equal(t, expected, volumes)
assert.Equal(t, "", buf.String())
}
func TestLoadV35(t *testing.T) {
actual, err := loadYAML(`
version: "3.5"
services:
foo:
image: busybox
isolation: process
configs:
foo:
name: fooqux
external: true
bar:
name: barqux
file: ./example1.env
secrets:
foo:
name: fooqux
external: true
bar:
name: barqux
file: ./full-example.yml
`)
require.NoError(t, err)
assert.Len(t, actual.Services, 1)
assert.Len(t, actual.Secrets, 2)
assert.Len(t, actual.Configs, 2)
assert.Equal(t, "process", actual.Services[0].Isolation)
}
func TestLoadV35InvalidIsolation(t *testing.T) {
// validation should be done only on the daemon side
actual, err := loadYAML(`
version: "3.5"
services:
foo:
image: busybox
isolation: invalid
configs:
super:
external: true
`)
require.NoError(t, err)
require.Len(t, actual.Services, 1)
assert.Equal(t, "invalid", actual.Services[0].Isolation)
}
func TestInvalidSecretExternalNameAndNameCombination(t *testing.T) {
_, err := loadYAML(`
version: "3.5"
secrets:
external_secret:
name: user_specified_name
external:
name: external_name
`)
require.Error(t, err)
assert.Contains(t, err.Error(), "secret.external.name and secret.name conflict; only use secret.name")
assert.Contains(t, err.Error(), "external_secret")
}
func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
buf, cleanup := patchLogrus()
defer cleanup()
source := map[string]interface{}{
"foo": map[string]interface{}{
"external": map[string]interface{}{
"name": "oops",
},
},
}
details := types.ConfigDetails{
Version: "3.5",
}
secrets, err := LoadSecrets(source, details)
require.NoError(t, err)
expected := map[string]types.SecretConfig{
"foo": {
Name: "oops",
External: types.External{External: true},
},
}
assert.Equal(t, expected, secrets)
assert.Contains(t, buf.String(), "secret.external.name is deprecated")
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,546 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v3.5.json",
"type": "object",
"required": ["version"],
"properties": {
"version": {
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false
},
"networks": {
"id": "#/properties/networks",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network"
}
}
},
"volumes": {
"id": "#/properties/volumes",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
},
"additionalProperties": false
},
"secrets": {
"id": "#/properties/secrets",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/secret"
}
},
"additionalProperties": false
},
"configs": {
"id": "#/properties/configs",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/config"
}
},
"additionalProperties": false
}
},
"patternProperties": {"^x-": {}},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"deploy": {"$ref": "#/definitions/deployment"},
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"cache_from": {"$ref": "#/definitions/list_of_strings"},
"network": {"type": "string"},
"target": {"type": "string"}
},
"additionalProperties": false
}
]
},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"configs": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"uid": {"type": "string"},
"gid": {"type": "string"},
"mode": {"type": "number"}
}
}
]
}
},
"container_name": {"type": "string"},
"credential_spec": {"type": "object", "properties": {
"file": {"type": "string"},
"registry": {"type": "string"}
}},
"depends_on": {"$ref": "#/definitions/list_of_strings"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"isolation": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number", "null"]}
}
}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"network_mode": {"type": "string"},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"oneOf": [
{
"type": "object",
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"oneOf": [
{"type": "number", "format": "ports"},
{"type": "string", "format": "ports"},
{
"type": "object",
"properties": {
"mode": {"type": "string"},
"target": {"type": "integer"},
"published": {"type": "integer"},
"protocol": {"type": "string"}
},
"additionalProperties": false
}
]
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"secrets": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"uid": {"type": "string"},
"gid": {"type": "string"},
"mode": {"type": "number"}
}
}
]
}
},
"sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
"volumes": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"required": ["type"],
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
"target": {"type": "string"},
"read_only": {"type": "boolean"},
"consistency": {"type": "string"},
"bind": {
"type": "object",
"properties": {
"propagation": {"type": "string"}
}
},
"volume": {
"type": "object",
"properties": {
"nocopy": {"type": "boolean"}
}
}
}
}
],
"uniqueItems": true
}
},
"working_dir": {"type": "string"}
},
"additionalProperties": false
},
"healthcheck": {
"id": "#/definitions/healthcheck",
"type": "object",
"additionalProperties": false,
"properties": {
"disable": {"type": "boolean"},
"interval": {"type": "string", "format": "duration"},
"retries": {"type": "number"},
"test": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"timeout": {"type": "string", "format": "duration"},
"start_period": {"type": "string", "format": "duration"}
}
},
"deployment": {
"id": "#/definitions/deployment",
"type": ["object", "null"],
"properties": {
"mode": {"type": "string"},
"endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"update_config": {
"type": "object",
"properties": {
"parallelism": {"type": "integer"},
"delay": {"type": "string", "format": "duration"},
"failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"},
"max_failure_ratio": {"type": "number"},
"order": {"type": "string", "enum": [
"start-first", "stop-first"
]}
},
"additionalProperties": false
},
"resources": {
"type": "object",
"properties": {
"limits": {"$ref": "#/definitions/resource"},
"reservations": {"$ref": "#/definitions/resource"}
},
"additionalProperties": false
},
"restart_policy": {
"type": "object",
"properties": {
"condition": {"type": "string"},
"delay": {"type": "string", "format": "duration"},
"max_attempts": {"type": "integer"},
"window": {"type": "string", "format": "duration"}
},
"additionalProperties": false
},
"placement": {
"type": "object",
"properties": {
"constraints": {"type": "array", "items": {"type": "string"}},
"preferences": {
"type": "array",
"items": {
"type": "object",
"properties": {
"spread": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"resource": {
"id": "#/definitions/resource",
"type": "object",
"properties": {
"cpus": {"type": "string"},
"memory": {"type": "string"}
},
"additionalProperties": false
},
"network": {
"id": "#/definitions/network",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subnet": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"internal": {"type": "boolean"},
"attachable": {"type": "boolean"},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"name": {"type": "string"},
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"secret": {
"id": "#/definitions/secret",
"type": "object",
"properties": {
"name": {"type": "string"},
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"config": {
"id": "#/definitions/config",
"type": "object",
"properties": {
"name": {"type": "string"},
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}

View File

@ -46,6 +46,25 @@ func TestValidateAllowsXTopLevelFields(t *testing.T) {
assert.NoError(t, err)
}
func TestValidateSecretConfigNames(t *testing.T) {
config := dict{
"version": "3.5",
"configs": dict{
"bar": dict{
"name": "foobar",
},
},
"secrets": dict{
"baz": dict{
"name": "foobaz",
},
},
}
err := Validate(config, "3.5")
assert.NoError(t, err)
}
func TestValidateInvalidVersion(t *testing.T) {
config := dict{
"version": "2.1",
@ -84,3 +103,16 @@ func TestValidatePlacement(t *testing.T) {
assert.NoError(t, Validate(config, "3.3"))
}
func TestValidateIsolation(t *testing.T) {
config := dict{
"version": "3.5",
"services": dict{
"foo": dict{
"image": "busybox",
"isolation": "some-isolation-value",
},
},
}
assert.NoError(t, Validate(config, "3.5"))
}

View File

@ -55,6 +55,7 @@ type ConfigFile struct {
// ConfigDetails are the details about a group of ConfigFiles
type ConfigDetails struct {
Version string
WorkingDir string
ConfigFiles []ConfigFile
Environment map[string]string
@ -125,6 +126,7 @@ type ServiceConfig struct {
User string
Volumes []ServiceVolumeConfig
WorkingDir string `mapstructure:"working_dir"`
Isolation string `mapstructure:"isolation"`
}
// BuildConfig is a type for build
@ -277,7 +279,8 @@ type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy"`
}
type fileReferenceConfig struct {
// FileReferenceConfig for a reference to a swarm file object
type FileReferenceConfig struct {
Source string
Target string
UID string
@ -286,10 +289,10 @@ type fileReferenceConfig struct {
}
// ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig fileReferenceConfig
type ServiceConfigObjConfig FileReferenceConfig
// ServiceSecretConfig is the secret configuration for a service
type ServiceSecretConfig fileReferenceConfig
type ServiceSecretConfig FileReferenceConfig
// UlimitsConfig the ulimit configuration
type UlimitsConfig struct {
@ -343,14 +346,16 @@ type CredentialSpecConfig struct {
Registry string
}
type fileObjectConfig struct {
// FileObjectConfig is a config type for a file used by a service
type FileObjectConfig struct {
Name string
File string
External External
Labels Labels
}
// SecretConfig for a secret
type SecretConfig fileObjectConfig
type SecretConfig FileObjectConfig
// ConfigObjConfig is the config for the swarm "Config" object
type ConfigObjConfig fileObjectConfig
type ConfigObjConfig FileObjectConfig

View File

@ -177,7 +177,7 @@ func (configFile *ConfigFile) Save() error {
return configFile.SaveToWriter(f)
}
// ParseProxyConfig computes proxy configuration by retreiving the config for the provided host and
// ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
// then checking this against any environment variables provided to the container
func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts []string) map[string]*string {
var cfgKey string

View File

@ -127,7 +127,7 @@ func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo
}
// Skip configuration headers since request is not going to Docker daemon
modifiers := registry.DockerHeaders(userAgent, http.Header{})
modifiers := registry.Headers(userAgent, http.Header{})
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
@ -299,7 +299,7 @@ type ImageRefAndAuth struct {
}
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
// as a ImageRefAndAuth struct
// as an ImageRefAndAuth struct
func GetImageReferencesAndAuth(ctx context.Context, authResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig, imgName string) (ImageRefAndAuth, error) {
ref, err := reference.ParseNormalizedNamed(imgName)
if err != nil {

View File

@ -1,29 +0,0 @@
// +build !daemon
package main
import (
"fmt"
"runtime"
"strings"
"github.com/spf13/cobra"
)
func newDaemonCommand() *cobra.Command {
return &cobra.Command{
Use: "daemon",
Hidden: true,
Args: cobra.ArbitraryArgs,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDaemon()
},
}
}
func runDaemon() error {
return fmt.Errorf(
"`docker daemon` is not supported on %s. Please run `dockerd` directly",
strings.Title(runtime.GOOS))
}

View File

@ -1,17 +0,0 @@
// +build !daemon
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDaemonCommand(t *testing.T) {
cmd := newDaemonCommand()
cmd.SetArgs([]string{"--version"})
err := cmd.Execute()
assert.EqualError(t, err, "Please run `dockerd`")
}

View File

@ -1,33 +0,0 @@
// +build daemon
package main
import (
"io/ioutil"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
func stubRun(cmd *cobra.Command, args []string) error {
return nil
}
func TestDaemonCommandHelp(t *testing.T) {
cmd := newDaemonCommand()
cmd.RunE = stubRun
cmd.SetArgs([]string{"--help"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.NoError(t, err)
}
func TestDaemonCommand(t *testing.T) {
cmd := newDaemonCommand()
cmd.RunE = stubRun
cmd.SetArgs([]string{"--containerd", "/foo"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.NoError(t, err)
}

View File

@ -1,79 +0,0 @@
// +build daemon
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/spf13/cobra"
)
const daemonBinary = "dockerd"
func newDaemonCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "daemon",
Hidden: true,
Args: cobra.ArbitraryArgs,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDaemon()
},
Deprecated: "and will be removed in Docker 17.12. Please run `dockerd` directly.",
}
cmd.SetHelpFunc(helpFunc)
return cmd
}
// CmdDaemon execs dockerd with the same flags
func runDaemon() error {
// Use os.Args[1:] so that "global" args are passed to dockerd
return execDaemon(stripDaemonArg(os.Args[1:]))
}
func execDaemon(args []string) error {
binaryPath, err := findDaemonBinary()
if err != nil {
return err
}
return syscall.Exec(
binaryPath,
append([]string{daemonBinary}, args...),
os.Environ())
}
func helpFunc(cmd *cobra.Command, args []string) {
if err := execDaemon([]string{"--help"}); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
}
}
// findDaemonBinary looks for the path to the dockerd binary starting with
// the directory of the current executable (if one exists) and followed by $PATH
func findDaemonBinary() (string, error) {
execDirname := filepath.Dir(os.Args[0])
if execDirname != "" {
binaryPath := filepath.Join(execDirname, daemonBinary)
if _, err := os.Stat(binaryPath); err == nil {
return binaryPath, nil
}
}
return exec.LookPath(daemonBinary)
}
// stripDaemonArg removes the `daemon` argument from the list
func stripDaemonArg(args []string) []string {
for i, arg := range args {
if arg == "daemon" {
return append(args[:i], args[i+1:]...)
}
}
return args
}

View File

@ -39,10 +39,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
return command.ShowHelp(dockerCli.Err())(cmd, args)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// daemon command is special, we redirect directly to another binary
if cmd.Name() == "daemon" {
return nil
}
// flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags)
dockerPreRun(opts)
@ -64,7 +60,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
setHelpFunc(dockerCli, cmd, flags, opts)
cmd.SetOutput(dockerCli.Out())
cmd.AddCommand(newDaemonCommand())
commands.AddCommands(cmd, dockerCli)
setValidateArgs(dockerCli, cmd, flags, opts)

View File

@ -3305,6 +3305,7 @@ _docker_service_update_and_create() {
--health-start-period
--health-timeout
--hostname
--isolation
--label -l
--limit-cpu
--limit-memory
@ -3493,6 +3494,10 @@ _docker_service_update_and_create() {
__docker_nospace
return
;;
--isolation)
__docker_complete_isolation
return
;;
--log-driver)
__docker_complete_log_drivers
return
@ -4510,7 +4515,7 @@ _docker_system() {
info
prune
"
__docker_subcommands "$subcommands $aliases" && return
__docker_subcommands "$subcommands" && return
case "$cur" in
-*)
@ -4665,6 +4670,68 @@ _docker_tag() {
_docker_image_tag
}
_docker_trust() {
local subcommands="
revoke
sign
view
"
__docker_subcommands "$subcommands" && return
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) )
;;
esac
}
_docker_trust_revoke() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --yes -y" -- "$cur" ) )
;;
*)
local counter=$(__docker_pos_first_nonflag)
if [ "$cword" -eq "$counter" ]; then
__docker_complete_images
fi
;;
esac
}
_docker_trust_sign() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
local counter=$(__docker_pos_first_nonflag)
if [ "$cword" -eq "$counter" ]; then
__docker_complete_images
fi
;;
esac
}
_docker_trust_view() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
local counter=$(__docker_pos_first_nonflag)
if [ "$cword" -eq "$counter" ]; then
__docker_complete_images
fi
;;
esac
}
_docker_unpause() {
_docker_container_unpause
}
@ -4892,6 +4959,7 @@ _docker() {
local experimental_commands=(
checkpoint
deploy
trust
)
local commands=(${management_commands[*]} ${top_level_commands[*]})

View File

@ -1955,6 +1955,7 @@ __docker_service_subcommand() {
"($help)--health-retries=[Consecutive failures needed to report unhealthy]:retries:(1 2 3 4 5)"
"($help)--health-timeout=[Maximum time to allow one check to run]:time: "
"($help)--hostname=[Service container hostname]:hostname: " \
"($help)--isolation=[Service container isolation mode]:isolation:(default process hyperv)" \
"($help)*--label=[Service labels]:label: "
"($help)--limit-cpu=[Limit CPUs]:value: "
"($help)--limit-memory=[Limit Memory]:value: "

View File

@ -1,3 +1,3 @@
FROM dockercore/golang-cross@sha256:8a347b3692ba925dcef753f2de289e11774410c1bc752b9d525cb05477a7697b
FROM dockercore/golang-cross@sha256:25ff84377e9d7f40639c33cc374166a3b0f1829b8462cf7001d742a846de2687
ENV DISABLE_WARN_OUTSIDE_CONTAINER=1
WORKDIR /go/src/github.com/docker/cli

View File

@ -1,5 +1,5 @@
FROM golang:1.8.4-alpine3.6
FROM golang:1.8.5-alpine3.6
RUN apk add -U git make bash coreutils ca-certificates

View File

@ -1,4 +1,4 @@
FROM golang:1.8.4-alpine3.6
FROM golang:1.8.5-alpine3.6
RUN apk add -U git

View File

@ -81,7 +81,7 @@ The `filter` param to filter the list of image by reference (name or name:tag) i
### `docker daemon` subcommand
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
**Target For Removal In Release: v17.12**
**Removed In Release: v17.12**
The daemon is moved to a separate binary (`dockerd`), and should be used instead.

View File

@ -126,7 +126,7 @@ Config provides the base accessible fields for working with V0 plugin format
- **`propagatedMount`** *string*
path to be mounted as rshared, so that mounts under that path are visible to docker. This is useful for volume plugins.
This path will be bind-mounted outisde of the plugin rootfs so it's contents
This path will be bind-mounted outside of the plugin rootfs so it's contents
are preserved on upgrade.
- **`env`** *PluginEnv array*

View File

@ -65,7 +65,7 @@ The sections below provide an inexhaustive overview of available plugins.
| [Convoy plugin](https://github.com/rancher/convoy) | A volume plugin for a variety of storage back-ends including device mapper and NFS. It's a simple standalone executable written in Go and provides the framework to support vendor-specific extensions such as snapshots, backups and restore. |
| [DigitalOcean Block Storage plugin](https://github.com/omallo/docker-volume-plugin-dostorage) | Integrates DigitalOcean's [block storage solution](https://www.digitalocean.com/products/storage/) into the Docker ecosystem by automatically attaching a given block storage volume to a DigitalOcean droplet and making the contents of the volume available to Docker containers running on that droplet. |
| [DRBD plugin](https://www.drbd.org/en/supported-projects/docker) | A volume plugin that provides highly available storage replicated by [DRBD](https://www.drbd.org). Data written to the docker volume is replicated in a cluster of DRBD nodes. |
| [Flocker plugin](https://clusterhq.com/docker-plugin/) | A volume plugin that provides multi-host portable volumes for Docker, enabling you to run databases and other stateful containers and move them around across a cluster of machines. |
| [Flocker plugin](https://github.com/ScatterHQ/flocker) | A volume plugin that provides multi-host portable volumes for Docker, enabling you to run databases and other stateful containers and move them around across a cluster of machines. |
| [Fuxi Volume Plugin](https://github.com/openstack/fuxi) | A volume plugin that is developed as part of the OpenStack Kuryr project and implements the Docker volume plugin API by utilizing Cinder, the OpenStack block storage service. |
| [gce-docker plugin](https://github.com/mcuadros/gce-docker) | A volume plugin able to attach, format and mount Google Compute [persistent-disks](https://cloud.google.com/compute/docs/disks/persistent-disks). |
| [GlusterFS plugin](https://github.com/calavera/docker-volume-glusterfs) | A volume plugin that provides multi-host volumes management for Docker using GlusterFS. |

View File

@ -199,7 +199,7 @@ Request the path to the volume with the given `volume_name`.
```json
{
"Mountpoin": "/path/to/directory/on/host",
"Mountpoint": "/path/to/directory/on/host",
"Err": ""
}
```

View File

@ -765,7 +765,7 @@ type of documentation between the person who builds the image and the person who
runs the container, about which ports are intended to be published. To actually
publish the port when running the container, use the `-p` flag on `docker run`
to publish and map one or more ports, or the `-P` flag to publish all exposed
ports and map them to to high-order ports.
ports and map them to high-order ports.
To set up port redirection on the host system, see [using the -P
flag](run.md#expose-incoming-ports). The `docker network` command supports
@ -1284,7 +1284,7 @@ consider the following Dockerfile snippet:
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol
This Dockerfile results in an image that causes `docker run`, to
This Dockerfile results in an image that causes `docker run` to
create a new mount point at `/myvol` and copy the `greeting` file
into the newly created volume.
@ -1306,8 +1306,8 @@ Keep the following things in mind about volumes in the `Dockerfile`.
- **The host directory is declared at container run-time**: The host directory
(the mountpoint) is, by its nature, host-dependent. This is to preserve image
portability. since a given host directory can't be guaranteed to be available
on all hosts.For this reason, you can't mount a host directory from
portability, since a given host directory can't be guaranteed to be available
on all hosts. For this reason, you can't mount a host directory from
within the Dockerfile. The `VOLUME` instruction does not support specifying a `host-dir`
parameter. You must specify the mountpoint when you create or run the container.
@ -1322,9 +1322,20 @@ group (or GID) to use when running the image and for any `RUN`, `CMD` and
`ENTRYPOINT` instructions that follow it in the `Dockerfile`.
> **Warning**:
> When the user does doesn't have a primary group then the image (or the next
> When the user doesn't have a primary group then the image (or the next
> instructions) will be run with the `root` group.
> On Windows, the user must be created first if it's not a built-in account.
> This can be done with the `net user` command called as part of a Dockerfile.
```Dockerfile
FROM microsoft/windowsservercore
# Create Windows user in the container
RUN net user /add patrick
# Set it for subsequent commands
USER patrick
```
## WORKDIR

View File

@ -512,7 +512,7 @@ The `--squash` option has a number of known limitations:
layers in tact, and one for the squashed version.
- While squashing layers may produce smaller images, it may have a negative
impact on performance, as a single layer takes longer to extract, and
downloading a single layer cannot be paralellized.
downloading a single layer cannot be parallelized.
- When attempting to squash an image that does not make changes to the
filesystem (for example, the Dockerfile only contains `ENV` instructions),
the squash step will fail (see [issue #33823](https://github.com/moby/moby/issues/33823)

View File

@ -217,7 +217,7 @@ For the `devicemapper`, `btrfs`, `windowsfilter` and `zfs` graph drivers,
user cannot pass a size less than the Default BaseFS Size.
For the `overlay2` storage driver, the size option is only available if the
backing fs is `xfs` and mounted with the `pquota` mount option.
Under these conditions, user can pass any size less then the backing fs size.
Under these conditions, user can pass any size less than the backing fs size.
### Specify isolation technology for container (--isolation)

View File

@ -393,7 +393,7 @@ $ sudo dockerd --storage-opt dm.thinp_autoextend_threshold=80
##### `dm.thinp_autoextend_percent`
Sets the value percentage value to increase the thin pool by when when `lvm`
Sets the value percentage value to increase the thin pool by when `lvm`
attempts to autoextend the available space [100 = disabled]
###### Example:

View File

@ -39,7 +39,7 @@ The command started using `docker exec` only runs while the container's primary
process (`PID 1`) is running, and it is not restarted if the container is
restarted.
COMMAND will run in the default directory of the container. It the
COMMAND will run in the default directory of the container. If the
underlying image has a custom directory specified with the WORKDIR directive
in its Dockerfile, this will be used instead.

View File

@ -47,7 +47,7 @@ describes all the details of the format.
The `docker inspect` command matches any type of object by either ID or name.
In some cases multiple type of objects (for example, a container and a volume)
exist with the same name, making the result ambigious.
exist with the same name, making the result ambiguous.
To restrict `docker inspect` to a specific type of object, use the `--type`
option.

View File

@ -25,6 +25,7 @@ Options:
-f, --follow Follow log output
--help Print usage
--since string Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)
--until string Show logs before timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)
--tail string Number of lines to show from the end of the logs (default "all")
-t, --timestamps Show timestamps
```
@ -66,3 +67,19 @@ that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap
seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
fraction of a second no more than nine digits long. You can combine the
`--since` option with either or both of the `--follow` or `--tail` options.
## Examples
### Retrieve logs until a specific point in time
In order to retrieve logs before a specific point in time, run:
```bash
$ docker run --name test -d busybox sh -c "while true; do $(echo date); sleep 1; done"
$ date
Tue 14 Nov 2017 16:40:00 CET
$ docker logs -f --until=2s
Tue 14 Nov 2017 16:40:00 CET
Tue 14 Nov 2017 16:40:01 CET
Tue 14 Nov 2017 16:40:02 CET
```

View File

@ -204,7 +204,7 @@ to create an externally isolated `overlay` network, you can specify the
You can create the network which will be used to provide the routing-mesh in the
swarm cluster. You do so by specifying `--ingress` when creating the network. Only
one ingress network can be created at the time. The network can be removed only
if no services depend on it. Any option available when creating a overlay network
if no services depend on it. Any option available when creating an overlay network
is also available when creating the ingress network, besides the `--attachable` option.
```bash

View File

@ -207,7 +207,7 @@ $ docker network inspect ingress
details such as the service's VIP and port mappings. It also shows IPs of service tasks,
and the IPs of the nodes where the tasks are running.
Following is an example output for a overlay network `ov1` that has one service `s1`
Following is an example output for an overlay network `ov1` that has one service `s1`
attached to. service `s1` in this case has three replicas.
```bash

View File

@ -240,7 +240,7 @@ For the `devicemapper`, `btrfs`, `windowsfilter` and `zfs` graph drivers,
user cannot pass a size less than the Default BaseFS Size.
For the `overlay2` storage driver, the size option is only available if the
backing fs is `xfs` and mounted with the `pquota` mount option.
Under these conditions, user can pass any size less then the backing fs size.
Under these conditions, user can pass any size less than the backing fs size.
### Mount tmpfs (--tmpfs)

View File

@ -42,6 +42,7 @@ Options:
--help Print usage
--host list Set one or more custom host-to-IP mappings (host:ip)
--hostname string Container hostname
--isolation string Service container isolation mode
-l, --label list Service labels
--limit-cpu decimal Limit CPUs
--limit-memory bytes Limit Memory
@ -353,7 +354,7 @@ volumes in a service:
<li><tt>default</tt>: Equivalent to <tt>consistent</tt>.</li>
<li><tt>consistent</tt>: Full consistency. The container runtime and the host maintain an identical view of the mount at all times.</li>
<li><tt>cached</tt>: The host's view of the mount is authoritative. There may be delays before updates made on the host are visible within a container.</li>
<li><tt>delegated</tt>: The container runtime's view of the mount is authoritative. There may be delays before updates made in a container are are visible on the host.</li>
<li><tt>delegated</tt>: The container runtime's view of the mount is authoritative. There may be delays before updates made in a container are visible on the host.</li>
</ul>
</p>
</td>
@ -588,27 +589,27 @@ follows:
<tr>
<td><tt>node.id</tt></td>
<td>Node ID</td>
<td><tt>node.id == 2ivku8v2gvtg4</tt></td>
<td><tt>node.id==2ivku8v2gvtg4</tt></td>
</tr>
<tr>
<td><tt>node.hostname</tt></td>
<td>Node hostname</td>
<td><tt>node.hostname != node-2</tt></td>
<td><tt>node.hostname!=node-2</tt></td>
</tr>
<tr>
<td><tt>node.role</tt></td>
<td>Node role</td>
<td><tt>node.role == manager</tt></td>
<td><tt>node.role==manager</tt></td>
</tr>
<tr>
<td><tt>node.labels</tt></td>
<td>user defined node labels</td>
<td><tt>node.labels.security == high</tt></td>
<td><tt>node.labels.security==high</tt></td>
</tr>
<tr>
<td><tt>engine.labels</tt></td>
<td>Docker Engine's labels</td>
<td><tt>engine.labels.operatingsystem == ubuntu 14.04</tt></td>
<td><tt>engine.labels.operatingsystem==ubuntu 14.04</tt></td>
</tr>
</table>
@ -735,52 +736,76 @@ Containers on the same network can access each other using
### Publish service ports externally to the swarm (-p, --publish)
You can publish service ports to make them available externally to the swarm
using the `--publish` flag:
```bash
$ docker service create --publish <TARGET-PORT>:<SERVICE-PORT> nginx
```
For example:
using the `--publish` flag. The `--publish` flag can take two different styles
of arguments. The short version is positional, and allows you to specify the
target port and container port separated by a colon.
```bash
$ docker service create --name my_web --replicas 3 --publish 8080:80 nginx
```
When you publish a service port, the swarm routing mesh makes the service
accessible at the target port on every node regardless if there is a task for
the service running on the node. For more information refer to
There is also a long format, which is easier to read and allows you to specify
more options. The long format is preferred. You cannot specify the service's
mode when using the short format. Here is an example of using the long format
for the same service as above:
```bash
$ docker service create --name my_web --replicas 3 --publish target=8080,port=80 nginx
```
The options you can specify are:
<table>
<thead>
<tr>
<th>Option</th>
<th>Short syntax</th>
<th>Long syntax</th>
<th>Description</th>
</tr>
<tr>
<td>target and container port </td>
<td><tt></tt></td>
<td><tt></tt></td>
<td></td>
</tr>
<tr>
<td>protocol</td>
<td><tt>--publish 8080:80</tt></td>
<td><tt>--publish target=8080,port=80</tt></td>
<td><p>
The container port to publish and the target port to bind it to on the
routing mesh or directly on the node.
</p></td>
</tr>
<tr>
<td>mode</td>
<td>Not possible to set using short syntax.</td>
<td><tt>--publish target=8080,port=80,mode=host</tt></td>
<td><p>
The mode to use for binding the port, either `ingress` or `host`. Defaults
to `ingress` to use the routing mesh.
</p></td>
</tr>
<tr>
<td>protocol</td>
<td><tt>--publish 8080:80/tcp</tt></td>
<td><tt>--publish target=8080,port=80,protocol=tcp</tt></td>
<td><p>
The protocol to use, either `tcp` or `udp`. Defaults to `tcp`. To bind a
port for both protocols, specify the `-p` or `--publish` flag twice.
</p></td>
</tr>
</table>
When you publish a service port using `ingres` mode, the swarm routing mesh
makes the service accessible at the target port on every node regardless if
there is a task for the service running on the node. If you use `host` mode,
the port is only bound on nodes where the service is running, and a given port
on a node can only be bound once. You can only set the publication mode using
the long syntax. For more information refer to
[Use swarm mode routing mesh](https://docs.docker.com/engine/swarm/ingress/).
### Publish a port for TCP only or UDP only
By default, when you publish a port, it is a TCP port. You can
specifically publish a UDP port instead of or in addition to a TCP port. When
you publish both TCP and UDP ports, Docker 1.12.2 and earlier require you to
add the suffix `/tcp` for TCP ports. Otherwise it is optional.
#### TCP only
The following two commands are equivalent.
```bash
$ docker service create --name dns-cache -p 53:53 dns-cache
$ docker service create --name dns-cache -p 53:53/tcp dns-cache
```
#### TCP and UDP
```bash
$ docker service create --name dns-cache -p 53:53/tcp -p 53:53/udp dns-cache
```
#### UDP only
```bash
$ docker service create --name dns-cache -p 53:53/udp dns-cache
```
### Provide credential specs for managed service accounts (Windows only)
This option is only used for services using Windows containers. The
@ -875,6 +900,22 @@ $ docker inspect --format="{{.Config.Hostname}}" 2e7a8a9c4da2-wo41w8hg8qanxwjwsg
x3ti0erg11rjpg64m75kej2mz-hosttempl
```
### Specify isolation mode (Windows)
By default, tasks scheduled on Windows nodes are run using the default isolation mode
configured for this particular node. To force a specific isolation mode, you can use
the `--isolation` flag:
```bash
$ docker service create --name myservice --isolation=process microsoft/nanoserver
```
Supported isolation modes on Windows are:
- `default`: use default settings specified on the node running the task
- `process`: use process isolation (Windows server only)
- `hyperv`: use Hyper-V isolation
## Related commands
* [service inspect](service_inspect.md)

View File

@ -53,6 +53,7 @@ Options:
--host-rm list Remove a custom host-to-IP mapping (host:ip)
--hostname string Container hostname
--image string Service image tag
--isolation string Service container isolation mode
--label-add list Add or update a service label
--label-rm list Remove a label by its key
--limit-cpu decimal Limit CPUs
@ -174,6 +175,21 @@ $ docker service update --mount-rm /somewhere myservice
myservice
```
### Add or remove port mappings
Use the `--port-add` or `--port-rm` flags to add or remove port mappings to or
from a service. You can use the short or long syntax discussed in the
[docker service update](service_create/#attach-a-service-to-an-existing-network-network)
reference.
The following example adds a port mapping to an existing service.
```bash
$ docker service update \
--port-add port=80,target=8080 \
myservice
```
### Roll back to the previous version of a service
Use the `--rollback` option to roll back to the previous version of the service.
@ -258,6 +274,12 @@ $ docker service update \
Some flags of `service update` support the use of templating.
See [`service create`](./service_create.md#templating) for the reference.
### Specify isolation mode (Windows)
`service update` supports the same `--isolation` flag as `service create`
See [`service create`](./service_create.md) for the reference.
## Related commands
* [service create](service_create.md)

View File

@ -16,10 +16,14 @@ keywords: "sign, notary, trust"
# trust sign
```markdown
Usage: docker trust sign IMAGE:TAG
Usage: docker trust sign [OPTIONS] IMAGE:TAG
Sign an image
Options:
--help print usage
--local force the signing of a local image
```
## Description

View File

@ -4,6 +4,7 @@ import (
"fmt"
"testing"
"github.com/docker/cli/e2e/internal/fixtures"
shlex "github.com/flynn-archive/go-shlex"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/gotestyourself/gotestyourself/icmd"
@ -11,8 +12,6 @@ import (
"github.com/stretchr/testify/require"
)
const alpineImage = "registry:5000/alpine:3.6"
func TestRunAttachedFromRemoteImageAndRemove(t *testing.T) {
image := createRemoteImage(t)
@ -27,8 +26,8 @@ func TestRunAttachedFromRemoteImageAndRemove(t *testing.T) {
// TODO: create this with registry API instead of engine API
func createRemoteImage(t *testing.T) string {
image := "registry:5000/alpine:test-run-pulls"
icmd.RunCommand("docker", "pull", alpineImage).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", alpineImage, image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "push", image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
return image

View File

@ -0,0 +1,83 @@
package image
import (
"fmt"
"strings"
"testing"
"github.com/docker/cli/e2e/internal/fixtures"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/icmd"
"github.com/pkg/errors"
)
func TestBuildFromContextDirectoryWithTag(t *testing.T) {
dir := fs.NewDir(t, "test-build-context-dir",
fs.WithFile("run", "echo running", fs.WithMode(0755)),
fs.WithDir("data", fs.WithFile("one", "1111")),
fs.WithFile("Dockerfile", fmt.Sprintf(`
FROM %s
COPY run /usr/bin/run
RUN run
COPY data /data
`, fixtures.AlpineImage)))
defer dir.Remove()
result := icmd.RunCmd(
icmd.Command("docker", "build", "-t", "myimage", "."),
withWorkingDir(dir))
result.Assert(t, icmd.Expected{Err: icmd.None})
assertBuildOutput(t, result.Stdout(), map[int]lineCompare{
0: prefix("Sending build context to Docker daemon"),
1: equals("Step 1/4 : FROM\tregistry:5000/alpine:3.6"),
3: equals("Step 2/4 : COPY\trun /usr/bin/run"),
5: equals("Step 3/4 : RUN\t\trun"),
7: equals("running"),
9: equals("Step 4/4 : COPY\tdata /data"),
11: prefix("Removing intermediate container "),
12: prefix("Successfully built "),
13: equals("Successfully tagged myimage:latest"),
})
}
func withWorkingDir(dir *fs.Dir) func(*icmd.Cmd) {
return func(cmd *icmd.Cmd) {
cmd.Dir = dir.Path()
}
}
func assertBuildOutput(t *testing.T, actual string, expectedLines map[int]lineCompare) {
for i, line := range strings.Split(actual, "\n") {
cmp, ok := expectedLines[i]
if !ok {
continue
}
if err := cmp(line); err != nil {
t.Errorf("line %d: %s", i, err)
}
}
if t.Failed() {
t.Log(actual)
}
}
type lineCompare func(string) error
func prefix(expected string) func(string) error {
return func(actual string) error {
if strings.HasPrefix(actual, expected) {
return nil
}
return errors.Errorf("expected %s to start with %s", actual, expected)
}
}
func equals(expected string) func(string) error {
return func(actual string) error {
if expected == actual {
return nil
}
return errors.Errorf("got %s, expected %s", actual, expected)
}
}

View File

@ -2,23 +2,19 @@ package image
import (
"fmt"
"os"
"testing"
"github.com/docker/cli/e2e/internal/fixtures"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/gotestyourself/gotestyourself/icmd"
)
const notaryURL = "https://notary-server:4443"
const registryPrefix = "registry:5000"
const alpineImage = "registry:5000/alpine:3.6"
const busyboxImage = "registry:5000/busybox:1.27.2"
func TestPullWithContentTrust(t *testing.T) {
image := createMaskedTrustedRemoteImage(t, "trust", "latest")
result := icmd.RunCmd(icmd.Command("docker", "pull", image), withTrustNoPassphrase)
result := icmd.RunCmd(icmd.Command("docker", "pull", image), fixtures.WithTrust, fixtures.WithNotary)
result.Assert(t, icmd.Success)
golden.Assert(t, result.Stderr(), "pull-with-content-trust-err.golden")
golden.Assert(t, result.Stdout(), "pull-with-content-trust.golden")
@ -34,39 +30,19 @@ func createMaskedTrustedRemoteImage(t *testing.T, repo, tag string) string {
func createTrustedRemoteImage(t *testing.T, repo, tag string) string {
image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
icmd.RunCommand("docker", "pull", alpineImage).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", alpineImage, image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
result := icmd.RunCmd(
icmd.Command("docker", "push", image),
withTrustAndPassphrase("root_password", "repo_password"))
fixtures.WithPassphrase("root_password", "repo_password"), fixtures.WithTrust, fixtures.WithNotary)
result.Assert(t, icmd.Success)
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
return image
}
func createNamedUnsignedImageFromBusyBox(t *testing.T, image string) {
icmd.RunCommand("docker", "pull", busyboxImage).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", busyboxImage, image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "pull", fixtures.BusyboxImage).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.BusyboxImage, image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "push", image).Assert(t, icmd.Success)
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
}
func withTrustAndPassphrase(rootPwd, repositoryPwd string) func(cmd *icmd.Cmd) {
return func(cmd *icmd.Cmd) {
env := append(os.Environ(),
"DOCKER_CONTENT_TRUST=1",
"DOCKER_CONTENT_TRUST_SERVER="+notaryURL,
"DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE="+rootPwd,
"DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE="+repositoryPwd,
)
cmd.Env = append(cmd.Env, env...)
}
}
func withTrustNoPassphrase(cmd *icmd.Cmd) {
env := append(os.Environ(),
"DOCKER_CONTENT_TRUST=1",
"DOCKER_CONTENT_TRUST_SERVER="+notaryURL,
)
cmd.Env = append(cmd.Env, env...)
}

View File

@ -0,0 +1,76 @@
package fixtures
import (
"os"
"testing"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/icmd"
)
const (
//NotaryURL is the location of the notary server
NotaryURL = "https://notary-server:4443"
//AlpineImage is an image in the test registry
AlpineImage = "registry:5000/alpine:3.6"
//AlpineSha is the sha of the alpine image
AlpineSha = "641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d"
//BusyboxImage is an image in the test registry
BusyboxImage = "registry:5000/busybox:1.27.2"
//BusyboxSha is the sha of the busybox image
BusyboxSha = "030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af"
)
//SetupConfigFile creates a config.json file for testing
func SetupConfigFile(t *testing.T) fs.Dir {
dir := fs.NewDir(t, "trust_test", fs.WithMode(0700), fs.WithFile("config.json", `
{
"auths": {
"registry:5000": {
"auth": "ZWlhaXM6cGFzc3dvcmQK"
},
"https://notary-server:4443": {
"auth": "ZWlhaXM6cGFzc3dvcmQK"
}
}
}
`))
return *dir
}
//WithConfig sets an environment variable for the docker config location
func WithConfig(dir string) func(cmd *icmd.Cmd) {
return func(cmd *icmd.Cmd) {
env := append(os.Environ(),
"DOCKER_CONFIG="+dir,
)
cmd.Env = append(cmd.Env, env...)
}
}
//WithPassphrase sets environment variables for passphrases
func WithPassphrase(rootPwd, repositoryPwd string) func(cmd *icmd.Cmd) {
return func(cmd *icmd.Cmd) {
env := append(os.Environ(),
"DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE="+rootPwd,
"DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE="+repositoryPwd,
)
cmd.Env = append(cmd.Env, env...)
}
}
//WithTrust sets DOCKER_CONTENT_TRUST to 1
func WithTrust(cmd *icmd.Cmd) {
env := append(os.Environ(),
"DOCKER_CONTENT_TRUST=1",
)
cmd.Env = append(cmd.Env, env...)
}
//WithNotary sets the location of the notary server
func WithNotary(cmd *icmd.Cmd) {
env := append(os.Environ(),
"DOCKER_CONTENT_TRUST_SERVER="+NotaryURL,
)
cmd.Env = append(cmd.Env, env...)
}

View File

@ -0,0 +1,17 @@
package trust
import (
"fmt"
"os"
"testing"
"github.com/docker/cli/internal/test/environment"
)
func TestMain(m *testing.M) {
if err := environment.Setup(); err != nil {
fmt.Println(err.Error())
os.Exit(3)
}
os.Exit(m.Run())
}

View File

@ -0,0 +1,64 @@
package trust
import (
"fmt"
"testing"
"github.com/docker/cli/e2e/internal/fixtures"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/icmd"
"github.com/stretchr/testify/assert"
)
const (
revokeImage = "registry:5000/revoke:v1"
revokeRepo = "registry:5000/revokerepo"
)
func TestRevokeImage(t *testing.T) {
dir := fixtures.SetupConfigFile(t)
defer dir.Remove()
setupTrustedImagesForRevoke(t, dir)
result := icmd.RunCmd(
icmd.Command("docker", "trust", "revoke", revokeImage),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithNotary, fixtures.WithConfig(dir.Path()))
result.Assert(t, icmd.Success)
assert.Contains(t, result.Stdout(), "Successfully deleted signature for registry:5000/revoke:v1")
}
func TestRevokeRepo(t *testing.T) {
dir := fixtures.SetupConfigFile(t)
defer dir.Remove()
setupTrustedImagesForRevokeRepo(t, dir)
result := icmd.RunCmd(
icmd.Command("docker", "trust", "revoke", revokeRepo, "-y"),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithNotary, fixtures.WithConfig(dir.Path()))
result.Assert(t, icmd.Success)
assert.Contains(t, result.Stdout(), "Successfully deleted signature for registry:5000/revoke")
}
func setupTrustedImagesForRevoke(t *testing.T, dir fs.Dir) {
icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, revokeImage).Assert(t, icmd.Success)
icmd.RunCmd(
icmd.Command("docker", "-D", "trust", "sign", revokeImage),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithConfig(dir.Path()), fixtures.WithNotary).Assert(t, icmd.Success)
}
func setupTrustedImagesForRevokeRepo(t *testing.T, dir fs.Dir) {
icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, fmt.Sprintf("%s:v1", revokeRepo)).Assert(t, icmd.Success)
icmd.RunCmd(
icmd.Command("docker", "-D", "trust", "sign", fmt.Sprintf("%s:v1", revokeRepo)),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithConfig(dir.Path()), fixtures.WithNotary).Assert(t, icmd.Success)
icmd.RunCmd(icmd.Command("docker", "pull", fixtures.BusyboxImage)).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.BusyboxImage, fmt.Sprintf("%s:v2", revokeRepo)).Assert(t, icmd.Success)
icmd.RunCmd(
icmd.Command("docker", "-D", "trust", "sign", fmt.Sprintf("%s:v2", revokeRepo)),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithConfig(dir.Path()), fixtures.WithNotary).Assert(t, icmd.Success)
}

View File

@ -0,0 +1,55 @@
package trust
import (
"fmt"
"testing"
"github.com/docker/cli/e2e/internal/fixtures"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/icmd"
"github.com/stretchr/testify/assert"
)
const (
localImage = "registry:5000/signlocal:v1"
signImage = "registry:5000/sign:v1"
)
func TestSignLocalImage(t *testing.T) {
dir := fixtures.SetupConfigFile(t)
defer dir.Remove()
icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, signImage).Assert(t, icmd.Success)
result := icmd.RunCmd(
icmd.Command("docker", "trust", "sign", signImage),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithConfig(dir.Path()), fixtures.WithNotary)
result.Assert(t, icmd.Success)
assert.Contains(t, result.Stdout(), fmt.Sprintf("v1: digest: sha256:%s", fixtures.AlpineSha))
}
func TestSignWithLocalFlag(t *testing.T) {
dir := fixtures.SetupConfigFile(t)
defer dir.Remove()
setupTrustedImageForOverwrite(t, dir)
result := icmd.RunCmd(
icmd.Command("docker", "trust", "sign", "--local", localImage),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithConfig(dir.Path()), fixtures.WithNotary)
result.Assert(t, icmd.Success)
assert.Contains(t, result.Stdout(), fmt.Sprintf("v1: digest: sha256:%s", fixtures.BusyboxSha))
}
func setupTrustedImageForOverwrite(t *testing.T, dir fs.Dir) {
icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, localImage).Assert(t, icmd.Success)
result := icmd.RunCmd(
icmd.Command("docker", "-D", "trust", "sign", localImage),
fixtures.WithPassphrase("root_password", "repo_password"),
fixtures.WithConfig(dir.Path()), fixtures.WithNotary)
result.Assert(t, icmd.Success)
assert.Contains(t, result.Stdout(), fmt.Sprintf("v1: digest: sha256:%s", fixtures.AlpineSha))
icmd.RunCmd(icmd.Command("docker", "pull", fixtures.BusyboxImage)).Assert(t, icmd.Success)
icmd.RunCommand("docker", "tag", fixtures.BusyboxImage, localImage).Assert(t, icmd.Success)
}

View File

@ -220,7 +220,7 @@ $$ ip a show eth0
The mode ` -o ipvlan_mode=l3` must be explicitly specified since the default ipvlan mode is `l2`.
The following example does not specify a parent interface. The network drivers will create a dummy type link for the user rather then rejecting the network creation and isolating containers from only communicating with one another.
The following example does not specify a parent interface. The network drivers will create a dummy type link for the user rather than rejecting the network creation and isolating containers from only communicating with one another.
```
# Create the Ipvlan L3 network

View File

@ -582,7 +582,7 @@ incompatible with any restart policy other than `none`.
This option is only available for the `devicemapper`, `btrfs`, `overlay2` and `zfs` graph drivers.
For the `devicemapper`, `btrfs` and `zfs` storage drivers, user cannot pass a size less than the Default BaseFS Size.
For the `overlay2` storage driver, the size option is only available if the backing fs is `xfs` and mounted with the `pquota` mount option.
Under these conditions, user can pass any size less then the backing fs size.
Under these conditions, user can pass any size less than the backing fs size.
**--stop-signal**=*SIGTERM*
Signal to stop a container. Default is SIGTERM.

View File

@ -480,7 +480,7 @@ autoextend the available space [100 = disabled]
##### dm.thinp_autoextend_percent
Sets the value percentage value to increase the thin pool by when when `lvm`
Sets the value percentage value to increase the thin pool by when `lvm`
attempts to autoextend the available space [100 = disabled]
###### Example:

View File

@ -34,6 +34,7 @@ func generateManPages(opts *options) error {
}
cmd.DisableAutoGenTag = true
cmd.DisableFlagsInUseLine = true
return doc.GenManTreeFromOpts(cmd, doc.GenManTreeOptions{
Header: header,
Path: opts.target,
@ -43,6 +44,7 @@ func generateManPages(opts *options) error {
func loadLongDescription(cmd *cobra.Command, path string) error {
for _, cmd := range cmd.Commands() {
cmd.DisableFlagsInUseLine = true
if cmd.Name() == "" {
continue
}

View File

@ -10,8 +10,8 @@ then continue streaming new output from the container's stdout and stderr.
**Warning**: This command works only for the **json-file** or **journald**
logging drivers.
The `--since` option can be Unix timestamps, date formatted timestamps, or Go
duration strings (e.g. `10m`, `1h30m`) computed relative to the client machine's
The `--since` and `--until` options can be Unix timestamps, date formatted timestamps,
or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the client machine's
time. Supported formats for date formatted time stamps include RFC3339Nano,
RFC3339, `2006-01-02T15:04:05`, `2006-01-02T15:04:05.999999999`,
`2006-01-02Z07:00`, and `2006-01-02`. The local timezone on the client will be
@ -20,9 +20,21 @@ end of the timestamp. When providing Unix timestamps enter
seconds[.nanoseconds], where seconds is the number of seconds that have elapsed
since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (aka Unix
epoch or Unix time), and the optional .nanoseconds field is a fraction of a
second no more than nine digits long. You can combine the `--since` option with
either or both of the `--follow` or `--tail` options.
second no more than nine digits long. You can combine the `--since` or `--until`
options with either or both of the `--follow` or `--tail` options.
The `docker container logs --details` command will add on extra attributes, such as
environment variables and labels, provided to `--log-opt` when creating the
container.
In order to retrieve logs before a specific point in time, run:
```bash
$ docker run --name test -d busybox sh -c "while true; do $(echo date); sleep 1; done"
$ date
Tue 14 Nov 2017 16:40:00 CET
$ docker logs -f --until=2s
Tue 14 Nov 2017 16:40:00 CET
Tue 14 Nov 2017 16:40:01 CET
Tue 14 Nov 2017 16:40:02 CET
```

View File

@ -134,7 +134,7 @@ Registry credentials are managed by **docker-login(1)**.
Docker uses the `https://` protocol to communicate with a registry, unless the
registry is allowed to be accessed over an insecure connection. Refer to the
[insecure registries](https://docs.docker.com/engine/reference/commandline/daemon/#insecure-registries)
[insecure registries](https://docs.docker.com/engine/reference/commandline/dockerd/#insecure-registries)
section in the online documentation for more information.

View File

@ -123,7 +123,7 @@ to create an externally isolated `overlay` network, you can specify the
You can create the network which will be used to provide the routing-mesh in the
swarm cluster. You do so by specifying `--ingress` when creating the network. Only
one ingress network can be created at the time. The network can be removed only
if no services depend on it. Any option available when creating a overlay network
if no services depend on it. Any option available when creating an overlay network
is also available when creating the ingress network, besides the `--attachable` option.
```bash

View File

@ -92,7 +92,7 @@ $ docker network inspect simple-network
details such as the service's VIP and port mappings. It also shows IPs of service tasks,
and the IPs of the nodes where the tasks are running.
Following is an example output for a overlay network `ov1` that has one service `s1`
Following is an example output for an overlay network `ov1` that has one service `s1`
attached to. service `s1` in this case has three replicas.
```bash

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -eu -o pipefail
go test -tags daemon -v "$@"
go test -v "$@"

View File

@ -1,11 +1,11 @@
github.com/agl/ed25519 d2b94fd789ea21d12fac1a4443dd3a3f79cda72c
github.com/Azure/go-ansiterm d6e3b3328b783f23731bc4d058875b0371ff8109
github.com/containerd/continuity 22694c680ee48fb8f50015b44618517e2bde77e8
github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20
github.com/containerd/continuity 35d55c5e8dd23b32037d56cf97174aff3efdfa83
github.com/coreos/etcd v3.2.1
github.com/cpuguy83/go-md2man a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c
github.com/docker/docker 1ffa10ee2630917e4f2a88f5e4daf34d700eaa1f
github.com/docker/docker f4d4f5863156b82ef146b6ff1e845f8dcf019f12
github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b
# the docker/go package contains a customized version of canonical/json
@ -14,7 +14,7 @@ github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06
github.com/docker/go-connections 3ede32e2033de7505e6500d6c868c2b9ed9f169d
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1
github.com/docker/swarmkit 872861d2ae46958af7ead1d5fffb092c73afbaf0
github.com/docker/swarmkit 28f91d87bd3f75fd039dbb9be49bfd2381019261
github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
github.com/gogo/protobuf v0.4
github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4
@ -23,7 +23,7 @@ github.com/gorilla/mux v1.1
github.com/gotestyourself/gotestyourself v1.2.0
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
github.com/mattn/go-shellwords v1.0.3
github.com/Microsoft/go-winio v0.4.4
github.com/Microsoft/go-winio v0.4.5
github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f
github.com/mitchellh/mapstructure f3009df150dadf309fdee4a54ed65c124afad715
github.com/moby/buildkit aaff9d591ef128560018433fe61beb802e149de8
@ -36,7 +36,7 @@ github.com/pmezard/go-difflib v1.0.0
github.com/russross/blackfriday 1d6b8e9301e720b08a8938b8c25c018285885438
github.com/shurcooL/sanitized_anchor_name 10ef21a441db47d8b13ebcc5fd2310f636973c77
github.com/sirupsen/logrus v1.0.3
github.com/spf13/cobra 7b2c5ac9fc04fc5efafb60700713d4fa609b777b
github.com/spf13/cobra 34ceca591bcf34a17a8b7bad5b3ce5f9c165bee5
github.com/spf13/pflag 97afa5e7ca8a08a383cb259e06636b5e2cc7897f
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987
github.com/theupdateframework/notary 05985dc5d1c71ee6c387e9cd276a00b9d424af53
@ -47,8 +47,8 @@ github.com/xeipuuv/gojsonschema 93e72a773fade158921402d6a24c819b48aba29d
golang.org/x/crypto 558b6879de74bc843225cde5686419267ff707ca
golang.org/x/net 7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6
golang.org/x/sync 450f422ab23cf9881c94e2db30cac0eb1b7cf80c
golang.org/x/sys 07c182904dbd53199946ba614a412c61d3c548f5
golang.org/x/text 825fc78a2fd6fa0a5447e300189e3219e05e1f25
golang.org/x/sys 95c6576299259db960f6c5b9b69ea52422860fce
golang.org/x/text f72d8390a633d5dfb0cc84043294db9f6c935756
golang.org/x/time a4bde12657593d5e90d0533a3e4fd95e635124cb
google.golang.org/genproto d80a6e20e776b0b17a324d0ba1ab50a39c8e8944
google.golang.org/grpc v1.3.0

View File

@ -68,10 +68,20 @@ func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
return &BackupStreamReader{r, 0}
}
// Next returns the next backup stream and prepares for calls to Write(). It skips the remainder of the current stream if
// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if
// it was not completely read.
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
if r.bytesLeft > 0 {
if s, ok := r.r.(io.Seeker); ok {
// Make sure Seek on io.SeekCurrent sometimes succeeds
// before trying the actual seek.
if _, err := s.Seek(0, io.SeekCurrent); err == nil {
if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil {
return nil, err
}
r.bytesLeft = 0
}
}
if _, err := io.Copy(ioutil.Discard, r); err != nil {
return nil, err
}
@ -220,7 +230,7 @@ type BackupFileWriter struct {
ctx uintptr
}
// NewBackupFileWrtier returns a new BackupFileWriter from a file handle. If includeSecurity is true,
// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true,
// Write() will attempt to restore the security descriptor from the stream.
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
w := &BackupFileWriter{f, includeSecurity, 0}

View File

@ -0,0 +1,137 @@
package winio
import (
"bytes"
"encoding/binary"
"errors"
)
type fileFullEaInformation struct {
NextEntryOffset uint32
Flags uint8
NameLength uint8
ValueLength uint16
}
var (
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
errEaNameTooLarge = errors.New("extended attribute name too large")
errEaValueTooLarge = errors.New("extended attribute value too large")
)
// ExtendedAttribute represents a single Windows EA.
type ExtendedAttribute struct {
Name string
Value []byte
Flags uint8
}
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
var info fileFullEaInformation
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
if err != nil {
err = errInvalidEaBuffer
return
}
nameOffset := fileFullEaInformationSize
nameLen := int(info.NameLength)
valueOffset := nameOffset + int(info.NameLength) + 1
valueLen := int(info.ValueLength)
nextOffset := int(info.NextEntryOffset)
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
err = errInvalidEaBuffer
return
}
ea.Name = string(b[nameOffset : nameOffset+nameLen])
ea.Value = b[valueOffset : valueOffset+valueLen]
ea.Flags = info.Flags
if info.NextEntryOffset != 0 {
nb = b[info.NextEntryOffset:]
}
return
}
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
for len(b) != 0 {
ea, nb, err := parseEa(b)
if err != nil {
return nil, err
}
eas = append(eas, ea)
b = nb
}
return
}
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
if int(uint8(len(ea.Name))) != len(ea.Name) {
return errEaNameTooLarge
}
if int(uint16(len(ea.Value))) != len(ea.Value) {
return errEaValueTooLarge
}
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
withPadding := (entrySize + 3) &^ 3
nextOffset := uint32(0)
if !last {
nextOffset = withPadding
}
info := fileFullEaInformation{
NextEntryOffset: nextOffset,
Flags: ea.Flags,
NameLength: uint8(len(ea.Name)),
ValueLength: uint16(len(ea.Value)),
}
err := binary.Write(buf, binary.LittleEndian, &info)
if err != nil {
return err
}
_, err = buf.Write([]byte(ea.Name))
if err != nil {
return err
}
err = buf.WriteByte(0)
if err != nil {
return err
}
_, err = buf.Write(ea.Value)
if err != nil {
return err
}
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
if err != nil {
return err
}
return nil
}
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
// buffer for use with BackupWrite, ZwSetEaFile, etc.
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
var buf bytes.Buffer
for i := range eas {
last := false
if i == len(eas)-1 {
last = true
}
err := writeEa(&buf, &eas[i], last)
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

View File

@ -78,6 +78,7 @@ func initIo() {
type win32File struct {
handle syscall.Handle
wg sync.WaitGroup
wgLock sync.RWMutex
closing atomicBool
readDeadline deadlineHandler
writeDeadline deadlineHandler
@ -114,14 +115,18 @@ func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
// closeHandle closes the resources associated with a Win32 handle
func (f *win32File) closeHandle() {
f.wgLock.Lock()
// Atomically set that we are closing, releasing the resources only once.
if !f.closing.swap(true) {
f.wgLock.Unlock()
// cancel all IO and wait for it to complete
cancelIoEx(f.handle, nil)
f.wg.Wait()
// at this point, no new IO can start
syscall.Close(f.handle)
f.handle = 0
} else {
f.wgLock.Unlock()
}
}
@ -134,10 +139,13 @@ func (f *win32File) Close() error {
// prepareIo prepares for a new IO operation.
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
func (f *win32File) prepareIo() (*ioOperation, error) {
f.wgLock.RLock()
if f.closing.isSet() {
f.wgLock.RUnlock()
return nil, ErrFileClosed
}
f.wg.Add(1)
f.wgLock.RUnlock()
c := &ioOperation{}
c.ch = make(chan ioResult)
return c, nil

View File

@ -265,9 +265,9 @@ func (l *win32PipeListener) listenerRoutine() {
if err == nil {
// Wait for the client to connect.
ch := make(chan error)
go func() {
go func(p *win32File) {
ch <- connectPipe(p)
}()
}(p)
select {
case err = <-ch:
if err != nil {

View File

@ -11,7 +11,7 @@
![etcd Logo](logos/etcd-horizontal-color.png)
etcd is a distributed, consistent key-value store for shared configuration and service discovery, with a focus on being:
etcd is a distributed reliable key-value store for the most critical data of a distributed system, with a focus on being:
* *Simple*: well-defined, user-facing API (gRPC)
* *Secure*: automatic TLS with optional client cert authentication
@ -37,13 +37,11 @@ See [etcdctl][etcdctl] for a simple command line client.
### Getting etcd
The easiest way to get etcd is to use one of the pre-built release binaries which are available for OSX, Linux, Windows, AppC (ACI), and Docker. Instructions for using these binaries are on the [GitHub releases page][github-release].
The easiest way to get etcd is to use one of the pre-built release binaries which are available for OSX, Linux, Windows, [rkt][rkt], and Docker. Instructions for using these binaries are on the [GitHub releases page][github-release].
For those wanting to try the very latest version, you can [build the latest version of etcd][dl-build] from the `master` branch.
You will first need [*Go*](https://golang.org/) installed on your machine (version 1.6+ is required).
All development occurs on `master`, including new features and bug fixes.
Bug fixes are first targeted at `master` and subsequently ported to release branches, as described in the [branch management][branch-management] guide.
For those wanting to try the very latest version, [build the latest version of etcd][dl-build] from the `master` branch. This first needs [*Go*](https://golang.org/) installed (version 1.8+ is required). All development occurs on `master`, including new features and bug fixes. Bug fixes are first targeted at `master` and subsequently ported to release branches, as described in the [branch management][branch-management] guide.
[rkt]: https://github.com/rkt/rkt/releases/
[github-release]: https://github.com/coreos/etcd/releases/
[branch-management]: ./Documentation/branch_management.md
[dl-build]: ./Documentation/dl_build.md#build-the-latest-version
@ -75,9 +73,9 @@ That's it! etcd is now running and serving client requests. For more
### etcd TCP ports
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication.
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication.
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
### Running a local etcd cluster
@ -95,7 +93,7 @@ Every cluster member and proxy accepts key value reads and key value writes.
### Running etcd on Kubernetes
If you want to run etcd cluster on Kubernetes, try [etcd operator](https://github.com/coreos/etcd-operator).
To run an etcd cluster on Kubernetes, try [etcd operator](https://github.com/coreos/etcd-operator).
### Next steps
@ -105,7 +103,7 @@ Now it's time to dig into the full etcd API and other guides.
- Explore the full gRPC [API][api].
- Set up a [multi-machine cluster][clustering].
- Learn the [config format, env variables and flags][configuration].
- Find [language bindings and tools][libraries-and-tools].
- Find [language bindings and tools][integrations].
- Use TLS to [secure an etcd cluster][security].
- [Tune etcd][tuning].
@ -113,7 +111,7 @@ Now it's time to dig into the full etcd API and other guides.
[api]: ./Documentation/dev-guide/api_reference_v3.md
[clustering]: ./Documentation/op-guide/clustering.md
[configuration]: ./Documentation/op-guide/configuration.md
[libraries-and-tools]: ./Documentation/libraries-and-tools.md
[integrations]: ./Documentation/integrations.md
[security]: ./Documentation/op-guide/security.md
[tuning]: ./Documentation/tuning.md
@ -130,10 +128,8 @@ See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the co
## Reporting bugs
See [reporting bugs](Documentation/reporting_bugs.md) for details about reporting any issue you may encounter.
See [reporting bugs](Documentation/reporting_bugs.md) for details about reporting any issues.
### License
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.

View File

@ -13,9 +13,7 @@ To keep the codebase small as well as provide flexibility, the library only impl
In order to easily test the Raft library, its behavior should be deterministic. To achieve this determinism, the library models Raft as a state machine. The state machine takes a `Message` as input. A message can either be a local timer update or a network message sent from a remote peer. The state machine's output is a 3-tuple `{[]Messages, []LogEntries, NextState}` consisting of an array of `Messages`, `log entries`, and `Raft state changes`. For state machines with the same state, the same state machine input should always generate the same state machine output.
A simple example application, _raftexample_, is also available to help illustrate
how to use this package in practice:
https://github.com/coreos/etcd/tree/master/contrib/raftexample
A simple example application, _raftexample_, is also available to help illustrate how to use this package in practice: https://github.com/coreos/etcd/tree/master/contrib/raftexample
# Features
@ -51,11 +49,11 @@ This raft implementation also includes a few optional enhancements:
- [etcd](https://github.com/coreos/etcd) A distributed reliable key-value store
- [tikv](https://github.com/pingcap/tikv) A Distributed transactional key value database powered by Rust and Raft
- [swarmkit](https://github.com/docker/swarmkit) A toolkit for orchestrating distributed systems at any scale.
- [chain core](https://github.com/chain/chain) Software for operating permissioned, multi-asset blockchain networks
## Usage
The primary object in raft is a Node. You either start a Node from scratch
using raft.StartNode or start a Node from some initial state using raft.RestartNode.
The primary object in raft is a Node. Either start a Node from scratch using raft.StartNode or start a Node from some initial state using raft.RestartNode.
To start a three-node cluster
```go
@ -73,7 +71,7 @@ To start a three-node cluster
n := raft.StartNode(c, []raft.Peer{{ID: 0x02}, {ID: 0x03}})
```
You can start a single node cluster, like so:
Start a single node cluster, like so:
```go
// Create storage and config as shown above.
// Set peer list to itself, so this node can become the leader of this single-node cluster.
@ -81,7 +79,7 @@ You can start a single node cluster, like so:
n := raft.StartNode(c, peers)
```
To allow a new node to join this cluster, do not pass in any peers. First, you need add the node to the existing cluster by calling `ProposeConfChange` on any existing node inside the cluster. Then, you can start the node with empty peer list, like so:
To allow a new node to join this cluster, do not pass in any peers. First, add the node to the existing cluster by calling `ProposeConfChange` on any existing node inside the cluster. Then, start the node with an empty peer list, like so:
```go
// Create storage and config as shown above.
n := raft.StartNode(c, nil)
@ -110,46 +108,21 @@ To restart a node from previous state:
n := raft.RestartNode(c)
```
Now that you are holding onto a Node you have a few responsibilities:
After creating a Node, the user has a few responsibilities:
First, you must read from the Node.Ready() channel and process the updates
it contains. These steps may be performed in parallel, except as noted in step
2.
First, read from the Node.Ready() channel and process the updates it contains. These steps may be performed in parallel, except as noted in step 2.
1. Write HardState, Entries, and Snapshot to persistent storage if they are
not empty. Note that when writing an Entry with Index i, any
previously-persisted entries with Index >= i must be discarded.
1. Write HardState, Entries, and Snapshot to persistent storage if they are not empty. Note that when writing an Entry with Index i, any previously-persisted entries with Index >= i must be discarded.
2. Send all Messages to the nodes named in the To field. It is important that
no messages be sent until the latest HardState has been persisted to disk,
and all Entries written by any previous Ready batch (Messages may be sent while
entries from the same batch are being persisted). To reduce the I/O latency, an
optimization can be applied to make leader write to disk in parallel with its
followers (as explained at section 10.2.1 in Raft thesis). If any Message has type
MsgSnap, call Node.ReportSnapshot() after it has been sent (these messages may be
large). Note: Marshalling messages is not thread-safe; it is important that you
make sure that no new entries are persisted while marshalling.
The easiest way to achieve this is to serialise the messages directly inside
your main raft loop.
2. Send all Messages to the nodes named in the To field. It is important that no messages be sent until the latest HardState has been persisted to disk, and all Entries written by any previous Ready batch (Messages may be sent while entries from the same batch are being persisted). To reduce the I/O latency, an optimization can be applied to make leader write to disk in parallel with its followers (as explained at section 10.2.1 in Raft thesis). If any Message has type MsgSnap, call Node.ReportSnapshot() after it has been sent (these messages may be large). Note: Marshalling messages is not thread-safe; it is important to make sure that no new entries are persisted while marshalling. The easiest way to achieve this is to serialise the messages directly inside the main raft loop.
3. Apply Snapshot (if any) and CommittedEntries to the state machine.
If any committed Entry has Type EntryConfChange, call Node.ApplyConfChange()
to apply it to the node. The configuration change may be cancelled at this point
by setting the NodeID field to zero before calling ApplyConfChange
(but ApplyConfChange must be called one way or the other, and the decision to cancel
must be based solely on the state machine and not external information such as
the observed health of the node).
3. Apply Snapshot (if any) and CommittedEntries to the state machine. If any committed Entry has Type EntryConfChange, call Node.ApplyConfChange() to apply it to the node. The configuration change may be cancelled at this point by setting the NodeID field to zero before calling ApplyConfChange (but ApplyConfChange must be called one way or the other, and the decision to cancel must be based solely on the state machine and not external information such as the observed health of the node).
4. Call Node.Advance() to signal readiness for the next batch of updates.
This may be done at any time after step 1, although all updates must be processed
in the order they were returned by Ready.
4. Call Node.Advance() to signal readiness for the next batch of updates. This may be done at any time after step 1, although all updates must be processed in the order they were returned by Ready.
Second, all persisted log entries must be made available via an
implementation of the Storage interface. The provided MemoryStorage
type can be used for this (if you repopulate its state upon a
restart), or you can supply your own disk-backed implementation.
Second, all persisted log entries must be made available via an implementation of the Storage interface. The provided MemoryStorage type can be used for this (if repopulating its state upon a restart), or a custom disk-backed implementation can be supplied.
Third, when you receive a message from another node, pass it to Node.Step:
Third, after receiving a message from another node, pass it to Node.Step:
```go
func recvRaftRPC(ctx context.Context, m raftpb.Message) {
@ -157,10 +130,7 @@ Third, when you receive a message from another node, pass it to Node.Step:
}
```
Finally, you need to call `Node.Tick()` at regular intervals (probably
via a `time.Ticker`). Raft has two important timeouts: heartbeat and the
election timeout. However, internally to the raft package time is
represented by an abstract "tick".
Finally, call `Node.Tick()` at regular intervals (probably via a `time.Ticker`). Raft has two important timeouts: heartbeat and the election timeout. However, internally to the raft package time is represented by an abstract "tick".
The total state machine handling loop will look something like this:
@ -190,16 +160,13 @@ The total state machine handling loop will look something like this:
}
```
To propose changes to the state machine from your node take your application
data, serialize it into a byte slice and call:
To propose changes to the state machine from the node to take application data, serialize it into a byte slice and call:
```go
n.Propose(ctx, data)
```
If the proposal is committed, data will appear in committed entries with type
raftpb.EntryNormal. There is no guarantee that a proposed command will be
committed; you may have to re-propose after a timeout.
If the proposal is committed, data will appear in committed entries with type raftpb.EntryNormal. There is no guarantee that a proposed command will be committed; the command may have to be reproposed after a timeout.
To add or remove node in a cluster, build ConfChange struct 'cc' and call:
@ -207,8 +174,7 @@ To add or remove node in a cluster, build ConfChange struct 'cc' and call:
n.ProposeConfChange(ctx, cc)
```
After config change is committed, some committed entry with type
raftpb.EntryConfChange will be returned. You must apply it to node through:
After config change is committed, some committed entry with type raftpb.EntryConfChange will be returned. This must be applied to node through:
```go
var cc raftpb.ConfChange
@ -223,25 +189,8 @@ may be reused. Node IDs must be non-zero.
## Implementation notes
This implementation is up to date with the final Raft thesis
(https://ramcloud.stanford.edu/~ongaro/thesis.pdf), although our
implementation of the membership change protocol differs somewhat from
that described in chapter 4. The key invariant that membership changes
happen one node at a time is preserved, but in our implementation the
membership change takes effect when its entry is applied, not when it
is added to the log (so the entry is committed under the old
membership instead of the new). This is equivalent in terms of safety,
since the old and new configurations are guaranteed to overlap.
This implementation is up to date with the final Raft thesis (https://ramcloud.stanford.edu/~ongaro/thesis.pdf), although this implementation of the membership change protocol differs somewhat from that described in chapter 4. The key invariant that membership changes happen one node at a time is preserved, but in our implementation the membership change takes effect when its entry is applied, not when it is added to the log (so the entry is committed under the old membership instead of the new). This is equivalent in terms of safety, since the old and new configurations are guaranteed to overlap.
To ensure that we do not attempt to commit two membership changes at
once by matching log positions (which would be unsafe since they
should have different quorum requirements), we simply disallow any
proposed membership change while any uncommitted change appears in
the leader's log.
To ensure there is no attempt to commit two membership changes at once by matching log positions (which would be unsafe since they should have different quorum requirements), any proposed membership change is simply disallowed while any uncommitted change appears in the leader's log.
This approach introduces a problem when you try to remove a member
from a two-member cluster: If one of the members dies before the
other one receives the commit of the confchange entry, then the member
cannot be removed any more since the cluster cannot make progress.
For this reason it is highly recommended to use three or more nodes in
every cluster.
This approach introduces a problem when removing a member from a two-member cluster: If one of the members dies before the other one receives the commit of the confchange entry, then the member cannot be removed any more since the cluster cannot make progress. For this reason it is highly recommended to use three or more nodes in every cluster.

View File

@ -1847,7 +1847,7 @@ func init() { proto.RegisterFile("raft.proto", fileDescriptorRaft) }
var fileDescriptorRaft = []byte{
// 790 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x54, 0xcd, 0x6e, 0xdb, 0x46,
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x54, 0xcd, 0x6e, 0xdb, 0x46,
0x10, 0x16, 0x29, 0xea, 0x6f, 0x28, 0xcb, 0xab, 0xb5, 0x5a, 0x2c, 0x0c, 0x43, 0x55, 0x85, 0x1e,
0x04, 0x17, 0x76, 0x5b, 0x1d, 0x7a, 0xe8, 0xcd, 0x96, 0x0a, 0x58, 0x40, 0x65, 0xb8, 0xb2, 0xdc,
0x43, 0x83, 0x20, 0x58, 0x8b, 0x2b, 0x4a, 0x89, 0xc9, 0x25, 0x96, 0x2b, 0xc7, 0xbe, 0x04, 0x79,

View File

@ -3,7 +3,7 @@ package api
// Common constants for daemon and client.
const (
// DefaultVersion of Current REST API
DefaultVersion string = "1.34"
DefaultVersion string = "1.35"
// NoBaseImageSpecifier is the symbol used by the FROM
// command to specify that no base image is to be used.

View File

@ -74,6 +74,7 @@ type ContainerLogsOptions struct {
ShowStdout bool
ShowStderr bool
Since string
Until string
Timestamps bool
Follow bool
Tail string

View File

@ -7,10 +7,22 @@ package container
// See hack/generate-swagger-api.sh
// ----------------------------------------------------------------------------
// ContainerWaitOKBodyError container waiting error, if any
// swagger:model ContainerWaitOKBodyError
type ContainerWaitOKBodyError struct {
// Details of an error
Message string `json:"Message,omitempty"`
}
// ContainerWaitOKBody container wait o k body
// swagger:model ContainerWaitOKBody
type ContainerWaitOKBody struct {
// error
// Required: true
Error *ContainerWaitOKBodyError `json:"Error"`
// Exit code of the container
// Required: true
StatusCode int64 `json:"StatusCode"`

View File

@ -20,6 +20,27 @@ func (i Isolation) IsDefault() bool {
return strings.ToLower(string(i)) == "default" || string(i) == ""
}
// IsHyperV indicates the use of a Hyper-V partition for isolation
func (i Isolation) IsHyperV() bool {
return strings.ToLower(string(i)) == "hyperv"
}
// IsProcess indicates the use of process isolation
func (i Isolation) IsProcess() bool {
return strings.ToLower(string(i)) == "process"
}
const (
// IsolationEmpty is unspecified (same behavior as default)
IsolationEmpty = Isolation("")
// IsolationDefault is the default isolation mode on current daemon
IsolationDefault = Isolation("default")
// IsolationProcess is process isolation mode
IsolationProcess = Isolation("process")
// IsolationHyperV is HyperV isolation mode
IsolationHyperV = Isolation("hyperv")
)
// IpcMode represents the container ipc stack.
type IpcMode string

View File

@ -1,9 +1,5 @@
package container
import (
"strings"
)
// IsBridge indicates whether container uses the bridge network stack
// in windows it is given the name NAT
func (n NetworkMode) IsBridge() bool {
@ -21,16 +17,6 @@ func (n NetworkMode) IsUserDefined() bool {
return !n.IsDefault() && !n.IsNone() && !n.IsBridge() && !n.IsContainer()
}
// IsHyperV indicates the use of a Hyper-V partition for isolation
func (i Isolation) IsHyperV() bool {
return strings.ToLower(string(i)) == "hyperv"
}
// IsProcess indicates the use of process isolation
func (i Isolation) IsProcess() bool {
return strings.ToLower(string(i)) == "process"
}
// IsValid indicates if an isolation technology is valid
func (i Isolation) IsValid() bool {
return i.IsDefault() || i.IsHyperV() || i.IsProcess()

View File

@ -65,8 +65,9 @@ type ContainerSpec struct {
// The format of extra hosts on swarmkit is specified in:
// http://man7.org/linux/man-pages/man5/hosts.5.html
// IP_address canonical_hostname [aliases...]
Hosts []string `json:",omitempty"`
DNSConfig *DNSConfig `json:",omitempty"`
Secrets []*SecretReference `json:",omitempty"`
Configs []*ConfigReference `json:",omitempty"`
Hosts []string `json:",omitempty"`
DNSConfig *DNSConfig `json:",omitempty"`
Secrets []*SecretReference `json:",omitempty"`
Configs []*ConfigReference `json:",omitempty"`
Isolation container.Isolation `json:",omitempty"`
}

View File

@ -1,4 +1,4 @@
// +build linux freebsd solaris openbsd darwin
// +build linux freebsd openbsd darwin
package client

View File

@ -51,6 +51,14 @@ func (cli *Client) ContainerLogs(ctx context.Context, container string, options
query.Set("since", ts)
}
if options.Until != "" {
ts, err := timetypes.GetTimestamp(options.Until, time.Now())
if err != nil {
return nil, err
}
query.Set("until", ts)
}
if options.Timestamps {
query.Set("timestamps", "1")
}

View File

@ -1,8 +1,8 @@
pkg/ is a collection of utility packages used by the Docker project without being specific to its internals.
pkg/ is a collection of utility packages used by the Moby project without being specific to its internals.
Utility packages are kept separate from the docker core codebase to keep it as small and concise as possible.
Utility packages are kept separate from the moby core codebase to keep it as small and concise as possible.
If some utilities grow larger and their APIs stabilize, they may be moved to their own repository under the
Docker organization, to facilitate re-use by other projects. However that is not the priority.
Moby organization, to facilitate re-use by other projects. However that is not the priority.
The directory `pkg` is named after the same directory in the camlistore project. Since Brad is a core
Go maintainer, we thought it made sense to copy his methods for organizing Go code :) Thanks Brad!

View File

@ -50,8 +50,8 @@ func setHeaderForSpecialDevice(hdr *tar.Header, name string, stat interface{}) (
// Currently go does not fill in the major/minors
if s.Mode&unix.S_IFBLK != 0 ||
s.Mode&unix.S_IFCHR != 0 {
hdr.Devmajor = int64(major(uint64(s.Rdev))) // nolint: unconvert
hdr.Devminor = int64(minor(uint64(s.Rdev))) // nolint: unconvert
hdr.Devmajor = int64(unix.Major(uint64(s.Rdev))) // nolint: unconvert
hdr.Devminor = int64(unix.Minor(uint64(s.Rdev))) // nolint: unconvert
}
}
@ -77,14 +77,6 @@ func getFileUIDGID(stat interface{}) (idtools.IDPair, error) {
return idtools.IDPair{UID: int(s.Uid), GID: int(s.Gid)}, nil
}
func major(device uint64) uint64 {
return (device >> 8) & 0xfff
}
func minor(device uint64) uint64 {
return (device & 0xff) | ((device >> 12) & 0xfff00)
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {

View File

@ -294,7 +294,7 @@ func OverlayChanges(layers []string, rw string) ([]Change, error) {
func overlayDeletedFile(root, path string, fi os.FileInfo) (string, error) {
if fi.Mode()&os.ModeCharDevice != 0 {
s := fi.Sys().(*syscall.Stat_t)
if major(s.Rdev) == 0 && minor(s.Rdev) == 0 {
if unix.Major(uint64(s.Rdev)) == 0 && unix.Minor(uint64(s.Rdev)) == 0 { // nolint: unconvert
return path, nil
}
}

View File

@ -1,7 +0,0 @@
package fileutils
// GetTotalUsedFds Returns the number of used File Descriptors.
// On Solaris these limits are per process and not systemwide
func GetTotalUsedFds() int {
return -1
}

View File

@ -26,14 +26,19 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown
// so that we can chown all of them properly at the end. If chownExisting is false, we won't
// chown the full directory path if it exists
var paths []string
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
paths = []string{path}
} else if err == nil && chownExisting {
stat, err := system.Stat(path)
if err == nil {
if !chownExisting {
return nil
}
// short-circuit--we were called with an existing directory and chown was requested
return os.Chown(path, ownerUID, ownerGID)
} else if err == nil {
// nothing to do; directory path fully exists already and chown was NOT requested
return nil
return lazyChown(path, ownerUID, ownerGID, stat)
}
if os.IsNotExist(err) {
paths = []string{path}
}
if mkAll {
@ -60,7 +65,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown
// even if it existed, we will chown the requested path + any subpaths that
// didn't exist when we called MkdirAll
for _, pathComponent := range paths {
if err := os.Chown(pathComponent, ownerUID, ownerGID); err != nil {
if err := lazyChown(pathComponent, ownerUID, ownerGID, nil); err != nil {
return err
}
}
@ -202,3 +207,20 @@ func callGetent(args string) (io.Reader, error) {
}
return bytes.NewReader(out), nil
}
// lazyChown performs a chown only if the uid/gid don't match what's requested
// Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the
// dir is on an NFS share, so don't call chown unless we absolutely must.
func lazyChown(p string, uid, gid int, stat *system.StatT) error {
if stat == nil {
var err error
stat, err = system.Stat(p)
if err != nil {
return err
}
}
if stat.UID() == uint32(uid) && stat.GID() == uint32(gid) {
return nil
}
return os.Chown(p, uid, gid)
}

Some files were not shown because too many files have changed in this diff Show More