mirror of https://github.com/docker/cli.git
Move api/client -> cli/command
Using gomvpkg -from github.com/docker/docker/api/client -to github.com/docker/docker/cli/command -vcs_mv_cmd 'git mv {{.Src}} {{.Dst}}' Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
efdd29abcf
commit
3bd1eb4b76
|
@ -0,0 +1,71 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package bundlefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bundlefile stores the contents of a bundlefile
|
||||||
|
type Bundlefile struct {
|
||||||
|
Version string
|
||||||
|
Services map[string]Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service is a service from a bundlefile
|
||||||
|
type Service struct {
|
||||||
|
Image string
|
||||||
|
Command []string `json:",omitempty"`
|
||||||
|
Args []string `json:",omitempty"`
|
||||||
|
Env []string `json:",omitempty"`
|
||||||
|
Labels map[string]string `json:",omitempty"`
|
||||||
|
Ports []Port `json:",omitempty"`
|
||||||
|
WorkingDir *string `json:",omitempty"`
|
||||||
|
User *string `json:",omitempty"`
|
||||||
|
Networks []string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port is a port as defined in a bundlefile
|
||||||
|
type Port struct {
|
||||||
|
Protocol string
|
||||||
|
Port uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile loads a bundlefile from a path to the file
|
||||||
|
func LoadFile(reader io.Reader) (*Bundlefile, error) {
|
||||||
|
bundlefile := &Bundlefile{}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
if err := decoder.Decode(bundlefile); err != nil {
|
||||||
|
switch jsonErr := err.(type) {
|
||||||
|
case *json.SyntaxError:
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"JSON syntax error at byte %v: %s",
|
||||||
|
jsonErr.Offset,
|
||||||
|
jsonErr.Error())
|
||||||
|
case *json.UnmarshalTypeError:
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Unexpected type at byte %v. Expected %s but received %s.",
|
||||||
|
jsonErr.Offset,
|
||||||
|
jsonErr.Type,
|
||||||
|
jsonErr.Value)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundlefile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print writes the contents of the bundlefile to the output writer
|
||||||
|
// as human readable json
|
||||||
|
func Print(out io.Writer, bundle *Bundlefile) error {
|
||||||
|
bytes, err := json.MarshalIndent(*bundle, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = out.Write(bytes)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package bundlefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadFileV01Success(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"Version": "0.1",
|
||||||
|
"Services": {
|
||||||
|
"redis": {
|
||||||
|
"Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce",
|
||||||
|
"Networks": ["default"]
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d",
|
||||||
|
"Networks": ["default"],
|
||||||
|
"User": "web"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
bundle, err := LoadFile(reader)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, bundle.Version, "0.1")
|
||||||
|
assert.Equal(t, len(bundle.Services), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFileSyntaxError(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"Version": "0.1",
|
||||||
|
"Services": unquoted string
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := LoadFile(reader)
|
||||||
|
assert.Error(t, err, "syntax error at byte 37: invalid character 'u'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFileTypeError(t *testing.T) {
|
||||||
|
reader := strings.NewReader(`{
|
||||||
|
"Version": "0.1",
|
||||||
|
"Services": {
|
||||||
|
"web": {
|
||||||
|
"Image": "redis",
|
||||||
|
"Networks": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
_, err := LoadFile(reader)
|
||||||
|
assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrint(t *testing.T) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
bundle := &Bundlefile{
|
||||||
|
Version: "0.1",
|
||||||
|
Services: map[string]Service{
|
||||||
|
"web": {
|
||||||
|
Image: "image",
|
||||||
|
Command: []string{"echo", "something"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.NilError(t, Print(&buffer, bundle))
|
||||||
|
output := buffer.String()
|
||||||
|
assert.Contains(t, output, "\"Image\": \"image\"")
|
||||||
|
assert.Contains(t, output,
|
||||||
|
`"Command": [
|
||||||
|
"echo",
|
||||||
|
"something"
|
||||||
|
]`)
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api"
|
||||||
|
cliflags "github.com/docker/docker/cli/flags"
|
||||||
|
"github.com/docker/docker/cliconfig"
|
||||||
|
"github.com/docker/docker/cliconfig/configfile"
|
||||||
|
"github.com/docker/docker/cliconfig/credentials"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/dockerversion"
|
||||||
|
dopts "github.com/docker/docker/opts"
|
||||||
|
"github.com/docker/go-connections/sockets"
|
||||||
|
"github.com/docker/go-connections/tlsconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DockerCli represents the docker command line client.
|
||||||
|
// Instances of the client can be returned from NewDockerCli.
|
||||||
|
type DockerCli struct {
|
||||||
|
configFile *configfile.ConfigFile
|
||||||
|
in *InStream
|
||||||
|
out *OutStream
|
||||||
|
err io.Writer
|
||||||
|
keyFile string
|
||||||
|
client client.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns the APIClient
|
||||||
|
func (cli *DockerCli) Client() client.APIClient {
|
||||||
|
return cli.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out returns the writer used for stdout
|
||||||
|
func (cli *DockerCli) Out() *OutStream {
|
||||||
|
return cli.out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the writer used for stderr
|
||||||
|
func (cli *DockerCli) Err() io.Writer {
|
||||||
|
return cli.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// In returns the reader used for stdin
|
||||||
|
func (cli *DockerCli) In() *InStream {
|
||||||
|
return cli.in
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigFile returns the ConfigFile
|
||||||
|
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
||||||
|
return cli.configFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the dockerCli runs initialization that must happen after command
|
||||||
|
// line flags are parsed.
|
||||||
|
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||||
|
cli.configFile = LoadDefaultConfigFile(cli.err)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.Common.TrustKey == "" {
|
||||||
|
cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile)
|
||||||
|
} else {
|
||||||
|
cli.keyFile = opts.Common.TrustKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
||||||
|
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
|
||||||
|
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadDefaultConfigFile attempts to load the default config file and returns
|
||||||
|
// an initialized ConfigFile struct if none is found.
|
||||||
|
func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile {
|
||||||
|
configFile, e := cliconfig.Load(cliconfig.ConfigDir())
|
||||||
|
if e != nil {
|
||||||
|
fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
|
||||||
|
}
|
||||||
|
if !configFile.ContainsAuth() {
|
||||||
|
credentials.DetectDefaultStore(configFile)
|
||||||
|
}
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||||
|
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||||
|
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
||||||
|
if err != nil {
|
||||||
|
return &client.Client{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
customHeaders := configFile.HTTPHeaders
|
||||||
|
if customHeaders == nil {
|
||||||
|
customHeaders = map[string]string{}
|
||||||
|
}
|
||||||
|
customHeaders["User-Agent"] = clientUserAgent()
|
||||||
|
|
||||||
|
verStr := api.DefaultVersion
|
||||||
|
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
|
||||||
|
verStr = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient, err := newHTTPClient(host, opts.TLSOptions)
|
||||||
|
if err != nil {
|
||||||
|
return &client.Client{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.NewClient(host, verStr, httpClient, customHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) {
|
||||||
|
switch len(hosts) {
|
||||||
|
case 0:
|
||||||
|
host = os.Getenv("DOCKER_HOST")
|
||||||
|
case 1:
|
||||||
|
host = hosts[0]
|
||||||
|
default:
|
||||||
|
return "", errors.New("Please specify only one -H")
|
||||||
|
}
|
||||||
|
|
||||||
|
host, err = dopts.ParseHost(tlsOptions != nil, host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, error) {
|
||||||
|
if tlsOptions == nil {
|
||||||
|
// let the api client configure the default transport.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := tlsconfig.Client(*tlsOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: config,
|
||||||
|
}
|
||||||
|
proto, addr, _, err := client.ParseHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sockets.ConfigureTransport(tr, proto, addr)
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientUserAgent() string {
|
||||||
|
return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/container"
|
||||||
|
"github.com/docker/docker/cli/command/image"
|
||||||
|
"github.com/docker/docker/cli/command/network"
|
||||||
|
"github.com/docker/docker/cli/command/node"
|
||||||
|
"github.com/docker/docker/cli/command/plugin"
|
||||||
|
"github.com/docker/docker/cli/command/registry"
|
||||||
|
"github.com/docker/docker/cli/command/service"
|
||||||
|
"github.com/docker/docker/cli/command/stack"
|
||||||
|
"github.com/docker/docker/cli/command/swarm"
|
||||||
|
"github.com/docker/docker/cli/command/system"
|
||||||
|
"github.com/docker/docker/cli/command/volume"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddCommands adds all the commands from api/client to the root command
|
||||||
|
func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||||
|
cmd.AddCommand(
|
||||||
|
node.NewNodeCommand(dockerCli),
|
||||||
|
service.NewServiceCommand(dockerCli),
|
||||||
|
stack.NewStackCommand(dockerCli),
|
||||||
|
stack.NewTopLevelDeployCommand(dockerCli),
|
||||||
|
swarm.NewSwarmCommand(dockerCli),
|
||||||
|
container.NewAttachCommand(dockerCli),
|
||||||
|
container.NewCommitCommand(dockerCli),
|
||||||
|
container.NewCopyCommand(dockerCli),
|
||||||
|
container.NewCreateCommand(dockerCli),
|
||||||
|
container.NewDiffCommand(dockerCli),
|
||||||
|
container.NewExecCommand(dockerCli),
|
||||||
|
container.NewExportCommand(dockerCli),
|
||||||
|
container.NewKillCommand(dockerCli),
|
||||||
|
container.NewLogsCommand(dockerCli),
|
||||||
|
container.NewPauseCommand(dockerCli),
|
||||||
|
container.NewPortCommand(dockerCli),
|
||||||
|
container.NewPsCommand(dockerCli),
|
||||||
|
container.NewRenameCommand(dockerCli),
|
||||||
|
container.NewRestartCommand(dockerCli),
|
||||||
|
container.NewRmCommand(dockerCli),
|
||||||
|
container.NewRunCommand(dockerCli),
|
||||||
|
container.NewStartCommand(dockerCli),
|
||||||
|
container.NewStatsCommand(dockerCli),
|
||||||
|
container.NewStopCommand(dockerCli),
|
||||||
|
container.NewTopCommand(dockerCli),
|
||||||
|
container.NewUnpauseCommand(dockerCli),
|
||||||
|
container.NewUpdateCommand(dockerCli),
|
||||||
|
container.NewWaitCommand(dockerCli),
|
||||||
|
image.NewBuildCommand(dockerCli),
|
||||||
|
image.NewHistoryCommand(dockerCli),
|
||||||
|
image.NewImagesCommand(dockerCli),
|
||||||
|
image.NewLoadCommand(dockerCli),
|
||||||
|
image.NewRemoveCommand(dockerCli),
|
||||||
|
image.NewSaveCommand(dockerCli),
|
||||||
|
image.NewPullCommand(dockerCli),
|
||||||
|
image.NewPushCommand(dockerCli),
|
||||||
|
image.NewSearchCommand(dockerCli),
|
||||||
|
image.NewImportCommand(dockerCli),
|
||||||
|
image.NewTagCommand(dockerCli),
|
||||||
|
network.NewNetworkCommand(dockerCli),
|
||||||
|
system.NewEventsCommand(dockerCli),
|
||||||
|
system.NewInspectCommand(dockerCli),
|
||||||
|
registry.NewLoginCommand(dockerCli),
|
||||||
|
registry.NewLogoutCommand(dockerCli),
|
||||||
|
system.NewVersionCommand(dockerCli),
|
||||||
|
volume.NewVolumeCommand(dockerCli),
|
||||||
|
system.NewInfoCommand(dockerCli),
|
||||||
|
)
|
||||||
|
plugin.NewPluginCommand(cmd, dockerCli)
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http/httputil"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/signal"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attachOptions struct {
|
||||||
|
noStdin bool
|
||||||
|
proxy bool
|
||||||
|
detachKeys string
|
||||||
|
|
||||||
|
container string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||||
|
func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts attachOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "attach [OPTIONS] CONTAINER",
|
||||||
|
Short: "Attach to a running container",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
return runAttach(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
|
||||||
|
flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
|
||||||
|
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
c, err := client.ContainerInspect(ctx, opts.container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.State.Running {
|
||||||
|
return fmt.Errorf("You cannot attach to a stopped container, start it first")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.State.Paused {
|
||||||
|
return fmt.Errorf("You cannot attach to a paused container, unpause it first")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.detachKeys != "" {
|
||||||
|
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ContainerAttachOptions{
|
||||||
|
Stream: true,
|
||||||
|
Stdin: !opts.noStdin && c.Config.OpenStdin,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
var in io.ReadCloser
|
||||||
|
if options.Stdin {
|
||||||
|
in = dockerCli.In()
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.proxy && !c.Config.Tty {
|
||||||
|
sigc := ForwardAllSignals(ctx, dockerCli, opts.container)
|
||||||
|
defer signal.StopCatch(sigc)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
|
||||||
|
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
|
||||||
|
// ContainerAttach returns an ErrPersistEOF (connection closed)
|
||||||
|
// means server met an error and put it in Hijacked connection
|
||||||
|
// keep the error and read detailed error message from hijacked connection later
|
||||||
|
return errAttach
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
|
||||||
|
if c.Config.Tty && dockerCli.Out().IsTerminal() {
|
||||||
|
height, width := dockerCli.Out().GetTtySize()
|
||||||
|
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
||||||
|
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||||
|
// resize it, then go back to normal. Without this, every attach after the first will
|
||||||
|
// require the user to manually resize or hit enter.
|
||||||
|
resizeTtyTo(ctx, client, opts.container, height+1, width+1, false)
|
||||||
|
|
||||||
|
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||||
|
// to the actual size.
|
||||||
|
if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil {
|
||||||
|
logrus.Debugf("Error monitoring TTY size: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errAttach != nil {
|
||||||
|
return errAttach
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := getExitCode(dockerCli, ctx, opts.container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != 0 {
|
||||||
|
return cli.StatusError{StatusCode: status}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
dockeropts "github.com/docker/docker/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commitOptions struct {
|
||||||
|
container string
|
||||||
|
reference string
|
||||||
|
|
||||||
|
pause bool
|
||||||
|
comment string
|
||||||
|
author string
|
||||||
|
changes dockeropts.ListOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||||
|
func NewCommitCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts commitOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]",
|
||||||
|
Short: "Create a new image from a container's changes",
|
||||||
|
Args: cli.RequiresRangeArgs(1, 2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
opts.reference = args[1]
|
||||||
|
}
|
||||||
|
return runCommit(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.pause, "pause", "p", true, "Pause container during commit")
|
||||||
|
flags.StringVarP(&opts.comment, "message", "m", "", "Commit message")
|
||||||
|
flags.StringVarP(&opts.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith <hannibal@a-team.com>\")")
|
||||||
|
|
||||||
|
opts.changes = dockeropts.NewListOpts(nil)
|
||||||
|
flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommit(dockerCli *command.DockerCli, opts *commitOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
name := opts.container
|
||||||
|
reference := opts.reference
|
||||||
|
|
||||||
|
options := types.ContainerCommitOptions{
|
||||||
|
Reference: reference,
|
||||||
|
Comment: opts.comment,
|
||||||
|
Author: opts.author,
|
||||||
|
Changes: opts.changes.GetAll(),
|
||||||
|
Pause: opts.pause,
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := dockerCli.Client().ContainerCommit(ctx, name, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,303 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/system"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type copyOptions struct {
|
||||||
|
source string
|
||||||
|
destination string
|
||||||
|
followLink bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type copyDirection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
fromContainer copyDirection = (1 << iota)
|
||||||
|
toContainer
|
||||||
|
acrossContainers = fromContainer | toContainer
|
||||||
|
)
|
||||||
|
|
||||||
|
type cpConfig struct {
|
||||||
|
followLink bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCopyCommand creates a new `docker cp` command
|
||||||
|
func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts copyOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
|
||||||
|
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
|
||||||
|
Short: "Copy files/folders between a container and the local filesystem",
|
||||||
|
Long: strings.Join([]string{
|
||||||
|
"Copy files/folders between a container and the local filesystem\n",
|
||||||
|
"\nUse '-' as the source to read a tar archive from stdin\n",
|
||||||
|
"and extract it to a directory destination in a container.\n",
|
||||||
|
"Use '-' as the destination to stream a tar archive of a\n",
|
||||||
|
"container source to stdout.",
|
||||||
|
}, ""),
|
||||||
|
Args: cli.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if args[0] == "" {
|
||||||
|
return fmt.Errorf("source can not be empty")
|
||||||
|
}
|
||||||
|
if args[1] == "" {
|
||||||
|
return fmt.Errorf("destination can not be empty")
|
||||||
|
}
|
||||||
|
opts.source = args[0]
|
||||||
|
opts.destination = args[1]
|
||||||
|
return runCopy(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCopy(dockerCli *command.DockerCli, opts copyOptions) error {
|
||||||
|
srcContainer, srcPath := splitCpArg(opts.source)
|
||||||
|
dstContainer, dstPath := splitCpArg(opts.destination)
|
||||||
|
|
||||||
|
var direction copyDirection
|
||||||
|
if srcContainer != "" {
|
||||||
|
direction |= fromContainer
|
||||||
|
}
|
||||||
|
if dstContainer != "" {
|
||||||
|
direction |= toContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
cpParam := &cpConfig{
|
||||||
|
followLink: opts.followLink,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case fromContainer:
|
||||||
|
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam)
|
||||||
|
case toContainer:
|
||||||
|
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam)
|
||||||
|
case acrossContainers:
|
||||||
|
// Copying between containers isn't supported.
|
||||||
|
return fmt.Errorf("copying between containers is not supported")
|
||||||
|
default:
|
||||||
|
// User didn't specify any container.
|
||||||
|
return fmt.Errorf("must specify at least one container source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func statContainerPath(ctx context.Context, dockerCli *command.DockerCli, 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), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
|
||||||
|
if dstPath != "-" {
|
||||||
|
// Get an absolute destination path.
|
||||||
|
dstPath, err = resolveLocalPath(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 the destination is a symbolic link, we should follow it.
|
||||||
|
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||||
|
linkTarget := srcStat.LinkTarget
|
||||||
|
if !system.IsAbs(linkTarget) {
|
||||||
|
// Join with the parent directory.
|
||||||
|
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||||
|
linkTarget = filepath.Join(srcParent, linkTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
|
||||||
|
srcPath = linkTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer content.Close()
|
||||||
|
|
||||||
|
if dstPath == "-" {
|
||||||
|
// Send the response to STDOUT.
|
||||||
|
_, err = io.Copy(os.Stdout, content)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare source copy info.
|
||||||
|
srcInfo := archive.CopyInfo{
|
||||||
|
Path: srcPath,
|
||||||
|
Exists: true,
|
||||||
|
IsDir: stat.Mode.IsDir(),
|
||||||
|
RebaseName: rebaseName,
|
||||||
|
}
|
||||||
|
|
||||||
|
preArchive := content
|
||||||
|
if len(srcInfo.RebaseName) != 0 {
|
||||||
|
_, 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.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) {
|
||||||
|
if srcPath != "-" {
|
||||||
|
// Get an absolute source path.
|
||||||
|
srcPath, err = resolveLocalPath(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// Prepare destination copy info by stat-ing the container path.
|
||||||
|
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||||
|
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath)
|
||||||
|
|
||||||
|
// If the destination is a symbolic link, we should evaluate it.
|
||||||
|
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||||
|
linkTarget := dstStat.LinkTarget
|
||||||
|
if !system.IsAbs(linkTarget) {
|
||||||
|
// Join with the parent directory.
|
||||||
|
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||||
|
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstInfo.Path = linkTarget
|
||||||
|
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore any error and assume that the parent directory of the destination
|
||||||
|
// path exists, in which case the copy may still succeed. If there is any
|
||||||
|
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||||
|
// or vice versa) the extraction will fail. If the destination simply did
|
||||||
|
// not exist, but the parent directory does, the extraction will still
|
||||||
|
// succeed.
|
||||||
|
if err == nil {
|
||||||
|
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
content io.Reader
|
||||||
|
resolvedDstPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
if srcPath == "-" {
|
||||||
|
// Use STDIN.
|
||||||
|
content = os.Stdin
|
||||||
|
resolvedDstPath = dstInfo.Path
|
||||||
|
if !dstInfo.IsDir {
|
||||||
|
return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Prepare source copy info.
|
||||||
|
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcArchive, err := archive.TarResource(srcInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcArchive.Close()
|
||||||
|
|
||||||
|
// With the stat info about the local source as well as the
|
||||||
|
// destination, we have enough information to know whether we need to
|
||||||
|
// alter the archive that we upload so that when the server extracts
|
||||||
|
// it to the specified directory in the container we get the desired
|
||||||
|
// copy behavior.
|
||||||
|
|
||||||
|
// See comments in the implementation of `archive.PrepareArchiveCopy`
|
||||||
|
// for exactly what goes into deciding how and whether the source
|
||||||
|
// archive needs to be altered for the correct copy behavior when it is
|
||||||
|
// extracted. This function also infers from the source and destination
|
||||||
|
// info which directory to extract to, which may be the parent of the
|
||||||
|
// destination that the user specified.
|
||||||
|
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer preparedArchive.Close()
|
||||||
|
|
||||||
|
resolvedDstPath = dstDir
|
||||||
|
content = preparedArchive
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.CopyToContainerOptions{
|
||||||
|
AllowOverwriteDirWithFile: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||||
|
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
|
||||||
|
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
|
||||||
|
// absolute path:
|
||||||
|
// `/path/to/file:name.txt` or `./file:name.txt`
|
||||||
|
//
|
||||||
|
// This is apparently how `scp` handles this as well:
|
||||||
|
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
|
||||||
|
//
|
||||||
|
// We can't simply check for a filepath separator because container names may
|
||||||
|
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
|
||||||
|
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
|
||||||
|
// client, a `:` could be part of an absolute Windows path, in which case it
|
||||||
|
// is immediately proceeded by a backslash.
|
||||||
|
func splitCpArg(arg string) (container, path string) {
|
||||||
|
if system.IsAbs(arg) {
|
||||||
|
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||||
|
return "", arg
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(arg, ":", 2)
|
||||||
|
|
||||||
|
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
|
||||||
|
// Either there's no `:` in the arg
|
||||||
|
// OR it's an explicit local relative path like `./file:name.txt`.
|
||||||
|
return "", arg
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[0], parts[1]
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
// FIXME migrate to docker/distribution/reference
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
networktypes "github.com/docker/docker/api/types/network"
|
||||||
|
apiclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createOptions struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||||
|
func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts createOptions
|
||||||
|
var copts *runconfigopts.ContainerOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||||
|
Short: "Create a new container",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
copts.Image = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
copts.Args = args[1:]
|
||||||
|
}
|
||||||
|
return runCreate(dockerCli, cmd.Flags(), &opts, copts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
|
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
|
||||||
|
|
||||||
|
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||||
|
// with hostname
|
||||||
|
flags.Bool("help", false, "Print usage")
|
||||||
|
|
||||||
|
command.AddTrustedFlags(flags, true)
|
||||||
|
copts = runconfigopts.AddFlags(flags)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *runconfigopts.ContainerOptions) error {
|
||||||
|
config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts)
|
||||||
|
if err != nil {
|
||||||
|
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||||
|
return cli.StatusError{StatusCode: 125}
|
||||||
|
}
|
||||||
|
response, err := createContainer(context.Background(), dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error {
|
||||||
|
ref, err := reference.ParseNamed(image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the Repository name from fqn to RepositoryInfo
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||||
|
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ImageCreateOptions{
|
||||||
|
RegistryAuth: encodedAuth,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer responseBody.Close()
|
||||||
|
|
||||||
|
return jsonmessage.DisplayJSONMessagesStream(
|
||||||
|
responseBody,
|
||||||
|
out,
|
||||||
|
dockerCli.Out().FD(),
|
||||||
|
dockerCli.Out().IsTerminal(),
|
||||||
|
nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cidFile struct {
|
||||||
|
path string
|
||||||
|
file *os.File
|
||||||
|
written bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cid *cidFile) Close() error {
|
||||||
|
cid.file.Close()
|
||||||
|
|
||||||
|
if !cid.written {
|
||||||
|
if err := os.Remove(cid.path); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cid *cidFile) Write(id string) error {
|
||||||
|
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write the container ID to the file: %s", err)
|
||||||
|
}
|
||||||
|
cid.written = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCIDFile(path string) (*cidFile, error) {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create the container ID file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cidFile{path: path, file: f}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) {
|
||||||
|
stderr := dockerCli.Err()
|
||||||
|
|
||||||
|
var containerIDFile *cidFile
|
||||||
|
if cidfile != "" {
|
||||||
|
var err error
|
||||||
|
if containerIDFile, err = newCIDFile(cidfile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer containerIDFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var trustedRef reference.Canonical
|
||||||
|
_, ref, err := reference.ParseIDOrReference(config.Image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ref != nil {
|
||||||
|
ref = reference.WithDefaultTag(ref)
|
||||||
|
|
||||||
|
if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
|
||||||
|
var err error
|
||||||
|
trustedRef, err = dockerCli.TrustedReference(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.Image = trustedRef.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//create the container
|
||||||
|
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name)
|
||||||
|
|
||||||
|
//if image not found try to pull it
|
||||||
|
if err != nil {
|
||||||
|
if apiclient.IsErrImageNotFound(err) && ref != nil {
|
||||||
|
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", ref.String())
|
||||||
|
|
||||||
|
// we don't want to write to stdout anything apart from container.ID
|
||||||
|
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil {
|
||||||
|
if err := dockerCli.TagTrusted(ctx, trustedRef, ref); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Retry
|
||||||
|
var retryErr error
|
||||||
|
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name)
|
||||||
|
if retryErr != nil {
|
||||||
|
return nil, retryErr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, warning := range response.Warnings {
|
||||||
|
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
|
||||||
|
}
|
||||||
|
if containerIDFile != nil {
|
||||||
|
if err = containerIDFile.Write(response.ID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type diffOptions struct {
|
||||||
|
container string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||||
|
func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts diffOptions
|
||||||
|
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "diff CONTAINER",
|
||||||
|
Short: "Inspect changes on a container's filesystem",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
return runDiff(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error {
|
||||||
|
if opts.container == "" {
|
||||||
|
return fmt.Errorf("Container name cannot be empty")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, change := range changes {
|
||||||
|
var kind string
|
||||||
|
switch change.Kind {
|
||||||
|
case archive.ChangeModify:
|
||||||
|
kind = "C"
|
||||||
|
case archive.ChangeAdd:
|
||||||
|
kind = "A"
|
||||||
|
case archive.ChangeDelete:
|
||||||
|
kind = "D"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s %s\n", kind, change.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
apiclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/promise"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execOptions struct {
|
||||||
|
detachKeys string
|
||||||
|
interactive bool
|
||||||
|
tty bool
|
||||||
|
detach bool
|
||||||
|
user string
|
||||||
|
privileged bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExecCommand creats a new cobra.Command for `docker exec`
|
||||||
|
func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts execOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
|
||||||
|
Short: "Run a command in a running container",
|
||||||
|
Args: cli.RequiresMinArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
container := args[0]
|
||||||
|
execCmd := args[1:]
|
||||||
|
return runExec(dockerCli, &opts, container, execCmd)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
|
flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
|
||||||
|
flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
|
||||||
|
flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
|
||||||
|
flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||||
|
flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||||
|
flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error {
|
||||||
|
execConfig, err := parseExec(opts, container, execCmd)
|
||||||
|
// just in case the ParseExec does not exit
|
||||||
|
if container == "" || err != nil {
|
||||||
|
return cli.StatusError{StatusCode: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.detachKeys != "" {
|
||||||
|
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send client escape keys
|
||||||
|
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
execID := response.ID
|
||||||
|
if execID == "" {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "exec ID empty")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Temp struct for execStart so that we don't need to transfer all the execConfig
|
||||||
|
if !execConfig.Detach {
|
||||||
|
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
execStartCheck := types.ExecStartCheck{
|
||||||
|
Detach: execConfig.Detach,
|
||||||
|
Tty: execConfig.Tty,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// For now don't print this - wait for when we support exec wait()
|
||||||
|
// fmt.Fprintf(dockerCli.Out(), "%s\n", execID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive exec requested.
|
||||||
|
var (
|
||||||
|
out, stderr io.Writer
|
||||||
|
in io.ReadCloser
|
||||||
|
errCh chan error
|
||||||
|
)
|
||||||
|
|
||||||
|
if execConfig.AttachStdin {
|
||||||
|
in = dockerCli.In()
|
||||||
|
}
|
||||||
|
if execConfig.AttachStdout {
|
||||||
|
out = dockerCli.Out()
|
||||||
|
}
|
||||||
|
if execConfig.AttachStderr {
|
||||||
|
if execConfig.Tty {
|
||||||
|
stderr = dockerCli.Out()
|
||||||
|
} else {
|
||||||
|
stderr = dockerCli.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
errCh = promise.Go(func() error {
|
||||||
|
return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||||
|
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
logrus.Debugf("Error hijack: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var status int
|
||||||
|
if _, status, err = getExecExitCode(ctx, client, execID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != 0 {
|
||||||
|
return cli.StatusError{StatusCode: status}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExecExitCode perform an inspect on the exec command. It returns
|
||||||
|
// the running state and the exit code.
|
||||||
|
func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) {
|
||||||
|
resp, err := client.ContainerExecInspect(ctx, execID)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't connect, then the daemon probably died.
|
||||||
|
if err != apiclient.ErrConnectionFailed {
|
||||||
|
return false, -1, err
|
||||||
|
}
|
||||||
|
return false, -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Running, resp.ExitCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExec parses the specified args for the specified command and generates
|
||||||
|
// an ExecConfig from it.
|
||||||
|
func parseExec(opts *execOptions, container string, execCmd []string) (*types.ExecConfig, error) {
|
||||||
|
execConfig := &types.ExecConfig{
|
||||||
|
User: opts.user,
|
||||||
|
Privileged: opts.privileged,
|
||||||
|
Tty: opts.tty,
|
||||||
|
Cmd: execCmd,
|
||||||
|
Detach: opts.detach,
|
||||||
|
// container is not used here
|
||||||
|
}
|
||||||
|
|
||||||
|
// If -d is not set, attach to everything by default
|
||||||
|
if !opts.detach {
|
||||||
|
execConfig.AttachStdout = true
|
||||||
|
execConfig.AttachStderr = true
|
||||||
|
if opts.interactive {
|
||||||
|
execConfig.AttachStdin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return execConfig, nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type arguments struct {
|
||||||
|
options execOptions
|
||||||
|
container string
|
||||||
|
execCmd []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseExec(t *testing.T) {
|
||||||
|
valids := map[*arguments]*types.ExecConfig{
|
||||||
|
&arguments{
|
||||||
|
execCmd: []string{"command"},
|
||||||
|
}: {
|
||||||
|
Cmd: []string{"command"},
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
},
|
||||||
|
&arguments{
|
||||||
|
execCmd: []string{"command1", "command2"},
|
||||||
|
}: {
|
||||||
|
Cmd: []string{"command1", "command2"},
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
},
|
||||||
|
&arguments{
|
||||||
|
options: execOptions{
|
||||||
|
interactive: true,
|
||||||
|
tty: true,
|
||||||
|
user: "uid",
|
||||||
|
},
|
||||||
|
execCmd: []string{"command"},
|
||||||
|
}: {
|
||||||
|
User: "uid",
|
||||||
|
AttachStdin: true,
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Tty: true,
|
||||||
|
Cmd: []string{"command"},
|
||||||
|
},
|
||||||
|
&arguments{
|
||||||
|
options: execOptions{
|
||||||
|
detach: true,
|
||||||
|
},
|
||||||
|
execCmd: []string{"command"},
|
||||||
|
}: {
|
||||||
|
AttachStdin: false,
|
||||||
|
AttachStdout: false,
|
||||||
|
AttachStderr: false,
|
||||||
|
Detach: true,
|
||||||
|
Cmd: []string{"command"},
|
||||||
|
},
|
||||||
|
&arguments{
|
||||||
|
options: execOptions{
|
||||||
|
tty: true,
|
||||||
|
interactive: true,
|
||||||
|
detach: true,
|
||||||
|
},
|
||||||
|
execCmd: []string{"command"},
|
||||||
|
}: {
|
||||||
|
AttachStdin: false,
|
||||||
|
AttachStdout: false,
|
||||||
|
AttachStderr: false,
|
||||||
|
Detach: true,
|
||||||
|
Tty: true,
|
||||||
|
Cmd: []string{"command"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for valid, expectedExecConfig := range valids {
|
||||||
|
execConfig, err := parseExec(&valid.options, valid.container, valid.execCmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !compareExecConfig(expectedExecConfig, execConfig) {
|
||||||
|
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
|
||||||
|
if config1.AttachStderr != config2.AttachStderr {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config1.AttachStdin != config2.AttachStdin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config1.AttachStdout != config2.AttachStdout {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config1.Detach != config2.Detach {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config1.Privileged != config2.Privileged {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config1.Tty != config2.Tty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if config1.User != config2.User {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(config1.Cmd) != len(config2.Cmd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index, value := range config1.Cmd {
|
||||||
|
if value != config2.Cmd[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type exportOptions struct {
|
||||||
|
container string
|
||||||
|
output string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExportCommand creates a new `docker export` command
|
||||||
|
func NewExportCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts exportOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "export [OPTIONS] CONTAINER",
|
||||||
|
Short: "Export a container's filesystem as a tar archive",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
return runExport(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExport(dockerCli *command.DockerCli, opts exportOptions) error {
|
||||||
|
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||||
|
return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
|
||||||
|
}
|
||||||
|
|
||||||
|
clnt := dockerCli.Client()
|
||||||
|
|
||||||
|
responseBody, err := clnt.ContainerExport(context.Background(), opts.container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer responseBody.Close()
|
||||||
|
|
||||||
|
if opts.output == "" {
|
||||||
|
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.CopyToFile(opts.output, responseBody)
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type streams interface {
|
||||||
|
In() *command.InStream
|
||||||
|
Out() *command.OutStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// holdHijackedConnection handles copying input to and output from streams to the
|
||||||
|
// connection
|
||||||
|
func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
restoreOnce sync.Once
|
||||||
|
)
|
||||||
|
if inputStream != nil && tty {
|
||||||
|
if err := setRawTerminal(streams); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
restoreOnce.Do(func() {
|
||||||
|
restoreTerminal(streams, inputStream)
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveStdout := make(chan error, 1)
|
||||||
|
if outputStream != nil || errorStream != nil {
|
||||||
|
go func() {
|
||||||
|
// When TTY is ON, use regular copy
|
||||||
|
if tty && outputStream != nil {
|
||||||
|
_, err = io.Copy(outputStream, resp.Reader)
|
||||||
|
// we should restore the terminal as soon as possible once connection end
|
||||||
|
// so any following print messages will be in normal type.
|
||||||
|
if inputStream != nil {
|
||||||
|
restoreOnce.Do(func() {
|
||||||
|
restoreTerminal(streams, inputStream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debug("[hijack] End of stdout")
|
||||||
|
receiveStdout <- err
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
stdinDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
if inputStream != nil {
|
||||||
|
io.Copy(resp.Conn, inputStream)
|
||||||
|
// we should restore the terminal as soon as possible once connection end
|
||||||
|
// so any following print messages will be in normal type.
|
||||||
|
if tty {
|
||||||
|
restoreOnce.Do(func() {
|
||||||
|
restoreTerminal(streams, inputStream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
logrus.Debug("[hijack] End of stdin")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := resp.CloseWrite(); err != nil {
|
||||||
|
logrus.Debugf("Couldn't send EOF: %s", err)
|
||||||
|
}
|
||||||
|
close(stdinDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-receiveStdout:
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Error receiveStdout: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-stdinDone:
|
||||||
|
if outputStream != nil || errorStream != nil {
|
||||||
|
select {
|
||||||
|
case err := <-receiveStdout:
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Error receiveStdout: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRawTerminal(streams streams) error {
|
||||||
|
if err := streams.In().SetRawTerminal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return streams.Out().SetRawTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreTerminal(streams streams, in io.Closer) error {
|
||||||
|
streams.In().RestoreTerminal()
|
||||||
|
streams.Out().RestoreTerminal()
|
||||||
|
// WARNING: DO NOT REMOVE THE OS CHECK !!!
|
||||||
|
// For some reason this Close call blocks on darwin..
|
||||||
|
// As the client exists right after, simply discard the close
|
||||||
|
// until we find a better solution.
|
||||||
|
if in != nil && runtime.GOOS != "darwin" {
|
||||||
|
return in.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type killOptions struct {
|
||||||
|
signal string
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKillCommand creates a new cobra.Command for `docker kill`
|
||||||
|
func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts killOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "kill [OPTIONS] CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Kill one or more running containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runKill(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVarP(&opts.signal, "signal", "s", "KILL", "Signal to send to the container")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKill(dockerCli *command.DockerCli, opts *killOptions) error {
|
||||||
|
var errs []string
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, name := range opts.containers {
|
||||||
|
if err := dockerCli.Client().ContainerKill(ctx, name, opts.signal); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validDrivers = map[string]bool{
|
||||||
|
"json-file": true,
|
||||||
|
"journald": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type logsOptions struct {
|
||||||
|
follow bool
|
||||||
|
since string
|
||||||
|
timestamps bool
|
||||||
|
details bool
|
||||||
|
tail string
|
||||||
|
|
||||||
|
container string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogsCommand creates a new cobra.Command for `docker logs`
|
||||||
|
func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts logsOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "logs [OPTIONS] CONTAINER",
|
||||||
|
Short: "Fetch the logs of a container",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
return runLogs(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
|
||||||
|
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp")
|
||||||
|
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")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validDrivers[c.HostConfig.LogConfig.Type] {
|
||||||
|
return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ContainerLogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
Since: opts.since,
|
||||||
|
Timestamps: opts.timestamps,
|
||||||
|
Follow: opts.follow,
|
||||||
|
Tail: opts.tail,
|
||||||
|
Details: opts.details,
|
||||||
|
}
|
||||||
|
responseBody, err := dockerCli.Client().ContainerLogs(ctx, opts.container, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer responseBody.Close()
|
||||||
|
|
||||||
|
if c.Config.Tty {
|
||||||
|
_, err = io.Copy(dockerCli.Out(), responseBody)
|
||||||
|
} else {
|
||||||
|
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pauseOptions struct {
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPauseCommand creates a new cobra.Command for `docker pause`
|
||||||
|
func NewPauseCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts pauseOptions
|
||||||
|
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "pause CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Pause all processes within one or more containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runPause(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, container := range opts.containers {
|
||||||
|
if err := dockerCli.Client().ContainerPause(ctx, container); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type portOptions struct {
|
||||||
|
container string
|
||||||
|
|
||||||
|
port string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPortCommand creates a new cobra.Command for `docker port`
|
||||||
|
func NewPortCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts portOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "port CONTAINER [PRIVATE_PORT[/PROTO]]",
|
||||||
|
Short: "List port mappings or a specific mapping for the container",
|
||||||
|
Args: cli.RequiresRangeArgs(1, 2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
opts.port = args[1]
|
||||||
|
}
|
||||||
|
return runPort(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPort(dockerCli *command.DockerCli, opts *portOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.port != "" {
|
||||||
|
port := opts.port
|
||||||
|
proto := "tcp"
|
||||||
|
parts := strings.SplitN(port, "/", 2)
|
||||||
|
|
||||||
|
if len(parts) == 2 && len(parts[1]) != 0 {
|
||||||
|
port = parts[0]
|
||||||
|
proto = parts[1]
|
||||||
|
}
|
||||||
|
natPort := port + "/" + proto
|
||||||
|
newP, err := nat.NewPort(proto, port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil {
|
||||||
|
for _, frontend := range frontends {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s:%s\n", frontend.HostIP, frontend.HostPort)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Error: No public port '%s' published for %s", natPort, opts.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
for from, frontends := range c.NetworkSettings.Ports {
|
||||||
|
for _, frontend := range frontends {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
|
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/docker/docker/utils/templates"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type psOptions struct {
|
||||||
|
quiet bool
|
||||||
|
size bool
|
||||||
|
all bool
|
||||||
|
noTrunc bool
|
||||||
|
nLatest bool
|
||||||
|
last int
|
||||||
|
format string
|
||||||
|
filter []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPsCommand creates a new cobra.Command for `docker ps`
|
||||||
|
func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts psOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ps [OPTIONS]",
|
||||||
|
Short: "List containers",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runPs(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display numeric IDs")
|
||||||
|
flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes")
|
||||||
|
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||||
|
flags.BoolVarP(&opts.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
|
||||||
|
flags.IntVarP(&opts.last, "last", "n", -1, "Show n last created containers (includes all states)")
|
||||||
|
flags.StringVarP(&opts.format, "format", "", "", "Pretty-print containers using a Go template")
|
||||||
|
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type preProcessor struct {
|
||||||
|
types.Container
|
||||||
|
opts *types.ContainerListOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size sets the size option when called by a template execution.
|
||||||
|
func (p *preProcessor) Size() bool {
|
||||||
|
p.opts.Size = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) {
|
||||||
|
|
||||||
|
options := &types.ContainerListOptions{
|
||||||
|
All: opts.all,
|
||||||
|
Limit: opts.last,
|
||||||
|
Size: opts.size,
|
||||||
|
Filter: filters.NewArgs(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.nLatest && opts.last == -1 {
|
||||||
|
options.Limit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range opts.filter {
|
||||||
|
var err error
|
||||||
|
options.Filter, err = filters.ParseFlag(f, options.Filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently only used with Size, so we can determine if the user
|
||||||
|
// put {{.Size}} in their format.
|
||||||
|
pre := &preProcessor{opts: options}
|
||||||
|
tmpl, err := templates.Parse(opts.format)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shouldn't error out but swallowing the error makes it harder
|
||||||
|
// to track down if preProcessor issues come up. Ref #24696
|
||||||
|
if err := tmpl.Execute(ioutil.Discard, pre); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return options, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPs(dockerCli *command.DockerCli, opts *psOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
listOptions, err := buildContainerListOptions(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
containers, err := dockerCli.Client().ContainerList(ctx, *listOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f := opts.format
|
||||||
|
if len(f) == 0 {
|
||||||
|
if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
|
||||||
|
f = dockerCli.ConfigFile().PsFormat
|
||||||
|
} else {
|
||||||
|
f = "table"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
psCtx := formatter.ContainerContext{
|
||||||
|
Context: formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: f,
|
||||||
|
Quiet: opts.quiet,
|
||||||
|
Trunc: !opts.noTrunc,
|
||||||
|
},
|
||||||
|
Size: listOptions.Size,
|
||||||
|
Containers: containers,
|
||||||
|
}
|
||||||
|
|
||||||
|
psCtx.Write()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBuildContainerListOptions(t *testing.T) {
|
||||||
|
|
||||||
|
contexts := []struct {
|
||||||
|
psOpts *psOptions
|
||||||
|
expectedAll bool
|
||||||
|
expectedSize bool
|
||||||
|
expectedLimit int
|
||||||
|
expectedFilters map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
psOpts: &psOptions{
|
||||||
|
all: true,
|
||||||
|
size: true,
|
||||||
|
last: 5,
|
||||||
|
filter: []string{"foo=bar", "baz=foo"},
|
||||||
|
},
|
||||||
|
expectedAll: true,
|
||||||
|
expectedSize: true,
|
||||||
|
expectedLimit: 5,
|
||||||
|
expectedFilters: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
psOpts: &psOptions{
|
||||||
|
all: true,
|
||||||
|
size: true,
|
||||||
|
last: -1,
|
||||||
|
nLatest: true,
|
||||||
|
},
|
||||||
|
expectedAll: true,
|
||||||
|
expectedSize: true,
|
||||||
|
expectedLimit: 1,
|
||||||
|
expectedFilters: make(map[string]string),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range contexts {
|
||||||
|
options, err := buildContainerListOptions(c.psOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.expectedAll != options.All {
|
||||||
|
t.Fatalf("Expected All to be %t but got %t", c.expectedAll, options.All)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.expectedSize != options.Size {
|
||||||
|
t.Fatalf("Expected Size to be %t but got %t", c.expectedSize, options.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.expectedLimit != options.Limit {
|
||||||
|
t.Fatalf("Expected Limit to be %d but got %d", c.expectedLimit, options.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := options.Filter
|
||||||
|
|
||||||
|
if f.Len() != len(c.expectedFilters) {
|
||||||
|
t.Fatalf("Expected %d filters but got %d", len(c.expectedFilters), f.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range c.expectedFilters {
|
||||||
|
f := options.Filter
|
||||||
|
if !f.ExactMatch(k, v) {
|
||||||
|
t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type renameOptions struct {
|
||||||
|
oldName string
|
||||||
|
newName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRenameCommand creates a new cobra.Command for `docker rename`
|
||||||
|
func NewRenameCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts renameOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rename CONTAINER NEW_NAME",
|
||||||
|
Short: "Rename a container",
|
||||||
|
Args: cli.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.oldName = args[0]
|
||||||
|
opts.newName = args[1]
|
||||||
|
return runRename(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRename(dockerCli *command.DockerCli, opts *renameOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
oldName := strings.TrimSpace(opts.oldName)
|
||||||
|
newName := strings.TrimSpace(opts.newName)
|
||||||
|
|
||||||
|
if oldName == "" || newName == "" {
|
||||||
|
return fmt.Errorf("Error: Neither old nor new names may be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "%s\n", err)
|
||||||
|
return fmt.Errorf("Error: failed to rename container named %s", oldName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type restartOptions struct {
|
||||||
|
nSeconds int
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRestartCommand creates a new cobra.Command for `docker restart`
|
||||||
|
func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts restartOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "restart [OPTIONS] CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Restart one or more containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runRestart(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.IntVarP(&opts.nSeconds, "time", "t", 10, "Seconds to wait for stop before killing the container")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
var errs []string
|
||||||
|
for _, name := range opts.containers {
|
||||||
|
timeout := time.Duration(opts.nSeconds) * time.Second
|
||||||
|
if err := dockerCli.Client().ContainerRestart(ctx, name, &timeout); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rmOptions struct {
|
||||||
|
rmVolumes bool
|
||||||
|
rmLink bool
|
||||||
|
force bool
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRmCommand creates a new cobra.Command for `docker rm`
|
||||||
|
func NewRmCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts rmOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm [OPTIONS] CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Remove one or more containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runRm(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.rmVolumes, "volumes", "v", false, "Remove the volumes associated with the container")
|
||||||
|
flags.BoolVarP(&opts.rmLink, "link", "l", false, "Remove the specified link")
|
||||||
|
flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of a running container (uses SIGKILL)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRm(dockerCli *command.DockerCli, opts *rmOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, name := range opts.containers {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("Container name cannot be empty")
|
||||||
|
}
|
||||||
|
name = strings.Trim(name, "/")
|
||||||
|
|
||||||
|
if err := removeContainer(dockerCli, ctx, name, opts.rmVolumes, opts.rmLink, opts.force); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeContainer(dockerCli *command.DockerCli, ctx context.Context, container string, removeVolumes, removeLinks, force bool) error {
|
||||||
|
options := types.ContainerRemoveOptions{
|
||||||
|
RemoveVolumes: removeVolumes,
|
||||||
|
RemoveLinks: removeLinks,
|
||||||
|
Force: force,
|
||||||
|
}
|
||||||
|
if err := dockerCli.Client().ContainerRemove(ctx, container, options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,288 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
opttypes "github.com/docker/docker/opts"
|
||||||
|
"github.com/docker/docker/pkg/promise"
|
||||||
|
"github.com/docker/docker/pkg/signal"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/docker/libnetwork/resolvconf/dns"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runOptions struct {
|
||||||
|
detach bool
|
||||||
|
sigProxy bool
|
||||||
|
name string
|
||||||
|
detachKeys string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRunCommand create a new `docker run` command
|
||||||
|
func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts runOptions
|
||||||
|
var copts *runconfigopts.ContainerOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||||
|
Short: "Run a command in a new container",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
copts.Image = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
copts.Args = args[1:]
|
||||||
|
}
|
||||||
|
return runRun(dockerCli, cmd.Flags(), &opts, copts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
|
// These are flags not stored in Config/HostConfig
|
||||||
|
flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID")
|
||||||
|
flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process")
|
||||||
|
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
|
||||||
|
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||||
|
|
||||||
|
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||||
|
// with hostname
|
||||||
|
flags.Bool("help", false, "Print usage")
|
||||||
|
|
||||||
|
command.AddTrustedFlags(flags, true)
|
||||||
|
copts = runconfigopts.AddFlags(flags)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *runconfigopts.ContainerOptions) error {
|
||||||
|
stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In()
|
||||||
|
client := dockerCli.Client()
|
||||||
|
// TODO: pass this as an argument
|
||||||
|
cmdPath := "run"
|
||||||
|
|
||||||
|
var (
|
||||||
|
flAttach *opttypes.ListOpts
|
||||||
|
ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d")
|
||||||
|
ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm")
|
||||||
|
)
|
||||||
|
|
||||||
|
config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts)
|
||||||
|
|
||||||
|
// just in case the Parse does not exit
|
||||||
|
if err != nil {
|
||||||
|
reportError(stderr, cmdPath, err.Error(), true)
|
||||||
|
return cli.StatusError{StatusCode: 125}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||||
|
return ErrConflictRestartPolicyAndAutoRemove
|
||||||
|
}
|
||||||
|
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
|
||||||
|
fmt.Fprintf(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostConfig.DNS) > 0 {
|
||||||
|
// check the DNS settings passed via --dns against
|
||||||
|
// localhost regexp to warn if they are trying to
|
||||||
|
// set a DNS to a localhost address
|
||||||
|
for _, dnsIP := range hostConfig.DNS {
|
||||||
|
if dns.IsLocalhost(dnsIP) {
|
||||||
|
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.ArgsEscaped = false
|
||||||
|
|
||||||
|
if !opts.detach {
|
||||||
|
if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if fl := flags.Lookup("attach"); fl != nil {
|
||||||
|
flAttach = fl.Value.(*opttypes.ListOpts)
|
||||||
|
if flAttach.Len() != 0 {
|
||||||
|
return ErrConflictAttachDetach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AttachStdin = false
|
||||||
|
config.AttachStdout = false
|
||||||
|
config.AttachStderr = false
|
||||||
|
config.StdinOnce = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable sigProxy when in TTY mode
|
||||||
|
if config.Tty {
|
||||||
|
opts.sigProxy = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telling the Windows daemon the initial size of the tty during start makes
|
||||||
|
// a far better user experience rather than relying on subsequent resizes
|
||||||
|
// to cause things to catch up.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancelFun := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
createResponse, err := createContainer(ctx, dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name)
|
||||||
|
if err != nil {
|
||||||
|
reportError(stderr, cmdPath, err.Error(), true)
|
||||||
|
return runStartContainerErr(err)
|
||||||
|
}
|
||||||
|
if opts.sigProxy {
|
||||||
|
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
|
||||||
|
defer signal.StopCatch(sigc)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
waitDisplayID chan struct{}
|
||||||
|
errCh chan error
|
||||||
|
)
|
||||||
|
if !config.AttachStdout && !config.AttachStderr {
|
||||||
|
// Make this asynchronous to allow the client to write to stdin before having to read the ID
|
||||||
|
waitDisplayID = make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(waitDisplayID)
|
||||||
|
fmt.Fprintf(stdout, "%s\n", createResponse.ID)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
|
||||||
|
if attach {
|
||||||
|
var (
|
||||||
|
out, cerr io.Writer
|
||||||
|
in io.ReadCloser
|
||||||
|
)
|
||||||
|
if config.AttachStdin {
|
||||||
|
in = stdin
|
||||||
|
}
|
||||||
|
if config.AttachStdout {
|
||||||
|
out = stdout
|
||||||
|
}
|
||||||
|
if config.AttachStderr {
|
||||||
|
if config.Tty {
|
||||||
|
cerr = stdout
|
||||||
|
} else {
|
||||||
|
cerr = stderr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.detachKeys != "" {
|
||||||
|
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ContainerAttachOptions{
|
||||||
|
Stream: true,
|
||||||
|
Stdin: config.AttachStdin,
|
||||||
|
Stdout: config.AttachStdout,
|
||||||
|
Stderr: config.AttachStderr,
|
||||||
|
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, errAttach := client.ContainerAttach(ctx, createResponse.ID, options)
|
||||||
|
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
|
||||||
|
// ContainerAttach returns an ErrPersistEOF (connection closed)
|
||||||
|
// means server met an error and put it in Hijacked connection
|
||||||
|
// keep the error and read detailed error message from hijacked connection later
|
||||||
|
return errAttach
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
|
||||||
|
errCh = promise.Go(func() error {
|
||||||
|
errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp)
|
||||||
|
if errHijack == nil {
|
||||||
|
return errAttach
|
||||||
|
}
|
||||||
|
return errHijack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
statusChan, err := waitExitOrRemoved(dockerCli, context.Background(), createResponse.ID, hostConfig.AutoRemove)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error waiting container's exit code: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//start the container
|
||||||
|
if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
|
||||||
|
// If we have holdHijackedConnection, we should notify
|
||||||
|
// holdHijackedConnection we are going to exit and wait
|
||||||
|
// to avoid the terminal are not restored.
|
||||||
|
if attach {
|
||||||
|
cancelFun()
|
||||||
|
<-errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError(stderr, cmdPath, err.Error(), false)
|
||||||
|
if hostConfig.AutoRemove {
|
||||||
|
// wait container to be removed
|
||||||
|
<-statusChan
|
||||||
|
}
|
||||||
|
return runStartContainerErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() {
|
||||||
|
if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "Error monitoring TTY size: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errCh != nil {
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
logrus.Debugf("Error hijack: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detached mode: wait for the id to be displayed and return.
|
||||||
|
if !config.AttachStdout && !config.AttachStderr {
|
||||||
|
// Detached mode
|
||||||
|
<-waitDisplayID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status := <-statusChan
|
||||||
|
if status != 0 {
|
||||||
|
return cli.StatusError{StatusCode: status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportError is a utility method that prints a user-friendly message
|
||||||
|
// containing the error that occurred during parsing and a suggestion to get help
|
||||||
|
func reportError(stderr io.Writer, name string, str string, withHelp bool) {
|
||||||
|
if withHelp {
|
||||||
|
str += ".\nSee '" + os.Args[0] + " " + name + " --help'"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stderr, "%s: %s.\n", os.Args[0], str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if container start fails with 'not found'/'no such' error, return 127
|
||||||
|
// if container start fails with 'permission denied' error, return 126
|
||||||
|
// return 125 for generic docker daemon failures
|
||||||
|
func runStartContainerErr(err error) error {
|
||||||
|
trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
|
||||||
|
statusError := cli.StatusError{StatusCode: 125}
|
||||||
|
if strings.Contains(trimmedErr, "executable file not found") ||
|
||||||
|
strings.Contains(trimmedErr, "no such file or directory") ||
|
||||||
|
strings.Contains(trimmedErr, "system cannot find the file specified") {
|
||||||
|
statusError = cli.StatusError{StatusCode: 127}
|
||||||
|
} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) {
|
||||||
|
statusError = cli.StatusError{StatusCode: 126}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusError
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http/httputil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/promise"
|
||||||
|
"github.com/docker/docker/pkg/signal"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type startOptions struct {
|
||||||
|
attach bool
|
||||||
|
openStdin bool
|
||||||
|
detachKeys string
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStartCommand creates a new cobra.Command for `docker start`
|
||||||
|
func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts startOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "start [OPTIONS] CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Start one or more stopped containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runStart(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals")
|
||||||
|
flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN")
|
||||||
|
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
|
||||||
|
ctx, cancelFun := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
if opts.attach || opts.openStdin {
|
||||||
|
// We're going to attach to a container.
|
||||||
|
// 1. Ensure we only have one container.
|
||||||
|
if len(opts.containers) > 1 {
|
||||||
|
return fmt.Errorf("You cannot start and attach multiple containers at once.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Attach to the container.
|
||||||
|
container := opts.containers[0]
|
||||||
|
c, err := dockerCli.Client().ContainerInspect(ctx, container)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always use c.ID instead of container to maintain consistency during `docker start`
|
||||||
|
if !c.Config.Tty {
|
||||||
|
sigc := ForwardAllSignals(ctx, dockerCli, c.ID)
|
||||||
|
defer signal.StopCatch(sigc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.detachKeys != "" {
|
||||||
|
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ContainerAttachOptions{
|
||||||
|
Stream: true,
|
||||||
|
Stdin: opts.openStdin && c.Config.OpenStdin,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
var in io.ReadCloser
|
||||||
|
|
||||||
|
if options.Stdin {
|
||||||
|
in = dockerCli.In()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options)
|
||||||
|
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
|
||||||
|
// ContainerAttach return an ErrPersistEOF (connection closed)
|
||||||
|
// means server met an error and already put it in Hijacked connection,
|
||||||
|
// we would keep the error and read the detailed error message from hijacked connection
|
||||||
|
return errAttach
|
||||||
|
}
|
||||||
|
defer resp.Close()
|
||||||
|
cErr := promise.Go(func() error {
|
||||||
|
errHijack := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp)
|
||||||
|
if errHijack == nil {
|
||||||
|
return errAttach
|
||||||
|
}
|
||||||
|
return errHijack
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. We should open a channel for receiving status code of the container
|
||||||
|
// no matter it's detached, removed on daemon side(--rm) or exit normally.
|
||||||
|
statusChan, statusErr := waitExitOrRemoved(dockerCli, context.Background(), c.ID, c.HostConfig.AutoRemove)
|
||||||
|
|
||||||
|
// 4. Start the container.
|
||||||
|
if err := dockerCli.Client().ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
|
||||||
|
cancelFun()
|
||||||
|
<-cErr
|
||||||
|
if c.HostConfig.AutoRemove && statusErr == nil {
|
||||||
|
// wait container to be removed
|
||||||
|
<-statusChan
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Wait for attachment to break.
|
||||||
|
if c.Config.Tty && dockerCli.Out().IsTerminal() {
|
||||||
|
if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if attchErr := <-cErr; attchErr != nil {
|
||||||
|
return attchErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusErr != nil {
|
||||||
|
return fmt.Errorf("can't get container's exit code: %v", statusErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := <-statusChan; status != 0 {
|
||||||
|
return cli.StatusError{StatusCode: status}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're not going to attach to anything.
|
||||||
|
// Start as many containers as we want.
|
||||||
|
return startContainersWithoutAttachments(dockerCli, ctx, opts.containers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startContainersWithoutAttachments(dockerCli *command.DockerCli, ctx context.Context, containers []string) error {
|
||||||
|
var failedContainers []string
|
||||||
|
for _, container := range containers {
|
||||||
|
if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "%s\n", err)
|
||||||
|
failedContainers = append(failedContainers, container)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(failedContainers) > 0 {
|
||||||
|
return fmt.Errorf("Error: failed to start containers: %v", strings.Join(failedContainers, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/events"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/system"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type statsOptions struct {
|
||||||
|
all bool
|
||||||
|
noStream bool
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatsCommand creates a new cobra.Command for `docker stats`
|
||||||
|
func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts statsOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "stats [OPTIONS] [CONTAINER...]",
|
||||||
|
Short: "Display a live stream of container(s) resource usage statistics",
|
||||||
|
Args: cli.RequiresMinArgs(0),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runStats(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
|
||||||
|
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// runStats displays a live stream of resource usage statistics for one or more containers.
|
||||||
|
// This shows real-time information on CPU usage, memory usage, and network I/O.
|
||||||
|
func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||||
|
showAll := len(opts.containers) == 0
|
||||||
|
closeChan := make(chan error)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// monitorContainerEvents watches for container creation and removal (only
|
||||||
|
// used when calling `docker stats` without arguments).
|
||||||
|
monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) {
|
||||||
|
f := filters.NewArgs()
|
||||||
|
f.Add("type", "container")
|
||||||
|
options := types.EventsOptions{
|
||||||
|
Filters: f,
|
||||||
|
}
|
||||||
|
resBody, err := dockerCli.Client().Events(ctx, options)
|
||||||
|
// Whether we successfully subscribed to events or not, we can now
|
||||||
|
// unblock the main goroutine.
|
||||||
|
close(started)
|
||||||
|
if err != nil {
|
||||||
|
closeChan <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resBody.Close()
|
||||||
|
|
||||||
|
system.DecodeEvents(resBody, func(event events.Message, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
closeChan <- err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c <- event
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitFirst is a WaitGroup to wait first stat data's reach for each container
|
||||||
|
waitFirst := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
cStats := stats{}
|
||||||
|
// getContainerList simulates creation event for all previously existing
|
||||||
|
// containers (only used when calling `docker stats` without arguments).
|
||||||
|
getContainerList := func() {
|
||||||
|
options := types.ContainerListOptions{
|
||||||
|
All: opts.all,
|
||||||
|
}
|
||||||
|
cs, err := dockerCli.Client().ContainerList(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
closeChan <- err
|
||||||
|
}
|
||||||
|
for _, container := range cs {
|
||||||
|
s := &containerStats{Name: container.ID[:12]}
|
||||||
|
if cStats.add(s) {
|
||||||
|
waitFirst.Add(1)
|
||||||
|
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if showAll {
|
||||||
|
// If no names were specified, start a long running goroutine which
|
||||||
|
// monitors container events. We make sure we're subscribed before
|
||||||
|
// retrieving the list of running containers to avoid a race where we
|
||||||
|
// would "miss" a creation.
|
||||||
|
started := make(chan struct{})
|
||||||
|
eh := system.InitEventHandler()
|
||||||
|
eh.Handle("create", func(e events.Message) {
|
||||||
|
if opts.all {
|
||||||
|
s := &containerStats{Name: e.ID[:12]}
|
||||||
|
if cStats.add(s) {
|
||||||
|
waitFirst.Add(1)
|
||||||
|
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eh.Handle("start", func(e events.Message) {
|
||||||
|
s := &containerStats{Name: e.ID[:12]}
|
||||||
|
if cStats.add(s) {
|
||||||
|
waitFirst.Add(1)
|
||||||
|
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eh.Handle("die", func(e events.Message) {
|
||||||
|
if !opts.all {
|
||||||
|
cStats.remove(e.ID[:12])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eventChan := make(chan events.Message)
|
||||||
|
go eh.Watch(eventChan)
|
||||||
|
go monitorContainerEvents(started, eventChan)
|
||||||
|
defer close(eventChan)
|
||||||
|
<-started
|
||||||
|
|
||||||
|
// Start a short-lived goroutine to retrieve the initial list of
|
||||||
|
// containers.
|
||||||
|
getContainerList()
|
||||||
|
} else {
|
||||||
|
// Artificially send creation events for the containers we were asked to
|
||||||
|
// monitor (same code path than we use when monitoring all containers).
|
||||||
|
for _, name := range opts.containers {
|
||||||
|
s := &containerStats{Name: name}
|
||||||
|
if cStats.add(s) {
|
||||||
|
waitFirst.Add(1)
|
||||||
|
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't expect any asynchronous errors: closeChan can be closed.
|
||||||
|
close(closeChan)
|
||||||
|
|
||||||
|
// Do a quick pause to detect any error with the provided list of
|
||||||
|
// container names.
|
||||||
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
var errs []string
|
||||||
|
cStats.mu.Lock()
|
||||||
|
for _, c := range cStats.cs {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
cStats.mu.Unlock()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// before print to screen, make sure each container get at least one valid stat data
|
||||||
|
waitFirst.Wait()
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||||
|
printHeader := func() {
|
||||||
|
if !opts.noStream {
|
||||||
|
fmt.Fprint(dockerCli.Out(), "\033[2J")
|
||||||
|
fmt.Fprint(dockerCli.Out(), "\033[H")
|
||||||
|
}
|
||||||
|
io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for range time.Tick(500 * time.Millisecond) {
|
||||||
|
printHeader()
|
||||||
|
toRemove := []string{}
|
||||||
|
cStats.mu.Lock()
|
||||||
|
for _, s := range cStats.cs {
|
||||||
|
if err := s.Display(w); err != nil && !opts.noStream {
|
||||||
|
logrus.Debugf("stats: got error for %s: %v", s.Name, err)
|
||||||
|
if err == io.EOF {
|
||||||
|
toRemove = append(toRemove, s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cStats.mu.Unlock()
|
||||||
|
for _, name := range toRemove {
|
||||||
|
cStats.remove(name)
|
||||||
|
}
|
||||||
|
if len(cStats.cs) == 0 && !showAll {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
if opts.noStream {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err, ok := <-closeChan:
|
||||||
|
if ok {
|
||||||
|
if err != nil {
|
||||||
|
// this is suppressing "unexpected EOF" in the cli when the
|
||||||
|
// daemon restarts so it shutdowns cleanly
|
||||||
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// just skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type containerStats struct {
|
||||||
|
Name string
|
||||||
|
CPUPercentage float64
|
||||||
|
Memory float64
|
||||||
|
MemoryLimit float64
|
||||||
|
MemoryPercentage float64
|
||||||
|
NetworkRx float64
|
||||||
|
NetworkTx float64
|
||||||
|
BlockRead float64
|
||||||
|
BlockWrite float64
|
||||||
|
PidsCurrent uint64
|
||||||
|
mu sync.Mutex
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type stats struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
cs []*containerStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stats) add(cs *containerStats) bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if _, exists := s.isKnownContainer(cs.Name); !exists {
|
||||||
|
s.cs = append(s.cs, cs)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stats) remove(id string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
if i, exists := s.isKnownContainer(id); exists {
|
||||||
|
s.cs = append(s.cs[:i], s.cs[i+1:]...)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stats) isKnownContainer(cid string) (int, bool) {
|
||||||
|
for i, c := range s.cs {
|
||||||
|
if c.Name == cid {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
|
||||||
|
logrus.Debugf("collecting stats for %s", s.Name)
|
||||||
|
var (
|
||||||
|
getFirst bool
|
||||||
|
previousCPU uint64
|
||||||
|
previousSystem uint64
|
||||||
|
u = make(chan error, 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// if error happens and we get nothing of stats, release wait group whatever
|
||||||
|
if !getFirst {
|
||||||
|
getFirst = true
|
||||||
|
waitFirst.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
responseBody, err := cli.ContainerStats(ctx, s.Name, streamStats)
|
||||||
|
if err != nil {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.err = err
|
||||||
|
s.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer responseBody.Close()
|
||||||
|
|
||||||
|
dec := json.NewDecoder(responseBody)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
var v *types.StatsJSON
|
||||||
|
|
||||||
|
if err := dec.Decode(&v); err != nil {
|
||||||
|
dec = json.NewDecoder(io.MultiReader(dec.Buffered(), responseBody))
|
||||||
|
u <- err
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var memPercent = 0.0
|
||||||
|
var cpuPercent = 0.0
|
||||||
|
|
||||||
|
// MemoryStats.Limit will never be 0 unless the container is not running and we haven't
|
||||||
|
// got any data from cgroup
|
||||||
|
if v.MemoryStats.Limit != 0 {
|
||||||
|
memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
|
||||||
|
previousSystem = v.PreCPUStats.SystemUsage
|
||||||
|
cpuPercent = calculateCPUPercent(previousCPU, previousSystem, v)
|
||||||
|
blkRead, blkWrite := calculateBlockIO(v.BlkioStats)
|
||||||
|
s.mu.Lock()
|
||||||
|
s.CPUPercentage = cpuPercent
|
||||||
|
s.Memory = float64(v.MemoryStats.Usage)
|
||||||
|
s.MemoryLimit = float64(v.MemoryStats.Limit)
|
||||||
|
s.MemoryPercentage = memPercent
|
||||||
|
s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks)
|
||||||
|
s.BlockRead = float64(blkRead)
|
||||||
|
s.BlockWrite = float64(blkWrite)
|
||||||
|
s.PidsCurrent = v.PidsStats.Current
|
||||||
|
s.mu.Unlock()
|
||||||
|
u <- nil
|
||||||
|
if !streamStats {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
// zero out the values if we have not received an update within
|
||||||
|
// the specified duration.
|
||||||
|
s.mu.Lock()
|
||||||
|
s.CPUPercentage = 0
|
||||||
|
s.Memory = 0
|
||||||
|
s.MemoryPercentage = 0
|
||||||
|
s.MemoryLimit = 0
|
||||||
|
s.NetworkRx = 0
|
||||||
|
s.NetworkTx = 0
|
||||||
|
s.BlockRead = 0
|
||||||
|
s.BlockWrite = 0
|
||||||
|
s.PidsCurrent = 0
|
||||||
|
s.err = errors.New("timeout waiting for stats")
|
||||||
|
s.mu.Unlock()
|
||||||
|
// if this is the first stat you get, release WaitGroup
|
||||||
|
if !getFirst {
|
||||||
|
getFirst = true
|
||||||
|
waitFirst.Done()
|
||||||
|
}
|
||||||
|
case err := <-u:
|
||||||
|
if err != nil {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.err = err
|
||||||
|
s.mu.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.err = nil
|
||||||
|
// if this is the first stat you get, release WaitGroup
|
||||||
|
if !getFirst {
|
||||||
|
getFirst = true
|
||||||
|
waitFirst.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !streamStats {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *containerStats) Display(w io.Writer) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
// NOTE: if you change this format, you must also change the err format below!
|
||||||
|
format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n"
|
||||||
|
if s.err != nil {
|
||||||
|
format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n"
|
||||||
|
errStr := "--"
|
||||||
|
fmt.Fprintf(w, format,
|
||||||
|
s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr,
|
||||||
|
)
|
||||||
|
err := s.err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, format,
|
||||||
|
s.Name,
|
||||||
|
s.CPUPercentage,
|
||||||
|
units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit),
|
||||||
|
s.MemoryPercentage,
|
||||||
|
units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx),
|
||||||
|
units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite),
|
||||||
|
s.PidsCurrent)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
|
||||||
|
var (
|
||||||
|
cpuPercent = 0.0
|
||||||
|
// calculate the change for the cpu usage of the container in between readings
|
||||||
|
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU)
|
||||||
|
// calculate the change for the entire system between readings
|
||||||
|
systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem)
|
||||||
|
)
|
||||||
|
|
||||||
|
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
||||||
|
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0
|
||||||
|
}
|
||||||
|
return cpuPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) {
|
||||||
|
for _, bioEntry := range blkio.IoServiceBytesRecursive {
|
||||||
|
switch strings.ToLower(bioEntry.Op) {
|
||||||
|
case "read":
|
||||||
|
blkRead = blkRead + bioEntry.Value
|
||||||
|
case "write":
|
||||||
|
blkWrite = blkWrite + bioEntry.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) {
|
||||||
|
var rx, tx float64
|
||||||
|
|
||||||
|
for _, v := range network {
|
||||||
|
rx += float64(v.RxBytes)
|
||||||
|
tx += float64(v.TxBytes)
|
||||||
|
}
|
||||||
|
return rx, tx
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDisplay(t *testing.T) {
|
||||||
|
c := &containerStats{
|
||||||
|
Name: "app",
|
||||||
|
CPUPercentage: 30.0,
|
||||||
|
Memory: 100 * 1024 * 1024.0,
|
||||||
|
MemoryLimit: 2048 * 1024 * 1024.0,
|
||||||
|
MemoryPercentage: 100.0 / 2048.0 * 100.0,
|
||||||
|
NetworkRx: 100 * 1024 * 1024,
|
||||||
|
NetworkTx: 800 * 1024 * 1024,
|
||||||
|
BlockRead: 100 * 1024 * 1024,
|
||||||
|
BlockWrite: 800 * 1024 * 1024,
|
||||||
|
PidsCurrent: 1,
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := c.Display(&b); err != nil {
|
||||||
|
t.Fatalf("c.Display() gave error: %s", err)
|
||||||
|
}
|
||||||
|
got := b.String()
|
||||||
|
want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\t1\n"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("c.Display() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculBlockIO(t *testing.T) {
|
||||||
|
blkio := types.BlkioStats{
|
||||||
|
IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}},
|
||||||
|
}
|
||||||
|
blkRead, blkWrite := calculateBlockIO(blkio)
|
||||||
|
if blkRead != 5801 {
|
||||||
|
t.Fatalf("blkRead = %d, want 5801", blkRead)
|
||||||
|
}
|
||||||
|
if blkWrite != 579 {
|
||||||
|
t.Fatalf("blkWrite = %d, want 579", blkWrite)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stopOptions struct {
|
||||||
|
time int
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStopCommand creates a new cobra.Command for `docker stop`
|
||||||
|
func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts stopOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "stop [OPTIONS] CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Stop one or more running containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runStop(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.IntVarP(&opts.time, "time", "t", 10, "Seconds to wait for stop before killing it")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStop(dockerCli *command.DockerCli, opts *stopOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, container := range opts.containers {
|
||||||
|
timeout := time.Duration(opts.time) * time.Second
|
||||||
|
if err := dockerCli.Client().ContainerStop(ctx, container, &timeout); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type topOptions struct {
|
||||||
|
container string
|
||||||
|
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTopCommand creates a new cobra.Command for `docker top`
|
||||||
|
func NewTopCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts topOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "top CONTAINER [ps OPTIONS]",
|
||||||
|
Short: "Display the running processes of a container",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.container = args[0]
|
||||||
|
opts.args = args[1:]
|
||||||
|
return runTop(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTop(dockerCli *command.DockerCli, opts *topOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||||
|
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))
|
||||||
|
|
||||||
|
for _, proc := range procList.Processes {
|
||||||
|
fmt.Fprintln(w, strings.Join(proc, "\t"))
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
gosignal "os/signal"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/signal"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resizeTtyTo resizes tty to specific height and width
|
||||||
|
func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width int, isExec bool) {
|
||||||
|
if height == 0 && width == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ResizeOptions{
|
||||||
|
Height: height,
|
||||||
|
Width: width,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if isExec {
|
||||||
|
err = client.ContainerExecResize(ctx, id, options)
|
||||||
|
} else {
|
||||||
|
err = client.ContainerResize(ctx, id, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Error resize: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorTtySize updates the container tty size when the terminal tty changes size
|
||||||
|
func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isExec bool) error {
|
||||||
|
resizeTty := func() {
|
||||||
|
height, width := cli.Out().GetTtySize()
|
||||||
|
resizeTtyTo(ctx, cli.Client(), id, height, width, isExec)
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeTty()
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
go func() {
|
||||||
|
prevH, prevW := cli.Out().GetTtySize()
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Millisecond * 250)
|
||||||
|
h, w := cli.Out().GetTtySize()
|
||||||
|
|
||||||
|
if prevW != w || prevH != h {
|
||||||
|
resizeTty()
|
||||||
|
}
|
||||||
|
prevH = h
|
||||||
|
prevW = w
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
sigchan := make(chan os.Signal, 1)
|
||||||
|
gosignal.Notify(sigchan, signal.SIGWINCH)
|
||||||
|
go func() {
|
||||||
|
for range sigchan {
|
||||||
|
resizeTty()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardAllSignals forwards signals to the container
|
||||||
|
func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal {
|
||||||
|
sigc := make(chan os.Signal, 128)
|
||||||
|
signal.CatchAll(sigc)
|
||||||
|
go func() {
|
||||||
|
for s := range sigc {
|
||||||
|
if s == signal.SIGCHLD || s == signal.SIGPIPE {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var sig string
|
||||||
|
for sigStr, sigN := range signal.SignalMap {
|
||||||
|
if sigN == s {
|
||||||
|
sig = sigStr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sig == "" {
|
||||||
|
fmt.Fprintf(cli.Err(), "Unsupported signal: %v. Discarding.\n", s)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil {
|
||||||
|
logrus.Debugf("Error sending signal: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return sigc
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type unpauseOptions struct {
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnpauseCommand creates a new cobra.Command for `docker unpause`
|
||||||
|
func NewUnpauseCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts unpauseOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "unpause CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Unpause all processes within one or more containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runUnpause(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, container := range opts.containers {
|
||||||
|
if err := dockerCli.Client().ContainerUnpause(ctx, container); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateOptions struct {
|
||||||
|
blkioWeight uint16
|
||||||
|
cpuPeriod int64
|
||||||
|
cpuQuota int64
|
||||||
|
cpusetCpus string
|
||||||
|
cpusetMems string
|
||||||
|
cpuShares int64
|
||||||
|
memoryString string
|
||||||
|
memoryReservation string
|
||||||
|
memorySwap string
|
||||||
|
kernelMemory string
|
||||||
|
restartPolicy string
|
||||||
|
|
||||||
|
nFlag int
|
||||||
|
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUpdateCommand creates a new cobra.Command for `docker update`
|
||||||
|
func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts updateOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update [OPTIONS] CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Update configuration of one or more containers",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
opts.nFlag = cmd.Flags().NFlag()
|
||||||
|
return runUpdate(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000")
|
||||||
|
flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period")
|
||||||
|
flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota")
|
||||||
|
flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
|
||||||
|
flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
|
||||||
|
flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
|
||||||
|
flags.StringVarP(&opts.memoryString, "memory", "m", "", "Memory limit")
|
||||||
|
flags.StringVar(&opts.memoryReservation, "memory-reservation", "", "Memory soft limit")
|
||||||
|
flags.StringVar(&opts.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
|
||||||
|
flags.StringVar(&opts.kernelMemory, "kernel-memory", "", "Kernel memory limit")
|
||||||
|
flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if opts.nFlag == 0 {
|
||||||
|
return fmt.Errorf("You must provide one or more flags when using this command.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var memory int64
|
||||||
|
if opts.memoryString != "" {
|
||||||
|
memory, err = units.RAMInBytes(opts.memoryString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var memoryReservation int64
|
||||||
|
if opts.memoryReservation != "" {
|
||||||
|
memoryReservation, err = units.RAMInBytes(opts.memoryReservation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var memorySwap int64
|
||||||
|
if opts.memorySwap != "" {
|
||||||
|
if opts.memorySwap == "-1" {
|
||||||
|
memorySwap = -1
|
||||||
|
} else {
|
||||||
|
memorySwap, err = units.RAMInBytes(opts.memorySwap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var kernelMemory int64
|
||||||
|
if opts.kernelMemory != "" {
|
||||||
|
kernelMemory, err = units.RAMInBytes(opts.kernelMemory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var restartPolicy containertypes.RestartPolicy
|
||||||
|
if opts.restartPolicy != "" {
|
||||||
|
restartPolicy, err = runconfigopts.ParseRestartPolicy(opts.restartPolicy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := containertypes.Resources{
|
||||||
|
BlkioWeight: opts.blkioWeight,
|
||||||
|
CpusetCpus: opts.cpusetCpus,
|
||||||
|
CpusetMems: opts.cpusetMems,
|
||||||
|
CPUShares: opts.cpuShares,
|
||||||
|
Memory: memory,
|
||||||
|
MemoryReservation: memoryReservation,
|
||||||
|
MemorySwap: memorySwap,
|
||||||
|
KernelMemory: kernelMemory,
|
||||||
|
CPUPeriod: opts.cpuPeriod,
|
||||||
|
CPUQuota: opts.cpuQuota,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig := containertypes.UpdateConfig{
|
||||||
|
Resources: resources,
|
||||||
|
RestartPolicy: restartPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var (
|
||||||
|
warns []string
|
||||||
|
errs []string
|
||||||
|
)
|
||||||
|
for _, container := range opts.containers {
|
||||||
|
r, err := dockerCli.Client().ContainerUpdate(ctx, container, updateConfig)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||||
|
}
|
||||||
|
warns = append(warns, r.Warnings...)
|
||||||
|
}
|
||||||
|
if len(warns) > 0 {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s", strings.Join(warns, "\n"))
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/events"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/system"
|
||||||
|
clientapi "github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, containerID string, waitRemove bool) (chan int, error) {
|
||||||
|
if len(containerID) == 0 {
|
||||||
|
// containerID can never be empty
|
||||||
|
panic("Internal Error: waitExitOrRemoved needs a containerID as parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusChan := make(chan int)
|
||||||
|
exitCode := 125
|
||||||
|
|
||||||
|
eventProcessor := func(e events.Message, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
statusChan <- exitCode
|
||||||
|
return fmt.Errorf("failed to decode event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopProcessing := false
|
||||||
|
switch e.Status {
|
||||||
|
case "die":
|
||||||
|
if v, ok := e.Actor.Attributes["exitCode"]; ok {
|
||||||
|
code, cerr := strconv.Atoi(v)
|
||||||
|
if cerr != nil {
|
||||||
|
logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr)
|
||||||
|
} else {
|
||||||
|
exitCode = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !waitRemove {
|
||||||
|
stopProcessing = true
|
||||||
|
}
|
||||||
|
case "detach":
|
||||||
|
exitCode = 0
|
||||||
|
stopProcessing = true
|
||||||
|
case "destroy":
|
||||||
|
stopProcessing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if stopProcessing {
|
||||||
|
statusChan <- exitCode
|
||||||
|
// stop the loop processing
|
||||||
|
return fmt.Errorf("done")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get events via Events API
|
||||||
|
f := filters.NewArgs()
|
||||||
|
f.Add("type", "container")
|
||||||
|
f.Add("container", containerID)
|
||||||
|
options := types.EventsOptions{
|
||||||
|
Filters: f,
|
||||||
|
}
|
||||||
|
resBody, err := dockerCli.Client().Events(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't get events from daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go system.DecodeEvents(resBody, eventProcessor)
|
||||||
|
|
||||||
|
return statusChan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExitCode performs an inspect on the container. It returns
|
||||||
|
// the running state and the exit code.
|
||||||
|
func getExitCode(dockerCli *command.DockerCli, ctx context.Context, containerID string) (bool, int, error) {
|
||||||
|
c, err := dockerCli.Client().ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't connect, then the daemon probably died.
|
||||||
|
if err != clientapi.ErrConnectionFailed {
|
||||||
|
return false, -1, err
|
||||||
|
}
|
||||||
|
return false, -1, nil
|
||||||
|
}
|
||||||
|
return c.State.Running, c.State.ExitCode, nil
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type waitOptions struct {
|
||||||
|
containers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWaitCommand creates a new cobra.Command for `docker wait`
|
||||||
|
func NewWaitCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts waitOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "wait CONTAINER [CONTAINER...]",
|
||||||
|
Short: "Block until one or more containers stop, then print their exit codes",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.containers = args
|
||||||
|
return runWait(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWait(dockerCli *command.DockerCli, opts *waitOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, container := range opts.containers {
|
||||||
|
status, err := dockerCli.Client().ContainerWait(ctx, container)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%d\n", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cliconfig/configfile"
|
||||||
|
"github.com/docker/docker/cliconfig/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCredentials loads the user credentials from a credentials store.
|
||||||
|
// The store is determined by the config file settings.
|
||||||
|
func GetCredentials(c *configfile.ConfigFile, serverAddress string) (types.AuthConfig, error) {
|
||||||
|
s := LoadCredentialsStore(c)
|
||||||
|
return s.Get(serverAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCredentials loads all credentials from a credentials store.
|
||||||
|
// The store is determined by the config file settings.
|
||||||
|
func GetAllCredentials(c *configfile.ConfigFile) (map[string]types.AuthConfig, error) {
|
||||||
|
s := LoadCredentialsStore(c)
|
||||||
|
return s.GetAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreCredentials saves the user credentials in a credentials store.
|
||||||
|
// The store is determined by the config file settings.
|
||||||
|
func StoreCredentials(c *configfile.ConfigFile, auth types.AuthConfig) error {
|
||||||
|
s := LoadCredentialsStore(c)
|
||||||
|
return s.Store(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EraseCredentials removes the user credentials from a credentials store.
|
||||||
|
// The store is determined by the config file settings.
|
||||||
|
func EraseCredentials(c *configfile.ConfigFile, serverAddress string) error {
|
||||||
|
s := LoadCredentialsStore(c)
|
||||||
|
return s.Erase(serverAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCredentialsStore initializes a new credentials store based
|
||||||
|
// in the settings provided in the configuration file.
|
||||||
|
func LoadCredentialsStore(c *configfile.ConfigFile) credentials.Store {
|
||||||
|
if c.CredentialsStore != "" {
|
||||||
|
return credentials.NewNativeStore(c)
|
||||||
|
}
|
||||||
|
return credentials.NewFileStore(c)
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/docker/pkg/stringutils"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
||||||
|
|
||||||
|
containerIDHeader = "CONTAINER ID"
|
||||||
|
namesHeader = "NAMES"
|
||||||
|
commandHeader = "COMMAND"
|
||||||
|
runningForHeader = "CREATED"
|
||||||
|
statusHeader = "STATUS"
|
||||||
|
portsHeader = "PORTS"
|
||||||
|
mountsHeader = "MOUNTS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
|
||||||
|
type ContainerContext struct {
|
||||||
|
Context
|
||||||
|
// Size when set to true will display the size of the output.
|
||||||
|
Size bool
|
||||||
|
// Containers
|
||||||
|
Containers []types.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx ContainerContext) Write() {
|
||||||
|
switch ctx.Format {
|
||||||
|
case tableFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = defaultQuietFormat
|
||||||
|
} else {
|
||||||
|
ctx.Format = defaultContainerTableFormat
|
||||||
|
if ctx.Size {
|
||||||
|
ctx.Format += `\t{{.Size}}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case rawFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = `container_id: {{.ID}}`
|
||||||
|
} else {
|
||||||
|
ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
||||||
|
if ctx.Size {
|
||||||
|
ctx.Format += `size: {{.Size}}\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.buffer = bytes.NewBufferString("")
|
||||||
|
ctx.preformat()
|
||||||
|
|
||||||
|
tmpl, err := ctx.parseFormat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range ctx.Containers {
|
||||||
|
containerCtx := &containerContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
c: container,
|
||||||
|
}
|
||||||
|
err = ctx.contextFormat(tmpl, containerCtx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.postformat(tmpl, &containerContext{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type containerContext struct {
|
||||||
|
baseSubContext
|
||||||
|
trunc bool
|
||||||
|
c types.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) ID() string {
|
||||||
|
c.addHeader(containerIDHeader)
|
||||||
|
if c.trunc {
|
||||||
|
return stringid.TruncateID(c.c.ID)
|
||||||
|
}
|
||||||
|
return c.c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Names() string {
|
||||||
|
c.addHeader(namesHeader)
|
||||||
|
names := stripNamePrefix(c.c.Names)
|
||||||
|
if c.trunc {
|
||||||
|
for _, name := range names {
|
||||||
|
if len(strings.Split(name, "/")) == 1 {
|
||||||
|
names = []string{name}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(names, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Image() string {
|
||||||
|
c.addHeader(imageHeader)
|
||||||
|
if c.c.Image == "" {
|
||||||
|
return "<no image>"
|
||||||
|
}
|
||||||
|
if c.trunc {
|
||||||
|
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
||||||
|
return trunc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.c.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Command() string {
|
||||||
|
c.addHeader(commandHeader)
|
||||||
|
command := c.c.Command
|
||||||
|
if c.trunc {
|
||||||
|
command = stringutils.Ellipsis(command, 20)
|
||||||
|
}
|
||||||
|
return strconv.Quote(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) CreatedAt() string {
|
||||||
|
c.addHeader(createdAtHeader)
|
||||||
|
return time.Unix(int64(c.c.Created), 0).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) RunningFor() string {
|
||||||
|
c.addHeader(runningForHeader)
|
||||||
|
createdAt := time.Unix(int64(c.c.Created), 0)
|
||||||
|
return units.HumanDuration(time.Now().UTC().Sub(createdAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Ports() string {
|
||||||
|
c.addHeader(portsHeader)
|
||||||
|
return api.DisplayablePorts(c.c.Ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Status() string {
|
||||||
|
c.addHeader(statusHeader)
|
||||||
|
return c.c.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Size() string {
|
||||||
|
c.addHeader(sizeHeader)
|
||||||
|
srw := units.HumanSize(float64(c.c.SizeRw))
|
||||||
|
sv := units.HumanSize(float64(c.c.SizeRootFs))
|
||||||
|
|
||||||
|
sf := srw
|
||||||
|
if c.c.SizeRootFs > 0 {
|
||||||
|
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
||||||
|
}
|
||||||
|
return sf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Labels() string {
|
||||||
|
c.addHeader(labelsHeader)
|
||||||
|
if c.c.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var joinLabels []string
|
||||||
|
for k, v := range c.c.Labels {
|
||||||
|
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
return strings.Join(joinLabels, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Label(name string) string {
|
||||||
|
n := strings.Split(name, ".")
|
||||||
|
r := strings.NewReplacer("-", " ", "_", " ")
|
||||||
|
h := r.Replace(n[len(n)-1])
|
||||||
|
|
||||||
|
c.addHeader(h)
|
||||||
|
|
||||||
|
if c.c.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.c.Labels[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *containerContext) Mounts() string {
|
||||||
|
c.addHeader(mountsHeader)
|
||||||
|
|
||||||
|
var name string
|
||||||
|
var mounts []string
|
||||||
|
for _, m := range c.c.Mounts {
|
||||||
|
if m.Name == "" {
|
||||||
|
name = m.Source
|
||||||
|
} else {
|
||||||
|
name = m.Name
|
||||||
|
}
|
||||||
|
if c.trunc {
|
||||||
|
name = stringutils.Ellipsis(name, 15)
|
||||||
|
}
|
||||||
|
mounts = append(mounts, name)
|
||||||
|
}
|
||||||
|
return strings.Join(mounts, ",")
|
||||||
|
}
|
|
@ -0,0 +1,404 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainerPsContext(t *testing.T) {
|
||||||
|
containerID := stringid.GenerateRandomID()
|
||||||
|
unix := time.Now().Add(-65 * time.Second).Unix()
|
||||||
|
|
||||||
|
var ctx containerContext
|
||||||
|
cases := []struct {
|
||||||
|
container types.Container
|
||||||
|
trunc bool
|
||||||
|
expValue string
|
||||||
|
expHeader string
|
||||||
|
call func() string
|
||||||
|
}{
|
||||||
|
{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
|
||||||
|
{types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
|
||||||
|
{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
|
||||||
|
{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
|
||||||
|
{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
|
||||||
|
{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
|
||||||
|
{types.Container{
|
||||||
|
Image: "a5a665ff33eced1e0803148700880edab4",
|
||||||
|
ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
"a5a665ff33ec",
|
||||||
|
imageHeader,
|
||||||
|
ctx.Image,
|
||||||
|
},
|
||||||
|
{types.Container{
|
||||||
|
Image: "a5a665ff33eced1e0803148700880edab4",
|
||||||
|
ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
"a5a665ff33eced1e0803148700880edab4",
|
||||||
|
imageHeader,
|
||||||
|
ctx.Image,
|
||||||
|
},
|
||||||
|
{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
|
||||||
|
{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
|
||||||
|
{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
||||||
|
{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
|
||||||
|
{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
|
||||||
|
{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
|
||||||
|
{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
|
||||||
|
{types.Container{}, true, "", labelsHeader, ctx.Labels},
|
||||||
|
{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
|
||||||
|
{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
|
||||||
|
{types.Container{
|
||||||
|
Mounts: []types.MountPoint{
|
||||||
|
{
|
||||||
|
Name: "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set",
|
||||||
|
Driver: "local",
|
||||||
|
Source: "/a/path",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, true, "this-is-a-lo...", mountsHeader, ctx.Mounts},
|
||||||
|
{types.Container{
|
||||||
|
Mounts: []types.MountPoint{
|
||||||
|
{
|
||||||
|
Driver: "local",
|
||||||
|
Source: "/a/path",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, false, "/a/path", mountsHeader, ctx.Mounts},
|
||||||
|
{types.Container{
|
||||||
|
Mounts: []types.MountPoint{
|
||||||
|
{
|
||||||
|
Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
|
||||||
|
Driver: "local",
|
||||||
|
Source: "/a/path",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
ctx = containerContext{c: c.container, trunc: c.trunc}
|
||||||
|
v := c.call()
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
compareMultipleValues(t, v, c.expValue)
|
||||||
|
} else if v != c.expValue {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ctx.fullHeader()
|
||||||
|
if h != c.expHeader {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
||||||
|
ctx = containerContext{c: c1, trunc: true}
|
||||||
|
|
||||||
|
sid := ctx.Label("com.docker.swarm.swarm-id")
|
||||||
|
node := ctx.Label("com.docker.swarm.node_name")
|
||||||
|
if sid != "33" {
|
||||||
|
t.Fatalf("Expected 33, was %s\n", sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if node != "ubuntu" {
|
||||||
|
t.Fatalf("Expected ubuntu, was %s\n", node)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ctx.fullHeader()
|
||||||
|
if h != "SWARM ID\tNODE NAME" {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
c2 := types.Container{}
|
||||||
|
ctx = containerContext{c: c2, trunc: true}
|
||||||
|
|
||||||
|
label := ctx.Label("anything.really")
|
||||||
|
if label != "" {
|
||||||
|
t.Fatalf("Expected an empty string, was %s", label)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = containerContext{c: c2, trunc: true}
|
||||||
|
fullHeader := ctx.fullHeader()
|
||||||
|
if fullHeader != "" {
|
||||||
|
t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerContextWrite(t *testing.T) {
|
||||||
|
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
||||||
|
expectedTime := time.Unix(unixTime, 0).String()
|
||||||
|
|
||||||
|
contexts := []struct {
|
||||||
|
context ContainerContext
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Errors
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{InvalidFunction}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{nil}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Table Format
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE
|
||||||
|
containerID1 ubuntu "" 24 hours ago foobar_baz 0 B
|
||||||
|
containerID2 ubuntu "" 24 hours ago foobar_bar 0 B
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
|
containerID1 ubuntu "" 24 hours ago foobar_baz
|
||||||
|
containerID2 ubuntu "" 24 hours ago foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"IMAGE\nubuntu\nubuntu\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}",
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
"IMAGE\nubuntu\nubuntu\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"IMAGE\nubuntu\nubuntu\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"containerID1\ncontainerID2\n",
|
||||||
|
},
|
||||||
|
// Raw Format
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fmt.Sprintf(`container_id: containerID1
|
||||||
|
image: ubuntu
|
||||||
|
command: ""
|
||||||
|
created_at: %s
|
||||||
|
status:
|
||||||
|
names: foobar_baz
|
||||||
|
labels:
|
||||||
|
ports:
|
||||||
|
|
||||||
|
container_id: containerID2
|
||||||
|
image: ubuntu
|
||||||
|
command: ""
|
||||||
|
created_at: %s
|
||||||
|
status:
|
||||||
|
names: foobar_bar
|
||||||
|
labels:
|
||||||
|
ports:
|
||||||
|
|
||||||
|
`, expectedTime, expectedTime),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
fmt.Sprintf(`container_id: containerID1
|
||||||
|
image: ubuntu
|
||||||
|
command: ""
|
||||||
|
created_at: %s
|
||||||
|
status:
|
||||||
|
names: foobar_baz
|
||||||
|
labels:
|
||||||
|
ports:
|
||||||
|
size: 0 B
|
||||||
|
|
||||||
|
container_id: containerID2
|
||||||
|
image: ubuntu
|
||||||
|
command: ""
|
||||||
|
created_at: %s
|
||||||
|
status:
|
||||||
|
names: foobar_bar
|
||||||
|
labels:
|
||||||
|
ports:
|
||||||
|
size: 0 B
|
||||||
|
|
||||||
|
`, expectedTime, expectedTime),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"container_id: containerID1\ncontainer_id: containerID2\n",
|
||||||
|
},
|
||||||
|
// Custom Format
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Image}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ubuntu\nubuntu\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Image}}",
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
"ubuntu\nubuntu\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, context := range contexts {
|
||||||
|
containers := []types.Container{
|
||||||
|
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
||||||
|
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
context.context.Output = out
|
||||||
|
context.context.Containers = containers
|
||||||
|
context.context.Write()
|
||||||
|
actual := out.String()
|
||||||
|
if actual != context.expected {
|
||||||
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||||
|
}
|
||||||
|
// Clean buffer
|
||||||
|
out.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
containers := []types.Container{}
|
||||||
|
|
||||||
|
contexts := []struct {
|
||||||
|
context ContainerContext
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Image}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"IMAGE\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Image}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
"IMAGE\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}\t{{.Size}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"IMAGE SIZE\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContainerContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Image}}\t{{.Size}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
Size: true,
|
||||||
|
},
|
||||||
|
"IMAGE SIZE\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, context := range contexts {
|
||||||
|
context.context.Containers = containers
|
||||||
|
context.context.Write()
|
||||||
|
actual := out.String()
|
||||||
|
if actual != context.expected {
|
||||||
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||||
|
}
|
||||||
|
// Clean buffer
|
||||||
|
out.Reset()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tableKey = "table"
|
||||||
|
|
||||||
|
imageHeader = "IMAGE"
|
||||||
|
createdSinceHeader = "CREATED"
|
||||||
|
createdAtHeader = "CREATED AT"
|
||||||
|
sizeHeader = "SIZE"
|
||||||
|
labelsHeader = "LABELS"
|
||||||
|
nameHeader = "NAME"
|
||||||
|
driverHeader = "DRIVER"
|
||||||
|
scopeHeader = "SCOPE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type subContext interface {
|
||||||
|
fullHeader() string
|
||||||
|
addHeader(header string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseSubContext struct {
|
||||||
|
header []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *baseSubContext) fullHeader() string {
|
||||||
|
if c.header == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(c.header, "\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *baseSubContext) addHeader(header string) {
|
||||||
|
if c.header == nil {
|
||||||
|
c.header = []string{}
|
||||||
|
}
|
||||||
|
c.header = append(c.header, strings.ToUpper(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripNamePrefix(ss []string) []string {
|
||||||
|
sss := make([]string, len(ss))
|
||||||
|
for i, s := range ss {
|
||||||
|
sss[i] = s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return sss
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func compareMultipleValues(t *testing.T, value, expected string) {
|
||||||
|
// comma-separated values means probably a map input, which won't
|
||||||
|
// be guaranteed to have the same order as our expected value
|
||||||
|
// We'll create maps and use reflect.DeepEquals to check instead:
|
||||||
|
entriesMap := make(map[string]string)
|
||||||
|
expMap := make(map[string]string)
|
||||||
|
entries := strings.Split(value, ",")
|
||||||
|
expectedEntries := strings.Split(expected, ",")
|
||||||
|
for _, entry := range entries {
|
||||||
|
keyval := strings.Split(entry, "=")
|
||||||
|
entriesMap[keyval[0]] = keyval[1]
|
||||||
|
}
|
||||||
|
for _, expected := range expectedEntries {
|
||||||
|
keyval := strings.Split(expected, "=")
|
||||||
|
expMap[keyval[0]] = keyval[1]
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expMap, entriesMap) {
|
||||||
|
t.Fatalf("Expected entries: %v, got: %v", expected, value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/docker/docker/utils/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tableFormatKey = "table"
|
||||||
|
rawFormatKey = "raw"
|
||||||
|
|
||||||
|
defaultQuietFormat = "{{.ID}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context contains information required by the formatter to print the output as desired.
|
||||||
|
type Context struct {
|
||||||
|
// Output is the output stream to which the formatted string is written.
|
||||||
|
Output io.Writer
|
||||||
|
// Format is used to choose raw, table or custom format for the output.
|
||||||
|
Format string
|
||||||
|
// Quiet when set to true will simply print minimal information.
|
||||||
|
Quiet bool
|
||||||
|
// Trunc when set to true will truncate the output of certain fields such as Container ID.
|
||||||
|
Trunc bool
|
||||||
|
|
||||||
|
// internal element
|
||||||
|
table bool
|
||||||
|
finalFormat string
|
||||||
|
header string
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) preformat() {
|
||||||
|
c.finalFormat = c.Format
|
||||||
|
|
||||||
|
if strings.HasPrefix(c.Format, tableKey) {
|
||||||
|
c.table = true
|
||||||
|
c.finalFormat = c.finalFormat[len(tableKey):]
|
||||||
|
}
|
||||||
|
|
||||||
|
c.finalFormat = strings.Trim(c.finalFormat, " ")
|
||||||
|
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
|
||||||
|
c.finalFormat = r.Replace(c.finalFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) parseFormat() (*template.Template, error) {
|
||||||
|
tmpl, err := templates.Parse(c.finalFormat)
|
||||||
|
if err != nil {
|
||||||
|
c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
||||||
|
c.buffer.WriteTo(c.Output)
|
||||||
|
}
|
||||||
|
return tmpl, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
||||||
|
if c.table {
|
||||||
|
if len(c.header) == 0 {
|
||||||
|
// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
|
||||||
|
tmpl.Execute(bytes.NewBufferString(""), subContext)
|
||||||
|
c.header = subContext.fullHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
|
||||||
|
t.Write([]byte(c.header))
|
||||||
|
t.Write([]byte("\n"))
|
||||||
|
c.buffer.WriteTo(t)
|
||||||
|
t.Flush()
|
||||||
|
} else {
|
||||||
|
c.buffer.WriteTo(c.Output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
|
||||||
|
if err := tmpl.Execute(c.buffer, subContext); err != nil {
|
||||||
|
c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
||||||
|
c.buffer.WriteTo(c.Output)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.table && len(c.header) == 0 {
|
||||||
|
c.header = subContext.fullHeader()
|
||||||
|
}
|
||||||
|
c.buffer.WriteString("\n")
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
||||||
|
defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
||||||
|
|
||||||
|
imageIDHeader = "IMAGE ID"
|
||||||
|
repositoryHeader = "REPOSITORY"
|
||||||
|
tagHeader = "TAG"
|
||||||
|
digestHeader = "DIGEST"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
|
||||||
|
type ImageContext struct {
|
||||||
|
Context
|
||||||
|
Digest bool
|
||||||
|
// Images
|
||||||
|
Images []types.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDangling(image types.Image) bool {
|
||||||
|
return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx ImageContext) Write() {
|
||||||
|
switch ctx.Format {
|
||||||
|
case tableFormatKey:
|
||||||
|
ctx.Format = defaultImageTableFormat
|
||||||
|
if ctx.Digest {
|
||||||
|
ctx.Format = defaultImageTableFormatWithDigest
|
||||||
|
}
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = defaultQuietFormat
|
||||||
|
}
|
||||||
|
case rawFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = `image_id: {{.ID}}`
|
||||||
|
} else {
|
||||||
|
if ctx.Digest {
|
||||||
|
ctx.Format = `repository: {{ .Repository }}
|
||||||
|
tag: {{.Tag}}
|
||||||
|
digest: {{.Digest}}
|
||||||
|
image_id: {{.ID}}
|
||||||
|
created_at: {{.CreatedAt}}
|
||||||
|
virtual_size: {{.Size}}
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
ctx.Format = `repository: {{ .Repository }}
|
||||||
|
tag: {{.Tag}}
|
||||||
|
image_id: {{.ID}}
|
||||||
|
created_at: {{.CreatedAt}}
|
||||||
|
virtual_size: {{.Size}}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.buffer = bytes.NewBufferString("")
|
||||||
|
ctx.preformat()
|
||||||
|
if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
||||||
|
ctx.finalFormat += "\t{{.Digest}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := ctx.parseFormat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, image := range ctx.Images {
|
||||||
|
images := []*imageContext{}
|
||||||
|
if isDangling(image) {
|
||||||
|
images = append(images, &imageContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
i: image,
|
||||||
|
repo: "<none>",
|
||||||
|
tag: "<none>",
|
||||||
|
digest: "<none>",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
repoTags := map[string][]string{}
|
||||||
|
repoDigests := map[string][]string{}
|
||||||
|
|
||||||
|
for _, refString := range append(image.RepoTags) {
|
||||||
|
ref, err := reference.ParseNamed(refString)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nt, ok := ref.(reference.NamedTagged); ok {
|
||||||
|
repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, refString := range append(image.RepoDigests) {
|
||||||
|
ref, err := reference.ParseNamed(refString)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c, ok := ref.(reference.Canonical); ok {
|
||||||
|
repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for repo, tags := range repoTags {
|
||||||
|
digests := repoDigests[repo]
|
||||||
|
|
||||||
|
// Do not display digests as their own row
|
||||||
|
delete(repoDigests, repo)
|
||||||
|
|
||||||
|
if !ctx.Digest {
|
||||||
|
// Ignore digest references, just show tag once
|
||||||
|
digests = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
if len(digests) == 0 {
|
||||||
|
images = append(images, &imageContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
i: image,
|
||||||
|
repo: repo,
|
||||||
|
tag: tag,
|
||||||
|
digest: "<none>",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Display the digests for each tag
|
||||||
|
for _, dgst := range digests {
|
||||||
|
images = append(images, &imageContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
i: image,
|
||||||
|
repo: repo,
|
||||||
|
tag: tag,
|
||||||
|
digest: dgst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show rows for remaining digest only references
|
||||||
|
for repo, digests := range repoDigests {
|
||||||
|
// If digests are displayed, show row per digest
|
||||||
|
if ctx.Digest {
|
||||||
|
for _, dgst := range digests {
|
||||||
|
images = append(images, &imageContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
i: image,
|
||||||
|
repo: repo,
|
||||||
|
tag: "<none>",
|
||||||
|
digest: dgst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
images = append(images, &imageContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
i: image,
|
||||||
|
repo: repo,
|
||||||
|
tag: "<none>",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, imageCtx := range images {
|
||||||
|
err = ctx.contextFormat(tmpl, imageCtx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.postformat(tmpl, &imageContext{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageContext struct {
|
||||||
|
baseSubContext
|
||||||
|
trunc bool
|
||||||
|
i types.Image
|
||||||
|
repo string
|
||||||
|
tag string
|
||||||
|
digest string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) ID() string {
|
||||||
|
c.addHeader(imageIDHeader)
|
||||||
|
if c.trunc {
|
||||||
|
return stringid.TruncateID(c.i.ID)
|
||||||
|
}
|
||||||
|
return c.i.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) Repository() string {
|
||||||
|
c.addHeader(repositoryHeader)
|
||||||
|
return c.repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) Tag() string {
|
||||||
|
c.addHeader(tagHeader)
|
||||||
|
return c.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) Digest() string {
|
||||||
|
c.addHeader(digestHeader)
|
||||||
|
return c.digest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) CreatedSince() string {
|
||||||
|
c.addHeader(createdSinceHeader)
|
||||||
|
createdAt := time.Unix(int64(c.i.Created), 0)
|
||||||
|
return units.HumanDuration(time.Now().UTC().Sub(createdAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) CreatedAt() string {
|
||||||
|
c.addHeader(createdAtHeader)
|
||||||
|
return time.Unix(int64(c.i.Created), 0).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *imageContext) Size() string {
|
||||||
|
c.addHeader(sizeHeader)
|
||||||
|
return units.HumanSize(float64(c.i.Size))
|
||||||
|
}
|
|
@ -0,0 +1,345 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestImageContext(t *testing.T) {
|
||||||
|
imageID := stringid.GenerateRandomID()
|
||||||
|
unix := time.Now().Unix()
|
||||||
|
|
||||||
|
var ctx imageContext
|
||||||
|
cases := []struct {
|
||||||
|
imageCtx imageContext
|
||||||
|
expValue string
|
||||||
|
expHeader string
|
||||||
|
call func() string
|
||||||
|
}{
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{ID: imageID},
|
||||||
|
trunc: true,
|
||||||
|
}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{ID: imageID},
|
||||||
|
trunc: false,
|
||||||
|
}, imageID, imageIDHeader, ctx.ID},
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{Size: 10},
|
||||||
|
trunc: true,
|
||||||
|
}, "10 B", sizeHeader, ctx.Size},
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{Created: unix},
|
||||||
|
trunc: true,
|
||||||
|
}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
||||||
|
// FIXME
|
||||||
|
// {imageContext{
|
||||||
|
// i: types.Image{Created: unix},
|
||||||
|
// trunc: true,
|
||||||
|
// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{},
|
||||||
|
repo: "busybox",
|
||||||
|
}, "busybox", repositoryHeader, ctx.Repository},
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{},
|
||||||
|
tag: "latest",
|
||||||
|
}, "latest", tagHeader, ctx.Tag},
|
||||||
|
{imageContext{
|
||||||
|
i: types.Image{},
|
||||||
|
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
|
||||||
|
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
ctx = c.imageCtx
|
||||||
|
v := c.call()
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
compareMultipleValues(t, v, c.expValue)
|
||||||
|
} else if v != c.expValue {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ctx.fullHeader()
|
||||||
|
if h != c.expHeader {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageContextWrite(t *testing.T) {
|
||||||
|
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
||||||
|
expectedTime := time.Unix(unixTime, 0).String()
|
||||||
|
|
||||||
|
contexts := []struct {
|
||||||
|
context ImageContext
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
// Errors
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{InvalidFunction}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{nil}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Table Format
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||||
|
image tag1 imageID1 24 hours ago 0 B
|
||||||
|
image tag2 imageID2 24 hours ago 0 B
|
||||||
|
<none> <none> imageID3 24 hours ago 0 B
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Repository}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"REPOSITORY\nimage\nimage\n<none>\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Repository}}",
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
`REPOSITORY DIGEST
|
||||||
|
image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
||||||
|
image <none>
|
||||||
|
<none> <none>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Repository}}",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"REPOSITORY\nimage\nimage\n<none>\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"imageID1\nimageID2\nimageID3\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
Quiet: false,
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
`REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
|
||||||
|
image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B
|
||||||
|
image tag2 <none> imageID2 24 hours ago 0 B
|
||||||
|
<none> <none> <none> imageID3 24 hours ago 0 B
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
"imageID1\nimageID2\nimageID3\n",
|
||||||
|
},
|
||||||
|
// Raw Format
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fmt.Sprintf(`repository: image
|
||||||
|
tag: tag1
|
||||||
|
image_id: imageID1
|
||||||
|
created_at: %s
|
||||||
|
virtual_size: 0 B
|
||||||
|
|
||||||
|
repository: image
|
||||||
|
tag: tag2
|
||||||
|
image_id: imageID2
|
||||||
|
created_at: %s
|
||||||
|
virtual_size: 0 B
|
||||||
|
|
||||||
|
repository: <none>
|
||||||
|
tag: <none>
|
||||||
|
image_id: imageID3
|
||||||
|
created_at: %s
|
||||||
|
virtual_size: 0 B
|
||||||
|
|
||||||
|
`, expectedTime, expectedTime, expectedTime),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
fmt.Sprintf(`repository: image
|
||||||
|
tag: tag1
|
||||||
|
digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
||||||
|
image_id: imageID1
|
||||||
|
created_at: %s
|
||||||
|
virtual_size: 0 B
|
||||||
|
|
||||||
|
repository: image
|
||||||
|
tag: tag2
|
||||||
|
digest: <none>
|
||||||
|
image_id: imageID2
|
||||||
|
created_at: %s
|
||||||
|
virtual_size: 0 B
|
||||||
|
|
||||||
|
repository: <none>
|
||||||
|
tag: <none>
|
||||||
|
digest: <none>
|
||||||
|
image_id: imageID3
|
||||||
|
created_at: %s
|
||||||
|
virtual_size: 0 B
|
||||||
|
|
||||||
|
`, expectedTime, expectedTime, expectedTime),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`image_id: imageID1
|
||||||
|
image_id: imageID2
|
||||||
|
image_id: imageID3
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Custom Format
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Repository}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"image\nimage\n<none>\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Repository}}",
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
"image\nimage\n<none>\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, context := range contexts {
|
||||||
|
images := []types.Image{
|
||||||
|
{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
||||||
|
{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
||||||
|
{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
context.context.Output = out
|
||||||
|
context.context.Images = images
|
||||||
|
context.context.Write()
|
||||||
|
actual := out.String()
|
||||||
|
if actual != context.expected {
|
||||||
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||||
|
}
|
||||||
|
// Clean buffer
|
||||||
|
out.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageContextWriteWithNoImage(t *testing.T) {
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
images := []types.Image{}
|
||||||
|
|
||||||
|
contexts := []struct {
|
||||||
|
context ImageContext
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Repository}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Repository}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"REPOSITORY\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Repository}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Repository}}",
|
||||||
|
Output: out,
|
||||||
|
},
|
||||||
|
Digest: true,
|
||||||
|
},
|
||||||
|
"REPOSITORY DIGEST\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, context := range contexts {
|
||||||
|
context.context.Images = images
|
||||||
|
context.context.Write()
|
||||||
|
actual := out.String()
|
||||||
|
if actual != context.expected {
|
||||||
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||||
|
}
|
||||||
|
// Clean buffer
|
||||||
|
out.Reset()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultNetworkTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}"
|
||||||
|
|
||||||
|
networkIDHeader = "NETWORK ID"
|
||||||
|
ipv6Header = "IPV6"
|
||||||
|
internalHeader = "INTERNAL"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetworkContext contains network specific information required by the formatter,
|
||||||
|
// encapsulate a Context struct.
|
||||||
|
type NetworkContext struct {
|
||||||
|
Context
|
||||||
|
// Networks
|
||||||
|
Networks []types.NetworkResource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx NetworkContext) Write() {
|
||||||
|
switch ctx.Format {
|
||||||
|
case tableFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = defaultQuietFormat
|
||||||
|
} else {
|
||||||
|
ctx.Format = defaultNetworkTableFormat
|
||||||
|
}
|
||||||
|
case rawFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = `network_id: {{.ID}}`
|
||||||
|
} else {
|
||||||
|
ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.buffer = bytes.NewBufferString("")
|
||||||
|
ctx.preformat()
|
||||||
|
|
||||||
|
tmpl, err := ctx.parseFormat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range ctx.Networks {
|
||||||
|
networkCtx := &networkContext{
|
||||||
|
trunc: ctx.Trunc,
|
||||||
|
n: network,
|
||||||
|
}
|
||||||
|
err = ctx.contextFormat(tmpl, networkCtx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.postformat(tmpl, &networkContext{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type networkContext struct {
|
||||||
|
baseSubContext
|
||||||
|
trunc bool
|
||||||
|
n types.NetworkResource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) ID() string {
|
||||||
|
c.addHeader(networkIDHeader)
|
||||||
|
if c.trunc {
|
||||||
|
return stringid.TruncateID(c.n.ID)
|
||||||
|
}
|
||||||
|
return c.n.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) Name() string {
|
||||||
|
c.addHeader(nameHeader)
|
||||||
|
return c.n.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) Driver() string {
|
||||||
|
c.addHeader(driverHeader)
|
||||||
|
return c.n.Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) Scope() string {
|
||||||
|
c.addHeader(scopeHeader)
|
||||||
|
return c.n.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) IPv6() string {
|
||||||
|
c.addHeader(ipv6Header)
|
||||||
|
return fmt.Sprintf("%v", c.n.EnableIPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) Internal() string {
|
||||||
|
c.addHeader(internalHeader)
|
||||||
|
return fmt.Sprintf("%v", c.n.Internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) Labels() string {
|
||||||
|
c.addHeader(labelsHeader)
|
||||||
|
if c.n.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var joinLabels []string
|
||||||
|
for k, v := range c.n.Labels {
|
||||||
|
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
return strings.Join(joinLabels, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *networkContext) Label(name string) string {
|
||||||
|
n := strings.Split(name, ".")
|
||||||
|
r := strings.NewReplacer("-", " ", "_", " ")
|
||||||
|
h := r.Replace(n[len(n)-1])
|
||||||
|
|
||||||
|
c.addHeader(h)
|
||||||
|
|
||||||
|
if c.n.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.n.Labels[name]
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNetworkContext(t *testing.T) {
|
||||||
|
networkID := stringid.GenerateRandomID()
|
||||||
|
|
||||||
|
var ctx networkContext
|
||||||
|
cases := []struct {
|
||||||
|
networkCtx networkContext
|
||||||
|
expValue string
|
||||||
|
expHeader string
|
||||||
|
call func() string
|
||||||
|
}{
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{ID: networkID},
|
||||||
|
trunc: false,
|
||||||
|
}, networkID, networkIDHeader, ctx.ID},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{ID: networkID},
|
||||||
|
trunc: true,
|
||||||
|
}, stringid.TruncateID(networkID), networkIDHeader, ctx.ID},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{Name: "network_name"},
|
||||||
|
}, "network_name", nameHeader, ctx.Name},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{Driver: "driver_name"},
|
||||||
|
}, "driver_name", driverHeader, ctx.Driver},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{EnableIPv6: true},
|
||||||
|
}, "true", ipv6Header, ctx.IPv6},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{EnableIPv6: false},
|
||||||
|
}, "false", ipv6Header, ctx.IPv6},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{Internal: true},
|
||||||
|
}, "true", internalHeader, ctx.Internal},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{Internal: false},
|
||||||
|
}, "false", internalHeader, ctx.Internal},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{},
|
||||||
|
}, "", labelsHeader, ctx.Labels},
|
||||||
|
{networkContext{
|
||||||
|
n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
|
||||||
|
}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
ctx = c.networkCtx
|
||||||
|
v := c.call()
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
compareMultipleValues(t, v, c.expValue)
|
||||||
|
} else if v != c.expValue {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ctx.fullHeader()
|
||||||
|
if h != c.expHeader {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkContextWrite(t *testing.T) {
|
||||||
|
contexts := []struct {
|
||||||
|
context NetworkContext
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{InvalidFunction}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{nil}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Table format
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`NETWORK ID NAME DRIVER SCOPE
|
||||||
|
networkID1 foobar_baz foo local
|
||||||
|
networkID2 foobar_bar bar local
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`networkID1
|
||||||
|
networkID2
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Name}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`NAME
|
||||||
|
foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Name}}",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`NAME
|
||||||
|
foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Raw Format
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
},
|
||||||
|
}, `network_id: networkID1
|
||||||
|
name: foobar_baz
|
||||||
|
driver: foo
|
||||||
|
scope: local
|
||||||
|
|
||||||
|
network_id: networkID2
|
||||||
|
name: foobar_bar
|
||||||
|
driver: bar
|
||||||
|
scope: local
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`network_id: networkID1
|
||||||
|
network_id: networkID2
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Custom Format
|
||||||
|
{
|
||||||
|
NetworkContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Name}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, context := range contexts {
|
||||||
|
networks := []types.NetworkResource{
|
||||||
|
{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
|
||||||
|
{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
context.context.Output = out
|
||||||
|
context.context.Networks = networks
|
||||||
|
context.context.Write()
|
||||||
|
actual := out.String()
|
||||||
|
if actual != context.expected {
|
||||||
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||||
|
}
|
||||||
|
// Clean buffer
|
||||||
|
out.Reset()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultVolumeQuietFormat = "{{.Name}}"
|
||||||
|
defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
|
||||||
|
|
||||||
|
mountpointHeader = "MOUNTPOINT"
|
||||||
|
// Status header ?
|
||||||
|
)
|
||||||
|
|
||||||
|
// VolumeContext contains volume specific information required by the formatter,
|
||||||
|
// encapsulate a Context struct.
|
||||||
|
type VolumeContext struct {
|
||||||
|
Context
|
||||||
|
// Volumes
|
||||||
|
Volumes []*types.Volume
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx VolumeContext) Write() {
|
||||||
|
switch ctx.Format {
|
||||||
|
case tableFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = defaultVolumeQuietFormat
|
||||||
|
} else {
|
||||||
|
ctx.Format = defaultVolumeTableFormat
|
||||||
|
}
|
||||||
|
case rawFormatKey:
|
||||||
|
if ctx.Quiet {
|
||||||
|
ctx.Format = `name: {{.Name}}`
|
||||||
|
} else {
|
||||||
|
ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.buffer = bytes.NewBufferString("")
|
||||||
|
ctx.preformat()
|
||||||
|
|
||||||
|
tmpl, err := ctx.parseFormat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, volume := range ctx.Volumes {
|
||||||
|
volumeCtx := &volumeContext{
|
||||||
|
v: volume,
|
||||||
|
}
|
||||||
|
err = ctx.contextFormat(tmpl, volumeCtx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.postformat(tmpl, &networkContext{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type volumeContext struct {
|
||||||
|
baseSubContext
|
||||||
|
v *types.Volume
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) Name() string {
|
||||||
|
c.addHeader(nameHeader)
|
||||||
|
return c.v.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) Driver() string {
|
||||||
|
c.addHeader(driverHeader)
|
||||||
|
return c.v.Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) Scope() string {
|
||||||
|
c.addHeader(scopeHeader)
|
||||||
|
return c.v.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) Mountpoint() string {
|
||||||
|
c.addHeader(mountpointHeader)
|
||||||
|
return c.v.Mountpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) Labels() string {
|
||||||
|
c.addHeader(labelsHeader)
|
||||||
|
if c.v.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var joinLabels []string
|
||||||
|
for k, v := range c.v.Labels {
|
||||||
|
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
return strings.Join(joinLabels, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *volumeContext) Label(name string) string {
|
||||||
|
|
||||||
|
n := strings.Split(name, ".")
|
||||||
|
r := strings.NewReplacer("-", " ", "_", " ")
|
||||||
|
h := r.Replace(n[len(n)-1])
|
||||||
|
|
||||||
|
c.addHeader(h)
|
||||||
|
|
||||||
|
if c.v.Labels == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.v.Labels[name]
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVolumeContext(t *testing.T) {
|
||||||
|
volumeName := stringid.GenerateRandomID()
|
||||||
|
|
||||||
|
var ctx volumeContext
|
||||||
|
cases := []struct {
|
||||||
|
volumeCtx volumeContext
|
||||||
|
expValue string
|
||||||
|
expHeader string
|
||||||
|
call func() string
|
||||||
|
}{
|
||||||
|
{volumeContext{
|
||||||
|
v: &types.Volume{Name: volumeName},
|
||||||
|
}, volumeName, nameHeader, ctx.Name},
|
||||||
|
{volumeContext{
|
||||||
|
v: &types.Volume{Driver: "driver_name"},
|
||||||
|
}, "driver_name", driverHeader, ctx.Driver},
|
||||||
|
{volumeContext{
|
||||||
|
v: &types.Volume{Scope: "local"},
|
||||||
|
}, "local", scopeHeader, ctx.Scope},
|
||||||
|
{volumeContext{
|
||||||
|
v: &types.Volume{Mountpoint: "mountpoint"},
|
||||||
|
}, "mountpoint", mountpointHeader, ctx.Mountpoint},
|
||||||
|
{volumeContext{
|
||||||
|
v: &types.Volume{},
|
||||||
|
}, "", labelsHeader, ctx.Labels},
|
||||||
|
{volumeContext{
|
||||||
|
v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
|
||||||
|
}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
ctx = c.volumeCtx
|
||||||
|
v := c.call()
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
compareMultipleValues(t, v, c.expValue)
|
||||||
|
} else if v != c.expValue {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ctx.fullHeader()
|
||||||
|
if h != c.expHeader {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVolumeContextWrite(t *testing.T) {
|
||||||
|
contexts := []struct {
|
||||||
|
context VolumeContext
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{InvalidFunction}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{nil}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Table format
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`DRIVER NAME
|
||||||
|
foo foobar_baz
|
||||||
|
bar foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Name}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`NAME
|
||||||
|
foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "table {{.Name}}",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`NAME
|
||||||
|
foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Raw Format
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
},
|
||||||
|
}, `name: foobar_baz
|
||||||
|
driver: foo
|
||||||
|
|
||||||
|
name: foobar_bar
|
||||||
|
driver: bar
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "raw",
|
||||||
|
Quiet: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`name: foobar_baz
|
||||||
|
name: foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Custom Format
|
||||||
|
{
|
||||||
|
VolumeContext{
|
||||||
|
Context: Context{
|
||||||
|
Format: "{{.Name}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, context := range contexts {
|
||||||
|
volumes := []*types.Volume{
|
||||||
|
{Name: "foobar_baz", Driver: "foo"},
|
||||||
|
{Name: "foobar_bar", Driver: "bar"},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
context.context.Output = out
|
||||||
|
context.context.Volumes = volumes
|
||||||
|
context.context.Write()
|
||||||
|
actual := out.String()
|
||||||
|
if actual != context.expected {
|
||||||
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||||
|
}
|
||||||
|
// Clean buffer
|
||||||
|
out.Reset()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package idresolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IDResolver provides ID to Name resolution.
|
||||||
|
type IDResolver struct {
|
||||||
|
client client.APIClient
|
||||||
|
noResolve bool
|
||||||
|
cache map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new IDResolver.
|
||||||
|
func New(client client.APIClient, noResolve bool) *IDResolver {
|
||||||
|
return &IDResolver{
|
||||||
|
client: client,
|
||||||
|
noResolve: noResolve,
|
||||||
|
cache: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) {
|
||||||
|
switch t.(type) {
|
||||||
|
case swarm.Node:
|
||||||
|
node, _, err := r.client.NodeInspectWithRaw(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
if node.Spec.Annotations.Name != "" {
|
||||||
|
return node.Spec.Annotations.Name, nil
|
||||||
|
}
|
||||||
|
if node.Description.Hostname != "" {
|
||||||
|
return node.Description.Hostname, nil
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
case swarm.Service:
|
||||||
|
service, _, err := r.client.ServiceInspectWithRaw(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
return service.Spec.Annotations.Name, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported type")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve will attempt to resolve an ID to a Name by querying the manager.
|
||||||
|
// Results are stored into a cache.
|
||||||
|
// If the `-n` flag is used in the command-line, resolution is disabled.
|
||||||
|
func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) {
|
||||||
|
if r.noResolve {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
if name, ok := r.cache[id]; ok {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
name, err := r.get(ctx, t, id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
r.cache[id] = name
|
||||||
|
return name, nil
|
||||||
|
}
|
|
@ -0,0 +1,452 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/builder"
|
||||||
|
"github.com/docker/docker/builder/dockerignore"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/fileutils"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/docker/docker/pkg/progress"
|
||||||
|
"github.com/docker/docker/pkg/streamformatter"
|
||||||
|
"github.com/docker/docker/pkg/urlutil"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buildOptions struct {
|
||||||
|
context string
|
||||||
|
dockerfileName string
|
||||||
|
tags opts.ListOpts
|
||||||
|
labels []string
|
||||||
|
buildArgs opts.ListOpts
|
||||||
|
ulimits *runconfigopts.UlimitOpt
|
||||||
|
memory string
|
||||||
|
memorySwap string
|
||||||
|
shmSize string
|
||||||
|
cpuShares int64
|
||||||
|
cpuPeriod int64
|
||||||
|
cpuQuota int64
|
||||||
|
cpuSetCpus string
|
||||||
|
cpuSetMems string
|
||||||
|
cgroupParent string
|
||||||
|
isolation string
|
||||||
|
quiet bool
|
||||||
|
noCache bool
|
||||||
|
rm bool
|
||||||
|
forceRm bool
|
||||||
|
pull bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuildCommand creates a new `docker build` command
|
||||||
|
func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
ulimits := make(map[string]*units.Ulimit)
|
||||||
|
options := buildOptions{
|
||||||
|
tags: opts.NewListOpts(validateTag),
|
||||||
|
buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||||
|
ulimits: runconfigopts.NewUlimitOpt(&ulimits),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "build [OPTIONS] PATH | URL | -",
|
||||||
|
Short: "Build an image from a Dockerfile",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
options.context = args[0]
|
||||||
|
return runBuild(dockerCli, options)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format")
|
||||||
|
flags.Var(&options.buildArgs, "build-arg", "Set build-time variables")
|
||||||
|
flags.Var(options.ulimits, "ulimit", "Ulimit options")
|
||||||
|
flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
|
||||||
|
flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit")
|
||||||
|
flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
|
||||||
|
flags.StringVar(&options.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB")
|
||||||
|
flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
|
||||||
|
flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period")
|
||||||
|
flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota")
|
||||||
|
flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
|
||||||
|
flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
|
||||||
|
flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
|
||||||
|
flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology")
|
||||||
|
flags.StringSliceVar(&options.labels, "label", []string{}, "Set metadata for an image")
|
||||||
|
flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image")
|
||||||
|
flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build")
|
||||||
|
flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers")
|
||||||
|
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
|
||||||
|
flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
|
||||||
|
|
||||||
|
command.AddTrustedFlags(flags, true)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastProgressOutput is the same as progress.Output except
|
||||||
|
// that it only output with the last update. It is used in
|
||||||
|
// non terminal scenarios to depresss verbose messages
|
||||||
|
type lastProgressOutput struct {
|
||||||
|
output progress.Output
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteProgress formats progress information from a ProgressReader.
|
||||||
|
func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
|
||||||
|
if !prog.LastUpdate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.output.WriteProgress(prog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
||||||
|
|
||||||
|
var (
|
||||||
|
buildCtx io.ReadCloser
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
specifiedContext := options.context
|
||||||
|
|
||||||
|
var (
|
||||||
|
contextDir string
|
||||||
|
tempDir string
|
||||||
|
relDockerfile string
|
||||||
|
progBuff io.Writer
|
||||||
|
buildBuff io.Writer
|
||||||
|
)
|
||||||
|
|
||||||
|
progBuff = dockerCli.Out()
|
||||||
|
buildBuff = dockerCli.Out()
|
||||||
|
if options.quiet {
|
||||||
|
progBuff = bytes.NewBuffer(nil)
|
||||||
|
buildBuff = bytes.NewBuffer(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case specifiedContext == "-":
|
||||||
|
buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName)
|
||||||
|
case urlutil.IsGitURL(specifiedContext):
|
||||||
|
tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName)
|
||||||
|
case urlutil.IsURL(specifiedContext):
|
||||||
|
buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
|
||||||
|
default:
|
||||||
|
contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if options.quiet && urlutil.IsURL(specifiedContext) {
|
||||||
|
fmt.Fprintln(dockerCli.Err(), progBuff)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to prepare context: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tempDir != "" {
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
contextDir = tempDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if buildCtx == nil {
|
||||||
|
// And canonicalize dockerfile name to a platform-independent one
|
||||||
|
relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var excludes []string
|
||||||
|
if err == nil {
|
||||||
|
excludes, err = dockerignore.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil {
|
||||||
|
return fmt.Errorf("Error checking context: '%s'.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If .dockerignore mentions .dockerignore or the Dockerfile
|
||||||
|
// then make sure we send both files over to the daemon
|
||||||
|
// because Dockerfile is, obviously, needed no matter what, and
|
||||||
|
// .dockerignore is needed to know if either one needs to be
|
||||||
|
// removed. The daemon will remove them for us, if needed, after it
|
||||||
|
// parses the Dockerfile. Ignore errors here, as they will have been
|
||||||
|
// caught by validateContextDirectory above.
|
||||||
|
var includes = []string{"."}
|
||||||
|
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
|
||||||
|
keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
|
||||||
|
if keepThem1 || keepThem2 {
|
||||||
|
includes = append(includes, ".dockerignore", relDockerfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||||
|
Compression: archive.Uncompressed,
|
||||||
|
ExcludePatterns: excludes,
|
||||||
|
IncludeFiles: includes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var resolvedTags []*resolvedTag
|
||||||
|
if command.IsTrusted() {
|
||||||
|
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
|
||||||
|
// Dockerfile which uses trusted pulls.
|
||||||
|
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup an upload progress bar
|
||||||
|
progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
|
||||||
|
if !dockerCli.Out().IsTerminal() {
|
||||||
|
progressOutput = &lastProgressOutput{output: progressOutput}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
|
||||||
|
|
||||||
|
var memory int64
|
||||||
|
if options.memory != "" {
|
||||||
|
parsedMemory, err := units.RAMInBytes(options.memory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
memory = parsedMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
var memorySwap int64
|
||||||
|
if options.memorySwap != "" {
|
||||||
|
if options.memorySwap == "-1" {
|
||||||
|
memorySwap = -1
|
||||||
|
} else {
|
||||||
|
parsedMemorySwap, err := units.RAMInBytes(options.memorySwap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
memorySwap = parsedMemorySwap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shmSize int64
|
||||||
|
if options.shmSize != "" {
|
||||||
|
shmSize, err = units.RAMInBytes(options.shmSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOptions := types.ImageBuildOptions{
|
||||||
|
Memory: memory,
|
||||||
|
MemorySwap: memorySwap,
|
||||||
|
Tags: options.tags.GetAll(),
|
||||||
|
SuppressOutput: options.quiet,
|
||||||
|
NoCache: options.noCache,
|
||||||
|
Remove: options.rm,
|
||||||
|
ForceRemove: options.forceRm,
|
||||||
|
PullParent: options.pull,
|
||||||
|
Isolation: container.Isolation(options.isolation),
|
||||||
|
CPUSetCPUs: options.cpuSetCpus,
|
||||||
|
CPUSetMems: options.cpuSetMems,
|
||||||
|
CPUShares: options.cpuShares,
|
||||||
|
CPUQuota: options.cpuQuota,
|
||||||
|
CPUPeriod: options.cpuPeriod,
|
||||||
|
CgroupParent: options.cgroupParent,
|
||||||
|
Dockerfile: relDockerfile,
|
||||||
|
ShmSize: shmSize,
|
||||||
|
Ulimits: options.ulimits.GetList(),
|
||||||
|
BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()),
|
||||||
|
AuthConfigs: dockerCli.RetrieveAuthConfigs(),
|
||||||
|
Labels: runconfigopts.ConvertKVStringsToMap(options.labels),
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil)
|
||||||
|
if err != nil {
|
||||||
|
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
||||||
|
// If no error code is set, default to 1
|
||||||
|
if jerr.Code == 0 {
|
||||||
|
jerr.Code = 1
|
||||||
|
}
|
||||||
|
if options.quiet {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
|
||||||
|
}
|
||||||
|
return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows: show error message about modified file permissions if the
|
||||||
|
// daemon isn't running Windows.
|
||||||
|
if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
|
||||||
|
fmt.Fprintln(dockerCli.Err(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything worked so if -q was provided the output from the daemon
|
||||||
|
// should be just the image ID and we'll print that to stdout.
|
||||||
|
if options.quiet {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s", buildBuff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if command.IsTrusted() {
|
||||||
|
// Since the build was successful, now we must tag any of the resolved
|
||||||
|
// images from the above Dockerfile rewrite.
|
||||||
|
for _, resolved := range resolvedTags {
|
||||||
|
if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error)
|
||||||
|
|
||||||
|
// validateTag checks if the given image name can be resolved.
|
||||||
|
func validateTag(rawRepo string) (string, error) {
|
||||||
|
_, err := reference.ParseNamed(rawRepo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawRepo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
|
||||||
|
|
||||||
|
// resolvedTag records the repository, tag, and resolved digest reference
|
||||||
|
// from a Dockerfile rewrite.
|
||||||
|
type resolvedTag struct {
|
||||||
|
digestRef reference.Canonical
|
||||||
|
tagRef reference.NamedTagged
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
|
||||||
|
// "FROM <image>" instructions to a digest reference. `translator` is a
|
||||||
|
// function that takes a repository name and tag reference and returns a
|
||||||
|
// trusted digest reference.
|
||||||
|
func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) {
|
||||||
|
scanner := bufio.NewScanner(dockerfile)
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
// Scan the lines of the Dockerfile, looking for a "FROM" line.
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
matches := dockerfileFromLinePattern.FindStringSubmatch(line)
|
||||||
|
if matches != nil && matches[1] != api.NoBaseImageSpecifier {
|
||||||
|
// Replace the line with a resolved "FROM repo@digest"
|
||||||
|
ref, err := reference.ParseNamed(matches[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ref = reference.WithDefaultTag(ref)
|
||||||
|
if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
|
||||||
|
trustedRef, err := translator(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String()))
|
||||||
|
resolvedTags = append(resolvedTags, &resolvedTag{
|
||||||
|
digestRef: trustedRef,
|
||||||
|
tagRef: ref,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fmt.Fprintln(buf, line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), resolvedTags, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceDockerfileTarWrapper wraps the given input tar archive stream and
|
||||||
|
// replaces the entry with the given Dockerfile name with the contents of the
|
||||||
|
// new Dockerfile. Returns a new tar archive stream with the replaced
|
||||||
|
// Dockerfile.
|
||||||
|
func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
tarReader := tar.NewReader(inputTarStream)
|
||||||
|
tarWriter := tar.NewWriter(pipeWriter)
|
||||||
|
|
||||||
|
defer inputTarStream.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
hdr, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
// Signals end of archive.
|
||||||
|
tarWriter.Close()
|
||||||
|
pipeWriter.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
pipeWriter.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := io.Reader(tarReader)
|
||||||
|
if hdr.Name == dockerfileName {
|
||||||
|
// This entry is the Dockerfile. Since the tar archive was
|
||||||
|
// generated from a directory on the local filesystem, the
|
||||||
|
// Dockerfile will only appear once in the archive.
|
||||||
|
var newDockerfile []byte
|
||||||
|
newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator)
|
||||||
|
if err != nil {
|
||||||
|
pipeWriter.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hdr.Size = int64(len(newDockerfile))
|
||||||
|
content = bytes.NewBuffer(newDockerfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tarWriter.WriteHeader(hdr); err != nil {
|
||||||
|
pipeWriter.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(tarWriter, content); err != nil {
|
||||||
|
pipeWriter.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return pipeReader
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/docker/pkg/stringutils"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type historyOptions struct {
|
||||||
|
image string
|
||||||
|
|
||||||
|
human bool
|
||||||
|
quiet bool
|
||||||
|
noTrunc bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistoryCommand creates a new `docker history` command
|
||||||
|
func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts historyOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "history [OPTIONS] IMAGE",
|
||||||
|
Short: "Show the history of an image",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.image = args[0]
|
||||||
|
return runHistory(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format")
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs")
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHistory(dockerCli *command.DockerCli, opts historyOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
history, err := dockerCli.Client().ImageHistory(ctx, opts.image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||||
|
|
||||||
|
if opts.quiet {
|
||||||
|
for _, entry := range history {
|
||||||
|
if opts.noTrunc {
|
||||||
|
fmt.Fprintf(w, "%s\n", entry.ID)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "%s\n", stringid.TruncateID(entry.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageID string
|
||||||
|
var createdBy string
|
||||||
|
var created string
|
||||||
|
var size string
|
||||||
|
|
||||||
|
fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT")
|
||||||
|
for _, entry := range history {
|
||||||
|
imageID = entry.ID
|
||||||
|
createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1)
|
||||||
|
if !opts.noTrunc {
|
||||||
|
createdBy = stringutils.Ellipsis(createdBy, 45)
|
||||||
|
imageID = stringid.TruncateID(entry.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.human {
|
||||||
|
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago"
|
||||||
|
size = units.HumanSize(float64(entry.Size))
|
||||||
|
} else {
|
||||||
|
created = time.Unix(entry.Created, 0).Format(time.RFC3339)
|
||||||
|
size = strconv.FormatInt(entry.Size, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", imageID, created, createdBy, size, entry.Comment)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imagesOptions struct {
|
||||||
|
matchName string
|
||||||
|
|
||||||
|
quiet bool
|
||||||
|
all bool
|
||||||
|
noTrunc bool
|
||||||
|
showDigests bool
|
||||||
|
format string
|
||||||
|
filter []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImagesCommand creates a new `docker images` command
|
||||||
|
func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts imagesOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "images [OPTIONS] [REPOSITORY[:TAG]]",
|
||||||
|
Short: "List images",
|
||||||
|
Args: cli.RequiresMaxArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts.matchName = args[0]
|
||||||
|
}
|
||||||
|
return runImages(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs")
|
||||||
|
flags.BoolVarP(&opts.all, "all", "a", false, "Show all images (default hides intermediate images)")
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||||
|
flags.BoolVar(&opts.showDigests, "digests", false, "Show digests")
|
||||||
|
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
|
||||||
|
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Consolidate all filter flags, and sanity check them early.
|
||||||
|
// They'll get process in the daemon/server.
|
||||||
|
imageFilterArgs := filters.NewArgs()
|
||||||
|
for _, f := range opts.filter {
|
||||||
|
var err error
|
||||||
|
imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchName := opts.matchName
|
||||||
|
|
||||||
|
options := types.ImageListOptions{
|
||||||
|
MatchName: matchName,
|
||||||
|
All: opts.all,
|
||||||
|
Filters: imageFilterArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, err := dockerCli.Client().ImageList(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f := opts.format
|
||||||
|
if len(f) == 0 {
|
||||||
|
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
|
||||||
|
f = dockerCli.ConfigFile().ImagesFormat
|
||||||
|
} else {
|
||||||
|
f = "table"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesCtx := formatter.ImageContext{
|
||||||
|
Context: formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: f,
|
||||||
|
Quiet: opts.quiet,
|
||||||
|
Trunc: !opts.noTrunc,
|
||||||
|
},
|
||||||
|
Digest: opts.showDigests,
|
||||||
|
Images: images,
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesCtx.Write()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
dockeropts "github.com/docker/docker/opts"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/docker/docker/pkg/urlutil"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type importOptions struct {
|
||||||
|
source string
|
||||||
|
reference string
|
||||||
|
changes dockeropts.ListOpts
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImportCommand creates a new `docker import` command
|
||||||
|
func NewImportCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts importOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]",
|
||||||
|
Short: "Import the contents from a tarball to create a filesystem image",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.source = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
opts.reference = args[1]
|
||||||
|
}
|
||||||
|
return runImport(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
opts.changes = dockeropts.NewListOpts(nil)
|
||||||
|
flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||||
|
flags.StringVarP(&opts.message, "message", "m", "", "Set commit message for imported image")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runImport(dockerCli *command.DockerCli, opts importOptions) error {
|
||||||
|
var (
|
||||||
|
in io.Reader
|
||||||
|
srcName = opts.source
|
||||||
|
)
|
||||||
|
|
||||||
|
if opts.source == "-" {
|
||||||
|
in = dockerCli.In()
|
||||||
|
} else if !urlutil.IsURL(opts.source) {
|
||||||
|
srcName = "-"
|
||||||
|
file, err := os.Open(opts.source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
in = file
|
||||||
|
}
|
||||||
|
|
||||||
|
source := types.ImageImportSource{
|
||||||
|
Source: in,
|
||||||
|
SourceName: srcName,
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ImageImportOptions{
|
||||||
|
Message: opts.message,
|
||||||
|
Changes: opts.changes.GetAll(),
|
||||||
|
}
|
||||||
|
|
||||||
|
clnt := dockerCli.Client()
|
||||||
|
|
||||||
|
responseBody, err := clnt.ImageImport(context.Background(), source, opts.reference, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer responseBody.Close()
|
||||||
|
|
||||||
|
return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loadOptions struct {
|
||||||
|
input string
|
||||||
|
quiet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoadCommand creates a new `docker load` command
|
||||||
|
func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts loadOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "load [OPTIONS]",
|
||||||
|
Short: "Load an image from a tar archive or STDIN",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runLoad(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLoad(dockerCli *command.DockerCli, opts loadOptions) error {
|
||||||
|
|
||||||
|
var input io.Reader = dockerCli.In()
|
||||||
|
if opts.input != "" {
|
||||||
|
file, err := os.Open(opts.input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
input = file
|
||||||
|
}
|
||||||
|
if !dockerCli.Out().IsTerminal() {
|
||||||
|
opts.quiet = true
|
||||||
|
}
|
||||||
|
response, err := dockerCli.Client().ImageLoad(context.Background(), input, opts.quiet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.Body != nil && response.JSON {
|
||||||
|
return jsonmessage.DisplayJSONMessagesToStream(response.Body, dockerCli.Out(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dockerCli.Out(), response.Body)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pullOptions struct {
|
||||||
|
remote string
|
||||||
|
all bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPullCommand creates a new `docker pull` command
|
||||||
|
func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts pullOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "pull [OPTIONS] NAME[:TAG|@DIGEST]",
|
||||||
|
Short: "Pull an image or a repository from a registry",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.remote = args[0]
|
||||||
|
return runPull(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
|
||||||
|
command.AddTrustedFlags(flags, true)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
|
||||||
|
distributionRef, err := reference.ParseNamed(opts.remote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.all && !reference.IsNameOnly(distributionRef) {
|
||||||
|
return errors.New("tag can't be used with --all-tags/-a")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.all && reference.IsNameOnly(distributionRef) {
|
||||||
|
distributionRef = reference.WithDefaultTag(distributionRef)
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag string
|
||||||
|
switch x := distributionRef.(type) {
|
||||||
|
case reference.Canonical:
|
||||||
|
tag = x.Digest().String()
|
||||||
|
case reference.NamedTagged:
|
||||||
|
tag = x.Tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
registryRef := registry.ParseReference(tag)
|
||||||
|
|
||||||
|
// Resolve the Repository name from fqn to RepositoryInfo
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||||
|
requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "pull")
|
||||||
|
|
||||||
|
if command.IsTrusted() && !registryRef.HasDigest() {
|
||||||
|
// Check if tag is digest
|
||||||
|
err = dockerCli.TrustedPull(ctx, repoInfo, registryRef, authConfig, requestPrivilege)
|
||||||
|
} else {
|
||||||
|
err = dockerCli.ImagePullPrivileged(ctx, authConfig, distributionRef.String(), requestPrivilege, opts.all)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "target is a plugin") {
|
||||||
|
return errors.New(err.Error() + " - Use `docker plugin install`")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPushCommand creates a new `docker push` command
|
||||||
|
func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "push [OPTIONS] NAME[:TAG]",
|
||||||
|
Short: "Push an image or a repository to a registry",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runPush(dockerCli, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
command.AddTrustedFlags(flags, true)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPush(dockerCli *command.DockerCli, remote string) error {
|
||||||
|
ref, err := reference.ParseNamed(remote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the Repository name from fqn to RepositoryInfo
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Resolve the Auth config relevant for this server
|
||||||
|
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||||
|
requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push")
|
||||||
|
|
||||||
|
if command.IsTrusted() {
|
||||||
|
return dockerCli.TrustedPush(ctx, repoInfo, ref, authConfig, requestPrivilege)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody, err := dockerCli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer responseBody.Close()
|
||||||
|
return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
force bool
|
||||||
|
noPrune bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRemoveCommand creates a new `docker remove` command
|
||||||
|
func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts removeOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rmi [OPTIONS] IMAGE [IMAGE...]",
|
||||||
|
Short: "Remove one or more images",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRemove(dockerCli, opts, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.force, "force", "f", false, "Force removal of the image")
|
||||||
|
flags.BoolVar(&opts.noPrune, "no-prune", false, "Do not delete untagged parents")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRemove(dockerCli *command.DockerCli, opts removeOptions, images []string) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
options := types.ImageRemoveOptions{
|
||||||
|
Force: opts.force,
|
||||||
|
PruneChildren: !opts.noPrune,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, image := range images {
|
||||||
|
dels, err := client.ImageRemove(ctx, image, options)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
} else {
|
||||||
|
for _, del := range dels {
|
||||||
|
if del.Deleted != "" {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Deleted: %s\n", del.Deleted)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Untagged: %s\n", del.Untagged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type saveOptions struct {
|
||||||
|
images []string
|
||||||
|
output string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSaveCommand creates a new `docker save` command
|
||||||
|
func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts saveOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "save [OPTIONS] IMAGE [IMAGE...]",
|
||||||
|
Short: "Save one or more images to a tar archive (streamed to STDOUT by default)",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.images = args
|
||||||
|
return runSave(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSave(dockerCli *command.DockerCli, opts saveOptions) error {
|
||||||
|
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||||
|
return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer responseBody.Close()
|
||||||
|
|
||||||
|
if opts.output == "" {
|
||||||
|
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.CopyToFile(opts.output, responseBody)
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/pkg/stringutils"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type searchOptions struct {
|
||||||
|
term string
|
||||||
|
noTrunc bool
|
||||||
|
limit int
|
||||||
|
filter []string
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
stars uint
|
||||||
|
automated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSearchCommand creates a new `docker search` command
|
||||||
|
func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts searchOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "search [OPTIONS] TERM",
|
||||||
|
Short: "Search the Docker Hub for images",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.term = args[0]
|
||||||
|
return runSearch(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||||
|
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided")
|
||||||
|
flags.IntVar(&opts.limit, "limit", registry.DefaultSearchLimit, "Max number of search results")
|
||||||
|
|
||||||
|
flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds")
|
||||||
|
flags.UintVarP(&opts.stars, "stars", "s", 0, "Only displays with at least x stars")
|
||||||
|
|
||||||
|
flags.MarkDeprecated("automated", "use --filter=automated=true instead")
|
||||||
|
flags.MarkDeprecated("stars", "use --filter=stars=3 instead")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSearch(dockerCli *command.DockerCli, opts searchOptions) error {
|
||||||
|
indexInfo, err := registry.ParseSearchIndexInfo(opts.term)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
authConfig := dockerCli.ResolveAuthConfig(ctx, indexInfo)
|
||||||
|
requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(indexInfo, "search")
|
||||||
|
|
||||||
|
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFilters := filters.NewArgs()
|
||||||
|
for _, f := range opts.filter {
|
||||||
|
var err error
|
||||||
|
searchFilters, err = filters.ParseFlag(f, searchFilters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.ImageSearchOptions{
|
||||||
|
RegistryAuth: encodedAuth,
|
||||||
|
PrivilegeFunc: requestPrivilege,
|
||||||
|
Filters: searchFilters,
|
||||||
|
Limit: opts.limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
clnt := dockerCli.Client()
|
||||||
|
|
||||||
|
unorderedResults, err := clnt.ImageSearch(ctx, opts.term, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := searchResultsByStars(unorderedResults)
|
||||||
|
sort.Sort(results)
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(dockerCli.Out(), 10, 1, 3, ' ', 0)
|
||||||
|
fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n")
|
||||||
|
for _, res := range results {
|
||||||
|
// --automated and -s, --stars are deprecated since Docker 1.12
|
||||||
|
if (opts.automated && !res.IsAutomated) || (int(opts.stars) > res.StarCount) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desc := strings.Replace(res.Description, "\n", " ", -1)
|
||||||
|
desc = strings.Replace(desc, "\r", " ", -1)
|
||||||
|
if !opts.noTrunc {
|
||||||
|
desc = stringutils.Ellipsis(desc, 45)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount)
|
||||||
|
if res.IsOfficial {
|
||||||
|
fmt.Fprint(w, "[OK]")
|
||||||
|
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "\t")
|
||||||
|
if res.IsAutomated {
|
||||||
|
fmt.Fprint(w, "[OK]")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "\n")
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResultsByStars sorts search results in descending order by number of stars.
|
||||||
|
type searchResultsByStars []registrytypes.SearchResult
|
||||||
|
|
||||||
|
func (r searchResultsByStars) Len() int { return len(r) }
|
||||||
|
func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||||
|
func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount }
|
|
@ -0,0 +1,41 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tagOptions struct {
|
||||||
|
image string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagCommand creates a new `docker tag` command
|
||||||
|
func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts tagOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "tag IMAGE[:TAG] IMAGE[:TAG]",
|
||||||
|
Short: "Tag an image into a repository",
|
||||||
|
Args: cli.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.image = args[0]
|
||||||
|
opts.name = args[1]
|
||||||
|
return runTag(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTag(dockerCli *command.DockerCli, opts tagOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
return dockerCli.Client().ImageTag(ctx, opts.image, opts.name)
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InStream is an input stream used by the DockerCli to read user input
|
||||||
|
type InStream struct {
|
||||||
|
in io.ReadCloser
|
||||||
|
fd uintptr
|
||||||
|
isTerminal bool
|
||||||
|
state *term.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InStream) Read(p []byte) (int, error) {
|
||||||
|
return i.in.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the Closer interface
|
||||||
|
func (i *InStream) Close() error {
|
||||||
|
return i.in.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FD returns the file descriptor number for this stream
|
||||||
|
func (i *InStream) FD() uintptr {
|
||||||
|
return i.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if this stream is connected to a terminal
|
||||||
|
func (i *InStream) IsTerminal() bool {
|
||||||
|
return i.isTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRawTerminal sets raw mode on the input terminal
|
||||||
|
func (i *InStream) SetRawTerminal() (err error) {
|
||||||
|
if os.Getenv("NORAW") != "" || !i.isTerminal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i.state, err = term.SetRawTerminal(i.fd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreTerminal restores normal mode to the terminal
|
||||||
|
func (i *InStream) RestoreTerminal() {
|
||||||
|
if i.state != nil {
|
||||||
|
term.RestoreTerminal(i.fd, i.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckTty checks if we are trying to attach to a container tty
|
||||||
|
// from a non-tty client input stream, and if so, returns an error.
|
||||||
|
func (i *InStream) CheckTty(attachStdin, ttyMode bool) error {
|
||||||
|
// In order to attach to a container tty, input stream for the client must
|
||||||
|
// be a tty itself: redirecting or piping the client standard input is
|
||||||
|
// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
|
||||||
|
if ttyMode && attachStdin && !i.isTerminal {
|
||||||
|
eText := "the input device is not a TTY"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'")
|
||||||
|
}
|
||||||
|
return errors.New(eText)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInStream returns a new OutStream object from a Writer
|
||||||
|
func NewInStream(in io.ReadCloser) *InStream {
|
||||||
|
fd, isTerminal := term.GetFdInfo(in)
|
||||||
|
return &InStream{in: in, fd: fd, isTerminal: isTerminal}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/utils/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inspector defines an interface to implement to process elements
|
||||||
|
type Inspector interface {
|
||||||
|
Inspect(typedElement interface{}, rawElement []byte) error
|
||||||
|
Flush() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateInspector uses a text template to inspect elements.
|
||||||
|
type TemplateInspector struct {
|
||||||
|
outputStream io.Writer
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
tmpl *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplateInspector creates a new inspector with a template.
|
||||||
|
func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector {
|
||||||
|
return &TemplateInspector{
|
||||||
|
outputStream: outputStream,
|
||||||
|
buffer: new(bytes.Buffer),
|
||||||
|
tmpl: tmpl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemplateInspectorFromString creates a new TemplateInspector from a string
|
||||||
|
// which is compiled into a template.
|
||||||
|
func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) {
|
||||||
|
if tmplStr == "" {
|
||||||
|
return NewIndentedInspector(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := templates.Parse(tmplStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Template parsing error: %s", err)
|
||||||
|
}
|
||||||
|
return NewTemplateInspector(out, tmpl), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefFunc is a function which used by Inspect to fetch an object from a
|
||||||
|
// reference
|
||||||
|
type GetRefFunc func(ref string) (interface{}, []byte, error)
|
||||||
|
|
||||||
|
// Inspect fetches objects by reference using GetRefFunc and writes the json
|
||||||
|
// representation to the output writer.
|
||||||
|
func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error {
|
||||||
|
inspector, err := NewTemplateInspectorFromString(out, tmplStr)
|
||||||
|
if err != nil {
|
||||||
|
return cli.StatusError{StatusCode: 64, Status: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
var inspectErr error
|
||||||
|
for _, ref := range references {
|
||||||
|
element, raw, err := getRef(ref)
|
||||||
|
if err != nil {
|
||||||
|
inspectErr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := inspector.Inspect(element, raw); err != nil {
|
||||||
|
inspectErr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := inspector.Flush(); err != nil {
|
||||||
|
logrus.Errorf("%s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inspectErr != nil {
|
||||||
|
return cli.StatusError{StatusCode: 1, Status: inspectErr.Error()}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect executes the inspect template.
|
||||||
|
// It decodes the raw element into a map if the initial execution fails.
|
||||||
|
// This allows docker cli to parse inspect structs injected with Swarm fields.
|
||||||
|
func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
if err := i.tmpl.Execute(buffer, typedElement); err != nil {
|
||||||
|
if rawElement == nil {
|
||||||
|
return fmt.Errorf("Template parsing error: %v", err)
|
||||||
|
}
|
||||||
|
return i.tryRawInspectFallback(rawElement)
|
||||||
|
}
|
||||||
|
i.buffer.Write(buffer.Bytes())
|
||||||
|
i.buffer.WriteByte('\n')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryRawInspectFallback executes the inspect template with a raw interface.
|
||||||
|
// This allows docker cli to parse inspect structs injected with Swarm fields.
|
||||||
|
func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error {
|
||||||
|
var raw interface{}
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
rdr := bytes.NewReader(rawElement)
|
||||||
|
dec := json.NewDecoder(rdr)
|
||||||
|
|
||||||
|
if rawErr := dec.Decode(&raw); rawErr != nil {
|
||||||
|
return fmt.Errorf("unable to read inspect data: %v", rawErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmplMissingKey := i.tmpl.Option("missingkey=error")
|
||||||
|
if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil {
|
||||||
|
return fmt.Errorf("Template parsing error: %v", rawErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.buffer.Write(buffer.Bytes())
|
||||||
|
i.buffer.WriteByte('\n')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush write the result of inspecting all elements into the output stream.
|
||||||
|
func (i *TemplateInspector) Flush() error {
|
||||||
|
if i.buffer.Len() == 0 {
|
||||||
|
_, err := io.WriteString(i.outputStream, "\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := io.Copy(i.outputStream, i.buffer)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndentedInspector uses a buffer to stop the indented representation of an element.
|
||||||
|
type IndentedInspector struct {
|
||||||
|
outputStream io.Writer
|
||||||
|
elements []interface{}
|
||||||
|
rawElements [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIndentedInspector generates a new IndentedInspector.
|
||||||
|
func NewIndentedInspector(outputStream io.Writer) Inspector {
|
||||||
|
return &IndentedInspector{
|
||||||
|
outputStream: outputStream,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect writes the raw element with an indented json format.
|
||||||
|
func (i *IndentedInspector) Inspect(typedElement interface{}, rawElement []byte) error {
|
||||||
|
if rawElement != nil {
|
||||||
|
i.rawElements = append(i.rawElements, rawElement)
|
||||||
|
} else {
|
||||||
|
i.elements = append(i.elements, typedElement)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush write the result of inspecting all elements into the output stream.
|
||||||
|
func (i *IndentedInspector) Flush() error {
|
||||||
|
if len(i.elements) == 0 && len(i.rawElements) == 0 {
|
||||||
|
_, err := io.WriteString(i.outputStream, "[]\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer io.Reader
|
||||||
|
if len(i.rawElements) > 0 {
|
||||||
|
bytesBuffer := new(bytes.Buffer)
|
||||||
|
bytesBuffer.WriteString("[")
|
||||||
|
for idx, r := range i.rawElements {
|
||||||
|
bytesBuffer.Write(r)
|
||||||
|
if idx < len(i.rawElements)-1 {
|
||||||
|
bytesBuffer.WriteString(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytesBuffer.WriteString("]")
|
||||||
|
indented := new(bytes.Buffer)
|
||||||
|
if err := json.Indent(indented, bytesBuffer.Bytes(), "", " "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer = indented
|
||||||
|
} else {
|
||||||
|
b, err := json.MarshalIndent(i.elements, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer = bytes.NewReader(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(i.outputStream, buffer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := io.WriteString(i.outputStream, "\n")
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
package inspect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/utils/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testElement struct {
|
||||||
|
DNS string `json:"Dns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateInspectorDefault(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
tmpl, err := templates.Parse("{{.DNS}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i := NewTemplateInspector(b, tmpl)
|
||||||
|
if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if b.String() != "0.0.0.0\n" {
|
||||||
|
t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateInspectorEmpty(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
tmpl, err := templates.Parse("{{.DNS}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i := NewTemplateInspector(b, tmpl)
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if b.String() != "\n" {
|
||||||
|
t.Fatalf("Expected `\\n`, got `%s`", b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateInspectorTemplateError(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
tmpl, err := templates.Parse("{{.Foo}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i := NewTemplateInspector(b, tmpl)
|
||||||
|
|
||||||
|
err = i.Inspect(testElement{"0.0.0.0"}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(err.Error(), "Template parsing error") {
|
||||||
|
t.Fatalf("Expected template error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateInspectorRawFallback(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
tmpl, err := templates.Parse("{{.Dns}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i := NewTemplateInspector(b, tmpl)
|
||||||
|
if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0"}`)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if b.String() != "0.0.0.0\n" {
|
||||||
|
t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateInspectorRawFallbackError(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
tmpl, err := templates.Parse("{{.Dns}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i := NewTemplateInspector(b, tmpl)
|
||||||
|
err = i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Foo": "0.0.0.0"}`))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(err.Error(), "Template parsing error") {
|
||||||
|
t.Fatalf("Expected template error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplateInspectorMultiple(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
tmpl, err := templates.Parse("{{.DNS}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
i := NewTemplateInspector(b, tmpl)
|
||||||
|
|
||||||
|
if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if b.String() != "0.0.0.0\n1.1.1.1\n" {
|
||||||
|
t.Fatalf("Expected `0.0.0.0\\n1.1.1.1\\n`, got `%s`", b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndentedInspectorDefault(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
i := NewIndentedInspector(b)
|
||||||
|
if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `[
|
||||||
|
{
|
||||||
|
"Dns": "0.0.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
if b.String() != expected {
|
||||||
|
t.Fatalf("Expected `%s`, got `%s`", expected, b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndentedInspectorMultiple(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
i := NewIndentedInspector(b)
|
||||||
|
if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `[
|
||||||
|
{
|
||||||
|
"Dns": "0.0.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Dns": "1.1.1.1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
if b.String() != expected {
|
||||||
|
t.Fatalf("Expected `%s`, got `%s`", expected, b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndentedInspectorEmpty(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
i := NewIndentedInspector(b)
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "[]\n"
|
||||||
|
if b.String() != expected {
|
||||||
|
t.Fatalf("Expected `%s`, got `%s`", expected, b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndentedInspectorRawElements(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
i := NewIndentedInspector(b)
|
||||||
|
if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0", "Node": "0"}`)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Inspect(testElement{"1.1.1.1"}, []byte(`{"Dns": "1.1.1.1", "Node": "1"}`)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Flush(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `[
|
||||||
|
{
|
||||||
|
"Dns": "0.0.0.0",
|
||||||
|
"Node": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Dns": "1.1.1.1",
|
||||||
|
"Node": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
if b.String() != expected {
|
||||||
|
t.Fatalf("Expected `%s`, got `%s`", expected, b.String())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewNetworkCommand returns a cobra command for `network` subcommands
|
||||||
|
func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "network",
|
||||||
|
Short: "Manage Docker networks",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
newConnectCommand(dockerCli),
|
||||||
|
newCreateCommand(dockerCli),
|
||||||
|
newDisconnectCommand(dockerCli),
|
||||||
|
newInspectCommand(dockerCli),
|
||||||
|
newListCommand(dockerCli),
|
||||||
|
newRemoveCommand(dockerCli),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type connectOptions struct {
|
||||||
|
network string
|
||||||
|
container string
|
||||||
|
ipaddress string
|
||||||
|
ipv6address string
|
||||||
|
links opts.ListOpts
|
||||||
|
aliases []string
|
||||||
|
linklocalips []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConnectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := connectOptions{
|
||||||
|
links: opts.NewListOpts(runconfigopts.ValidateLink),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "connect [OPTIONS] NETWORK CONTAINER",
|
||||||
|
Short: "Connect a container to a network",
|
||||||
|
Args: cli.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.network = args[0]
|
||||||
|
opts.container = args[1]
|
||||||
|
return runConnect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVar(&opts.ipaddress, "ip", "", "IP Address")
|
||||||
|
flags.StringVar(&opts.ipv6address, "ip6", "", "IPv6 Address")
|
||||||
|
flags.Var(&opts.links, "link", "Add link to another container")
|
||||||
|
flags.StringSliceVar(&opts.aliases, "alias", []string{}, "Add network-scoped alias for the container")
|
||||||
|
flags.StringSliceVar(&opts.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConnect(dockerCli *command.DockerCli, opts connectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
epConfig := &network.EndpointSettings{
|
||||||
|
IPAMConfig: &network.EndpointIPAMConfig{
|
||||||
|
IPv4Address: opts.ipaddress,
|
||||||
|
IPv6Address: opts.ipv6address,
|
||||||
|
LinkLocalIPs: opts.linklocalips,
|
||||||
|
},
|
||||||
|
Links: opts.links.GetAll(),
|
||||||
|
Aliases: opts.aliases,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.NetworkConnect(context.Background(), opts.network, opts.container, epConfig)
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createOptions struct {
|
||||||
|
name string
|
||||||
|
driver string
|
||||||
|
driverOpts opts.MapOpts
|
||||||
|
labels []string
|
||||||
|
internal bool
|
||||||
|
ipv6 bool
|
||||||
|
attachable bool
|
||||||
|
|
||||||
|
ipamDriver string
|
||||||
|
ipamSubnet []string
|
||||||
|
ipamIPRange []string
|
||||||
|
ipamGateway []string
|
||||||
|
ipamAux opts.MapOpts
|
||||||
|
ipamOpt opts.MapOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := createOptions{
|
||||||
|
driverOpts: *opts.NewMapOpts(nil, nil),
|
||||||
|
ipamAux: *opts.NewMapOpts(nil, nil),
|
||||||
|
ipamOpt: *opts.NewMapOpts(nil, nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [OPTIONS] NETWORK",
|
||||||
|
Short: "Create a network",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.name = args[0]
|
||||||
|
return runCreate(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVarP(&opts.driver, "driver", "d", "bridge", "Driver to manage the Network")
|
||||||
|
flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options")
|
||||||
|
flags.StringSliceVar(&opts.labels, "label", []string{}, "Set metadata on a network")
|
||||||
|
flags.BoolVar(&opts.internal, "internal", false, "Restrict external access to the network")
|
||||||
|
flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking")
|
||||||
|
flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment")
|
||||||
|
|
||||||
|
flags.StringVar(&opts.ipamDriver, "ipam-driver", "default", "IP Address Management Driver")
|
||||||
|
flags.StringSliceVar(&opts.ipamSubnet, "subnet", []string{}, "Subnet in CIDR format that represents a network segment")
|
||||||
|
flags.StringSliceVar(&opts.ipamIPRange, "ip-range", []string{}, "Allocate container ip from a sub-range")
|
||||||
|
flags.StringSliceVar(&opts.ipamGateway, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet")
|
||||||
|
|
||||||
|
flags.Var(&opts.ipamAux, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver")
|
||||||
|
flags.Var(&opts.ipamOpt, "ipam-opt", "Set IPAM driver specific options")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCreate(dockerCli *command.DockerCli, opts createOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
ipamCfg, err := consolidateIpam(opts.ipamSubnet, opts.ipamIPRange, opts.ipamGateway, opts.ipamAux.GetAll())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct network create request body
|
||||||
|
nc := types.NetworkCreate{
|
||||||
|
Driver: opts.driver,
|
||||||
|
Options: opts.driverOpts.GetAll(),
|
||||||
|
IPAM: &network.IPAM{
|
||||||
|
Driver: opts.ipamDriver,
|
||||||
|
Config: ipamCfg,
|
||||||
|
Options: opts.ipamOpt.GetAll(),
|
||||||
|
},
|
||||||
|
CheckDuplicate: true,
|
||||||
|
Internal: opts.internal,
|
||||||
|
EnableIPv6: opts.ipv6,
|
||||||
|
Attachable: opts.attachable,
|
||||||
|
Labels: runconfigopts.ConvertKVStringsToMap(opts.labels),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.NetworkCreate(context.Background(), opts.name, nc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", resp.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consolidates the ipam configuration as a group from different related configurations
|
||||||
|
// user can configure network with multiple non-overlapping subnets and hence it is
|
||||||
|
// possible to correlate the various related parameters and consolidate them.
|
||||||
|
// consoidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into
|
||||||
|
// structured ipam data.
|
||||||
|
func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) {
|
||||||
|
if len(subnets) < len(ranges) || len(subnets) < len(gateways) {
|
||||||
|
return nil, fmt.Errorf("every ip-range or gateway must have a corresponding subnet")
|
||||||
|
}
|
||||||
|
iData := map[string]*network.IPAMConfig{}
|
||||||
|
|
||||||
|
// Populate non-overlapping subnets into consolidation map
|
||||||
|
for _, s := range subnets {
|
||||||
|
for k := range iData {
|
||||||
|
ok1, err := subnetMatches(s, k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ok2, err := subnetMatches(k, s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ok1 || ok2 {
|
||||||
|
return nil, fmt.Errorf("multiple overlapping subnet configuration is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and add valid ip ranges
|
||||||
|
for _, r := range ranges {
|
||||||
|
match := false
|
||||||
|
for _, s := range subnets {
|
||||||
|
ok, err := subnetMatches(s, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if iData[s].IPRange != "" {
|
||||||
|
return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s)
|
||||||
|
}
|
||||||
|
d := iData[s]
|
||||||
|
d.IPRange = r
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return nil, fmt.Errorf("no matching subnet for range %s", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and add valid gateways
|
||||||
|
for _, g := range gateways {
|
||||||
|
match := false
|
||||||
|
for _, s := range subnets {
|
||||||
|
ok, err := subnetMatches(s, g)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if iData[s].Gateway != "" {
|
||||||
|
return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s)
|
||||||
|
}
|
||||||
|
d := iData[s]
|
||||||
|
d.Gateway = g
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return nil, fmt.Errorf("no matching subnet for gateway %s", g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and add aux-addresses
|
||||||
|
for key, aa := range auxaddrs {
|
||||||
|
match := false
|
||||||
|
for _, s := range subnets {
|
||||||
|
ok, err := subnetMatches(s, aa)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
iData[s].AuxAddress[key] = aa
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return nil, fmt.Errorf("no matching subnet for aux-address %s", aa)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idl := []network.IPAMConfig{}
|
||||||
|
for _, v := range iData {
|
||||||
|
idl = append(idl, *v)
|
||||||
|
}
|
||||||
|
return idl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func subnetMatches(subnet, data string) (bool, error) {
|
||||||
|
var (
|
||||||
|
ip net.IP
|
||||||
|
)
|
||||||
|
|
||||||
|
_, s, err := net.ParseCIDR(subnet)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Invalid subnet %s : %v", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(data, "/") {
|
||||||
|
ip, _, err = net.ParseCIDR(data)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Invalid cidr %s : %v", data, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ip = net.ParseIP(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Contains(ip), nil
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type disconnectOptions struct {
|
||||||
|
network string
|
||||||
|
container string
|
||||||
|
force bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDisconnectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := disconnectOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "disconnect [OPTIONS] NETWORK CONTAINER",
|
||||||
|
Short: "Disconnect a container from a network",
|
||||||
|
Args: cli.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.network = args[0]
|
||||||
|
opts.container = args[1]
|
||||||
|
return runDisconnect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.force, "force", "f", false, "Force the container to disconnect from a network")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDisconnect(dockerCli *command.DockerCli, opts disconnectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
return client.NetworkDisconnect(context.Background(), opts.network, opts.container, opts.force)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/inspect"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
format string
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts inspectOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect [OPTIONS] NETWORK [NETWORK...]",
|
||||||
|
Short: "Display detailed information on one or more networks",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.names = args
|
||||||
|
return runInspect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
getNetFunc := func(name string) (interface{}, []byte, error) {
|
||||||
|
return client.NetworkInspectWithRaw(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc)
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type byNetworkName []types.NetworkResource
|
||||||
|
|
||||||
|
func (r byNetworkName) Len() int { return len(r) }
|
||||||
|
func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||||
|
func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name }
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
quiet bool
|
||||||
|
noTrunc bool
|
||||||
|
format string
|
||||||
|
filter []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts listOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls [OPTIONS]",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List networks",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runList(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display network IDs")
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output")
|
||||||
|
flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template")
|
||||||
|
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
netFilterArgs := filters.NewArgs()
|
||||||
|
for _, f := range opts.filter {
|
||||||
|
var err error
|
||||||
|
netFilterArgs, err = filters.ParseFlag(f, netFilterArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := types.NetworkListOptions{
|
||||||
|
Filters: netFilterArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
networkResources, err := client.NetworkList(context.Background(), options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f := opts.format
|
||||||
|
if len(f) == 0 {
|
||||||
|
if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
|
||||||
|
f = dockerCli.ConfigFile().NetworksFormat
|
||||||
|
} else {
|
||||||
|
f = "table"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(byNetworkName(networkResources))
|
||||||
|
|
||||||
|
networksCtx := formatter.NetworkContext{
|
||||||
|
Context: formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: f,
|
||||||
|
Quiet: opts.quiet,
|
||||||
|
Trunc: !opts.noTrunc,
|
||||||
|
},
|
||||||
|
Networks: networkResources,
|
||||||
|
}
|
||||||
|
|
||||||
|
networksCtx.Write()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "rm NETWORK [NETWORK...]",
|
||||||
|
Aliases: []string{"remove"},
|
||||||
|
Short: "Remove one or more networks",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRemove(dockerCli, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRemove(dockerCli *command.DockerCli, networks []string) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
status := 0
|
||||||
|
|
||||||
|
for _, name := range networks {
|
||||||
|
if err := client.NetworkRemove(ctx, name); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "%s\n", err)
|
||||||
|
status = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != 0 {
|
||||||
|
return cli.StatusError{StatusCode: status}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
apiclient "github.com/docker/docker/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewNodeCommand returns a cobra command for `node` subcommands
|
||||||
|
func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "node",
|
||||||
|
Short: "Manage Docker Swarm nodes",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
newDemoteCommand(dockerCli),
|
||||||
|
newInspectCommand(dockerCli),
|
||||||
|
newListCommand(dockerCli),
|
||||||
|
newPromoteCommand(dockerCli),
|
||||||
|
newRemoveCommand(dockerCli),
|
||||||
|
newPsCommand(dockerCli),
|
||||||
|
newUpdateCommand(dockerCli),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference returns the reference of a node. The special value "self" for a node
|
||||||
|
// reference is mapped to the current node, hence the node ID is retrieved using
|
||||||
|
// the `/info` endpoint.
|
||||||
|
func Reference(ctx context.Context, client apiclient.APIClient, ref string) (string, error) {
|
||||||
|
if ref == "self" {
|
||||||
|
info, err := client.Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return info.Swarm.NodeID, nil
|
||||||
|
}
|
||||||
|
return ref, nil
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDemoteCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "demote NODE [NODE...]",
|
||||||
|
Short: "Demote one or more nodes from manager in the swarm",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDemote(dockerCli, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDemote(dockerCli *command.DockerCli, nodes []string) error {
|
||||||
|
demote := func(node *swarm.Node) error {
|
||||||
|
if node.Spec.Role == swarm.NodeRoleWorker {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID)
|
||||||
|
return errNoRoleChange
|
||||||
|
}
|
||||||
|
node.Spec.Role = swarm.NodeRoleWorker
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
success := func(nodeID string) {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID)
|
||||||
|
}
|
||||||
|
return updateNodes(dockerCli, nodes, demote, success)
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/inspect"
|
||||||
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
nodeIds []string
|
||||||
|
format string
|
||||||
|
pretty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts inspectOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect [OPTIONS] self|NODE [NODE...]",
|
||||||
|
Short: "Display detailed information on one or more nodes",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.nodeIds = args
|
||||||
|
return runInspect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||||
|
flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
getRef := func(ref string) (interface{}, []byte, error) {
|
||||||
|
nodeRef, err := Reference(ctx, client, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
node, _, err := client.NodeInspectWithRaw(ctx, nodeRef)
|
||||||
|
return node, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.pretty {
|
||||||
|
return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef)
|
||||||
|
}
|
||||||
|
return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
|
||||||
|
for idx, ref := range refs {
|
||||||
|
obj, _, err := getRef(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printNode(out, obj.(swarm.Node))
|
||||||
|
|
||||||
|
// TODO: better way to do this?
|
||||||
|
// print extra space between objects, but not after the last one
|
||||||
|
if idx+1 != len(refs) {
|
||||||
|
fmt.Fprintf(out, "\n\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(out, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use a template
|
||||||
|
func printNode(out io.Writer, node swarm.Node) {
|
||||||
|
fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID)
|
||||||
|
ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name)
|
||||||
|
if node.Spec.Labels != nil {
|
||||||
|
fmt.Fprintln(out, "Labels:")
|
||||||
|
for k, v := range node.Spec.Labels {
|
||||||
|
fmt.Fprintf(out, " - %s = %s\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname)
|
||||||
|
fmt.Fprintf(out, "Joined at:\t\t%s\n", command.PrettyPrint(node.CreatedAt))
|
||||||
|
fmt.Fprintln(out, "Status:")
|
||||||
|
fmt.Fprintf(out, " State:\t\t\t%s\n", command.PrettyPrint(node.Status.State))
|
||||||
|
ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", command.PrettyPrint(node.Status.Message))
|
||||||
|
fmt.Fprintf(out, " Availability:\t\t%s\n", command.PrettyPrint(node.Spec.Availability))
|
||||||
|
|
||||||
|
if node.ManagerStatus != nil {
|
||||||
|
fmt.Fprintln(out, "Manager Status:")
|
||||||
|
fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr)
|
||||||
|
fmt.Fprintf(out, " Raft Status:\t\t%s\n", command.PrettyPrint(node.ManagerStatus.Reachability))
|
||||||
|
leader := "No"
|
||||||
|
if node.ManagerStatus.Leader {
|
||||||
|
leader = "Yes"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, " Leader:\t\t%s\n", leader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(out, "Platform:")
|
||||||
|
fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS)
|
||||||
|
fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture)
|
||||||
|
|
||||||
|
fmt.Fprintln(out, "Resources:")
|
||||||
|
fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9)
|
||||||
|
fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes)))
|
||||||
|
|
||||||
|
var pluginTypes []string
|
||||||
|
pluginNamesByType := map[string][]string{}
|
||||||
|
for _, p := range node.Description.Engine.Plugins {
|
||||||
|
// append to pluginTypes only if not done previously
|
||||||
|
if _, ok := pluginNamesByType[p.Type]; !ok {
|
||||||
|
pluginTypes = append(pluginTypes, p.Type)
|
||||||
|
}
|
||||||
|
pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pluginTypes) > 0 {
|
||||||
|
fmt.Fprintln(out, "Plugins:")
|
||||||
|
sort.Strings(pluginTypes) // ensure stable output
|
||||||
|
for _, pluginType := range pluginTypes {
|
||||||
|
fmt.Fprintf(out, " %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion)
|
||||||
|
|
||||||
|
if len(node.Description.Engine.Labels) != 0 {
|
||||||
|
fmt.Fprintln(out, "Engine Labels:")
|
||||||
|
for k, v := range node.Description.Engine.Labels {
|
||||||
|
fmt.Fprintf(out, " - %s = %s", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
quiet bool
|
||||||
|
filter opts.FilterOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls [OPTIONS]",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List nodes in the swarm",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runList(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||||
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
nodes, err := client.NodeList(
|
||||||
|
ctx,
|
||||||
|
types.NodeListOptions{Filter: opts.filter.Value()})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := client.Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := dockerCli.Out()
|
||||||
|
if opts.quiet {
|
||||||
|
printQuiet(out, nodes)
|
||||||
|
} else {
|
||||||
|
printTable(out, nodes, info)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
|
||||||
|
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
|
||||||
|
|
||||||
|
// Ignore flushing errors
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
fmt.Fprintf(writer, listItemFmt, "ID", "HOSTNAME", "STATUS", "AVAILABILITY", "MANAGER STATUS")
|
||||||
|
for _, node := range nodes {
|
||||||
|
name := node.Description.Hostname
|
||||||
|
availability := string(node.Spec.Availability)
|
||||||
|
|
||||||
|
reachability := ""
|
||||||
|
if node.ManagerStatus != nil {
|
||||||
|
if node.ManagerStatus.Leader {
|
||||||
|
reachability = "Leader"
|
||||||
|
} else {
|
||||||
|
reachability = string(node.ManagerStatus.Reachability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ID := node.ID
|
||||||
|
if node.ID == info.Swarm.NodeID {
|
||||||
|
ID = ID + " *"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(
|
||||||
|
writer,
|
||||||
|
listItemFmt,
|
||||||
|
ID,
|
||||||
|
name,
|
||||||
|
command.PrettyPrint(string(node.Status.State)),
|
||||||
|
command.PrettyPrint(availability),
|
||||||
|
command.PrettyPrint(reachability))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printQuiet(out io.Writer, nodes []swarm.Node) {
|
||||||
|
for _, node := range nodes {
|
||||||
|
fmt.Fprintln(out, node.ID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodeOptions struct {
|
||||||
|
annotations
|
||||||
|
role string
|
||||||
|
availability string
|
||||||
|
}
|
||||||
|
|
||||||
|
type annotations struct {
|
||||||
|
name string
|
||||||
|
labels opts.ListOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNodeOptions() *nodeOptions {
|
||||||
|
return &nodeOptions{
|
||||||
|
annotations: annotations{
|
||||||
|
labels: opts.NewListOpts(nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) {
|
||||||
|
var spec swarm.NodeSpec
|
||||||
|
|
||||||
|
spec.Annotations.Name = opts.annotations.name
|
||||||
|
spec.Annotations.Labels = runconfigopts.ConvertKVStringsToMap(opts.annotations.labels.GetAll())
|
||||||
|
|
||||||
|
switch swarm.NodeRole(strings.ToLower(opts.role)) {
|
||||||
|
case swarm.NodeRoleWorker:
|
||||||
|
spec.Role = swarm.NodeRoleWorker
|
||||||
|
case swarm.NodeRoleManager:
|
||||||
|
spec.Role = swarm.NodeRoleManager
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch swarm.NodeAvailability(strings.ToLower(opts.availability)) {
|
||||||
|
case swarm.NodeAvailabilityActive:
|
||||||
|
spec.Availability = swarm.NodeAvailabilityActive
|
||||||
|
case swarm.NodeAvailabilityPause:
|
||||||
|
spec.Availability = swarm.NodeAvailabilityPause
|
||||||
|
case swarm.NodeAvailabilityDrain:
|
||||||
|
spec.Availability = swarm.NodeAvailabilityDrain
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, nil
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPromoteCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "promote NODE [NODE...]",
|
||||||
|
Short: "Promote one or more nodes to manager in the swarm",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runPromote(dockerCli, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPromote(dockerCli *command.DockerCli, nodes []string) error {
|
||||||
|
promote := func(node *swarm.Node) error {
|
||||||
|
if node.Spec.Role == swarm.NodeRoleManager {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID)
|
||||||
|
return errNoRoleChange
|
||||||
|
}
|
||||||
|
node.Spec.Role = swarm.NodeRoleManager
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
success := func(nodeID string) {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID)
|
||||||
|
}
|
||||||
|
return updateNodes(dockerCli, nodes, promote, success)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/idresolver"
|
||||||
|
"github.com/docker/docker/cli/command/task"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type psOptions struct {
|
||||||
|
nodeID string
|
||||||
|
noResolve bool
|
||||||
|
noTrunc bool
|
||||||
|
filter opts.FilterOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := psOptions{filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ps [OPTIONS] [NODE]",
|
||||||
|
Short: "List tasks running on a node, defaults to current node",
|
||||||
|
Args: cli.RequiresRangeArgs(0, 1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.nodeID = "self"
|
||||||
|
|
||||||
|
if len(args) != 0 {
|
||||||
|
opts.nodeID = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return runPs(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||||
|
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||||
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPs(dockerCli *command.DockerCli, opts psOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
nodeRef, err := Reference(ctx, client, opts.nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node, _, err := client.NodeInspectWithRaw(ctx, nodeRef)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := opts.filter.Value()
|
||||||
|
filter.Add("node", node.ID)
|
||||||
|
tasks, err := client.TaskList(
|
||||||
|
ctx,
|
||||||
|
types.TaskListOptions{Filter: filter})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type removeOptions struct {
|
||||||
|
force bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := removeOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm [OPTIONS] NODE [NODE...]",
|
||||||
|
Aliases: []string{"remove"},
|
||||||
|
Short: "Remove one or more nodes from the swarm",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRemove(dockerCli, args, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVar(&opts.force, "force", false, "Force remove an active node")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRemove(dockerCli *command.DockerCli, args []string, opts removeOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, nodeID := range args {
|
||||||
|
err := client.NodeRemove(ctx, nodeID, types.NodeRemoveOptions{Force: opts.force})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoRoleChange = errors.New("role was already set to the requested value")
|
||||||
|
)
|
||||||
|
|
||||||
|
func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
nodeOpts := newNodeOptions()
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update [OPTIONS] NODE",
|
||||||
|
Short: "Update a node",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runUpdate(dockerCli, cmd.Flags(), args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVar(&nodeOpts.role, flagRole, "", "Role of the node (worker/manager)")
|
||||||
|
flags.StringVar(&nodeOpts.availability, flagAvailability, "", "Availability of the node (active/pause/drain)")
|
||||||
|
flags.Var(&nodeOpts.annotations.labels, flagLabelAdd, "Add or update a node label (key=value)")
|
||||||
|
labelKeys := opts.NewListOpts(nil)
|
||||||
|
flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, nodeID string) error {
|
||||||
|
success := func(_ string) {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), nodeID)
|
||||||
|
}
|
||||||
|
return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNodes(dockerCli *command.DockerCli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, nodeID := range nodes {
|
||||||
|
node, _, err := client.NodeInspectWithRaw(ctx, nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mergeNode(&node)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoRoleChange {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success(nodeID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error {
|
||||||
|
return func(node *swarm.Node) error {
|
||||||
|
spec := &node.Spec
|
||||||
|
|
||||||
|
if flags.Changed(flagRole) {
|
||||||
|
str, err := flags.GetString(flagRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
spec.Role = swarm.NodeRole(str)
|
||||||
|
}
|
||||||
|
if flags.Changed(flagAvailability) {
|
||||||
|
str, err := flags.GetString(flagAvailability)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
spec.Availability = swarm.NodeAvailability(str)
|
||||||
|
}
|
||||||
|
if spec.Annotations.Labels == nil {
|
||||||
|
spec.Annotations.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
|
if flags.Changed(flagLabelAdd) {
|
||||||
|
labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll()
|
||||||
|
for k, v := range runconfigopts.ConvertKVStringsToMap(labels) {
|
||||||
|
spec.Annotations.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flags.Changed(flagLabelRemove) {
|
||||||
|
keys := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll()
|
||||||
|
for _, k := range keys {
|
||||||
|
// if a key doesn't exist, fail the command explicitly
|
||||||
|
if _, exists := spec.Annotations.Labels[k]; !exists {
|
||||||
|
return fmt.Errorf("key %s doesn't exist in node's labels", k)
|
||||||
|
}
|
||||||
|
delete(spec.Annotations.Labels, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagRole = "role"
|
||||||
|
flagAvailability = "availability"
|
||||||
|
flagLabelAdd = "label-add"
|
||||||
|
flagLabelRemove = "label-rm"
|
||||||
|
)
|
|
@ -0,0 +1,69 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/pkg/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutStream is an output stream used by the DockerCli to write normal program
|
||||||
|
// output.
|
||||||
|
type OutStream struct {
|
||||||
|
out io.Writer
|
||||||
|
fd uintptr
|
||||||
|
isTerminal bool
|
||||||
|
state *term.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutStream) Write(p []byte) (int, error) {
|
||||||
|
return o.out.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FD returns the file descriptor number for this stream
|
||||||
|
func (o *OutStream) FD() uintptr {
|
||||||
|
return o.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if this stream is connected to a terminal
|
||||||
|
func (o *OutStream) IsTerminal() bool {
|
||||||
|
return o.isTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRawTerminal sets raw mode on the output terminal
|
||||||
|
func (o *OutStream) SetRawTerminal() (err error) {
|
||||||
|
if os.Getenv("NORAW") != "" || !o.isTerminal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
o.state, err = term.SetRawTerminalOutput(o.fd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreTerminal restores normal mode to the terminal
|
||||||
|
func (o *OutStream) RestoreTerminal() {
|
||||||
|
if o.state != nil {
|
||||||
|
term.RestoreTerminal(o.fd, o.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTtySize returns the height and width in characters of the tty
|
||||||
|
func (o *OutStream) GetTtySize() (int, int) {
|
||||||
|
if !o.isTerminal {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
ws, err := term.GetWinsize(o.fd)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("Error getting size: %s", err)
|
||||||
|
if ws == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return int(ws.Height), int(ws.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOutStream returns a new OutStream object from a Writer
|
||||||
|
func NewOutStream(out io.Writer) *OutStream {
|
||||||
|
fd, isTerminal := term.GetFdInfo(out)
|
||||||
|
return &OutStream{out: out, fd: fd, isTerminal: isTerminal}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// +build !experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPluginCommand returns a cobra command for `plugin` subcommands
|
||||||
|
func NewPluginCommand(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPluginCommand returns a cobra command for `plugin` subcommands
|
||||||
|
func NewPluginCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "plugin",
|
||||||
|
Short: "Manage Docker plugins",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newDisableCommand(dockerCli),
|
||||||
|
newEnableCommand(dockerCli),
|
||||||
|
newInspectCommand(dockerCli),
|
||||||
|
newInstallCommand(dockerCli),
|
||||||
|
newListCommand(dockerCli),
|
||||||
|
newRemoveCommand(dockerCli),
|
||||||
|
newSetCommand(dockerCli),
|
||||||
|
newPushCommand(dockerCli),
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(cmd)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDisableCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "disable PLUGIN",
|
||||||
|
Short: "Disable a plugin",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDisable(dockerCli, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDisable(dockerCli *client.DockerCli, name string) error {
|
||||||
|
named, err := reference.ParseNamed(name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
if err := dockerCli.Client().PluginDisable(context.Background(), ref.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(dockerCli.Out(), name)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newEnableCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "enable PLUGIN",
|
||||||
|
Short: "Enable a plugin",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runEnable(dockerCli, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runEnable(dockerCli *client.DockerCli, name string) error {
|
||||||
|
named, err := reference.ParseNamed(name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
if err := dockerCli.Client().PluginEnable(context.Background(), ref.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(dockerCli.Out(), name)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/api/client/inspect"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
pluginNames []string
|
||||||
|
format string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
var opts inspectOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]",
|
||||||
|
Short: "Display detailed information on one or more plugins",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.pluginNames = args
|
||||||
|
return runInspect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
getRef := func(name string) (interface{}, []byte, error) {
|
||||||
|
named, err := reference.ParseNamed(name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.PluginInspectWithRaw(ctx, ref.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef)
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pluginOptions struct {
|
||||||
|
name string
|
||||||
|
grantPerms bool
|
||||||
|
disable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInstallCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
var options pluginOptions
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "install [OPTIONS] PLUGIN",
|
||||||
|
Short: "Install a plugin",
|
||||||
|
Args: cli.ExactArgs(1), // TODO: allow for set args
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
options.name = args[0]
|
||||||
|
return runInstall(dockerCli, options)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin")
|
||||||
|
flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInstall(dockerCli *client.DockerCli, opts pluginOptions) error {
|
||||||
|
named, err := reference.ParseNamed(opts.name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(named)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||||
|
|
||||||
|
encodedAuth, err := client.EncodeAuthToBase64(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
registryAuthFunc := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "plugin install")
|
||||||
|
|
||||||
|
options := types.PluginInstallOptions{
|
||||||
|
RegistryAuth: encodedAuth,
|
||||||
|
Disabled: opts.disable,
|
||||||
|
AcceptAllPermissions: opts.grantPerms,
|
||||||
|
AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name),
|
||||||
|
// TODO: Rename PrivilegeFunc, it has nothing to do with privileges
|
||||||
|
PrivilegeFunc: registryAuthFunc,
|
||||||
|
}
|
||||||
|
if err := dockerCli.Client().PluginInstall(ctx, ref.String(), options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(dockerCli.Out(), opts.name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptPrivileges(dockerCli *client.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) {
|
||||||
|
return func(privileges types.PluginPrivileges) (bool, error) {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Plugin %q is requesting the following privileges:\n", name)
|
||||||
|
for _, privilege := range privileges {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(dockerCli.Out(), "Do you grant the above permissions? [y/N] ")
|
||||||
|
reader := bufio.NewReader(dockerCli.In())
|
||||||
|
line, _, err := reader.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strings.ToLower(string(line)) == "y", nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/pkg/stringutils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
noTrunc bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
var opts listOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls [OPTIONS]",
|
||||||
|
Short: "List plugins",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runList(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
||||||
|
plugins, err := dockerCli.Client().PluginList(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||||
|
fmt.Fprintf(w, "NAME \tTAG \tDESCRIPTION\tENABLED")
|
||||||
|
fmt.Fprintf(w, "\n")
|
||||||
|
|
||||||
|
for _, p := range plugins {
|
||||||
|
desc := strings.Replace(p.Manifest.Description, "\n", " ", -1)
|
||||||
|
desc = strings.Replace(desc, "\r", " ", -1)
|
||||||
|
if !opts.noTrunc {
|
||||||
|
desc = stringutils.Ellipsis(desc, 45)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", p.Name, p.Tag, desc, p.Enabled)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newPushCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "push PLUGIN",
|
||||||
|
Short: "Push a plugin",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runPush(dockerCli, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPush(dockerCli *client.DockerCli, name string) error {
|
||||||
|
named, err := reference.ParseNamed(name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(named)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||||
|
|
||||||
|
encodedAuth, err := client.EncodeAuthToBase64(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rmOptions struct {
|
||||||
|
force bool
|
||||||
|
|
||||||
|
plugins []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
var opts rmOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm [OPTIONS] PLUGIN [PLUGIN...]",
|
||||||
|
Short: "Remove one or more plugins",
|
||||||
|
Aliases: []string{"remove"},
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.plugins = args
|
||||||
|
return runRemove(dockerCli, &opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of an active plugin")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRemove(dockerCli *client.DockerCli, opts *rmOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs cli.Errors
|
||||||
|
for _, name := range opts.plugins {
|
||||||
|
named, err := reference.ParseNamed(name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
// TODO: pass names to api instead of making multiple api calls
|
||||||
|
if err := dockerCli.Client().PluginRemove(ctx, ref.String(), types.PluginRemoveOptions{Force: opts.force}); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintln(dockerCli.Out(), name)
|
||||||
|
}
|
||||||
|
// Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value.
|
||||||
|
if errs != nil {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// +build experimental
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/client"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSetCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "set PLUGIN key1=value1 [key2=value2...]",
|
||||||
|
Short: "Change settings for a plugin",
|
||||||
|
Args: cli.RequiresMinArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runSet(dockerCli, args[0], args[1:])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSet(dockerCli *client.DockerCli, name string, args []string) error {
|
||||||
|
named, err := reference.ParseNamed(name) // FIXME: validate
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(named) {
|
||||||
|
named = reference.WithDefaultTag(named)
|
||||||
|
}
|
||||||
|
ref, ok := named.(reference.NamedTagged)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid name: %s", named.String())
|
||||||
|
}
|
||||||
|
return dockerCli.Client().PluginSet(context.Background(), ref.String(), args)
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
|
"github.com/docker/docker/pkg/term"
|
||||||
|
"github.com/docker/docker/reference"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ElectAuthServer returns the default registry to use (by asking the daemon)
|
||||||
|
func (cli *DockerCli) ElectAuthServer(ctx context.Context) string {
|
||||||
|
// The daemon `/info` endpoint informs us of the default registry being
|
||||||
|
// used. This is essential in cross-platforms environment, where for
|
||||||
|
// example a Linux client might be interacting with a Windows daemon, hence
|
||||||
|
// the default registry URL might be Windows specific.
|
||||||
|
serverAddress := registry.IndexServer
|
||||||
|
if info, err := cli.client.Info(ctx); err != nil {
|
||||||
|
fmt.Fprintf(cli.out, "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress)
|
||||||
|
} else {
|
||||||
|
serverAddress = info.IndexServerAddress
|
||||||
|
}
|
||||||
|
return serverAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload
|
||||||
|
func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
|
||||||
|
buf, err := json.Marshal(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.EncodeToString(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
||||||
|
// for the given command.
|
||||||
|
func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
|
||||||
|
return func() (string, error) {
|
||||||
|
fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName)
|
||||||
|
indexServer := registry.GetAuthConfigKey(index)
|
||||||
|
isDefaultRegistry := indexServer == cli.ElectAuthServer(context.Background())
|
||||||
|
authConfig, err := cli.ConfigureAuth("", "", indexServer, isDefaultRegistry)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return EncodeAuthToBase64(authConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) {
|
||||||
|
if configDefault == "" {
|
||||||
|
fmt.Fprintf(cli.out, "%s: ", prompt)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the
|
||||||
|
// default index, it uses the default index name for the daemon's platform,
|
||||||
|
// not the client's platform.
|
||||||
|
func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||||
|
configKey := index.Name
|
||||||
|
if index.Official {
|
||||||
|
configKey = cli.ElectAuthServer(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, _ := GetCredentials(cli.configFile, configKey)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrieveAuthConfigs return all credentials.
|
||||||
|
func (cli *DockerCli) RetrieveAuthConfigs() map[string]types.AuthConfig {
|
||||||
|
acs, _ := GetAllCredentials(cli.configFile)
|
||||||
|
return acs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigureAuth returns an AuthConfig from the specified user, password and server.
|
||||||
|
func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) {
|
||||||
|
// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cli.in = NewInStream(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDefaultRegistry {
|
||||||
|
serverAddress = registry.ConvertToHostname(serverAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
authconfig, err := GetCredentials(cli.configFile, serverAddress)
|
||||||
|
if err != nil {
|
||||||
|
return authconfig, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some links documenting this:
|
||||||
|
// - https://code.google.com/archive/p/mintty/issues/56
|
||||||
|
// - https://github.com/docker/docker/issues/15272
|
||||||
|
// - https://mintty.github.io/ (compatibility)
|
||||||
|
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
||||||
|
// will hit this if you attempt docker login from mintty where stdin
|
||||||
|
// is a pipe, not a character based console.
|
||||||
|
if flPassword == "" && !cli.In().IsTerminal() {
|
||||||
|
return authconfig, fmt.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||||
|
}
|
||||||
|
|
||||||
|
authconfig.Username = strings.TrimSpace(authconfig.Username)
|
||||||
|
|
||||||
|
if flUser = strings.TrimSpace(flUser); flUser == "" {
|
||||||
|
if isDefaultRegistry {
|
||||||
|
// if this is a default registry (docker hub), then display the following message.
|
||||||
|
fmt.Fprintln(cli.out, "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.")
|
||||||
|
}
|
||||||
|
cli.promptWithDefault("Username", authconfig.Username)
|
||||||
|
flUser = readInput(cli.in, cli.out)
|
||||||
|
flUser = strings.TrimSpace(flUser)
|
||||||
|
if flUser == "" {
|
||||||
|
flUser = authconfig.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flUser == "" {
|
||||||
|
return authconfig, fmt.Errorf("Error: Non-null Username Required")
|
||||||
|
}
|
||||||
|
if flPassword == "" {
|
||||||
|
oldState, err := term.SaveState(cli.In().FD())
|
||||||
|
if err != nil {
|
||||||
|
return authconfig, err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(cli.out, "Password: ")
|
||||||
|
term.DisableEcho(cli.In().FD(), oldState)
|
||||||
|
|
||||||
|
flPassword = readInput(cli.in, cli.out)
|
||||||
|
fmt.Fprint(cli.out, "\n")
|
||||||
|
|
||||||
|
term.RestoreTerminal(cli.In().FD(), oldState)
|
||||||
|
if flPassword == "" {
|
||||||
|
return authconfig, fmt.Errorf("Error: Password Required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authconfig.Username = flUser
|
||||||
|
authconfig.Password = flPassword
|
||||||
|
authconfig.ServerAddress = serverAddress
|
||||||
|
authconfig.IdentityToken = ""
|
||||||
|
|
||||||
|
return authconfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAuthConfigFromImage retrieves that AuthConfig using the image string
|
||||||
|
func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image string) (types.AuthConfig, error) {
|
||||||
|
registryRef, err := reference.ParseNamed(image)
|
||||||
|
if err != nil {
|
||||||
|
return types.AuthConfig{}, err
|
||||||
|
}
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(registryRef)
|
||||||
|
if err != nil {
|
||||||
|
return types.AuthConfig{}, err
|
||||||
|
}
|
||||||
|
authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||||
|
return authConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image
|
||||||
|
func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image string) (string, error) {
|
||||||
|
// Retrieve encoded auth token from the image reference
|
||||||
|
authConfig, err := cli.resolveAuthConfigFromImage(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
encodedAuth, err := EncodeAuthToBase64(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return encodedAuth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInput(in io.Reader, out io.Writer) string {
|
||||||
|
reader := bufio.NewReader(in)
|
||||||
|
line, _, err := reader.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(out, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return string(line)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginOptions struct {
|
||||||
|
serverAddress string
|
||||||
|
user string
|
||||||
|
password string
|
||||||
|
email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoginCommand creates a new `docker login` command
|
||||||
|
func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts loginOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "login [OPTIONS] [SERVER]",
|
||||||
|
Short: "Log in to a Docker registry.",
|
||||||
|
Long: "Log in to a Docker registry.\nIf no server is specified, the default is defined by the daemon.",
|
||||||
|
Args: cli.RequiresMaxArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts.serverAddress = args[0]
|
||||||
|
}
|
||||||
|
return runLogin(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.StringVarP(&opts.user, "username", "u", "", "Username")
|
||||||
|
flags.StringVarP(&opts.password, "password", "p", "", "Password")
|
||||||
|
|
||||||
|
// Deprecated in 1.11: Should be removed in docker 1.13
|
||||||
|
flags.StringVarP(&opts.email, "email", "e", "", "Email")
|
||||||
|
flags.MarkDeprecated("email", "will be removed in 1.13.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogin(dockerCli *command.DockerCli, opts loginOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
clnt := dockerCli.Client()
|
||||||
|
|
||||||
|
var (
|
||||||
|
serverAddress string
|
||||||
|
authServer = dockerCli.ElectAuthServer(ctx)
|
||||||
|
)
|
||||||
|
if opts.serverAddress != "" {
|
||||||
|
serverAddress = opts.serverAddress
|
||||||
|
} else {
|
||||||
|
serverAddress = authServer
|
||||||
|
}
|
||||||
|
|
||||||
|
isDefaultRegistry := serverAddress == authServer
|
||||||
|
|
||||||
|
authConfig, err := dockerCli.ConfigureAuth(opts.user, opts.password, serverAddress, isDefaultRegistry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response, err := clnt.RegistryLogin(ctx, authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.IdentityToken != "" {
|
||||||
|
authConfig.Password = ""
|
||||||
|
authConfig.IdentityToken = response.IdentityToken
|
||||||
|
}
|
||||||
|
if err := command.StoreCredentials(dockerCli.ConfigFile(), authConfig); err != nil {
|
||||||
|
return fmt.Errorf("Error saving credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Status != "" {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), response.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLogoutCommand creates a new `docker login` command
|
||||||
|
func NewLogoutCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "logout [SERVER]",
|
||||||
|
Short: "Log out from a Docker registry.",
|
||||||
|
Long: "Log out from a Docker registry.\nIf no server is specified, the default is defined by the daemon.",
|
||||||
|
Args: cli.RequiresMaxArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
var serverAddress string
|
||||||
|
if len(args) > 0 {
|
||||||
|
serverAddress = args[0]
|
||||||
|
}
|
||||||
|
return runLogout(dockerCli, serverAddress)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogout(dockerCli *command.DockerCli, serverAddress string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
var isDefaultRegistry bool
|
||||||
|
|
||||||
|
if serverAddress == "" {
|
||||||
|
serverAddress = dockerCli.ElectAuthServer(ctx)
|
||||||
|
isDefaultRegistry = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
loggedIn bool
|
||||||
|
regsToLogout []string
|
||||||
|
hostnameAddress = serverAddress
|
||||||
|
regsToTry = []string{serverAddress}
|
||||||
|
)
|
||||||
|
if !isDefaultRegistry {
|
||||||
|
hostnameAddress = registry.ConvertToHostname(serverAddress)
|
||||||
|
// the tries below are kept for backward compatibily where a user could have
|
||||||
|
// saved the registry in one of the following format.
|
||||||
|
regsToTry = append(regsToTry, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we're logged in based on the records in the config file
|
||||||
|
// which means it couldn't have user/pass cause they may be in the creds store
|
||||||
|
for _, s := range regsToTry {
|
||||||
|
if _, ok := dockerCli.ConfigFile().AuthConfigs[s]; ok {
|
||||||
|
loggedIn = true
|
||||||
|
regsToLogout = append(regsToLogout, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loggedIn {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Not logged in to %s\n", hostnameAddress)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress)
|
||||||
|
for _, r := range regsToLogout {
|
||||||
|
if err := command.EraseCredentials(dockerCli.ConfigFile(), r); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServiceCommand returns a cobra command for `service` subcommands
|
||||||
|
func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "service",
|
||||||
|
Short: "Manage Docker services",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(
|
||||||
|
newCreateCommand(dockerCli),
|
||||||
|
newInspectCommand(dockerCli),
|
||||||
|
newPsCommand(dockerCli),
|
||||||
|
newListCommand(dockerCli),
|
||||||
|
newRemoveCommand(dockerCli),
|
||||||
|
newScaleCommand(dockerCli),
|
||||||
|
newUpdateCommand(dockerCli),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := newServiceOptions()
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||||
|
Short: "Create a new service",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.image = args[0]
|
||||||
|
if len(args) > 1 {
|
||||||
|
opts.args = args[1:]
|
||||||
|
}
|
||||||
|
return runCreate(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)")
|
||||||
|
addServiceFlags(cmd, opts)
|
||||||
|
|
||||||
|
flags.VarP(&opts.labels, flagLabel, "l", "Service labels")
|
||||||
|
flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels")
|
||||||
|
flags.VarP(&opts.env, flagEnv, "e", "Set environment variables")
|
||||||
|
flags.Var(&opts.mounts, flagMount, "Attach a mount to the service")
|
||||||
|
flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
|
||||||
|
flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
|
||||||
|
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
|
||||||
|
|
||||||
|
flags.SetInterspersed(false)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
|
||||||
|
apiClient := dockerCli.Client()
|
||||||
|
createOpts := types.ServiceCreateOptions{}
|
||||||
|
|
||||||
|
service, err := opts.ToService()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// only send auth if flag was set
|
||||||
|
if opts.registryAuth {
|
||||||
|
// Retrieve encoded auth token from the image reference
|
||||||
|
encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, opts.image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
createOpts.EncodedRegistryAuth = encodedAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := apiClient.ServiceCreate(ctx, service, createOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/inspect"
|
||||||
|
apiclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inspectOptions struct {
|
||||||
|
refs []string
|
||||||
|
format string
|
||||||
|
pretty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
var opts inspectOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "inspect [OPTIONS] SERVICE [SERVICE...]",
|
||||||
|
Short: "Display detailed information on one or more services",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.refs = args
|
||||||
|
|
||||||
|
if opts.pretty && len(opts.format) > 0 {
|
||||||
|
return fmt.Errorf("--format is incompatible with human friendly format")
|
||||||
|
}
|
||||||
|
return runInspect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||||
|
flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
getRef := func(ref string) (interface{}, []byte, error) {
|
||||||
|
service, _, err := client.ServiceInspectWithRaw(ctx, ref)
|
||||||
|
if err == nil || !apiclient.IsErrServiceNotFound(err) {
|
||||||
|
return service, nil, err
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("Error: no such service: %s", ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.pretty {
|
||||||
|
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
|
||||||
|
for idx, ref := range refs {
|
||||||
|
obj, _, err := getRef(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printService(out, obj.(swarm.Service))
|
||||||
|
|
||||||
|
// TODO: better way to do this?
|
||||||
|
// print extra space between objects, but not after the last one
|
||||||
|
if idx+1 != len(refs) {
|
||||||
|
fmt.Fprintf(out, "\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use a template
|
||||||
|
func printService(out io.Writer, service swarm.Service) {
|
||||||
|
fmt.Fprintf(out, "ID:\t\t%s\n", service.ID)
|
||||||
|
fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name)
|
||||||
|
if service.Spec.Labels != nil {
|
||||||
|
fmt.Fprintln(out, "Labels:")
|
||||||
|
for k, v := range service.Spec.Labels {
|
||||||
|
fmt.Fprintf(out, " - %s=%s\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Spec.Mode.Global != nil {
|
||||||
|
fmt.Fprintln(out, "Mode:\t\tGlobal")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(out, "Mode:\t\tReplicated")
|
||||||
|
if service.Spec.Mode.Replicated.Replicas != nil {
|
||||||
|
fmt.Fprintf(out, " Replicas:\t%d\n", *service.Spec.Mode.Replicated.Replicas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.UpdateStatus.State != "" {
|
||||||
|
fmt.Fprintln(out, "Update status:")
|
||||||
|
fmt.Fprintf(out, " State:\t\t%s\n", service.UpdateStatus.State)
|
||||||
|
fmt.Fprintf(out, " Started:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.StartedAt))))
|
||||||
|
if service.UpdateStatus.State == swarm.UpdateStateCompleted {
|
||||||
|
fmt.Fprintf(out, " Completed:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.CompletedAt))))
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, " Message:\t%s\n", service.UpdateStatus.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(out, "Placement:")
|
||||||
|
if service.Spec.TaskTemplate.Placement != nil && len(service.Spec.TaskTemplate.Placement.Constraints) > 0 {
|
||||||
|
ioutils.FprintfIfNotEmpty(out, " Constraints\t: %s\n", strings.Join(service.Spec.TaskTemplate.Placement.Constraints, ", "))
|
||||||
|
}
|
||||||
|
if service.Spec.UpdateConfig != nil {
|
||||||
|
fmt.Fprintf(out, "UpdateConfig:\n")
|
||||||
|
fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism)
|
||||||
|
if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 {
|
||||||
|
fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, " On failure:\t%s\n", service.Spec.UpdateConfig.FailureAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(out, "ContainerSpec:\n")
|
||||||
|
printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec)
|
||||||
|
|
||||||
|
resources := service.Spec.TaskTemplate.Resources
|
||||||
|
if resources != nil {
|
||||||
|
fmt.Fprintln(out, "Resources:")
|
||||||
|
printResources := func(out io.Writer, requirement string, r *swarm.Resources) {
|
||||||
|
if r == nil || (r.MemoryBytes == 0 && r.NanoCPUs == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, " %s:\n", requirement)
|
||||||
|
if r.NanoCPUs != 0 {
|
||||||
|
fmt.Fprintf(out, " CPU:\t\t%g\n", float64(r.NanoCPUs)/1e9)
|
||||||
|
}
|
||||||
|
if r.MemoryBytes != 0 {
|
||||||
|
fmt.Fprintf(out, " Memory:\t%s\n", units.BytesSize(float64(r.MemoryBytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printResources(out, "Reservations", resources.Reservations)
|
||||||
|
printResources(out, "Limits", resources.Limits)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Networks) > 0 {
|
||||||
|
fmt.Fprintf(out, "Networks:")
|
||||||
|
for _, n := range service.Spec.Networks {
|
||||||
|
fmt.Fprintf(out, " %s", n.Target)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(out, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(service.Endpoint.Ports) > 0 {
|
||||||
|
fmt.Fprintln(out, "Ports:")
|
||||||
|
for _, port := range service.Endpoint.Ports {
|
||||||
|
ioutils.FprintfIfNotEmpty(out, " Name = %s\n", port.Name)
|
||||||
|
fmt.Fprintf(out, " Protocol = %s\n", port.Protocol)
|
||||||
|
fmt.Fprintf(out, " TargetPort = %d\n", port.TargetPort)
|
||||||
|
fmt.Fprintf(out, " PublishedPort = %d\n", port.PublishedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) {
|
||||||
|
fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image)
|
||||||
|
if len(containerSpec.Args) > 0 {
|
||||||
|
fmt.Fprintf(out, " Args:\t\t%s\n", strings.Join(containerSpec.Args, " "))
|
||||||
|
}
|
||||||
|
if len(containerSpec.Env) > 0 {
|
||||||
|
fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " "))
|
||||||
|
}
|
||||||
|
ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir)
|
||||||
|
ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User)
|
||||||
|
if len(containerSpec.Mounts) > 0 {
|
||||||
|
fmt.Fprintln(out, " Mounts:")
|
||||||
|
for _, v := range containerSpec.Mounts {
|
||||||
|
fmt.Fprintf(out, " Target = %s\n", v.Target)
|
||||||
|
fmt.Fprintf(out, " Source = %s\n", v.Source)
|
||||||
|
fmt.Fprintf(out, " ReadOnly = %v\n", v.ReadOnly)
|
||||||
|
fmt.Fprintf(out, " Type = %v\n", v.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
|
||||||
|
endpointSpec := &swarm.EndpointSpec{
|
||||||
|
Mode: "vip",
|
||||||
|
Ports: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: swarm.PortConfigProtocolTCP,
|
||||||
|
TargetPort: 5000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
two := uint64(2)
|
||||||
|
|
||||||
|
s := swarm.Service{
|
||||||
|
ID: "de179gar9d0o7ltdybungplod",
|
||||||
|
Meta: swarm.Meta{
|
||||||
|
Version: swarm.Version{Index: 315},
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
},
|
||||||
|
Spec: swarm.ServiceSpec{
|
||||||
|
Annotations: swarm.Annotations{
|
||||||
|
Name: "my_service",
|
||||||
|
Labels: map[string]string{"com.label": "foo"},
|
||||||
|
},
|
||||||
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
ContainerSpec: swarm.ContainerSpec{
|
||||||
|
Image: "foo/bar@sha256:this_is_a_test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{
|
||||||
|
Replicas: &two,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UpdateConfig: nil,
|
||||||
|
Networks: []swarm.NetworkAttachmentConfig{
|
||||||
|
{
|
||||||
|
Target: "5vpyomhb6ievnk0i0o60gcnei",
|
||||||
|
Aliases: []string{"web"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EndpointSpec: endpointSpec,
|
||||||
|
},
|
||||||
|
Endpoint: swarm.Endpoint{
|
||||||
|
Spec: *endpointSpec,
|
||||||
|
Ports: []swarm.PortConfig{
|
||||||
|
{
|
||||||
|
Protocol: swarm.PortConfigProtocolTCP,
|
||||||
|
TargetPort: 5000,
|
||||||
|
PublishedPort: 30000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VirtualIPs: []swarm.EndpointVirtualIP{
|
||||||
|
{
|
||||||
|
NetworkID: "6o4107cj2jx9tihgb0jyts6pj",
|
||||||
|
Addr: "10.255.0.4/16",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UpdateStatus: swarm.UpdateStatus{
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
CompletedAt: time.Now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
printService(b, s)
|
||||||
|
if strings.Contains(b.String(), "UpdateStatus") {
|
||||||
|
t.Fatal("Pretty print failed before parsing UpdateStatus")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listOptions struct {
|
||||||
|
quiet bool
|
||||||
|
filter opts.FilterOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls [OPTIONS]",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List services",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runList(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||||
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filter: opts.filter.Value()})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := dockerCli.Out()
|
||||||
|
if opts.quiet {
|
||||||
|
PrintQuiet(out, services)
|
||||||
|
} else {
|
||||||
|
taskFilter := filters.NewArgs()
|
||||||
|
for _, service := range services {
|
||||||
|
taskFilter.Add("service", service.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: taskFilter})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintNotQuiet(out, services, nodes, tasks)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintNotQuiet shows service list in a non-quiet way.
|
||||||
|
// Besides this, command `docker stack services xxx` will call this, too.
|
||||||
|
func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) {
|
||||||
|
activeNodes := make(map[string]struct{})
|
||||||
|
for _, n := range nodes {
|
||||||
|
if n.Status.State != swarm.NodeStateDown {
|
||||||
|
activeNodes[n.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
running := map[string]int{}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == "running" {
|
||||||
|
running[task.ServiceID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printTable(out, services, running)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTable(out io.Writer, services []swarm.Service, running map[string]int) {
|
||||||
|
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
|
||||||
|
|
||||||
|
// Ignore flushing errors
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "REPLICAS", "IMAGE", "COMMAND")
|
||||||
|
for _, service := range services {
|
||||||
|
replicas := ""
|
||||||
|
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
||||||
|
replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas)
|
||||||
|
} else if service.Spec.Mode.Global != nil {
|
||||||
|
replicas = "global"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(
|
||||||
|
writer,
|
||||||
|
listItemFmt,
|
||||||
|
stringid.TruncateID(service.ID),
|
||||||
|
service.Spec.Name,
|
||||||
|
replicas,
|
||||||
|
service.Spec.TaskTemplate.ContainerSpec.Image,
|
||||||
|
strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintQuiet shows service list in a quiet way.
|
||||||
|
// Besides this, command `docker stack services xxx` will call this, too.
|
||||||
|
func PrintQuiet(out io.Writer, services []swarm.Service) {
|
||||||
|
for _, service := range services {
|
||||||
|
fmt.Fprintln(out, service.ID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,567 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type int64Value interface {
|
||||||
|
Value() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type memBytes int64
|
||||||
|
|
||||||
|
func (m *memBytes) String() string {
|
||||||
|
return units.BytesSize(float64(m.Value()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBytes) Set(value string) error {
|
||||||
|
val, err := units.RAMInBytes(value)
|
||||||
|
*m = memBytes(val)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBytes) Type() string {
|
||||||
|
return "MemoryBytes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memBytes) Value() int64 {
|
||||||
|
return int64(*m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type nanoCPUs int64
|
||||||
|
|
||||||
|
func (c *nanoCPUs) String() string {
|
||||||
|
return big.NewRat(c.Value(), 1e9).FloatString(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nanoCPUs) Set(value string) error {
|
||||||
|
cpu, ok := new(big.Rat).SetString(value)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Failed to parse %v as a rational number", value)
|
||||||
|
}
|
||||||
|
nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
|
||||||
|
if !nano.IsInt() {
|
||||||
|
return fmt.Errorf("value is too precise")
|
||||||
|
}
|
||||||
|
*c = nanoCPUs(nano.Num().Int64())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nanoCPUs) Type() string {
|
||||||
|
return "NanoCPUs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nanoCPUs) Value() int64 {
|
||||||
|
return int64(*c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationOpt is an option type for time.Duration that uses a pointer. This
|
||||||
|
// allows us to get nil values outside, instead of defaulting to 0
|
||||||
|
type DurationOpt struct {
|
||||||
|
value *time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new value on the option
|
||||||
|
func (d *DurationOpt) Set(s string) error {
|
||||||
|
v, err := time.ParseDuration(s)
|
||||||
|
d.value = &v
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type of this option
|
||||||
|
func (d *DurationOpt) Type() string {
|
||||||
|
return "duration-ptr"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string repr of this option
|
||||||
|
func (d *DurationOpt) String() string {
|
||||||
|
if d.value != nil {
|
||||||
|
return d.value.String()
|
||||||
|
}
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the time.Duration
|
||||||
|
func (d *DurationOpt) Value() *time.Duration {
|
||||||
|
return d.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64Opt represents a uint64.
|
||||||
|
type Uint64Opt struct {
|
||||||
|
value *uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new value on the option
|
||||||
|
func (i *Uint64Opt) Set(s string) error {
|
||||||
|
v, err := strconv.ParseUint(s, 0, 64)
|
||||||
|
i.value = &v
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type of this option
|
||||||
|
func (i *Uint64Opt) Type() string {
|
||||||
|
return "uint64-ptr"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string repr of this option
|
||||||
|
func (i *Uint64Opt) String() string {
|
||||||
|
if i.value != nil {
|
||||||
|
return fmt.Sprintf("%v", *i.value)
|
||||||
|
}
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the uint64
|
||||||
|
func (i *Uint64Opt) Value() *uint64 {
|
||||||
|
return i.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// MountOpt is a Value type for parsing mounts
|
||||||
|
type MountOpt struct {
|
||||||
|
values []mounttypes.Mount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new mount value
|
||||||
|
func (m *MountOpt) Set(value string) error {
|
||||||
|
csvReader := csv.NewReader(strings.NewReader(value))
|
||||||
|
fields, err := csvReader.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mount := mounttypes.Mount{}
|
||||||
|
|
||||||
|
volumeOptions := func() *mounttypes.VolumeOptions {
|
||||||
|
if mount.VolumeOptions == nil {
|
||||||
|
mount.VolumeOptions = &mounttypes.VolumeOptions{
|
||||||
|
Labels: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mount.VolumeOptions.DriverConfig == nil {
|
||||||
|
mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
|
||||||
|
}
|
||||||
|
return mount.VolumeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOptions := func() *mounttypes.BindOptions {
|
||||||
|
if mount.BindOptions == nil {
|
||||||
|
mount.BindOptions = new(mounttypes.BindOptions)
|
||||||
|
}
|
||||||
|
return mount.BindOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueOnMap := func(target map[string]string, value string) {
|
||||||
|
parts := strings.SplitN(value, "=", 2)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
target[value] = ""
|
||||||
|
} else {
|
||||||
|
target[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mount.Type = mounttypes.TypeVolume // default to volume mounts
|
||||||
|
// Set writable as the default
|
||||||
|
for _, field := range fields {
|
||||||
|
parts := strings.SplitN(field, "=", 2)
|
||||||
|
key := strings.ToLower(parts[0])
|
||||||
|
|
||||||
|
if len(parts) == 1 {
|
||||||
|
switch key {
|
||||||
|
case "readonly", "ro":
|
||||||
|
mount.ReadOnly = true
|
||||||
|
continue
|
||||||
|
case "volume-nocopy":
|
||||||
|
volumeOptions().NoCopy = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := parts[1]
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
mount.Type = mounttypes.Type(strings.ToLower(value))
|
||||||
|
case "source", "src":
|
||||||
|
mount.Source = value
|
||||||
|
case "target", "dst", "destination":
|
||||||
|
mount.Target = value
|
||||||
|
case "readonly", "ro":
|
||||||
|
mount.ReadOnly, err = strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid value for %s: %s", key, value)
|
||||||
|
}
|
||||||
|
case "bind-propagation":
|
||||||
|
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
|
||||||
|
case "volume-nocopy":
|
||||||
|
volumeOptions().NoCopy, err = strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid value for populate: %s", value)
|
||||||
|
}
|
||||||
|
case "volume-label":
|
||||||
|
setValueOnMap(volumeOptions().Labels, value)
|
||||||
|
case "volume-driver":
|
||||||
|
volumeOptions().DriverConfig.Name = value
|
||||||
|
case "volume-opt":
|
||||||
|
if volumeOptions().DriverConfig.Options == nil {
|
||||||
|
volumeOptions().DriverConfig.Options = make(map[string]string)
|
||||||
|
}
|
||||||
|
setValueOnMap(volumeOptions().DriverConfig.Options, value)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount.Type == "" {
|
||||||
|
return fmt.Errorf("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount.Target == "" {
|
||||||
|
return fmt.Errorf("target is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount.VolumeOptions != nil && mount.Source == "" {
|
||||||
|
return fmt.Errorf("source is required when specifying volume-* options")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
|
||||||
|
}
|
||||||
|
if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
|
||||||
|
return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.values = append(m.values, mount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type of this option
|
||||||
|
func (m *MountOpt) Type() string {
|
||||||
|
return "mount"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string repr of this option
|
||||||
|
func (m *MountOpt) String() string {
|
||||||
|
mounts := []string{}
|
||||||
|
for _, mount := range m.values {
|
||||||
|
repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
|
||||||
|
mounts = append(mounts, repr)
|
||||||
|
}
|
||||||
|
return strings.Join(mounts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the mounts
|
||||||
|
func (m *MountOpt) Value() []mounttypes.Mount {
|
||||||
|
return m.values
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateOptions struct {
|
||||||
|
parallelism uint64
|
||||||
|
delay time.Duration
|
||||||
|
onFailure string
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceOptions struct {
|
||||||
|
limitCPU nanoCPUs
|
||||||
|
limitMemBytes memBytes
|
||||||
|
resCPU nanoCPUs
|
||||||
|
resMemBytes memBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
|
||||||
|
return &swarm.ResourceRequirements{
|
||||||
|
Limits: &swarm.Resources{
|
||||||
|
NanoCPUs: r.limitCPU.Value(),
|
||||||
|
MemoryBytes: r.limitMemBytes.Value(),
|
||||||
|
},
|
||||||
|
Reservations: &swarm.Resources{
|
||||||
|
NanoCPUs: r.resCPU.Value(),
|
||||||
|
MemoryBytes: r.resMemBytes.Value(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type restartPolicyOptions struct {
|
||||||
|
condition string
|
||||||
|
delay DurationOpt
|
||||||
|
maxAttempts Uint64Opt
|
||||||
|
window DurationOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
|
||||||
|
return &swarm.RestartPolicy{
|
||||||
|
Condition: swarm.RestartPolicyCondition(r.condition),
|
||||||
|
Delay: r.delay.Value(),
|
||||||
|
MaxAttempts: r.maxAttempts.Value(),
|
||||||
|
Window: r.window.Value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
||||||
|
nets := []swarm.NetworkAttachmentConfig{}
|
||||||
|
for _, network := range networks {
|
||||||
|
nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
|
||||||
|
}
|
||||||
|
return nets
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpointOptions struct {
|
||||||
|
mode string
|
||||||
|
ports opts.ListOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
||||||
|
portConfigs := []swarm.PortConfig{}
|
||||||
|
// We can ignore errors because the format was already validated by ValidatePort
|
||||||
|
ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
|
||||||
|
|
||||||
|
for port := range ports {
|
||||||
|
portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &swarm.EndpointSpec{
|
||||||
|
Mode: swarm.ResolutionMode(strings.ToLower(e.mode)),
|
||||||
|
Ports: portConfigs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPortToPortConfig(
|
||||||
|
port nat.Port,
|
||||||
|
portBindings map[nat.Port][]nat.PortBinding,
|
||||||
|
) []swarm.PortConfig {
|
||||||
|
ports := []swarm.PortConfig{}
|
||||||
|
|
||||||
|
for _, binding := range portBindings[port] {
|
||||||
|
hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
|
||||||
|
ports = append(ports, swarm.PortConfig{
|
||||||
|
//TODO Name: ?
|
||||||
|
Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
|
||||||
|
TargetPort: uint32(port.Int()),
|
||||||
|
PublishedPort: uint32(hostPort),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}
|
||||||
|
|
||||||
|
type logDriverOptions struct {
|
||||||
|
name string
|
||||||
|
opts opts.ListOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogDriverOptions() logDriverOptions {
|
||||||
|
return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
|
||||||
|
if ldo.name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the log driver only if specified.
|
||||||
|
return &swarm.Driver{
|
||||||
|
Name: ldo.name,
|
||||||
|
Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePort validates a string is in the expected format for a port definition
|
||||||
|
func ValidatePort(value string) (string, error) {
|
||||||
|
portMappings, err := nat.ParsePortSpec(value)
|
||||||
|
for _, portMapping := range portMappings {
|
||||||
|
if portMapping.Binding.HostIP != "" {
|
||||||
|
return "", fmt.Errorf("HostIP is not supported by a service.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceOptions struct {
|
||||||
|
name string
|
||||||
|
labels opts.ListOpts
|
||||||
|
containerLabels opts.ListOpts
|
||||||
|
image string
|
||||||
|
args []string
|
||||||
|
env opts.ListOpts
|
||||||
|
workdir string
|
||||||
|
user string
|
||||||
|
groups []string
|
||||||
|
mounts MountOpt
|
||||||
|
|
||||||
|
resources resourceOptions
|
||||||
|
stopGrace DurationOpt
|
||||||
|
|
||||||
|
replicas Uint64Opt
|
||||||
|
mode string
|
||||||
|
|
||||||
|
restartPolicy restartPolicyOptions
|
||||||
|
constraints []string
|
||||||
|
update updateOptions
|
||||||
|
networks []string
|
||||||
|
endpoint endpointOptions
|
||||||
|
|
||||||
|
registryAuth bool
|
||||||
|
|
||||||
|
logDriver logDriverOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServiceOptions() *serviceOptions {
|
||||||
|
return &serviceOptions{
|
||||||
|
labels: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||||
|
containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||||
|
env: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||||
|
endpoint: endpointOptions{
|
||||||
|
ports: opts.NewListOpts(ValidatePort),
|
||||||
|
},
|
||||||
|
logDriver: newLogDriverOptions(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
||||||
|
var service swarm.ServiceSpec
|
||||||
|
|
||||||
|
service = swarm.ServiceSpec{
|
||||||
|
Annotations: swarm.Annotations{
|
||||||
|
Name: opts.name,
|
||||||
|
Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
|
||||||
|
},
|
||||||
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
ContainerSpec: swarm.ContainerSpec{
|
||||||
|
Image: opts.image,
|
||||||
|
Args: opts.args,
|
||||||
|
Env: opts.env.GetAll(),
|
||||||
|
Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
|
||||||
|
Dir: opts.workdir,
|
||||||
|
User: opts.user,
|
||||||
|
Groups: opts.groups,
|
||||||
|
Mounts: opts.mounts.Value(),
|
||||||
|
StopGracePeriod: opts.stopGrace.Value(),
|
||||||
|
},
|
||||||
|
Networks: convertNetworks(opts.networks),
|
||||||
|
Resources: opts.resources.ToResourceRequirements(),
|
||||||
|
RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
|
||||||
|
Placement: &swarm.Placement{
|
||||||
|
Constraints: opts.constraints,
|
||||||
|
},
|
||||||
|
LogDriver: opts.logDriver.toLogDriver(),
|
||||||
|
},
|
||||||
|
Networks: convertNetworks(opts.networks),
|
||||||
|
Mode: swarm.ServiceMode{},
|
||||||
|
UpdateConfig: &swarm.UpdateConfig{
|
||||||
|
Parallelism: opts.update.parallelism,
|
||||||
|
Delay: opts.update.delay,
|
||||||
|
FailureAction: opts.update.onFailure,
|
||||||
|
},
|
||||||
|
EndpointSpec: opts.endpoint.ToEndpointSpec(),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.mode {
|
||||||
|
case "global":
|
||||||
|
if opts.replicas.Value() != nil {
|
||||||
|
return service, fmt.Errorf("replicas can only be used with replicated mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
service.Mode.Global = &swarm.GlobalService{}
|
||||||
|
case "replicated":
|
||||||
|
service.Mode.Replicated = &swarm.ReplicatedService{
|
||||||
|
Replicas: opts.replicas.Value(),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return service, fmt.Errorf("Unknown mode: %s", opts.mode)
|
||||||
|
}
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addServiceFlags adds all flags that are common to both `create` and `update`.
|
||||||
|
// Any flags that are not common are added separately in the individual command
|
||||||
|
func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVar(&opts.name, flagName, "", "Service name")
|
||||||
|
|
||||||
|
flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container")
|
||||||
|
flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||||
|
flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional user groups to the container")
|
||||||
|
|
||||||
|
flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
|
||||||
|
flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
|
||||||
|
flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
|
||||||
|
flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
|
||||||
|
flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
|
||||||
|
|
||||||
|
flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
|
||||||
|
|
||||||
|
flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
|
||||||
|
flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
|
||||||
|
flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
|
||||||
|
flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
|
||||||
|
|
||||||
|
flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)")
|
||||||
|
flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
|
||||||
|
flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)")
|
||||||
|
|
||||||
|
flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
|
||||||
|
|
||||||
|
flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
|
||||||
|
|
||||||
|
flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
|
||||||
|
flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagConstraint = "constraint"
|
||||||
|
flagConstraintRemove = "constraint-rm"
|
||||||
|
flagConstraintAdd = "constraint-add"
|
||||||
|
flagContainerLabel = "container-label"
|
||||||
|
flagContainerLabelRemove = "container-label-rm"
|
||||||
|
flagContainerLabelAdd = "container-label-add"
|
||||||
|
flagEndpointMode = "endpoint-mode"
|
||||||
|
flagEnv = "env"
|
||||||
|
flagEnvRemove = "env-rm"
|
||||||
|
flagEnvAdd = "env-add"
|
||||||
|
flagGroupAdd = "group-add"
|
||||||
|
flagGroupRemove = "group-rm"
|
||||||
|
flagLabel = "label"
|
||||||
|
flagLabelRemove = "label-rm"
|
||||||
|
flagLabelAdd = "label-add"
|
||||||
|
flagLimitCPU = "limit-cpu"
|
||||||
|
flagLimitMemory = "limit-memory"
|
||||||
|
flagMode = "mode"
|
||||||
|
flagMount = "mount"
|
||||||
|
flagMountRemove = "mount-rm"
|
||||||
|
flagMountAdd = "mount-add"
|
||||||
|
flagName = "name"
|
||||||
|
flagNetwork = "network"
|
||||||
|
flagPublish = "publish"
|
||||||
|
flagPublishRemove = "publish-rm"
|
||||||
|
flagPublishAdd = "publish-add"
|
||||||
|
flagReplicas = "replicas"
|
||||||
|
flagReserveCPU = "reserve-cpu"
|
||||||
|
flagReserveMemory = "reserve-memory"
|
||||||
|
flagRestartCondition = "restart-condition"
|
||||||
|
flagRestartDelay = "restart-delay"
|
||||||
|
flagRestartMaxAttempts = "restart-max-attempts"
|
||||||
|
flagRestartWindow = "restart-window"
|
||||||
|
flagStopGracePeriod = "stop-grace-period"
|
||||||
|
flagUpdateDelay = "update-delay"
|
||||||
|
flagUpdateFailureAction = "update-failure-action"
|
||||||
|
flagUpdateParallelism = "update-parallelism"
|
||||||
|
flagUser = "user"
|
||||||
|
flagWorkdir = "workdir"
|
||||||
|
flagRegistryAuth = "with-registry-auth"
|
||||||
|
flagLogDriver = "log-driver"
|
||||||
|
flagLogOpt = "log-opt"
|
||||||
|
)
|
|
@ -0,0 +1,176 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemBytesString(t *testing.T) {
|
||||||
|
var mem memBytes = 1048576
|
||||||
|
assert.Equal(t, mem.String(), "1 MiB")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemBytesSetAndValue(t *testing.T) {
|
||||||
|
var mem memBytes
|
||||||
|
assert.NilError(t, mem.Set("5kb"))
|
||||||
|
assert.Equal(t, mem.Value(), int64(5120))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNanoCPUsString(t *testing.T) {
|
||||||
|
var cpus nanoCPUs = 6100000000
|
||||||
|
assert.Equal(t, cpus.String(), "6.100")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNanoCPUsSetAndValue(t *testing.T) {
|
||||||
|
var cpus nanoCPUs
|
||||||
|
assert.NilError(t, cpus.Set("0.35"))
|
||||||
|
assert.Equal(t, cpus.Value(), int64(350000000))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationOptString(t *testing.T) {
|
||||||
|
dur := time.Duration(300 * 10e8)
|
||||||
|
duration := DurationOpt{value: &dur}
|
||||||
|
assert.Equal(t, duration.String(), "5m0s")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationOptSetAndValue(t *testing.T) {
|
||||||
|
var duration DurationOpt
|
||||||
|
assert.NilError(t, duration.Set("300s"))
|
||||||
|
assert.Equal(t, *duration.Value(), time.Duration(300*10e8))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUint64OptString(t *testing.T) {
|
||||||
|
value := uint64(2345678)
|
||||||
|
opt := Uint64Opt{value: &value}
|
||||||
|
assert.Equal(t, opt.String(), "2345678")
|
||||||
|
|
||||||
|
opt = Uint64Opt{}
|
||||||
|
assert.Equal(t, opt.String(), "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUint64OptSetAndValue(t *testing.T) {
|
||||||
|
var opt Uint64Opt
|
||||||
|
assert.NilError(t, opt.Set("14445"))
|
||||||
|
assert.Equal(t, *opt.Value(), uint64(14445))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptString(t *testing.T) {
|
||||||
|
mount := MountOpt{
|
||||||
|
values: []mounttypes.Mount{
|
||||||
|
{
|
||||||
|
Type: mounttypes.TypeBind,
|
||||||
|
Source: "/home/path",
|
||||||
|
Target: "/target",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: mounttypes.TypeVolume,
|
||||||
|
Source: "foo",
|
||||||
|
Target: "/target/foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expected := "bind /home/path /target, volume foo /target/foo"
|
||||||
|
assert.Equal(t, mount.String(), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptSetNoError(t *testing.T) {
|
||||||
|
for _, testcase := range []string{
|
||||||
|
// tests several aliases that should have same result.
|
||||||
|
"type=bind,target=/target,source=/source",
|
||||||
|
"type=bind,src=/source,dst=/target",
|
||||||
|
"type=bind,source=/source,dst=/target",
|
||||||
|
"type=bind,src=/source,target=/target",
|
||||||
|
} {
|
||||||
|
var mount MountOpt
|
||||||
|
|
||||||
|
assert.NilError(t, mount.Set(testcase))
|
||||||
|
|
||||||
|
mounts := mount.Value()
|
||||||
|
assert.Equal(t, len(mounts), 1)
|
||||||
|
assert.Equal(t, mounts[0], mounttypes.Mount{
|
||||||
|
Type: mounttypes.TypeBind,
|
||||||
|
Source: "/source",
|
||||||
|
Target: "/target",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMountOptDefaultType ensures that a mount without the type defaults to a
|
||||||
|
// volume mount.
|
||||||
|
func TestMountOptDefaultType(t *testing.T) {
|
||||||
|
var mount MountOpt
|
||||||
|
assert.NilError(t, mount.Set("target=/target,source=/foo"))
|
||||||
|
assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptSetErrorNoTarget(t *testing.T) {
|
||||||
|
var mount MountOpt
|
||||||
|
assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptSetErrorInvalidKey(t *testing.T) {
|
||||||
|
var mount MountOpt
|
||||||
|
assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptSetErrorInvalidField(t *testing.T) {
|
||||||
|
var mount MountOpt
|
||||||
|
assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
|
||||||
|
var mount MountOpt
|
||||||
|
assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
|
||||||
|
assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptDefaultEnableReadOnly(t *testing.T) {
|
||||||
|
var m MountOpt
|
||||||
|
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
|
||||||
|
assert.Equal(t, m.values[0].ReadOnly, false)
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
|
||||||
|
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
|
||||||
|
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
|
||||||
|
assert.Equal(t, m.values[0].ReadOnly, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptVolumeNoCopy(t *testing.T) {
|
||||||
|
var m MountOpt
|
||||||
|
assert.Error(t, m.Set("type=volume,target=/foo,volume-nocopy"), "source is required")
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions == nil, true)
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||||
|
|
||||||
|
m = MountOpt{}
|
||||||
|
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||||
|
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountOptTypeConflict(t *testing.T) {
|
||||||
|
var m MountOpt
|
||||||
|
assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
|
||||||
|
assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/idresolver"
|
||||||
|
"github.com/docker/docker/cli/command/node"
|
||||||
|
"github.com/docker/docker/cli/command/task"
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type psOptions struct {
|
||||||
|
serviceID string
|
||||||
|
noResolve bool
|
||||||
|
noTrunc bool
|
||||||
|
filter opts.FilterOpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
opts := psOptions{filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ps [OPTIONS] SERVICE",
|
||||||
|
Short: "List the tasks of a service",
|
||||||
|
Args: cli.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts.serviceID = args[0]
|
||||||
|
return runPS(dockerCli, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||||
|
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||||
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPS(dockerCli *command.DockerCli, opts psOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
service, _, err := client.ServiceInspectWithRaw(ctx, opts.serviceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := opts.filter.Value()
|
||||||
|
filter.Add("service", service.ID)
|
||||||
|
if filter.Include("node") {
|
||||||
|
nodeFilters := filter.Get("node")
|
||||||
|
for _, nodeFilter := range nodeFilters {
|
||||||
|
nodeReference, err := node.Reference(ctx, client, nodeFilter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filter.Del("node", nodeFilter)
|
||||||
|
filter.Add("node", nodeReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc)
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/cli"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rm SERVICE [SERVICE...]",
|
||||||
|
Aliases: []string{"remove"},
|
||||||
|
Short: "Remove one or more services",
|
||||||
|
Args: cli.RequiresMinArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRemove(dockerCli, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags()
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRemove(dockerCli *command.DockerCli, sids []string) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, sid := range sids {
|
||||||
|
err := client.ServiceRemove(ctx, sid)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf(strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue