Import docker/docker/cli

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
master
Daniel Nephin 2017-04-17 17:40:59 -04:00
commit 1630fc40f8
325 changed files with 38711 additions and 0 deletions

150
cli/cobra.go Normal file
View File

@ -0,0 +1,150 @@
package cli
import (
"fmt"
"strings"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// SetupRootCommand sets default usage, help, and error handling for the
// root command.
func SetupRootCommand(rootCmd *cobra.Command) {
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
rootCmd.SetUsageTemplate(usageTemplate)
rootCmd.SetHelpTemplate(helpTemplate)
rootCmd.SetFlagErrorFunc(FlagErrorFunc)
rootCmd.SetHelpCommand(helpCommand)
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
}
// FlagErrorFunc prints an error message which matches the format of the
// docker/docker/cli error messages
func FlagErrorFunc(cmd *cobra.Command, err error) error {
if err == nil {
return nil
}
usage := ""
if cmd.HasSubCommands() {
usage = "\n\n" + cmd.UsageString()
}
return StatusError{
Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
StatusCode: 125,
}
}
var helpCommand = &cobra.Command{
Use: "help [command]",
Short: "Help about the command",
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
PersistentPostRun: func(cmd *cobra.Command, args []string) {},
RunE: func(c *cobra.Command, args []string) error {
cmd, args, e := c.Root().Find(args)
if cmd == nil || e != nil || len(args) > 0 {
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
}
helpFunc := cmd.HelpFunc()
helpFunc(cmd, args)
return nil
},
}
func hasSubCommands(cmd *cobra.Command) bool {
return len(operationSubCommands(cmd)) > 0
}
func hasManagementSubCommands(cmd *cobra.Command) bool {
return len(managementSubCommands(cmd)) > 0
}
func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if sub.IsAvailableCommand() && !sub.HasSubCommands() {
cmds = append(cmds, sub)
}
}
return cmds
}
func wrappedFlagUsages(cmd *cobra.Command) string {
width := 80
if ws, err := term.GetWinsize(0); err == nil {
width = int(ws.Width)
}
return cmd.Flags().FlagUsagesWrapped(width - 1)
}
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() {
if sub.IsAvailableCommand() && sub.HasSubCommands() {
cmds = append(cmds, sub)
}
}
return cmds
}
var usageTemplate = `Usage:
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
{{- if .HasSubCommands}} {{ .CommandPath}} COMMAND{{end}}
{{ .Short | trim }}
{{- if gt .Aliases 0}}
Aliases:
{{.NameAndAliases}}
{{- end}}
{{- if .HasExample}}
Examples:
{{ .Example }}
{{- end}}
{{- if .HasFlags}}
Options:
{{ wrappedFlagUsages . | trimRightSpace}}
{{- end}}
{{- if hasManagementSubCommands . }}
Management Commands:
{{- range managementSubCommands . }}
{{rpad .Name .NamePadding }} {{.Short}}
{{- end}}
{{- end}}
{{- if hasSubCommands .}}
Commands:
{{- range operationSubCommands . }}
{{rpad .Name .NamePadding }} {{.Short}}
{{- end}}
{{- end}}
{{- if .HasSubCommands }}
Run '{{.CommandPath}} COMMAND --help' for more information on a command.
{{- end}}
`
var helpTemplate = `
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`

View File

@ -0,0 +1,70 @@
package bundlefile
import (
"encoding/json"
"io"
"github.com/pkg/errors"
)
// 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, errors.Errorf(
"JSON syntax error at byte %v: %s",
jsonErr.Offset,
jsonErr.Error())
case *json.UnmarshalTypeError:
return nil, errors.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
}

View File

@ -0,0 +1,77 @@
package bundlefile
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/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.NoError(t, err)
assert.Equal(t, "0.1", bundle.Version)
assert.Len(t, bundle.Services, 2)
}
func TestLoadFileSyntaxError(t *testing.T) {
reader := strings.NewReader(`{
"Version": "0.1",
"Services": unquoted string
}`)
_, err := LoadFile(reader)
assert.EqualError(t, err, "JSON syntax error at byte 37: invalid character 'u' looking for beginning of value")
}
func TestLoadFileTypeError(t *testing.T) {
reader := strings.NewReader(`{
"Version": "0.1",
"Services": {
"web": {
"Image": "redis",
"Networks": "none"
}
}
}`)
_, err := LoadFile(reader)
assert.EqualError(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.NoError(t, Print(&buffer, bundle))
output := buffer.String()
assert.Contains(t, output, "\"Image\": \"image\"")
assert.Contains(t, output,
`"Command": [
"echo",
"something"
]`)
}

View File

@ -0,0 +1,24 @@
package checkpoint
import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/spf13/cobra"
)
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "checkpoint",
Short: "Manage checkpoints",
Args: cli.NoArgs,
RunE: dockerCli.ShowHelp,
Tags: map[string]string{"experimental": "", "version": "1.25"},
}
cmd.AddCommand(
newCreateCommand(dockerCli),
newListCommand(dockerCli),
newRemoveCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,58 @@
package checkpoint
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 createOptions struct {
container string
checkpoint string
checkpointDir string
leaveRunning bool
}
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts createOptions
cmd := &cobra.Command{
Use: "create [OPTIONS] CONTAINER CHECKPOINT",
Short: "Create a checkpoint from a running container",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0]
opts.checkpoint = args[1]
return runCreate(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}
func runCreate(dockerCli *command.DockerCli, opts createOptions) error {
client := dockerCli.Client()
checkpointOpts := types.CheckpointCreateOptions{
CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning,
}
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
if err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
return nil
}

View File

@ -0,0 +1,54 @@
package checkpoint
import (
"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/cli/command/formatter"
"github.com/spf13/cobra"
)
type listOptions struct {
checkpointDir string
}
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
Use: "ls [OPTIONS] CONTAINER",
Aliases: []string{"list"},
Short: "List checkpoints for a container",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runList(dockerCli, args[0], opts)
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}
func runList(dockerCli *command.DockerCli, container string, opts listOptions) error {
client := dockerCli.Client()
listOpts := types.CheckpointListOptions{
CheckpointDir: opts.checkpointDir,
}
checkpoints, err := client.CheckpointList(context.Background(), container, listOpts)
if err != nil {
return err
}
cpCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewCheckpointFormat(formatter.TableFormatKey),
}
return formatter.CheckpointWrite(cpCtx, checkpoints)
}

View File

@ -0,0 +1,44 @@
package checkpoint
import (
"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 {
checkpointDir string
}
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts removeOptions
cmd := &cobra.Command{
Use: "rm [OPTIONS] CONTAINER CHECKPOINT",
Aliases: []string{"remove"},
Short: "Remove a checkpoint",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(dockerCli, args[0], args[1], opts)
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}
func runRemove(dockerCli *command.DockerCli, container string, checkpoint string, opts removeOptions) error {
client := dockerCli.Client()
removeOpts := types.CheckpointDeleteOptions{
CheckpointID: checkpoint,
CheckpointDir: opts.checkpointDir,
}
return client.CheckpointDelete(context.Background(), container, removeOpts)
}

303
cli/command/cli.go Normal file
View File

@ -0,0 +1,303 @@
package command
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
cliconfig "github.com/docker/docker/cli/config"
"github.com/docker/docker/cli/config/configfile"
"github.com/docker/docker/cli/config/credentials"
cliflags "github.com/docker/docker/cli/flags"
"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"
"github.com/docker/notary/passphrase"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
// Streams is an interface which exposes the standard input and output streams
type Streams interface {
In() *InStream
Out() *OutStream
Err() io.Writer
}
// Cli represents the docker command line client.
type Cli interface {
Client() client.APIClient
Out() *OutStream
Err() io.Writer
In() *InStream
ConfigFile() *configfile.ConfigFile
}
// DockerCli is an instance 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
defaultVersion string
server ServerInfo
}
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
func (cli *DockerCli) DefaultVersion() string {
return cli.defaultVersion
}
// 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
}
// ShowHelp shows the command help.
func (cli *DockerCli) ShowHelp(cmd *cobra.Command, args []string) error {
cmd.SetOutput(cli.err)
cmd.HelpFunc()(cmd, args)
return nil
}
// ConfigFile returns the ConfigFile
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
return cli.configFile
}
// ServerInfo returns the server version details for the host this client is
// connected to
func (cli *DockerCli) ServerInfo() ServerInfo {
return cli.server
}
// GetAllCredentials returns all of the credentials stored in all of the
// configured credential stores.
func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) {
auths := make(map[string]types.AuthConfig)
for registry := range cli.configFile.CredentialHelpers {
helper := cli.CredentialsStore(registry)
newAuths, err := helper.GetAll()
if err != nil {
return nil, err
}
addAll(auths, newAuths)
}
defaultStore := cli.CredentialsStore("")
newAuths, err := defaultStore.GetAll()
if err != nil {
return nil, err
}
addAll(auths, newAuths)
return auths, nil
}
func addAll(to, from map[string]types.AuthConfig) {
for reg, ac := range from {
to[reg] = ac
}
}
// CredentialsStore returns a new credentials store based
// on the settings provided in the configuration file. Empty string returns
// the default credential store.
func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store {
if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" {
return credentials.NewNativeStore(cli.configFile, helper)
}
return credentials.NewFileStore(cli.configFile)
}
// getConfiguredCredentialStore returns the credential helper configured for the
// given registry, the default credsStore, or the empty string if neither are
// configured.
func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string {
if c.CredentialHelpers != nil && serverAddress != "" {
if helper, exists := c.CredentialHelpers[serverAddress]; exists {
return helper
}
}
return c.CredentialsStore
}
// 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 tlsconfig.IsErrEncryptedKey(err) {
var (
passwd string
giveup bool
)
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
// some code and comments borrowed from notary/trustmanager/keystore.go
passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
// Check if the passphrase retriever got an error or if it is telling us to give up
if giveup || err != nil {
return errors.Wrap(err, "private key is encrypted, but could not get passphrase")
}
opts.Common.TLSOptions.Passphrase = passwd
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
}
}
if err != nil {
return err
}
cli.defaultVersion = cli.client.ClientVersion()
if opts.Common.TrustKey == "" {
cli.keyFile = filepath.Join(cliconfig.Dir(), cliflags.DefaultTrustKeyFile)
} else {
cli.keyFile = opts.Common.TrustKey
}
if ping, err := cli.client.Ping(context.Background()); err == nil {
cli.server = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
}
// since the new header was added in 1.25, assume server is 1.24 if header is not present.
if ping.APIVersion == "" {
ping.APIVersion = "1.24"
}
// if server version is lower than the current cli, downgrade
if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) {
cli.client.UpdateClientVersion(ping.APIVersion)
}
}
return nil
}
// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
HasExperimental bool
OSType string
}
// 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.Dir())
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"] = UserAgent()
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
}
opts := *tlsOptions
opts.ExclusiveRootPools = true
config, err := tlsconfig.Client(opts)
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
}
// UserAgent returns the user agent string used for making API requests
func UserAgent() string {
return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
}

View File

@ -0,0 +1,121 @@
package commands
import (
"os"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/checkpoint"
"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/secret"
"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 cli/command to the root command
func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
cmd.AddCommand(
// checkpoint
checkpoint.NewCheckpointCommand(dockerCli),
// container
container.NewContainerCommand(dockerCli),
container.NewRunCommand(dockerCli),
// image
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),
// node
node.NewNodeCommand(dockerCli),
// network
network.NewNetworkCommand(dockerCli),
// plugin
plugin.NewPluginCommand(dockerCli),
// registry
registry.NewLoginCommand(dockerCli),
registry.NewLogoutCommand(dockerCli),
registry.NewSearchCommand(dockerCli),
// secret
secret.NewSecretCommand(dockerCli),
// service
service.NewServiceCommand(dockerCli),
// system
system.NewSystemCommand(dockerCli),
system.NewVersionCommand(dockerCli),
// stack
stack.NewStackCommand(dockerCli),
stack.NewTopLevelDeployCommand(dockerCli),
// swarm
swarm.NewSwarmCommand(dockerCli),
// volume
volume.NewVolumeCommand(dockerCli),
// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),
hide(system.NewInspectCommand(dockerCli)),
hide(container.NewAttachCommand(dockerCli)),
hide(container.NewCommitCommand(dockerCli)),
hide(container.NewCopyCommand(dockerCli)),
hide(container.NewCreateCommand(dockerCli)),
hide(container.NewDiffCommand(dockerCli)),
hide(container.NewExecCommand(dockerCli)),
hide(container.NewExportCommand(dockerCli)),
hide(container.NewKillCommand(dockerCli)),
hide(container.NewLogsCommand(dockerCli)),
hide(container.NewPauseCommand(dockerCli)),
hide(container.NewPortCommand(dockerCli)),
hide(container.NewPsCommand(dockerCli)),
hide(container.NewRenameCommand(dockerCli)),
hide(container.NewRestartCommand(dockerCli)),
hide(container.NewRmCommand(dockerCli)),
hide(container.NewStartCommand(dockerCli)),
hide(container.NewStatsCommand(dockerCli)),
hide(container.NewStopCommand(dockerCli)),
hide(container.NewTopCommand(dockerCli)),
hide(container.NewUnpauseCommand(dockerCli)),
hide(container.NewUpdateCommand(dockerCli)),
hide(container.NewWaitCommand(dockerCli)),
hide(image.NewHistoryCommand(dockerCli)),
hide(image.NewImagesCommand(dockerCli)),
hide(image.NewImportCommand(dockerCli)),
hide(image.NewLoadCommand(dockerCli)),
hide(image.NewPullCommand(dockerCli)),
hide(image.NewPushCommand(dockerCli)),
hide(image.NewRemoveCommand(dockerCli)),
hide(image.NewSaveCommand(dockerCli)),
hide(image.NewTagCommand(dockerCli)),
)
}
func hide(cmd *cobra.Command) *cobra.Command {
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
// these legacy commands (such as `docker ps`, `docker exec`, etc)
// will not be shown in output console.
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
return cmd
}
cmdCopy := *cmd
cmdCopy.Hidden = true
cmdCopy.Aliases = []string{}
return &cmdCopy
}

View File

@ -0,0 +1,129 @@
package container
import (
"io"
"net/http/httputil"
"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/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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 local standard input, output, and error streams 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 errors.New("You cannot attach to a stopped container, start it first")
}
if c.State.Paused {
return errors.New("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(ctx, dockerCli, opts.container)
if err != nil {
return err
}
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}

View File

@ -0,0 +1,45 @@
package container
import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/spf13/cobra"
)
// NewContainerCommand returns a cobra command for `container` subcommands
func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "container",
Short: "Manage containers",
Args: cli.NoArgs,
RunE: dockerCli.ShowHelp,
}
cmd.AddCommand(
NewAttachCommand(dockerCli),
NewCommitCommand(dockerCli),
NewCopyCommand(dockerCli),
NewCreateCommand(dockerCli),
NewDiffCommand(dockerCli),
NewExecCommand(dockerCli),
NewExportCommand(dockerCli),
NewKillCommand(dockerCli),
NewLogsCommand(dockerCli),
NewPauseCommand(dockerCli),
NewPortCommand(dockerCli),
NewRenameCommand(dockerCli),
NewRestartCommand(dockerCli),
NewRmCommand(dockerCli),
NewRunCommand(dockerCli),
NewStartCommand(dockerCli),
NewStatsCommand(dockerCli),
NewStopCommand(dockerCli),
NewTopCommand(dockerCli),
NewUnpauseCommand(dockerCli),
NewUpdateCommand(dockerCli),
NewWaitCommand(dockerCli),
newListCommand(dockerCli),
newInspectCommand(dockerCli),
NewPruneCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,75 @@
package container
import (
"fmt"
"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"
"golang.org/x/net/context"
)
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
}

305
cli/command/container/cp.go Normal file
View File

@ -0,0 +1,305 @@
package container
import (
"io"
"os"
"path/filepath"
"strings"
"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/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type copyOptions struct {
source string
destination string
followLink bool
copyUIDGID 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 errors.New("source can not be empty")
}
if args[1] == "" {
return errors.New("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")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
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, opts.copyUIDGID)
case acrossContainers:
// Copying between containers isn't supported.
return errors.New("copying between containers is not supported")
default:
// User didn't specify any container.
return errors.New("must specify at least one container source")
}
}
func statContainerPath(ctx context.Context, dockerCli *command.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, copyUIDGID bool) (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 errors.Errorf("destination \"%s:%s\" must be a directory", 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,
CopyUIDGID: copyUIDGID,
}
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]
}

View File

@ -0,0 +1,224 @@
package container
import (
"fmt"
"io"
"os"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/image"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/context"
)
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 *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.AddTrustVerificationFlags(flags)
copts = addFlags(flags)
return cmd
}
func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *containerOptions) error {
containerConfig, err := parse(flags, copts)
if err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name)
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), response.ID)
return nil
}
func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error {
ref, err := reference.ParseNormalizedNamed(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 := command.ResolveAuthConfig(ctx, dockerCli, 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 {
return nil
}
if err := os.Remove(cid.path); err != nil {
return errors.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 errors.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, errors.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, errors.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, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) {
config := containerConfig.Config
hostConfig := containerConfig.HostConfig
networkingConfig := containerConfig.NetworkingConfig
stderr := dockerCli.Err()
var (
containerIDFile *cidFile
trustedRef reference.Canonical
namedRef reference.Named
)
cidfile := hostConfig.ContainerIDFile
if cidfile != "" {
var err error
if containerIDFile, err = newCIDFile(cidfile); err != nil {
return nil, err
}
defer containerIDFile.Close()
}
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
return nil, err
}
if named, ok := ref.(reference.Named); ok {
namedRef = reference.TagNameOnly(named)
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && command.IsTrusted() {
var err error
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil)
if err != nil {
return nil, err
}
config.Image = reference.FamiliarString(trustedRef)
}
}
//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) && namedRef != nil {
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
if err := image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef); 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
}

View File

@ -0,0 +1,46 @@
package container
import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/formatter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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 to files or directories 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 errors.New("Container name cannot be empty")
}
ctx := context.Background()
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
if err != nil {
return err
}
diffCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewDiffFormat("{{.Type}} {{.Path}}"),
}
return formatter.DiffWrite(diffCtx, changes)
}

View File

@ -0,0 +1,205 @@
package container
import (
"fmt"
"io"
"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"
options "github.com/docker/docker/opts"
"github.com/docker/docker/pkg/promise"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type execOptions struct {
detachKeys string
interactive bool
tty bool
detach bool
user string
privileged bool
env *options.ListOpts
}
func newExecOptions() *execOptions {
var values []string
return &execOptions{
env: options.NewListOptsRef(&values, options.ValidateEnv),
}
}
// NewExecCommand creates a new cobra.Command for `docker exec`
func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := newExecOptions()
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")
flags.VarP(opts.env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"})
return cmd
}
func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error {
execConfig, err := parseExec(opts, 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.Fprintln(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.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", 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 !apiclient.IsErrConnectionFailed(err) {
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, execCmd []string) (*types.ExecConfig, error) {
execConfig := &types.ExecConfig{
User: opts.user,
Privileged: opts.privileged,
Tty: opts.tty,
Cmd: execCmd,
Detach: opts.detach,
}
// 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
}
}
if opts.env != nil {
execConfig.Env = opts.env.GetAll()
}
return execConfig, nil
}

View File

@ -0,0 +1,116 @@
package container
import (
"testing"
"github.com/docker/docker/api/types"
)
type arguments struct {
options execOptions
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.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
}

View File

@ -0,0 +1,58 @@
package container
import (
"io"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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)
}

View File

@ -0,0 +1,124 @@
package container
import (
"io"
"runtime"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/pkg/stdcopy"
"golang.org/x/net/context"
)
// holdHijackedConnection handles copying input to and output from streams to the
// connection
func holdHijackedConnection(ctx context.Context, streams command.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 command.Streams) error {
if err := streams.In().SetRawTerminal(); err != nil {
return err
}
return streams.Out().SetRawTerminal()
}
func restoreTerminal(streams command.Streams, in io.Closer) error {
streams.In().RestoreTerminal()
streams.Out().RestoreTerminal()
// WARNING: DO NOT REMOVE THE OS CHECKS !!!
// For some reason this Close call blocks on darwin..
// As the client exits right after, simply discard the close
// until we find a better solution.
//
// This can also cause the client on Windows to get stuck in Win32 CloseHandle()
// in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442
// Tracked internally at Microsoft by VSO #11352156. In the
// Windows case, you hit this if you are using the native/v2 console,
// not the "legacy" console, and you start the client in a new window. eg
// `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar`
// will hang. Remove start, and it won't repro.
if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
return in.Close()
}
return nil
}

View File

@ -0,0 +1,46 @@
package container
import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/inspect"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type inspectOptions struct {
format string
size bool
refs []string
}
// newInspectCommand creates a new cobra.Command for `docker container inspect`
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
Use: "inspect [OPTIONS] CONTAINER [CONTAINER...]",
Short: "Display detailed information on one or more containers",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.refs = args
return runInspect(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes")
return cmd
}
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
client := dockerCli.Client()
ctx := context.Background()
getRefFunc := func(ref string) (interface{}, []byte, error) {
return client.ContainerInspectWithRaw(ctx, ref, opts.size)
}
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)
}

View File

@ -0,0 +1,56 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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()
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
return dockerCli.Client().ContainerKill(ctx, container, opts.signal)
})
for _, name := range opts.containers {
if err := <-errChan; err != nil {
errs = append(errs, err.Error())
} else {
fmt.Fprintln(dockerCli.Out(), name)
}
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,140 @@
package container
import (
"io/ioutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/formatter"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/templates"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type psOptions struct {
quiet bool
size bool
all bool
noTrunc bool
nLatest bool
last int
format string
filter opts.FilterOpt
}
// NewPsCommand creates a new cobra.Command for `docker ps`
func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := psOptions{filter: opts.NewFilterOpt()}
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.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := *NewPsCommand(dockerCli)
cmd.Aliases = []string{"ps", "list"}
cmd.Use = "ls [OPTIONS]"
return &cmd
}
// listOptionsProcessor is used to set any container list options which may only
// be embedded in the format template.
// This is passed directly into tmpl.Execute in order to allow the preprocessor
// to set any list options that were not provided by flags (e.g. `.Size`).
// It is using a `map[string]bool` so that unknown fields passed into the
// template format do not cause errors. These errors will get picked up when
// running through the actual template processor.
type listOptionsProcessor map[string]bool
// Size sets the size of the map when called by a template execution.
func (o listOptionsProcessor) Size() bool {
o["size"] = true
return true
}
// Label is needed here as it allows the correct pre-processing
// because Label() is a method with arguments
func (o listOptionsProcessor) Label(name string) string {
return ""
}
func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) {
options := &types.ContainerListOptions{
All: opts.all,
Limit: opts.last,
Size: opts.size,
Filters: opts.filter.Value(),
}
if opts.nLatest && opts.last == -1 {
options.Limit = 1
}
tmpl, err := templates.Parse(opts.format)
if err != nil {
return nil, err
}
optionsProcessor := listOptionsProcessor{}
// 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, optionsProcessor); err != nil {
return nil, err
}
// At the moment all we need is to capture .Size for preprocessor
options.Size = opts.size || optionsProcessor["size"]
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
}
format := opts.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
format = dockerCli.ConfigFile().PsFormat
} else {
format = formatter.TableFormatKey
}
}
containerCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewContainerFormat(format, opts.quiet, listOptions.Size),
Trunc: !opts.noTrunc,
}
return formatter.ContainerWrite(containerCtx, containers)
}

View File

@ -0,0 +1,76 @@
package container
import (
"io"
"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"
"golang.org/x/net/context"
)
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 (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
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()
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()
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
if err != nil {
return err
}
if c.Config.Tty {
_, err = io.Copy(dockerCli.Out(), responseBody)
} else {
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody)
}
return err
}

View File

@ -0,0 +1,903 @@
package container
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"path"
"regexp"
"strconv"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/signal"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
var (
deviceCgroupRuleRegexp = regexp.MustCompile("^[acb] ([0-9]+|\\*):([0-9]+|\\*) [rwm]{1,3}$")
)
// containerOptions is a data object with all the options for creating a container
type containerOptions struct {
attach opts.ListOpts
volumes opts.ListOpts
tmpfs opts.ListOpts
mounts opts.MountOpt
blkioWeightDevice opts.WeightdeviceOpt
deviceReadBps opts.ThrottledeviceOpt
deviceWriteBps opts.ThrottledeviceOpt
links opts.ListOpts
aliases opts.ListOpts
linkLocalIPs opts.ListOpts
deviceReadIOps opts.ThrottledeviceOpt
deviceWriteIOps opts.ThrottledeviceOpt
env opts.ListOpts
labels opts.ListOpts
deviceCgroupRules opts.ListOpts
devices opts.ListOpts
ulimits *opts.UlimitOpt
sysctls *opts.MapOpts
publish opts.ListOpts
expose opts.ListOpts
dns opts.ListOpts
dnsSearch opts.ListOpts
dnsOptions opts.ListOpts
extraHosts opts.ListOpts
volumesFrom opts.ListOpts
envFile opts.ListOpts
capAdd opts.ListOpts
capDrop opts.ListOpts
groupAdd opts.ListOpts
securityOpt opts.ListOpts
storageOpt opts.ListOpts
labelsFile opts.ListOpts
loggingOpts opts.ListOpts
privileged bool
pidMode string
utsMode string
usernsMode string
publishAll bool
stdin bool
tty bool
oomKillDisable bool
oomScoreAdj int
containerIDFile string
entrypoint string
hostname string
memory opts.MemBytes
memoryReservation opts.MemBytes
memorySwap opts.MemSwapBytes
kernelMemory opts.MemBytes
user string
workingDir string
cpuCount int64
cpuShares int64
cpuPercent int64
cpuPeriod int64
cpuRealtimePeriod int64
cpuRealtimeRuntime int64
cpuQuota int64
cpus opts.NanoCPUs
cpusetCpus string
cpusetMems string
blkioWeight uint16
ioMaxBandwidth opts.MemBytes
ioMaxIOps uint64
swappiness int64
netMode string
macAddress string
ipv4Address string
ipv6Address string
ipcMode string
pidsLimit int64
restartPolicy string
readonlyRootfs bool
loggingDriver string
cgroupParent string
volumeDriver string
stopSignal string
stopTimeout int
isolation string
shmSize opts.MemBytes
noHealthcheck bool
healthCmd string
healthInterval time.Duration
healthTimeout time.Duration
healthStartPeriod time.Duration
healthRetries int
runtime string
autoRemove bool
init bool
initPath string
Image string
Args []string
}
// addFlags adds all command line flags that will be used by parse to the FlagSet
func addFlags(flags *pflag.FlagSet) *containerOptions {
copts := &containerOptions{
aliases: opts.NewListOpts(nil),
attach: opts.NewListOpts(validateAttach),
blkioWeightDevice: opts.NewWeightdeviceOpt(opts.ValidateWeightDevice),
capAdd: opts.NewListOpts(nil),
capDrop: opts.NewListOpts(nil),
dns: opts.NewListOpts(opts.ValidateIPAddress),
dnsOptions: opts.NewListOpts(nil),
dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
deviceCgroupRules: opts.NewListOpts(validateDeviceCgroupRule),
deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice),
deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice),
deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice),
deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice),
devices: opts.NewListOpts(validateDevice),
env: opts.NewListOpts(opts.ValidateEnv),
envFile: opts.NewListOpts(nil),
expose: opts.NewListOpts(nil),
extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
groupAdd: opts.NewListOpts(nil),
labels: opts.NewListOpts(opts.ValidateEnv),
labelsFile: opts.NewListOpts(nil),
linkLocalIPs: opts.NewListOpts(nil),
links: opts.NewListOpts(opts.ValidateLink),
loggingOpts: opts.NewListOpts(nil),
publish: opts.NewListOpts(nil),
securityOpt: opts.NewListOpts(nil),
storageOpt: opts.NewListOpts(nil),
sysctls: opts.NewMapOpts(nil, opts.ValidateSysctl),
tmpfs: opts.NewListOpts(nil),
ulimits: opts.NewUlimitOpt(nil),
volumes: opts.NewListOpts(nil),
volumesFrom: opts.NewListOpts(nil),
}
// General purpose flags
flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR")
flags.Var(&copts.deviceCgroupRules, "device-cgroup-rule", "Add a rule to the cgroup allowed devices list")
flags.Var(&copts.devices, "device", "Add a host device to the container")
flags.VarP(&copts.env, "env", "e", "Set environment variables")
flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables")
flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image")
flags.Var(&copts.groupAdd, "group-add", "Add additional groups to join")
flags.StringVarP(&copts.hostname, "hostname", "h", "", "Container host name")
flags.BoolVarP(&copts.stdin, "interactive", "i", false, "Keep STDIN open even if not attached")
flags.VarP(&copts.labels, "label", "l", "Set meta data on a container")
flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels")
flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only")
flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits")
flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, "Signal to stop a container")
flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container")
flags.SetAnnotation("stop-timeout", "version", []string{"1.25"})
flags.Var(copts.sysctls, "sysctl", "Sysctl options")
flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
flags.Var(copts.ulimits, "ulimit", "Ulimit options")
flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container")
flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container when it exits")
// Security
flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities")
flags.Var(&copts.capDrop, "cap-drop", "Drop Linux capabilities")
flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container")
flags.Var(&copts.securityOpt, "security-opt", "Security Options")
flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use")
// Network and port publishing flag
flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
flags.Var(&copts.dns, "dns", "Set custom DNS servers")
// We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way.
// This is to be consistent with service create/update
flags.Var(&copts.dnsOptions, "dns-opt", "Set DNS options")
flags.Var(&copts.dnsOptions, "dns-option", "Set DNS options")
flags.MarkHidden("dns-opt")
flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains")
flags.Var(&copts.expose, "expose", "Expose a port or a range of ports")
flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)")
flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)")
flags.Var(&copts.links, "link", "Add link to another container")
flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses")
flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)")
flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host")
flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports")
// We allow for both "--net" and "--network", although the latter is the recommended way.
flags.StringVar(&copts.netMode, "net", "default", "Connect a container to a network")
flags.StringVar(&copts.netMode, "network", "default", "Connect a container to a network")
flags.MarkHidden("net")
// We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way.
flags.Var(&copts.aliases, "net-alias", "Add network-scoped alias for the container")
flags.Var(&copts.aliases, "network-alias", "Add network-scoped alias for the container")
flags.MarkHidden("net-alias")
// Logging and storage
flags.StringVar(&copts.loggingDriver, "log-driver", "", "Logging driver for the container")
flags.StringVar(&copts.volumeDriver, "volume-driver", "", "Optional volume driver for the container")
flags.Var(&copts.loggingOpts, "log-opt", "Log driver options")
flags.Var(&copts.storageOpt, "storage-opt", "Storage driver options for the container")
flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory")
flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)")
flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume")
flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container")
// Health-checking
flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health")
flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ns|us|ms|s|m|h) (default 0s)")
flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy")
flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ns|us|ms|s|m|h) (default 0s)")
flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ns|us|ms|s|m|h) (default 0s)")
flags.SetAnnotation("health-start-period", "version", []string{"1.29"})
flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK")
// Resource management
flags.Uint16Var(&copts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)")
flags.Var(&copts.blkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)")
flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file")
flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)")
flags.SetAnnotation("cpu-count", "ostype", []string{"windows"})
flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)")
flags.SetAnnotation("cpu-percent", "ostype", []string{"windows"})
flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period")
flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota")
flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds")
flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"})
flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds")
flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"})
flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
flags.Var(&copts.cpus, "cpus", "Number of CPUs")
flags.SetAnnotation("cpus", "version", []string{"1.25"})
flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device")
flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device")
flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device")
flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device")
flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)")
flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"})
flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)")
flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"})
flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit")
flags.VarP(&copts.memory, "memory", "m", "Memory limit")
flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit")
flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)")
flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer")
flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)")
flags.Int64Var(&copts.pidsLimit, "pids-limit", 0, "Tune container pids limit (set -1 for unlimited)")
// Low-level execution (cgroups, namespaces, ...)
flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use")
flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology")
flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use")
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use")
flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container")
flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes")
flags.SetAnnotation("init", "version", []string{"1.25"})
flags.StringVar(&copts.initPath, "init-path", "", "Path to the docker-init binary")
flags.SetAnnotation("init-path", "version", []string{"1.25"})
return copts
}
type containerConfig struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *networktypes.NetworkingConfig
}
// parse parses the args for the specified command and generates a Config,
// a HostConfig and returns them with the specified command.
// If the specified args are not valid, it will return an error.
func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, error) {
var (
attachStdin = copts.attach.Get("stdin")
attachStdout = copts.attach.Get("stdout")
attachStderr = copts.attach.Get("stderr")
)
// Validate the input mac address
if copts.macAddress != "" {
if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil {
return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress)
}
}
if copts.stdin {
attachStdin = true
}
// If -a is not set, attach to stdout and stderr
if copts.attach.Len() == 0 {
attachStdout = true
attachStderr = true
}
var err error
swappiness := copts.swappiness
if swappiness != -1 && (swappiness < 0 || swappiness > 100) {
return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
}
mounts := copts.mounts.Value()
if len(mounts) > 0 && copts.volumeDriver != "" {
logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.")
}
var binds []string
volumes := copts.volumes.GetMap()
// add any bind targets to the list of container volumes
for bind := range copts.volumes.GetMap() {
if arr := volumeSplitN(bind, 2); len(arr) > 1 {
// after creating the bind mount we want to delete it from the copts.volumes values because
// we do not want bind mounts being committed to image configs
binds = append(binds, bind)
// We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if
// there are duplicates entries.
delete(volumes, bind)
}
}
// Can't evaluate options passed into --tmpfs until we actually mount
tmpfs := make(map[string]string)
for _, t := range copts.tmpfs.GetAll() {
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
tmpfs[arr[0]] = arr[1]
} else {
tmpfs[arr[0]] = ""
}
}
var (
runCmd strslice.StrSlice
entrypoint strslice.StrSlice
)
if len(copts.Args) > 0 {
runCmd = strslice.StrSlice(copts.Args)
}
if copts.entrypoint != "" {
entrypoint = strslice.StrSlice{copts.entrypoint}
} else if flags.Changed("entrypoint") {
// if `--entrypoint=` is parsed then Entrypoint is reset
entrypoint = []string{""}
}
ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll())
if err != nil {
return nil, err
}
// Merge in exposed ports to the map of published ports
for _, e := range copts.expose.GetAll() {
if strings.Contains(e, ":") {
return nil, errors.Errorf("invalid port format for --expose: %s", e)
}
//support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
proto, port := nat.SplitProtoPort(e)
//parse the start and end port and create a sequence of ports to expose
//if expose a port, the start and end port are the same
start, end, err := nat.ParsePortRange(port)
if err != nil {
return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err)
}
for i := start; i <= end; i++ {
p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
if err != nil {
return nil, err
}
if _, exists := ports[p]; !exists {
ports[p] = struct{}{}
}
}
}
// parse device mappings
deviceMappings := []container.DeviceMapping{}
for _, device := range copts.devices.GetAll() {
deviceMapping, err := parseDevice(device)
if err != nil {
return nil, err
}
deviceMappings = append(deviceMappings, deviceMapping)
}
// collect all the environment variables for the container
envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
if err != nil {
return nil, err
}
// collect all the labels for the container
labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
if err != nil {
return nil, err
}
ipcMode := container.IpcMode(copts.ipcMode)
if !ipcMode.Valid() {
return nil, errors.Errorf("--ipc: invalid IPC mode")
}
pidMode := container.PidMode(copts.pidMode)
if !pidMode.Valid() {
return nil, errors.Errorf("--pid: invalid PID mode")
}
utsMode := container.UTSMode(copts.utsMode)
if !utsMode.Valid() {
return nil, errors.Errorf("--uts: invalid UTS mode")
}
usernsMode := container.UsernsMode(copts.usernsMode)
if !usernsMode.Valid() {
return nil, errors.Errorf("--userns: invalid USER mode")
}
restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy)
if err != nil {
return nil, err
}
loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll())
if err != nil {
return nil, err
}
securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll())
if err != nil {
return nil, err
}
storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll())
if err != nil {
return nil, err
}
// Healthcheck
var healthConfig *container.HealthConfig
haveHealthSettings := copts.healthCmd != "" ||
copts.healthInterval != 0 ||
copts.healthTimeout != 0 ||
copts.healthStartPeriod != 0 ||
copts.healthRetries != 0
if copts.noHealthcheck {
if haveHealthSettings {
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
}
test := strslice.StrSlice{"NONE"}
healthConfig = &container.HealthConfig{Test: test}
} else if haveHealthSettings {
var probe strslice.StrSlice
if copts.healthCmd != "" {
args := []string{"CMD-SHELL", copts.healthCmd}
probe = strslice.StrSlice(args)
}
if copts.healthInterval < 0 {
return nil, errors.Errorf("--health-interval cannot be negative")
}
if copts.healthTimeout < 0 {
return nil, errors.Errorf("--health-timeout cannot be negative")
}
if copts.healthRetries < 0 {
return nil, errors.Errorf("--health-retries cannot be negative")
}
if copts.healthStartPeriod < 0 {
return nil, fmt.Errorf("--health-start-period cannot be negative")
}
healthConfig = &container.HealthConfig{
Test: probe,
Interval: copts.healthInterval,
Timeout: copts.healthTimeout,
StartPeriod: copts.healthStartPeriod,
Retries: copts.healthRetries,
}
}
resources := container.Resources{
CgroupParent: copts.cgroupParent,
Memory: copts.memory.Value(),
MemoryReservation: copts.memoryReservation.Value(),
MemorySwap: copts.memorySwap.Value(),
MemorySwappiness: &copts.swappiness,
KernelMemory: copts.kernelMemory.Value(),
OomKillDisable: &copts.oomKillDisable,
NanoCPUs: copts.cpus.Value(),
CPUCount: copts.cpuCount,
CPUPercent: copts.cpuPercent,
CPUShares: copts.cpuShares,
CPUPeriod: copts.cpuPeriod,
CpusetCpus: copts.cpusetCpus,
CpusetMems: copts.cpusetMems,
CPUQuota: copts.cpuQuota,
CPURealtimePeriod: copts.cpuRealtimePeriod,
CPURealtimeRuntime: copts.cpuRealtimeRuntime,
PidsLimit: copts.pidsLimit,
BlkioWeight: copts.blkioWeight,
BlkioWeightDevice: copts.blkioWeightDevice.GetList(),
BlkioDeviceReadBps: copts.deviceReadBps.GetList(),
BlkioDeviceWriteBps: copts.deviceWriteBps.GetList(),
BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(),
BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(),
IOMaximumIOps: copts.ioMaxIOps,
IOMaximumBandwidth: uint64(copts.ioMaxBandwidth),
Ulimits: copts.ulimits.GetList(),
DeviceCgroupRules: copts.deviceCgroupRules.GetAll(),
Devices: deviceMappings,
}
config := &container.Config{
Hostname: copts.hostname,
ExposedPorts: ports,
User: copts.user,
Tty: copts.tty,
// TODO: deprecated, it comes from -n, --networking
// it's still needed internally to set the network to disabled
// if e.g. bridge is none in daemon opts, and in inspect
NetworkDisabled: false,
OpenStdin: copts.stdin,
AttachStdin: attachStdin,
AttachStdout: attachStdout,
AttachStderr: attachStderr,
Env: envVariables,
Cmd: runCmd,
Image: copts.Image,
Volumes: volumes,
MacAddress: copts.macAddress,
Entrypoint: entrypoint,
WorkingDir: copts.workingDir,
Labels: runconfigopts.ConvertKVStringsToMap(labels),
Healthcheck: healthConfig,
}
if flags.Changed("stop-signal") {
config.StopSignal = copts.stopSignal
}
if flags.Changed("stop-timeout") {
config.StopTimeout = &copts.stopTimeout
}
hostConfig := &container.HostConfig{
Binds: binds,
ContainerIDFile: copts.containerIDFile,
OomScoreAdj: copts.oomScoreAdj,
AutoRemove: copts.autoRemove,
Privileged: copts.privileged,
PortBindings: portBindings,
Links: copts.links.GetAll(),
PublishAllPorts: copts.publishAll,
// Make sure the dns fields are never nil.
// New containers don't ever have those fields nil,
// but pre created containers can still have those nil values.
// See https://github.com/docker/docker/pull/17779
// for a more detailed explanation on why we don't want that.
DNS: copts.dns.GetAllOrEmpty(),
DNSSearch: copts.dnsSearch.GetAllOrEmpty(),
DNSOptions: copts.dnsOptions.GetAllOrEmpty(),
ExtraHosts: copts.extraHosts.GetAll(),
VolumesFrom: copts.volumesFrom.GetAll(),
NetworkMode: container.NetworkMode(copts.netMode),
IpcMode: ipcMode,
PidMode: pidMode,
UTSMode: utsMode,
UsernsMode: usernsMode,
CapAdd: strslice.StrSlice(copts.capAdd.GetAll()),
CapDrop: strslice.StrSlice(copts.capDrop.GetAll()),
GroupAdd: copts.groupAdd.GetAll(),
RestartPolicy: restartPolicy,
SecurityOpt: securityOpts,
StorageOpt: storageOpts,
ReadonlyRootfs: copts.readonlyRootfs,
LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts},
VolumeDriver: copts.volumeDriver,
Isolation: container.Isolation(copts.isolation),
ShmSize: copts.shmSize.Value(),
Resources: resources,
Tmpfs: tmpfs,
Sysctls: copts.sysctls.GetAll(),
Runtime: copts.runtime,
Mounts: mounts,
}
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
return nil, errors.Errorf("Conflicting options: --restart and --rm")
}
// only set this value if the user provided the flag, else it should default to nil
if flags.Changed("init") {
hostConfig.Init = &copts.init
}
// When allocating stdin in attached mode, close stdin at client disconnect
if config.OpenStdin && config.AttachStdin {
config.StdinOnce = true
}
networkingConfig := &networktypes.NetworkingConfig{
EndpointsConfig: make(map[string]*networktypes.EndpointSettings),
}
if copts.ipv4Address != "" || copts.ipv6Address != "" || copts.linkLocalIPs.Len() > 0 {
epConfig := &networktypes.EndpointSettings{}
networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig
epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{
IPv4Address: copts.ipv4Address,
IPv6Address: copts.ipv6Address,
}
if copts.linkLocalIPs.Len() > 0 {
epConfig.IPAMConfig.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len())
copy(epConfig.IPAMConfig.LinkLocalIPs, copts.linkLocalIPs.GetAll())
}
}
if hostConfig.NetworkMode.IsUserDefined() && len(hostConfig.Links) > 0 {
epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)]
if epConfig == nil {
epConfig = &networktypes.EndpointSettings{}
}
epConfig.Links = make([]string, len(hostConfig.Links))
copy(epConfig.Links, hostConfig.Links)
networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig
}
if copts.aliases.Len() > 0 {
epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)]
if epConfig == nil {
epConfig = &networktypes.EndpointSettings{}
}
epConfig.Aliases = make([]string, copts.aliases.Len())
copy(epConfig.Aliases, copts.aliases.GetAll())
networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig
}
return &containerConfig{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
}, nil
}
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts)
if loggingDriver == "none" && len(loggingOpts) > 0 {
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
}
return loggingOptsMap, nil
}
// takes a local seccomp daemon, reads the file contents for sending to the daemon
func parseSecurityOpts(securityOpts []string) ([]string, error) {
for key, opt := range securityOpts {
con := strings.SplitN(opt, "=", 2)
if len(con) == 1 && con[0] != "no-new-privileges" {
if strings.Contains(opt, ":") {
con = strings.SplitN(opt, ":", 2)
} else {
return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt)
}
}
if con[0] == "seccomp" && con[1] != "unconfined" {
f, err := ioutil.ReadFile(con[1])
if err != nil {
return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err)
}
b := bytes.NewBuffer(nil)
if err := json.Compact(b, f); err != nil {
return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err)
}
securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
}
}
return securityOpts, nil
}
// parses storage options per container into a map
func parseStorageOpts(storageOpts []string) (map[string]string, error) {
m := make(map[string]string)
for _, option := range storageOpts {
if strings.Contains(option, "=") {
opt := strings.SplitN(option, "=", 2)
m[opt[0]] = opt[1]
} else {
return nil, errors.Errorf("invalid storage option")
}
}
return m, nil
}
// parseDevice parses a device mapping string to a container.DeviceMapping struct
func parseDevice(device string) (container.DeviceMapping, error) {
src := ""
dst := ""
permissions := "rwm"
arr := strings.Split(device, ":")
switch len(arr) {
case 3:
permissions = arr[2]
fallthrough
case 2:
if validDeviceMode(arr[1]) {
permissions = arr[1]
} else {
dst = arr[1]
}
fallthrough
case 1:
src = arr[0]
default:
return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device)
}
if dst == "" {
dst = src
}
deviceMapping := container.DeviceMapping{
PathOnHost: src,
PathInContainer: dst,
CgroupPermissions: permissions,
}
return deviceMapping, nil
}
// validateDeviceCgroupRule validates a device cgroup rule string format
// It will make sure 'val' is in the form:
// 'type major:minor mode'
func validateDeviceCgroupRule(val string) (string, error) {
if deviceCgroupRuleRegexp.MatchString(val) {
return val, nil
}
return val, errors.Errorf("invalid device cgroup format '%s'", val)
}
// validDeviceMode checks if the mode for device is valid or not.
// Valid mode is a composition of r (read), w (write), and m (mknod).
func validDeviceMode(mode string) bool {
var legalDeviceMode = map[rune]bool{
'r': true,
'w': true,
'm': true,
}
if mode == "" {
return false
}
for _, c := range mode {
if !legalDeviceMode[c] {
return false
}
legalDeviceMode[c] = false
}
return true
}
// validateDevice validates a path for devices
// It will make sure 'val' is in the form:
// [host-dir:]container-path[:mode]
// It also validates the device mode.
func validateDevice(val string) (string, error) {
return validatePath(val, validDeviceMode)
}
func validatePath(val string, validator func(string) bool) (string, error) {
var containerPath string
var mode string
if strings.Count(val, ":") > 2 {
return val, errors.Errorf("bad format for path: %s", val)
}
split := strings.SplitN(val, ":", 3)
if split[0] == "" {
return val, errors.Errorf("bad format for path: %s", val)
}
switch len(split) {
case 1:
containerPath = split[0]
val = path.Clean(containerPath)
case 2:
if isValid := validator(split[1]); isValid {
containerPath = split[0]
mode = split[1]
val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode)
} else {
containerPath = split[1]
val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath))
}
case 3:
containerPath = split[1]
mode = split[2]
if isValid := validator(split[2]); !isValid {
return val, errors.Errorf("bad mode specified: %s", mode)
}
val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode)
}
if !path.IsAbs(containerPath) {
return val, errors.Errorf("%s is not an absolute path", containerPath)
}
return val, nil
}
// volumeSplitN splits raw into a maximum of n parts, separated by a separator colon.
// A separator colon is the last `:` character in the regex `[:\\]?[a-zA-Z]:` (note `\\` is `\` escaped).
// In Windows driver letter appears in two situations:
// a. `^[a-zA-Z]:` (A colon followed by `^[a-zA-Z]:` is OK as colon is the separator in volume option)
// b. A string in the format like `\\?\C:\Windows\...` (UNC).
// Therefore, a driver letter can only follow either a `:` or `\\`
// This allows to correctly split strings such as `C:\foo:D:\:rw` or `/tmp/q:/foo`.
func volumeSplitN(raw string, n int) []string {
var array []string
if len(raw) == 0 || raw[0] == ':' {
// invalid
return nil
}
// numberOfParts counts the number of parts separated by a separator colon
numberOfParts := 0
// left represents the left-most cursor in raw, updated at every `:` character considered as a separator.
left := 0
// right represents the right-most cursor in raw incremented with the loop. Note this
// starts at index 1 as index 0 is already handle above as a special case.
for right := 1; right < len(raw); right++ {
// stop parsing if reached maximum number of parts
if n >= 0 && numberOfParts >= n {
break
}
if raw[right] != ':' {
continue
}
potentialDriveLetter := raw[right-1]
if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') {
if right > 1 {
beforePotentialDriveLetter := raw[right-2]
// Only `:` or `\\` are checked (`/` could fall into the case of `/tmp/q:/foo`)
if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '\\' {
// e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`.
array = append(array, raw[left:right])
left = right + 1
numberOfParts++
}
// else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing.
}
// if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing.
} else {
// if `:` is not preceded by a potential drive letter, then consider it as a delimiter.
array = append(array, raw[left:right])
left = right + 1
numberOfParts++
}
}
// need to take care of the last part
if left < len(raw) {
if n >= 0 && numberOfParts >= n {
// if the maximum number of parts is reached, just append the rest to the last part
// left-1 is at the last `:` that needs to be included since not considered a separator.
array[n-1] += raw[left-1:]
} else {
array = append(array, raw[left:])
}
}
return array
}
// validateAttach validates that the specified string is a valid attach option.
func validateAttach(val string) (string, error) {
s := strings.ToLower(val)
for _, str := range []string{"stdin", "stdout", "stderr"} {
if s == str {
return s, nil
}
}
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
}

View File

@ -0,0 +1,870 @@
package container
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/runconfig"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
func TestValidateAttach(t *testing.T) {
valid := []string{
"stdin",
"stdout",
"stderr",
"STDIN",
"STDOUT",
"STDERR",
}
if _, err := validateAttach("invalid"); err == nil {
t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing")
}
for _, attach := range valid {
value, err := validateAttach(attach)
if err != nil {
t.Fatal(err)
}
if value != strings.ToLower(attach) {
t.Fatalf("Expected [%v], got [%v]", attach, value)
}
}
}
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
flags.SetOutput(ioutil.Discard)
flags.Usage = nil
copts := addFlags(flags)
if err := flags.Parse(args); err != nil {
return nil, nil, nil, err
}
// TODO: fix tests to accept ContainerConfig
containerConfig, err := parse(flags, copts)
if err != nil {
return nil, nil, nil, err
}
return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err
}
func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) {
config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
return config, hostConfig, err
}
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
config, hostConfig, err := parsetest(t, args)
if err != nil {
t.Fatal(err)
}
return config, hostConfig
}
func TestParseRunLinks(t *testing.T) {
if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
}
if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
}
if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 {
t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
}
}
func TestParseRunAttach(t *testing.T) {
if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
}
if config, _ := mustParse(t, "-a stdin -a stdout"); !config.AttachStdin || !config.AttachStdout || config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect only Stdin and Stdout enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
}
if config, _ := mustParse(t, "-a stdin -a stdout -a stderr"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect all attach enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
}
if config, _ := mustParse(t, ""); config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect Stdin disabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
}
if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
}
if _, _, err := parsetest(t, "-a"); err == nil {
t.Fatal("Error parsing attach flags, `-a` should be an error but is not")
}
if _, _, err := parsetest(t, "-a invalid"); err == nil {
t.Fatal("Error parsing attach flags, `-a invalid` should be an error but is not")
}
if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stdin -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdin -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stdout -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stdout -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-a stderr -d"); err == nil {
t.Fatal("Error parsing attach flags, `-a stderr -d` should be an error but is not")
}
if _, _, err := parsetest(t, "-d --rm"); err == nil {
t.Fatal("Error parsing attach flags, `-d --rm` should be an error but is not")
}
}
func TestParseRunVolumes(t *testing.T) {
// A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
} else if _, exists := config.Volumes[arr[0]]; !exists {
t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
}
// Two volumes
arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
} else if _, exists := config.Volumes[arr[0]]; !exists {
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
} else if _, exists := config.Volumes[arr[1]]; !exists {
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
}
// A single bind-mount
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
}
// Two bind-mounts.
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
}
// Two bind-mounts, first read-only, second read-write.
// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
}
// Similar to previous test but with alternate modes which are only supported by Linux
if runtime.GOOS != "windows" {
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
}
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
}
}
// One bind mount and one volume
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
} else if _, exists := config.Volumes[arr[1]]; !exists {
t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
}
// Root to non-c: drive letter (Windows specific)
if runtime.GOOS == "windows" {
arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
}
}
}
// setupPlatformVolume takes two arrays of volume specs - a Unix style
// spec and a Windows style spec. Depending on the platform being unit tested,
// it returns one of them, along with a volume string that would be passed
// on the docker CLI (e.g. -v /bar -v /foo).
func setupPlatformVolume(u []string, w []string) ([]string, string) {
var a []string
if runtime.GOOS == "windows" {
a = w
} else {
a = u
}
s := ""
for _, v := range a {
s = s + "-v " + v + " "
}
return a, s
}
// check if (a == c && b == d) || (a == d && b == c)
// because maps are randomized
func compareRandomizedStrings(a, b, c, d string) error {
if a == c && b == d {
return nil
}
if a == d && b == c {
return nil
}
return errors.Errorf("strings don't match")
}
// Simple parse with MacAddress validation
func TestParseWithMacAddress(t *testing.T) {
invalidMacAddress := "--mac-address=invalidMacAddress"
validMacAddress := "--mac-address=92:d0:c6:0a:29:33"
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
}
if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" {
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress)
}
}
func TestParseWithMemory(t *testing.T) {
invalidMemory := "--memory=invalid"
_, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"})
testutil.ErrorContains(t, err, invalidMemory)
_, hostconfig := mustParse(t, "--memory=1G")
assert.Equal(t, int64(1073741824), hostconfig.Memory)
}
func TestParseWithMemorySwap(t *testing.T) {
invalidMemory := "--memory-swap=invalid"
_, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"})
testutil.ErrorContains(t, err, invalidMemory)
_, hostconfig := mustParse(t, "--memory-swap=1G")
assert.Equal(t, int64(1073741824), hostconfig.MemorySwap)
_, hostconfig = mustParse(t, "--memory-swap=-1")
assert.Equal(t, int64(-1), hostconfig.MemorySwap)
}
func TestParseHostname(t *testing.T) {
validHostnames := map[string]string{
"hostname": "hostname",
"host-name": "host-name",
"hostname123": "hostname123",
"123hostname": "123hostname",
"hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error",
}
hostnameWithDomain := "--hostname=hostname.domainname"
hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
for hostname, expectedHostname := range validHostnames {
if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname {
t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname)
}
}
if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" {
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname)
}
if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" && config.Domainname != "" {
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got '%v'", config.Hostname)
}
}
func TestParseWithExpose(t *testing.T) {
invalids := map[string]string{
":": "invalid port format for --expose: :",
"8080:9090": "invalid port format for --expose: 8080:9090",
"/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.",
"/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.",
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
}
valids := map[string][]nat.Port{
"8080/tcp": {"8080/tcp"},
"8080/udp": {"8080/udp"},
"8080/ncp": {"8080/ncp"},
"8080-8080/udp": {"8080/udp"},
"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
}
for expose, expectedError := range invalids {
if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError {
t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err)
}
}
for expose, exposedPorts := range valids {
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.ExposedPorts) != len(exposedPorts) {
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
}
for _, port := range exposedPorts {
if _, ok := config.ExposedPorts[port]; !ok {
t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
}
}
}
// Merge with actual published port
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.ExposedPorts) != 2 {
t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts)
}
ports := []nat.Port{"80/tcp", "81/tcp"}
for _, port := range ports {
if _, ok := config.ExposedPorts[port]; !ok {
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
}
}
}
func TestParseDevice(t *testing.T) {
valids := map[string]container.DeviceMapping{
"/dev/snd": {
PathOnHost: "/dev/snd",
PathInContainer: "/dev/snd",
CgroupPermissions: "rwm",
},
"/dev/snd:rw": {
PathOnHost: "/dev/snd",
PathInContainer: "/dev/snd",
CgroupPermissions: "rw",
},
"/dev/snd:/something": {
PathOnHost: "/dev/snd",
PathInContainer: "/something",
CgroupPermissions: "rwm",
},
"/dev/snd:/something:rw": {
PathOnHost: "/dev/snd",
PathInContainer: "/something",
CgroupPermissions: "rw",
},
}
for device, deviceMapping := range valids {
_, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(hostconfig.Devices) != 1 {
t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices)
}
if hostconfig.Devices[0] != deviceMapping {
t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices)
}
}
}
func TestParseModes(t *testing.T) {
// ipc ko
if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" {
t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err)
}
// ipc ok
_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if !hostconfig.IpcMode.Valid() {
t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
}
// pid ko
if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" {
t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err)
}
// pid ok
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if !hostconfig.PidMode.Valid() {
t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
}
// uts ko
if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" {
t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err)
}
// uts ok
_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if !hostconfig.UTSMode.Valid() {
t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode)
}
// shm-size ko
expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'`
if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr {
t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err)
}
// shm-size ok
_, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if hostconfig.ShmSize != 134217728 {
t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
}
}
func TestParseRestartPolicy(t *testing.T) {
invalids := map[string]string{
"always:2:3": "invalid restart policy format",
"on-failure:invalid": "maximum retry count must be an integer",
}
valids := map[string]container.RestartPolicy{
"": {},
"always": {
Name: "always",
MaximumRetryCount: 0,
},
"on-failure:1": {
Name: "on-failure",
MaximumRetryCount: 1,
},
}
for restart, expectedError := range invalids {
if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError {
t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err)
}
}
for restart, expected := range valids {
_, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if hostconfig.RestartPolicy != expected {
t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy)
}
}
}
func TestParseRestartPolicyAutoRemove(t *testing.T) {
expected := "Conflicting options: --restart and --rm"
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"})
if err == nil || err.Error() != expected {
t.Fatalf("Expected error %v, but got none", expected)
}
}
func TestParseHealth(t *testing.T) {
checkOk := func(args ...string) *container.HealthConfig {
config, _, _, err := parseRun(args)
if err != nil {
t.Fatalf("%#v: %v", args, err)
}
return config.Healthcheck
}
checkError := func(expected string, args ...string) {
config, _, _, err := parseRun(args)
if err == nil {
t.Fatalf("Expected error, but got %#v", config)
}
if err.Error() != expected {
t.Fatalf("Expected %#v, got %#v", expected, err)
}
}
health := checkOk("--no-healthcheck", "img", "cmd")
if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" {
t.Fatalf("--no-healthcheck failed: %#v", health)
}
health = checkOk("--health-cmd=/check.sh -q", "img", "cmd")
if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" {
t.Fatalf("--health-cmd: got %#v", health.Test)
}
if health.Timeout != 0 {
t.Fatalf("--health-cmd: timeout = %s", health.Timeout)
}
checkError("--no-healthcheck conflicts with --health-* options",
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "img", "cmd")
if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second {
t.Fatalf("--health-*: got %#v", health)
}
}
func TestParseLoggingOpts(t *testing.T) {
// logging opts ko
if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" {
t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err)
}
// logging opts ok
_, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 {
t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy)
}
}
func TestParseEnvfileVariables(t *testing.T) {
e := "open nonexistent: no such file or directory"
if runtime.GOOS == "windows" {
e = "open nonexistent: The system cannot find the file specified."
}
// env ko
if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
t.Fatalf("Expected an error with message '%s', got %v", e, err)
}
// env ok
config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" {
t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env)
}
config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" {
t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env)
}
}
func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
// UTF8 with BOM
config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"}
if len(config.Env) != len(env) {
t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env)
}
for i, v := range env {
if config.Env[i] != v {
t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i]))
}
}
// UTF16 with BOM
e := "contains invalid utf8 bytes at line"
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
t.Fatalf("Expected an error with message '%s', got %v", e, err)
}
// UTF16BE with BOM
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
t.Fatalf("Expected an error with message '%s', got %v", e, err)
}
}
func TestParseLabelfileVariables(t *testing.T) {
e := "open nonexistent: no such file or directory"
if runtime.GOOS == "windows" {
e = "open nonexistent: The system cannot find the file specified."
}
// label ko
if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
t.Fatalf("Expected an error with message '%s', got %v", e, err)
}
// label ok
config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" {
t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels)
}
config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"})
if err != nil {
t.Fatal(err)
}
if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" {
t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels)
}
}
func TestParseEntryPoint(t *testing.T) {
config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
if err != nil {
t.Fatal(err)
}
if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" {
t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint)
}
}
// This tests the cases for binds which are generated through
// DecodeContainerConfig rather than Parse()
func TestDecodeContainerConfigVolumes(t *testing.T) {
// Root to root
bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// No destination path
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// // No destination path or mode
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing
bindsOrVols = []string{`:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing with no mode
bindsOrVols = []string{`::`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Too much including an invalid mode
wTmp := os.Getenv("TEMP")
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Windows specific error tests
if runtime.GOOS == "windows" {
// Volume which does not include a drive letter
bindsOrVols = []string{`\tmp`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Root to C-Drive
bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Container path that does not include a drive letter
bindsOrVols = []string{`c:\windows:\somewhere`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
}
// Linux-specific error tests
if runtime.GOOS != "windows" {
// Just root
bindsOrVols = []string{`/`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A single volume that looks like a bind mount passed in Volumes.
// This should be handled as a bind mount, not a volume.
vols := []string{`/foo:/bar`}
if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
} else if hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
} else if _, exists := config.Volumes[vols[0]]; !exists {
t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
}
}
}
// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes
// to call DecodeContainerConfig. It effectively does what a client would
// do when calling the daemon by constructing a JSON stream of a
// ContainerConfigWrapper which is populated by the set of volume specs
// passed into it. It returns a config and a hostconfig which can be
// validated to ensure DecodeContainerConfig has manipulated the structures
// correctly.
func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) {
var (
b []byte
err error
c *container.Config
h *container.HostConfig
)
w := runconfig.ContainerConfigWrapper{
Config: &container.Config{
Volumes: map[string]struct{}{},
},
HostConfig: &container.HostConfig{
NetworkMode: "none",
Binds: binds,
},
}
for _, v := range volumes {
w.Config.Volumes[v] = struct{}{}
}
if b, err = json.Marshal(w); err != nil {
return nil, nil, errors.Errorf("Error on marshal %s", err.Error())
}
c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b))
if err != nil {
return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err)
}
if c == nil || h == nil {
return nil, nil, errors.Errorf("Empty config or hostconfig")
}
return c, h, err
}
func TestVolumeSplitN(t *testing.T) {
for _, x := range []struct {
input string
n int
expected []string
}{
{`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}},
{`:C:\foo:d:`, -1, nil},
{`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}},
{`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}},
{`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}},
{`d:\`, -1, []string{`d:\`}},
{`d:`, -1, []string{`d:`}},
{`d:\path`, -1, []string{`d:\path`}},
{`d:\path with space`, -1, []string{`d:\path with space`}},
{`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}},
{`c:\:d:\`, -1, []string{`c:\`, `d:\`}},
{`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}},
{`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}},
{`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}},
{`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}},
{`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}},
{`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}},
{`name:D:`, -1, []string{`name`, `D:`}},
{`name:D::rW`, -1, []string{`name`, `D:`, `rW`}},
{`name:D::RW`, -1, []string{`name`, `D:`, `RW`}},
{`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}},
{`c:\Windows`, -1, []string{`c:\Windows`}},
{`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}},
{``, -1, nil},
{`.`, -1, []string{`.`}},
{`..\`, -1, []string{`..\`}},
{`c:\:..\`, -1, []string{`c:\`, `..\`}},
{`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}},
// Cover directories with one-character name
{`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}},
} {
res := volumeSplitN(x.input, x.n)
if len(res) < len(x.expected) {
t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
}
for i, e := range res {
if e != x.expected[i] {
t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
}
}
}
}
func TestValidateDevice(t *testing.T) {
valid := []string{
"/home",
"/home:/home",
"/home:/something/else",
"/with space",
"/home:/with space",
"relative:/absolute-path",
"hostPath:/containerPath:r",
"/hostPath:/containerPath:rw",
"/hostPath:/containerPath:mrw",
}
invalid := map[string]string{
"": "bad format for path: ",
"./": "./ is not an absolute path",
"../": "../ is not an absolute path",
"/:../": "../ is not an absolute path",
"/:path": "path is not an absolute path",
":": "bad format for path: :",
"/tmp:": " is not an absolute path",
":test": "bad format for path: :test",
":/test": "bad format for path: :/test",
"tmp:": " is not an absolute path",
":test:": "bad format for path: :test:",
"::": "bad format for path: ::",
":::": "bad format for path: :::",
"/tmp:::": "bad format for path: /tmp:::",
":/tmp::": "bad format for path: :/tmp::",
"path:ro": "ro is not an absolute path",
"path:rr": "rr is not an absolute path",
"a:/b:ro": "bad mode specified: ro",
"a:/b:rr": "bad mode specified: rr",
}
for _, path := range valid {
if _, err := validateDevice(path); err != nil {
t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err)
}
}
for path, expectedError := range invalid {
if _, err := validateDevice(path); err == nil {
t.Fatalf("ValidateDevice(`%q`) should have failed validation", path)
} else {
if err.Error() != expectedError {
t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error())
}
}
}
}

View File

@ -0,0 +1,49 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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
errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause)
for _, container := range opts.containers {
if err := <-errChan; err != nil {
errs = append(errs, err.Error())
continue
}
fmt.Fprintln(dockerCli.Out(), container)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,78 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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 errors.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
}

View File

@ -0,0 +1,78 @@
package container
import (
"fmt"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/opts"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type pruneOptions struct {
force bool
filter opts.FilterOpt
}
// NewPruneCommand returns a new cobra prune command for containers
func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := pruneOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "prune [OPTIONS]",
Short: "Remove all stopped containers",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(dockerCli, opts)
if err != nil {
return err
}
if output != "" {
fmt.Fprintln(dockerCli.Out(), output)
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil
},
Tags: map[string]string{"version": "1.25"},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')")
return cmd
}
const warning = `WARNING! This will remove all stopped containers.
Are you sure you want to continue?`
func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value())
if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return
}
report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters)
if err != nil {
return
}
if len(report.ContainersDeleted) > 0 {
output = "Deleted Containers:\n"
for _, id := range report.ContainersDeleted {
output += id + "\n"
}
spaceReclaimed = report.SpaceReclaimed
}
return
}
// RunPrune calls the Container Prune API
// This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
}

View File

@ -0,0 +1,118 @@
package container
import (
"testing"
"github.com/docker/docker/opts"
"github.com/stretchr/testify/assert"
)
func TestBuildContainerListOptions(t *testing.T) {
filters := opts.NewFilterOpt()
assert.NoError(t, filters.Set("foo=bar"))
assert.NoError(t, filters.Set("baz=foo"))
contexts := []struct {
psOpts *psOptions
expectedAll bool
expectedSize bool
expectedLimit int
expectedFilters map[string]string
}{
{
psOpts: &psOptions{
all: true,
size: true,
last: 5,
filter: filters,
},
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),
},
{
psOpts: &psOptions{
all: true,
size: false,
last: 5,
filter: filters,
// With .Size, size should be true
format: "{{.Size}}",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
{
psOpts: &psOptions{
all: true,
size: false,
last: 5,
filter: filters,
// With .Size, size should be true
format: "{{.Size}} {{.CreatedAt}} {{.Networks}}",
},
expectedAll: true,
expectedSize: true,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
{
psOpts: &psOptions{
all: true,
size: false,
last: 5,
filter: filters,
// Without .Size, size should be false
format: "{{.CreatedAt}} {{.Networks}}",
},
expectedAll: true,
expectedSize: false,
expectedLimit: 5,
expectedFilters: map[string]string{
"foo": "bar",
"baz": "foo",
},
},
}
for _, c := range contexts {
options, err := buildContainerListOptions(c.psOpts)
assert.NoError(t, err)
assert.Equal(t, c.expectedAll, options.All)
assert.Equal(t, c.expectedSize, options.Size)
assert.Equal(t, c.expectedLimit, options.Limit)
assert.Equal(t, len(c.expectedFilters), options.Filters.Len())
for k, v := range c.expectedFilters {
f := options.Filters
if !f.ExactMatch(k, v) {
t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k))
}
}
}
}

View File

@ -0,0 +1,51 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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 errors.New("Error: Neither old nor new names may be empty")
}
if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil {
fmt.Fprintln(dockerCli.Err(), err)
return errors.Errorf("Error: failed to rename container named %s", oldName)
}
return nil
}

View File

@ -0,0 +1,62 @@
package container
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type restartOptions struct {
nSeconds int
nSecondsChanged bool
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
opts.nSecondsChanged = cmd.Flags().Changed("time")
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
var timeout *time.Duration
if opts.nSecondsChanged {
timeoutValue := time.Duration(opts.nSeconds) * time.Second
timeout = &timeoutValue
}
for _, name := range opts.containers {
if err := dockerCli.Client().ContainerRestart(ctx, name, timeout); err != nil {
errs = append(errs, err.Error())
continue
}
fmt.Fprintln(dockerCli.Out(), name)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,73 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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
options := types.ContainerRemoveOptions{
RemoveVolumes: opts.rmVolumes,
RemoveLinks: opts.rmLink,
Force: opts.force,
}
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
container = strings.Trim(container, "/")
if container == "" {
return errors.New("Container name cannot be empty")
}
return dockerCli.Client().ContainerRemove(ctx, container, options)
})
for _, name := range opts.containers {
if err := <-errChan; err != nil {
errs = append(errs, err.Error())
continue
}
fmt.Fprintln(dockerCli.Out(), name)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,296 @@
package container
import (
"fmt"
"io"
"net/http/httputil"
"os"
"runtime"
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"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/docker/libnetwork/resolvconf/dns"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/context"
)
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 *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.AddTrustVerificationFlags(flags)
copts = addFlags(flags)
return cmd
}
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
}
}
// check the DNS settings passed via --dns against localhost regexp to warn if
// they are trying to set a DNS to a localhost address
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
for _, dnsIP := range hostConfig.DNS {
if dns.IsLocalhost(dnsIP) {
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
return
}
}
}
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error {
containerConfig, err := parse(flags, copts)
// just in case the parse does not exit
if err != nil {
reportError(dockerCli.Err(), "run", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
return runContainer(dockerCli, opts, copts, containerConfig)
}
func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error {
config := containerConfig.Config
hostConfig := containerConfig.HostConfig
stdout, stderr := dockerCli.Out(), dockerCli.Err()
client := dockerCli.Client()
// TODO: pass this as an argument
cmdPath := "run"
warnOnOomKillDisable(*hostConfig, stderr)
warnOnLocalhostDNS(*hostConfig, stderr)
config.ArgsEscaped = false
if !opts.detach {
if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil {
return err
}
} else {
if copts.attach.Len() != 0 {
return errors.New("Conflicting options: -a and -d")
}
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, containerConfig, 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.Fprintln(stdout, createResponse.ID)
}()
}
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
if attach {
if opts.detachKeys != "" {
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
}
close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID)
defer close()
if err != nil {
return err
}
}
statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove)
//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 copts.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.Fprintln(stderr, "Error monitoring TTY size:", 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
}
func attachContainer(
ctx context.Context,
dockerCli *command.DockerCli,
errCh *chan error,
config *container.Config,
containerID string,
) (func(), error) {
stdout, stderr := dockerCli.Out(), dockerCli.Err()
var (
out, cerr io.Writer
in io.ReadCloser
)
if config.AttachStdin {
in = dockerCli.In()
}
if config.AttachStdout {
out = stdout
}
if config.AttachStderr {
if config.Tty {
cerr = stdout
} else {
cerr = stderr
}
}
options := types.ContainerAttachOptions{
Stream: true,
Stdin: config.AttachStdin,
Stdout: config.AttachStdout,
Stderr: config.AttachStderr,
DetachKeys: dockerCli.ConfigFile().DetachKeys,
}
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, 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 nil, errAttach
}
*errCh = promise.Go(func() error {
if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil {
return errHijack
}
return errAttach
})
return resp.Close, 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) {
str = strings.TrimSuffix(str, ".") + "."
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
}

View File

@ -0,0 +1,179 @@
package container
import (
"fmt"
"io"
"net/http/httputil"
"strings"
"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/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type startOptions struct {
attach bool
openStdin bool
detachKeys string
checkpoint string
checkpointDir 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")
flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint")
flags.SetAnnotation("checkpoint", "experimental", nil)
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.SetAnnotation("checkpoint-dir", "experimental", nil)
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 errors.New("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 := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove)
startOptions := types.ContainerStartOptions{
CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir,
}
// 4. Start the container.
if err := dockerCli.Client().ContainerStart(ctx, c.ID, startOptions); err != nil {
cancelFun()
<-cErr
if c.HostConfig.AutoRemove {
// 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.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
}
}
if attchErr := <-cErr; attchErr != nil {
return attchErr
}
if status := <-statusChan; status != 0 {
return cli.StatusError{StatusCode: status}
}
} else if opts.checkpoint != "" {
if len(opts.containers) > 1 {
return errors.New("You cannot restore multiple containers at once.")
}
container := opts.containers[0]
startOptions := types.ContainerStartOptions{
CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir,
}
return dockerCli.Client().ContainerStart(ctx, container, startOptions)
} else {
// We're not going to attach to anything.
// Start as many containers as we want.
return startContainersWithoutAttachments(ctx, dockerCli, opts.containers)
}
return nil
}
func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.DockerCli, containers []string) error {
var failedContainers []string
for _, container := range containers {
if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil {
fmt.Fprintln(dockerCli.Err(), err)
failedContainers = append(failedContainers, container)
continue
}
fmt.Fprintln(dockerCli.Out(), container)
}
if len(failedContainers) > 0 {
return errors.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", "))
}
return nil
}

View File

@ -0,0 +1,242 @@
package container
import (
"fmt"
"io"
"strings"
"sync"
"time"
"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/formatter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type statsOptions struct {
all bool
noStream bool
format string
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")
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
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,
}
eventq, errq := dockerCli.Client().Events(ctx, options)
// Whether we successfully subscribed to eventq or not, we can now
// unblock the main goroutine.
close(started)
for {
select {
case event := <-eventq:
c <- event
case err := <-errq:
closeChan <- err
return
}
}
}
// Get the daemonOSType if not set already
if daemonOSType == "" {
svctx := context.Background()
sv, err := dockerCli.Client().ServerVersion(svctx)
if err != nil {
return err
}
daemonOSType = sv.Os
}
// 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 := formatter.NewContainerStats(container.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, 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 := command.InitEventHandler()
eh.Handle("create", func(e events.Message) {
if opts.all {
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
}
}
})
eh.Handle("start", func(e events.Message) {
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, 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 := formatter.NewContainerStats(name, daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, 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 {
if err := c.GetError(); err != nil {
errs = append(errs, err.Error())
}
}
cStats.mu.Unlock()
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
}
// before print to screen, make sure each container get at least one valid stat data
waitFirst.Wait()
format := opts.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().StatsFormat) > 0 {
format = dockerCli.ConfigFile().StatsFormat
} else {
format = formatter.TableFormatKey
}
}
statsCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewStatsFormat(format, daemonOSType),
}
cleanScreen := func() {
if !opts.noStream {
fmt.Fprint(dockerCli.Out(), "\033[2J")
fmt.Fprint(dockerCli.Out(), "\033[H")
}
}
var err error
for range time.Tick(500 * time.Millisecond) {
cleanScreen()
ccstats := []formatter.StatsEntry{}
cStats.mu.Lock()
for _, c := range cStats.cs {
ccstats = append(ccstats, c.GetStatistics())
}
cStats.mu.Unlock()
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil {
break
}
if len(cStats.cs) == 0 && !showAll {
break
}
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 err
}

View File

@ -0,0 +1,229 @@
package container
import (
"encoding/json"
"io"
"strings"
"sync"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli/command/formatter"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
type stats struct {
ostype string
mu sync.Mutex
cs []*formatter.ContainerStats
}
// daemonOSType is set once we have at least one stat for a container
// from the daemon. It is used to ensure we print the right header based
// on the daemon platform.
var daemonOSType string
func (s *stats) add(cs *formatter.ContainerStats) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.isKnownContainer(cs.Container); !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.Container == cid {
return i, true
}
}
return -1, false
}
func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
logrus.Debugf("collecting stats for %s", s.Container)
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()
}
}()
response, err := cli.ContainerStats(ctx, s.Container, streamStats)
if err != nil {
s.SetError(err)
return
}
defer response.Body.Close()
dec := json.NewDecoder(response.Body)
go func() {
for {
var (
v *types.StatsJSON
memPercent, cpuPercent float64
blkRead, blkWrite uint64 // Only used on Linux
mem, memLimit, memPerc float64
pidsStatsCurrent uint64
)
if err := dec.Decode(&v); err != nil {
dec = json.NewDecoder(io.MultiReader(dec.Buffered(), response.Body))
u <- err
if err == io.EOF {
break
}
time.Sleep(100 * time.Millisecond)
continue
}
daemonOSType = response.OSType
if daemonOSType != "windows" {
// 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 = calculateCPUPercentUnix(previousCPU, previousSystem, v)
blkRead, blkWrite = calculateBlockIO(v.BlkioStats)
mem = float64(v.MemoryStats.Usage)
memLimit = float64(v.MemoryStats.Limit)
memPerc = memPercent
pidsStatsCurrent = v.PidsStats.Current
} else {
cpuPercent = calculateCPUPercentWindows(v)
blkRead = v.StorageStats.ReadSizeBytes
blkWrite = v.StorageStats.WriteSizeBytes
mem = float64(v.MemoryStats.PrivateWorkingSet)
}
netRx, netTx := calculateNetwork(v.Networks)
s.SetStatistics(formatter.StatsEntry{
Name: v.Name,
ID: v.ID,
CPUPercentage: cpuPercent,
Memory: mem,
MemoryPercentage: memPerc,
MemoryLimit: memLimit,
NetworkRx: netRx,
NetworkTx: netTx,
BlockRead: float64(blkRead),
BlockWrite: float64(blkWrite),
PidsCurrent: pidsStatsCurrent,
})
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.SetErrorAndReset(errors.New("timeout waiting for stats"))
// if this is the first stat you get, release WaitGroup
if !getFirst {
getFirst = true
waitFirst.Done()
}
case err := <-u:
s.SetError(err)
if err == io.EOF {
break
}
if err != nil {
continue
}
// if this is the first stat you get, release WaitGroup
if !getFirst {
getFirst = true
waitFirst.Done()
}
}
if !streamStats {
return
}
}
}
func calculateCPUPercentUnix(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)
onlineCPUs = float64(v.CPUStats.OnlineCPUs)
)
if onlineCPUs == 0.0 {
onlineCPUs = float64(len(v.CPUStats.CPUUsage.PercpuUsage))
}
if systemDelta > 0.0 && cpuDelta > 0.0 {
cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0
}
return cpuPercent
}
func calculateCPUPercentWindows(v *types.StatsJSON) float64 {
// Max number of 100ns intervals between the previous time read and now
possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals
possIntervals /= 100 // Convert to number of 100ns intervals
possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors
// Intervals used
intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage
// Percentage avoiding divide-by-zero
if possIntervals > 0 {
return float64(intervalsUsed) / float64(possIntervals) * 100.0
}
return 0.00
}
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
}

View File

@ -0,0 +1,20 @@
package container
import (
"testing"
"github.com/docker/docker/api/types"
)
func TestCalculateBlockIO(t *testing.T) {
blkio := types.BlkioStats{
IoServiceBytesRecursive: []types.BlkioStatEntry{{Major: 8, Minor: 0, Op: "read", Value: 1234}, {Major: 8, Minor: 1, Op: "read", Value: 4567}, {Major: 8, Minor: 0, Op: "write", Value: 123}, {Major: 8, Minor: 1, Op: "write", Value: 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)
}
}

View File

@ -0,0 +1,67 @@
package container
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type stopOptions struct {
time int
timeChanged bool
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
opts.timeChanged = cmd.Flags().Changed("time")
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 timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.time) * time.Second
timeout = &timeoutValue
}
var errs []string
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error {
return dockerCli.Client().ContainerStop(ctx, id, timeout)
})
for _, container := range opts.containers {
if err := <-errChan; err != nil {
errs = append(errs, err.Error())
continue
}
fmt.Fprintln(dockerCli.Out(), container)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

BIN
cli/command/container/testdata/utf16.env vendored Executable file

Binary file not shown.

BIN
cli/command/container/testdata/utf16be.env vendored Executable file

Binary file not shown.

3
cli/command/container/testdata/utf8.env vendored Executable file
View File

@ -0,0 +1,3 @@
FOO=BAR
HELLO=您好
BAR=FOO

View File

@ -0,0 +1 @@
ENV1=value1

View File

@ -0,0 +1 @@
LABEL1=value1

View File

@ -0,0 +1,57 @@
package container
import (
"fmt"
"strings"
"text/tabwriter"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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
}

View File

@ -0,0 +1,103 @@
package container
import (
"fmt"
"os"
gosignal "os/signal"
"runtime"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli/command"
"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 uint, 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
}

View File

@ -0,0 +1,50 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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
errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerUnpause)
for _, container := range opts.containers {
if err := <-errChan; err != nil {
errs = append(errs, err.Error())
continue
}
fmt.Fprintln(dockerCli.Out(), container)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,134 @@
package container
import (
"fmt"
"strings"
containertypes "github.com/docker/docker/api/types/container"
"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/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type updateOptions struct {
blkioWeight uint16
cpuPeriod int64
cpuQuota int64
cpuRealtimePeriod int64
cpuRealtimeRuntime int64
cpusetCpus string
cpusetMems string
cpuShares int64
memory opts.MemBytes
memoryReservation opts.MemBytes
memorySwap opts.MemSwapBytes
kernelMemory opts.MemBytes
restartPolicy string
cpus opts.NanoCPUs
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, or 0 to disable (default 0)")
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.Int64Var(&opts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds")
flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"})
flags.Int64Var(&opts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit the CPU real-time runtime in microseconds")
flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"})
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.VarP(&opts.memory, "memory", "m", "Memory limit")
flags.Var(&opts.memoryReservation, "memory-reservation", "Memory soft limit")
flags.Var(&opts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
flags.Var(&opts.kernelMemory, "kernel-memory", "Kernel memory limit")
flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits")
flags.Var(&opts.cpus, "cpus", "Number of CPUs")
flags.SetAnnotation("cpus", "version", []string{"1.29"})
return cmd
}
func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error {
var err error
if opts.nFlag == 0 {
return errors.New("You must provide one or more flags when using this command.")
}
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: opts.memory.Value(),
MemoryReservation: opts.memoryReservation.Value(),
MemorySwap: opts.memorySwap.Value(),
KernelMemory: opts.kernelMemory.Value(),
CPUPeriod: opts.cpuPeriod,
CPUQuota: opts.cpuQuota,
CPURealtimePeriod: opts.cpuRealtimePeriod,
CPURealtimeRuntime: opts.cpuRealtimeRuntime,
NanoCPUs: opts.cpus.Value(),
}
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.Fprintln(dockerCli.Out(), container)
}
warns = append(warns, r.Warnings...)
}
if len(warns) > 0 {
fmt.Fprintln(dockerCli.Out(), strings.Join(warns, "\n"))
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,142 @@
package container
import (
"strconv"
"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/api/types/versions"
"github.com/docker/docker/cli/command"
clientapi "github.com/docker/docker/client"
"golang.org/x/net/context"
)
func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) chan int {
if len(containerID) == 0 {
// containerID can never be empty
panic("Internal Error: waitExitOrRemoved needs a containerID as parameter")
}
var removeErr error
statusChan := make(chan int)
exitCode := 125
// Get events via Events API
f := filters.NewArgs()
f.Add("type", "container")
f.Add("container", containerID)
options := types.EventsOptions{
Filters: f,
}
eventCtx, cancel := context.WithCancel(ctx)
eventq, errq := dockerCli.Client().Events(eventCtx, options)
eventProcessor := func(e events.Message) bool {
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
} else {
// If we are talking to an older daemon, `AutoRemove` is not supported.
// We need to fall back to the old behavior, which is client-side removal
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.25") {
go func() {
removeErr = dockerCli.Client().ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{RemoveVolumes: true})
if removeErr != nil {
logrus.Errorf("error removing container: %v", removeErr)
cancel() // cancel the event Q
}
}()
}
}
case "detach":
exitCode = 0
stopProcessing = true
case "destroy":
stopProcessing = true
}
return stopProcessing
}
go func() {
defer func() {
statusChan <- exitCode // must always send an exit code or the caller will block
cancel()
}()
for {
select {
case <-eventCtx.Done():
if removeErr != nil {
return
}
case evt := <-eventq:
if eventProcessor(evt) {
return
}
case err := <-errq:
logrus.Errorf("error getting events from daemon: %v", err)
return
}
}
}()
return statusChan
}
// getExitCode performs an inspect on the container. It returns
// the running state and the exit code.
func getExitCode(ctx context.Context, dockerCli *command.DockerCli, 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 !clientapi.IsErrConnectionFailed(err) {
return false, -1, err
}
return false, -1, nil
}
return c.State.Running, c.State.ExitCode, nil
}
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil
}
const defaultParallel int = 50
sem := make(chan struct{}, defaultParallel)
errChan := make(chan error)
// make sure result is printed in correct order
output := map[string]chan error{}
for _, c := range containers {
output[c] = make(chan error, 1)
}
go func() {
for _, c := range containers {
err := <-output[c]
errChan <- err
}
}()
go func() {
for _, c := range containers {
sem <- struct{}{} // Wait for active queue sem to drain.
go func(container string) {
output[container] <- op(ctx, container)
<-sem
}(c)
}
}()
return errChan
}

View File

@ -0,0 +1,50 @@
package container
import (
"fmt"
"strings"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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())
continue
}
fmt.Fprintf(dockerCli.Out(), "%d\n", status)
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

View File

@ -0,0 +1,49 @@
package command
import (
"sync"
"github.com/Sirupsen/logrus"
eventtypes "github.com/docker/docker/api/types/events"
)
type eventProcessor func(eventtypes.Message, error) error
// EventHandler is abstract interface for user to customize
// own handle functions of each type of events
type EventHandler interface {
Handle(action string, h func(eventtypes.Message))
Watch(c <-chan eventtypes.Message)
}
// InitEventHandler initializes and returns an EventHandler
func InitEventHandler() EventHandler {
return &eventHandler{handlers: make(map[string]func(eventtypes.Message))}
}
type eventHandler struct {
handlers map[string]func(eventtypes.Message)
mu sync.Mutex
}
func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) {
w.mu.Lock()
w.handlers[action] = h
w.mu.Unlock()
}
// Watch ranges over the passed in event chan and processes the events based on the
// handlers created for a given action.
// To stop watching, close the event chan.
func (w *eventHandler) Watch(c <-chan eventtypes.Message) {
for e := range c {
w.mu.Lock()
h, exists := w.handlers[e.Action]
w.mu.Unlock()
if !exists {
continue
}
logrus.Debugf("event handler: received event: %v", e)
go h(e)
}
}

View File

@ -0,0 +1,52 @@
package formatter
import "github.com/docker/docker/api/types"
const (
defaultCheckpointFormat = "table {{.Name}}"
checkpointNameHeader = "CHECKPOINT NAME"
)
// NewCheckpointFormat returns a format for use with a checkpoint Context
func NewCheckpointFormat(source string) Format {
switch source {
case TableFormatKey:
return defaultCheckpointFormat
}
return Format(source)
}
// CheckpointWrite writes formatted checkpoints using the Context
func CheckpointWrite(ctx Context, checkpoints []types.Checkpoint) error {
render := func(format func(subContext subContext) error) error {
for _, checkpoint := range checkpoints {
if err := format(&checkpointContext{c: checkpoint}); err != nil {
return err
}
}
return nil
}
return ctx.Write(newCheckpointContext(), render)
}
type checkpointContext struct {
HeaderContext
c types.Checkpoint
}
func newCheckpointContext() *checkpointContext {
cpCtx := checkpointContext{}
cpCtx.header = volumeHeaderContext{
"Name": checkpointNameHeader,
}
return &cpCtx
}
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *checkpointContext) Name() string {
return c.c.Name
}

View File

@ -0,0 +1,55 @@
package formatter
import (
"bytes"
"testing"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func TestCheckpointContextFormatWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
{
Context{Format: NewCheckpointFormat(defaultCheckpointFormat)},
`CHECKPOINT NAME
checkpoint-1
checkpoint-2
checkpoint-3
`,
},
{
Context{Format: NewCheckpointFormat("{{.Name}}")},
`checkpoint-1
checkpoint-2
checkpoint-3
`,
},
{
Context{Format: NewCheckpointFormat("{{.Name}}:")},
`checkpoint-1:
checkpoint-2:
checkpoint-3:
`,
},
}
checkpoints := []types.Checkpoint{
{"checkpoint-1"},
{"checkpoint-2"},
{"checkpoint-3"},
}
for _, testcase := range cases {
out := bytes.NewBufferString("")
testcase.context.Output = out
err := CheckpointWrite(testcase.context, checkpoints)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}

View File

@ -0,0 +1,259 @@
package formatter
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
units "github.com/docker/go-units"
)
const (
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
containerIDHeader = "CONTAINER ID"
namesHeader = "NAMES"
commandHeader = "COMMAND"
runningForHeader = "CREATED"
statusHeader = "STATUS"
portsHeader = "PORTS"
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
)
// NewContainerFormat returns a Format for rendering using a Context
func NewContainerFormat(source string, quiet bool, size bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
format := defaultContainerTableFormat
if size {
format += `\t{{.Size}}`
}
return Format(format)
case RawFormatKey:
if quiet {
return `container_id: {{.ID}}`
}
format := `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
status: {{- pad .Status 1 0}}
names: {{.Names}}
labels: {{- pad .Labels 1 0}}
ports: {{- pad .Ports 1 0}}
`
if size {
format += `size: {{.Size}}\n`
}
return Format(format)
}
return Format(source)
}
// ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx Context, containers []types.Container) error {
render := func(format func(subContext subContext) error) error {
for _, container := range containers {
err := format(&containerContext{trunc: ctx.Trunc, c: container})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(newContainerContext(), render)
}
type containerHeaderContext map[string]string
func (c containerHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
type containerContext struct {
HeaderContext
trunc bool
c types.Container
}
func newContainerContext() *containerContext {
containerCtx := containerContext{}
containerCtx.header = containerHeaderContext{
"ID": containerIDHeader,
"Names": namesHeader,
"Image": imageHeader,
"Command": commandHeader,
"CreatedAt": createdAtHeader,
"RunningFor": runningForHeader,
"Ports": portsHeader,
"Status": statusHeader,
"Size": sizeHeader,
"Labels": labelsHeader,
"Mounts": mountsHeader,
"LocalVolumes": localVolumes,
"Networks": networksHeader,
}
return &containerCtx
}
func (c *containerContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *containerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
}
return c.c.ID
}
func (c *containerContext) Names() string {
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 {
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
}
// truncate digest if no-trunc option was not selected
ref, err := reference.ParseNormalizedNamed(c.c.Image)
if err == nil {
if nt, ok := ref.(reference.NamedTagged); ok {
// case for when a tag is provided
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
return reference.FamiliarString(namedTagged)
}
} else {
// case for when a tag is not provided
named := reference.TrimNamed(ref)
return reference.FamiliarString(named)
}
}
}
return c.c.Image
}
func (c *containerContext) Command() string {
command := c.c.Command
if c.trunc {
command = stringutils.Ellipsis(command, 20)
}
return strconv.Quote(command)
}
func (c *containerContext) CreatedAt() string {
return time.Unix(int64(c.c.Created), 0).String()
}
func (c *containerContext) RunningFor() string {
createdAt := time.Unix(int64(c.c.Created), 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *containerContext) Ports() string {
return api.DisplayablePorts(c.c.Ports)
}
func (c *containerContext) Status() string {
return c.c.Status
}
func (c *containerContext) Size() string {
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
sf := srw
if c.c.SizeRootFs > 0 {
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
}
return sf
}
func (c *containerContext) Labels() string {
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 {
if c.c.Labels == nil {
return ""
}
return c.c.Labels[name]
}
func (c *containerContext) Mounts() string {
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, ",")
}
func (c *containerContext) LocalVolumes() string {
count := 0
for _, m := range c.c.Mounts {
if m.Driver == "local" {
count++
}
}
return fmt.Sprintf("%d", count)
}
func (c *containerContext) Networks() string {
if c.c.NetworkSettings == nil {
return ""
}
networks := []string{}
for k := range c.c.NetworkSettings.Networks {
networks = append(networks, k)
}
return strings.Join(networks, ",")
}

View File

@ -0,0 +1,385 @@
package formatter
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
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
call func() string
}{
{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID},
{types.Container{ID: containerID}, false, containerID, ctx.ID},
{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names},
{types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image},
{types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image},
{types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image},
{types.Container{
Image: "a5a665ff33eced1e0803148700880edab4",
ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
},
true,
"a5a665ff33ec",
ctx.Image,
},
{types.Container{
Image: "a5a665ff33eced1e0803148700880edab4",
ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
},
false,
"a5a665ff33eced1e0803148700880edab4",
ctx.Image,
},
{types.Container{Image: ""}, true, "<no image>", ctx.Image},
{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command},
{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt},
{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports},
{types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status},
{types.Container{SizeRw: 10}, true, "10B", ctx.Size},
{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size},
{types.Container{}, true, "", ctx.Labels},
{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels},
{types.Container{Created: unix}, true, "About a minute ago", 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...", ctx.Mounts},
{types.Container{
Mounts: []types.MountPoint{
{
Driver: "local",
Source: "/a/path",
},
},
}, false, "/a/path", ctx.Mounts},
{types.Container{
Mounts: []types.MountPoint{
{
Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
Driver: "local",
Source: "/a/path",
},
},
}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", 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)
}
}
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)
}
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)
}
}
func TestContainerContextWrite(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -1).Unix()
expectedTime := time.Unix(unixTime, 0).String()
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table Format
{
Context{Format: NewContainerFormat("table", false, true)},
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE
containerID1 ubuntu "" 24 hours ago foobar_baz 0B
containerID2 ubuntu "" 24 hours ago foobar_bar 0B
`,
},
{
Context{Format: NewContainerFormat("table", false, false)},
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
containerID1 ubuntu "" 24 hours ago foobar_baz
containerID2 ubuntu "" 24 hours ago foobar_bar
`,
},
{
Context{Format: NewContainerFormat("table {{.Image}}", false, false)},
"IMAGE\nubuntu\nubuntu\n",
},
{
Context{Format: NewContainerFormat("table {{.Image}}", false, true)},
"IMAGE\nubuntu\nubuntu\n",
},
{
Context{Format: NewContainerFormat("table {{.Image}}", true, false)},
"IMAGE\nubuntu\nubuntu\n",
},
{
Context{Format: NewContainerFormat("table", true, false)},
"containerID1\ncontainerID2\n",
},
// Raw Format
{
Context{Format: NewContainerFormat("raw", false, false)},
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),
},
{
Context{Format: NewContainerFormat("raw", false, true)},
fmt.Sprintf(`container_id: containerID1
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_baz
labels:
ports:
size: 0B
container_id: containerID2
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_bar
labels:
ports:
size: 0B
`, expectedTime, expectedTime),
},
{
Context{Format: NewContainerFormat("raw", true, false)},
"container_id: containerID1\ncontainer_id: containerID2\n",
},
// Custom Format
{
Context{Format: "{{.Image}}"},
"ubuntu\nubuntu\n",
},
{
Context{Format: NewContainerFormat("{{.Image}}", false, true)},
"ubuntu\nubuntu\n",
},
// Special headers for customerized table format
{
Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
`CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
`,
},
}
for _, testcase := range cases {
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("")
testcase.context.Output = out
err := ContainerWrite(testcase.context, containers)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestContainerContextWriteWithNoContainers(t *testing.T) {
out := bytes.NewBufferString("")
containers := []types.Container{}
contexts := []struct {
context Context
expected string
}{
{
Context{
Format: "{{.Image}}",
Output: out,
},
"",
},
{
Context{
Format: "table {{.Image}}",
Output: out,
},
"IMAGE\n",
},
{
Context{
Format: NewContainerFormat("{{.Image}}", false, true),
Output: out,
},
"",
},
{
Context{
Format: NewContainerFormat("table {{.Image}}", false, true),
Output: out,
},
"IMAGE\n",
},
{
Context{
Format: "table {{.Image}}\t{{.Size}}",
Output: out,
},
"IMAGE SIZE\n",
},
{
Context{
Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true),
Output: out,
},
"IMAGE SIZE\n",
},
}
for _, context := range contexts {
ContainerWrite(context.context, containers)
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}
func TestContainerContextWriteJSON(t *testing.T) {
unix := time.Now().Add(-65 * time.Second).Unix()
containers := []types.Container{
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix},
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix},
}
expectedCreated := time.Unix(unix, 0).String()
expectedJSONs := []map[string]interface{}{
{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""},
{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""},
}
out := bytes.NewBufferString("")
err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestContainerContextWriteJSONField(t *testing.T) {
containers := []types.Container{
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"},
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"},
}
out := bytes.NewBufferString("")
err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, containers[i].ID, s)
}
}
func TestContainerBackCompat(t *testing.T) {
containers := []types.Container{{ID: "brewhaha"}}
cases := []string{
"ID",
"Names",
"Image",
"Command",
"CreatedAt",
"RunningFor",
"Ports",
"Status",
"Size",
"Labels",
"Mounts",
}
buf := bytes.NewBuffer(nil)
for _, c := range cases {
ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf}
if err := ContainerWrite(ctx, containers); err != nil {
t.Logf("could not render template for field '%s': %v", c, err)
t.Fail()
}
buf.Reset()
}
}

View File

@ -0,0 +1,35 @@
package formatter
const (
imageHeader = "IMAGE"
createdSinceHeader = "CREATED"
createdAtHeader = "CREATED AT"
sizeHeader = "SIZE"
labelsHeader = "LABELS"
nameHeader = "NAME"
driverHeader = "DRIVER"
scopeHeader = "SCOPE"
)
type subContext interface {
FullHeader() interface{}
}
// HeaderContext provides the subContext interface for managing headers
type HeaderContext struct {
header interface{}
}
// FullHeader returns the header as an interface
func (c *HeaderContext) FullHeader() interface{} {
return c.header
}
func stripNamePrefix(ss []string) []string {
sss := make([]string, len(ss))
for i, s := range ss {
sss[i] = s[1:]
}
return sss
}

View File

@ -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)
}
}

View File

@ -0,0 +1,72 @@
package formatter
import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive"
)
const (
defaultDiffTableFormat = "table {{.Type}}\t{{.Path}}"
changeTypeHeader = "CHANGE TYPE"
pathHeader = "PATH"
)
// NewDiffFormat returns a format for use with a diff Context
func NewDiffFormat(source string) Format {
switch source {
case TableFormatKey:
return defaultDiffTableFormat
}
return Format(source)
}
// DiffWrite writes formatted diff using the Context
func DiffWrite(ctx Context, changes []container.ContainerChangeResponseItem) error {
render := func(format func(subContext subContext) error) error {
for _, change := range changes {
if err := format(&diffContext{c: change}); err != nil {
return err
}
}
return nil
}
return ctx.Write(newDiffContext(), render)
}
type diffContext struct {
HeaderContext
c container.ContainerChangeResponseItem
}
func newDiffContext() *diffContext {
diffCtx := diffContext{}
diffCtx.header = map[string]string{
"Type": changeTypeHeader,
"Path": pathHeader,
}
return &diffCtx
}
func (d *diffContext) MarshalJSON() ([]byte, error) {
return marshalJSON(d)
}
func (d *diffContext) Type() string {
var kind string
switch d.c.Kind {
case archive.ChangeModify:
kind = "C"
case archive.ChangeAdd:
kind = "A"
case archive.ChangeDelete:
kind = "D"
}
return kind
}
func (d *diffContext) Path() string {
return d.c.Path
}

View File

@ -0,0 +1,59 @@
package formatter
import (
"bytes"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive"
"github.com/stretchr/testify/assert"
)
func TestDiffContextFormatWrite(t *testing.T) {
// Check default output format (verbose and non-verbose mode) for table headers
cases := []struct {
context Context
expected string
}{
{
Context{Format: NewDiffFormat("table")},
`CHANGE TYPE PATH
C /var/log/app.log
A /usr/app/app.js
D /usr/app/old_app.js
`,
},
{
Context{Format: NewDiffFormat("table {{.Path}}")},
`PATH
/var/log/app.log
/usr/app/app.js
/usr/app/old_app.js
`,
},
{
Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")},
`C: /var/log/app.log
A: /usr/app/app.js
D: /usr/app/old_app.js
`,
},
}
diffs := []container.ContainerChangeResponseItem{
{archive.ChangeModify, "/var/log/app.log"},
{archive.ChangeAdd, "/usr/app/app.js"},
{archive.ChangeDelete, "/usr/app/old_app.js"},
}
for _, testcase := range cases {
out := bytes.NewBufferString("")
testcase.context.Output = out
err := DiffWrite(testcase.context, diffs)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}

View File

@ -0,0 +1,358 @@
package formatter
import (
"bytes"
"fmt"
"strings"
"text/template"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
units "github.com/docker/go-units"
)
const (
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}"
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
typeHeader = "TYPE"
totalHeader = "TOTAL"
activeHeader = "ACTIVE"
reclaimableHeader = "RECLAIMABLE"
containersHeader = "CONTAINERS"
sharedSizeHeader = "SHARED SIZE"
uniqueSizeHeader = "UNIQUE SiZE"
)
// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
type DiskUsageContext struct {
Context
Verbose bool
LayersSize int64
Images []*types.ImageSummary
Containers []*types.Container
Volumes []*types.Volume
}
func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
ctx.buffer = bytes.NewBufferString("")
ctx.header = ""
ctx.Format = Format(format)
ctx.preFormat()
return ctx.parseFormat()
}
//
// NewDiskUsageFormat returns a format for rendering an DiskUsageContext
func NewDiskUsageFormat(source string) Format {
switch source {
case TableFormatKey:
format := defaultDiskUsageTableFormat
return Format(format)
case RawFormatKey:
format := `type: {{.Type}}
total: {{.TotalCount}}
active: {{.Active}}
size: {{.Size}}
reclaimable: {{.Reclaimable}}
`
return Format(format)
}
return Format(source)
}
func (ctx *DiskUsageContext) Write() (err error) {
if ctx.Verbose == false {
ctx.buffer = bytes.NewBufferString("")
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
totalSize: ctx.LayersSize,
images: ctx.Images,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
containers: ctx.Containers,
})
if err != nil {
return err
}
err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
volumes: ctx.Volumes,
})
if err != nil {
return err
}
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
diskUsageContainersCtx.header = map[string]string{
"Type": typeHeader,
"TotalCount": totalHeader,
"Active": activeHeader,
"Size": sizeHeader,
"Reclaimable": reclaimableHeader,
}
ctx.postFormat(tmpl, &diskUsageContainersCtx)
return err
}
// First images
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
if err != nil {
return
}
ctx.Output.Write([]byte("Images space usage:\n\n"))
for _, i := range ctx.Images {
repo := "<none>"
tag := "<none>"
if len(i.RepoTags) > 0 && !isDangling(*i) {
// Only show the first tag
ref, err := reference.ParseNormalizedNamed(i.RepoTags[0])
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
repo = reference.FamiliarName(ref)
tag = nt.Tag()
}
}
err = ctx.contextFormat(tmpl, &imageContext{
repo: repo,
tag: tag,
trunc: true,
i: *i,
})
if err != nil {
return
}
}
ctx.postFormat(tmpl, newImageContext())
// Now containers
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
if err != nil {
return
}
for _, c := range ctx.Containers {
// Don't display the virtual size
c.SizeRootFs = 0
err = ctx.contextFormat(tmpl, &containerContext{
trunc: true,
c: *c,
})
if err != nil {
return
}
}
ctx.postFormat(tmpl, newContainerContext())
// And volumes
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
if err != nil {
return
}
for _, v := range ctx.Volumes {
err = ctx.contextFormat(tmpl, &volumeContext{
v: *v,
})
if err != nil {
return
}
}
ctx.postFormat(tmpl, newVolumeContext())
return
}
type diskUsageImagesContext struct {
HeaderContext
totalSize int64
images []*types.ImageSummary
}
func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *diskUsageImagesContext) Type() string {
return "Images"
}
func (c *diskUsageImagesContext) TotalCount() string {
return fmt.Sprintf("%d", len(c.images))
}
func (c *diskUsageImagesContext) Active() string {
used := 0
for _, i := range c.images {
if i.Containers > 0 {
used++
}
}
return fmt.Sprintf("%d", used)
}
func (c *diskUsageImagesContext) Size() string {
return units.HumanSize(float64(c.totalSize))
}
func (c *diskUsageImagesContext) Reclaimable() string {
var used int64
for _, i := range c.images {
if i.Containers != 0 {
if i.VirtualSize == -1 || i.SharedSize == -1 {
continue
}
used += i.VirtualSize - i.SharedSize
}
}
reclaimable := c.totalSize - used
if c.totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
}
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}
type diskUsageContainersContext struct {
HeaderContext
verbose bool
containers []*types.Container
}
func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *diskUsageContainersContext) Type() string {
return "Containers"
}
func (c *diskUsageContainersContext) TotalCount() string {
return fmt.Sprintf("%d", len(c.containers))
}
func (c *diskUsageContainersContext) isActive(container types.Container) bool {
return strings.Contains(container.State, "running") ||
strings.Contains(container.State, "paused") ||
strings.Contains(container.State, "restarting")
}
func (c *diskUsageContainersContext) Active() string {
used := 0
for _, container := range c.containers {
if c.isActive(*container) {
used++
}
}
return fmt.Sprintf("%d", used)
}
func (c *diskUsageContainersContext) Size() string {
var size int64
for _, container := range c.containers {
size += container.SizeRw
}
return units.HumanSize(float64(size))
}
func (c *diskUsageContainersContext) Reclaimable() string {
var reclaimable int64
var totalSize int64
for _, container := range c.containers {
if !c.isActive(*container) {
reclaimable += container.SizeRw
}
totalSize += container.SizeRw
}
if totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
}
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}
type diskUsageVolumesContext struct {
HeaderContext
verbose bool
volumes []*types.Volume
}
func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *diskUsageVolumesContext) Type() string {
return "Local Volumes"
}
func (c *diskUsageVolumesContext) TotalCount() string {
return fmt.Sprintf("%d", len(c.volumes))
}
func (c *diskUsageVolumesContext) Active() string {
used := 0
for _, v := range c.volumes {
if v.UsageData.RefCount > 0 {
used++
}
}
return fmt.Sprintf("%d", used)
}
func (c *diskUsageVolumesContext) Size() string {
var size int64
for _, v := range c.volumes {
if v.UsageData.Size != -1 {
size += v.UsageData.Size
}
}
return units.HumanSize(float64(size))
}
func (c *diskUsageVolumesContext) Reclaimable() string {
var reclaimable int64
var totalSize int64
for _, v := range c.volumes {
if v.UsageData.Size != -1 {
if v.UsageData.RefCount == 0 {
reclaimable += v.UsageData.Size
}
totalSize += v.UsageData.Size
}
}
if totalSize > 0 {
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
}
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
}

View File

@ -0,0 +1,125 @@
package formatter
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDiskUsageContextFormatWrite(t *testing.T) {
cases := []struct {
context DiskUsageContext
expected string
}{
// Check default output format (verbose and non-verbose mode) for table headers
{
DiskUsageContext{
Context: Context{
Format: NewDiskUsageFormat("table"),
},
Verbose: false},
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 0 0 0B 0B
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
`,
},
{
DiskUsageContext{Verbose: true},
`Images space usage:
REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS
Containers space usage:
CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES
Local Volumes space usage:
VOLUME NAME LINKS SIZE
`,
},
// Errors
{
DiskUsageContext{
Context: Context{
Format: "{{InvalidFunction}}",
},
},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
DiskUsageContext{
Context: Context{
Format: "{{nil}}",
},
},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table Format
{
DiskUsageContext{
Context: Context{
Format: NewDiskUsageFormat("table"),
},
},
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 0 0 0B 0B
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
`,
},
{
DiskUsageContext{
Context: Context{
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
},
},
`TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
`,
},
// Raw Format
{
DiskUsageContext{
Context: Context{
Format: NewDiskUsageFormat("raw"),
},
},
`type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
`,
},
}
for _, testcase := range cases {
out := bytes.NewBufferString("")
testcase.context.Output = out
if err := testcase.context.Write(); err != nil {
assert.Equal(t, testcase.expected, err.Error())
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}

View File

@ -0,0 +1,119 @@
package formatter
import (
"bytes"
"io"
"strings"
"text/tabwriter"
"text/template"
"github.com/docker/docker/pkg/templates"
"github.com/pkg/errors"
)
// Format keys used to specify certain kinds of output formats
const (
TableFormatKey = "table"
RawFormatKey = "raw"
PrettyFormatKey = "pretty"
defaultQuietFormat = "{{.ID}}"
)
// Format is the format string rendered using the Context
type Format string
// IsTable returns true if the format is a table-type format
func (f Format) IsTable() bool {
return strings.HasPrefix(string(f), TableFormatKey)
}
// Contains returns true if the format contains the substring
func (f Format) Contains(sub string) bool {
return strings.Contains(string(f), sub)
}
// 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 Format
// Trunc when set to true will truncate the output of certain fields such as Container ID.
Trunc bool
// internal element
finalFormat string
header interface{}
buffer *bytes.Buffer
}
func (c *Context) preFormat() {
c.finalFormat = string(c.Format)
// TODO: handle this in the Format type
if c.Format.IsTable() {
c.finalFormat = c.finalFormat[len(TableFormatKey):]
}
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 {
return tmpl, errors.Errorf("Template parsing error: %v\n", err)
}
return tmpl, err
}
func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
if c.Format.IsTable() {
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
buffer := bytes.NewBufferString("")
tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader())
buffer.WriteTo(t)
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 {
return errors.Errorf("Template parsing error: %v\n", err)
}
if c.Format.IsTable() && c.header != nil {
c.header = subContext.FullHeader()
}
c.buffer.WriteString("\n")
return nil
}
// SubFormat is a function type accepted by Write()
type SubFormat func(func(subContext) error) error
// Write the template to the buffer using this Context
func (c *Context) Write(sub subContext, f SubFormat) error {
c.buffer = bytes.NewBufferString("")
c.preFormat()
tmpl, err := c.parseFormat()
if err != nil {
return err
}
subFormat := func(subContext subContext) error {
return c.contextFormat(tmpl, subContext)
}
if err := f(subFormat); err != nil {
return err
}
c.postFormat(tmpl, sub)
return nil
}

View File

@ -0,0 +1,113 @@
package formatter
import (
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
units "github.com/docker/go-units"
)
const (
defaultHistoryTableFormat = "table {{.ID}}\t{{.CreatedSince}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}"
nonHumanHistoryTableFormat = "table {{.ID}}\t{{.CreatedAt}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}"
historyIDHeader = "IMAGE"
createdByHeader = "CREATED BY"
commentHeader = "COMMENT"
)
// NewHistoryFormat returns a format for rendering an HistoryContext
func NewHistoryFormat(source string, quiet bool, human bool) Format {
switch source {
case TableFormatKey:
switch {
case quiet:
return defaultQuietFormat
case !human:
return nonHumanHistoryTableFormat
default:
return defaultHistoryTableFormat
}
}
return Format(source)
}
// HistoryWrite writes the context
func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error {
render := func(format func(subContext subContext) error) error {
for _, history := range histories {
historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human}
if err := format(historyCtx); err != nil {
return err
}
}
return nil
}
historyCtx := &historyContext{}
historyCtx.header = map[string]string{
"ID": historyIDHeader,
"CreatedSince": createdSinceHeader,
"CreatedAt": createdAtHeader,
"CreatedBy": createdByHeader,
"Size": sizeHeader,
"Comment": commentHeader,
}
return ctx.Write(historyCtx, render)
}
type historyContext struct {
HeaderContext
trunc bool
human bool
h image.HistoryResponseItem
}
func (c *historyContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *historyContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.h.ID)
}
return c.h.ID
}
func (c *historyContext) CreatedAt() string {
var created string
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
return created
}
func (c *historyContext) CreatedSince() string {
var created string
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
return created + " ago"
}
func (c *historyContext) CreatedBy() string {
createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1)
if c.trunc {
createdBy = stringutils.Ellipsis(createdBy, 45)
}
return createdBy
}
func (c *historyContext) Size() string {
size := ""
if c.human {
size = units.HumanSizeWithPrecision(float64(c.h.Size), 3)
} else {
size = strconv.FormatInt(c.h.Size, 10)
}
return size
}
func (c *historyContext) Comment() string {
return c.h.Comment
}

View File

@ -0,0 +1,213 @@
package formatter
import (
"strconv"
"strings"
"testing"
"time"
"bytes"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
"github.com/stretchr/testify/assert"
)
type historyCase struct {
historyCtx historyContext
expValue string
call func() string
}
func TestHistoryContext_ID(t *testing.T) {
id := stringid.GenerateRandomID()
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{ID: id},
trunc: false,
}, id, ctx.ID,
},
{
historyContext{
h: image.HistoryResponseItem{ID: id},
trunc: true,
}, stringid.TruncateID(id), ctx.ID,
},
}
for _, c := range cases {
ctx = c.historyCtx
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)
}
}
}
func TestHistoryContext_CreatedSince(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -7).Unix()
expected := "7 days ago"
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Created: unixTime},
trunc: false,
human: true,
}, expected, ctx.CreatedSince,
},
}
for _, c := range cases {
ctx = c.historyCtx
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)
}
}
}
func TestHistoryContext_CreatedBy(t *testing.T) {
withTabs := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*`
expected := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*`
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{CreatedBy: withTabs},
trunc: false,
}, expected, ctx.CreatedBy,
},
{
historyContext{
h: image.HistoryResponseItem{CreatedBy: withTabs},
trunc: true,
}, stringutils.Ellipsis(expected, 45), ctx.CreatedBy,
},
}
for _, c := range cases {
ctx = c.historyCtx
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)
}
}
}
func TestHistoryContext_Size(t *testing.T) {
size := int64(182964289)
expected := "183MB"
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Size: size},
trunc: false,
human: true,
}, expected, ctx.Size,
}, {
historyContext{
h: image.HistoryResponseItem{Size: size},
trunc: false,
human: false,
}, strconv.Itoa(182964289), ctx.Size,
},
}
for _, c := range cases {
ctx = c.historyCtx
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)
}
}
}
func TestHistoryContext_Comment(t *testing.T) {
comment := "Some comment"
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Comment: comment},
trunc: false,
}, comment, ctx.Comment,
},
}
for _, c := range cases {
ctx = c.historyCtx
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)
}
}
}
func TestHistoryContext_Table(t *testing.T) {
out := bytes.NewBufferString("")
unixTime := time.Now().AddDate(0, 0, -1).Unix()
histories := []image.HistoryResponseItem{
{ID: "imageID1", Created: unixTime, CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
{ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
{ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
{ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
}
expectedNoTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT
imageID1 24 hours ago /bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on 183MB Hi
imageID2 24 hours ago /bin/bash echo 183MB Hi
imageID3 24 hours ago /bin/bash ls 183MB Hi
imageID4 24 hours ago /bin/bash grep 183MB Hi
`
expectedTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT
imageID1 24 hours ago /bin/bash ls && npm i && npm run test && k... 183MB Hi
imageID2 24 hours ago /bin/bash echo 183MB Hi
imageID3 24 hours ago /bin/bash ls 183MB Hi
imageID4 24 hours ago /bin/bash grep 183MB Hi
`
contexts := []struct {
context Context
expected string
}{
{Context{
Format: NewHistoryFormat("table", false, true),
Trunc: true,
Output: out,
},
expectedTrunc,
},
{Context{
Format: NewHistoryFormat("table", false, true),
Trunc: false,
Output: out,
},
expectedNoTrunc,
},
}
for _, context := range contexts {
HistoryWrite(context.context, true, histories)
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}

View File

@ -0,0 +1,272 @@
package formatter
import (
"fmt"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
const (
defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"
defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"
imageIDHeader = "IMAGE ID"
repositoryHeader = "REPOSITORY"
tagHeader = "TAG"
digestHeader = "DIGEST"
)
// ImageContext contains image specific information required by the formatter, encapsulate a Context struct.
type ImageContext struct {
Context
Digest bool
}
func isDangling(image types.ImageSummary) bool {
return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
}
// NewImageFormat returns a format for rendering an ImageContext
func NewImageFormat(source string, quiet bool, digest bool) Format {
switch source {
case TableFormatKey:
switch {
case quiet:
return defaultQuietFormat
case digest:
return defaultImageTableFormatWithDigest
default:
return defaultImageTableFormat
}
case RawFormatKey:
switch {
case quiet:
return `image_id: {{.ID}}`
case digest:
return `repository: {{ .Repository }}
tag: {{.Tag}}
digest: {{.Digest}}
image_id: {{.ID}}
created_at: {{.CreatedAt}}
virtual_size: {{.Size}}
`
default:
return `repository: {{ .Repository }}
tag: {{.Tag}}
image_id: {{.ID}}
created_at: {{.CreatedAt}}
virtual_size: {{.Size}}
`
}
}
format := Format(source)
if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
format += "\t{{.Digest}}"
}
return format
}
// ImageWrite writes the formatter images using the ImageContext
func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
render := func(format func(subContext subContext) error) error {
return imageFormat(ctx, images, format)
}
return ctx.Write(newImageContext(), render)
}
func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
for _, image := range 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 image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], 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 {
if err := format(imageCtx); err != nil {
return err
}
}
}
return nil
}
type imageContext struct {
HeaderContext
trunc bool
i types.ImageSummary
repo string
tag string
digest string
}
func newImageContext() *imageContext {
imageCtx := imageContext{}
imageCtx.header = map[string]string{
"ID": imageIDHeader,
"Repository": repositoryHeader,
"Tag": tagHeader,
"Digest": digestHeader,
"CreatedSince": createdSinceHeader,
"CreatedAt": createdAtHeader,
"Size": sizeHeader,
"Containers": containersHeader,
"VirtualSize": sizeHeader,
"SharedSize": sharedSizeHeader,
"UniqueSize": uniqueSizeHeader,
}
return &imageCtx
}
func (c *imageContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *imageContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.i.ID)
}
return c.i.ID
}
func (c *imageContext) Repository() string {
return c.repo
}
func (c *imageContext) Tag() string {
return c.tag
}
func (c *imageContext) Digest() string {
return c.digest
}
func (c *imageContext) CreatedSince() string {
createdAt := time.Unix(int64(c.i.Created), 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *imageContext) CreatedAt() string {
return time.Unix(int64(c.i.Created), 0).String()
}
func (c *imageContext) Size() string {
return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
}
func (c *imageContext) Containers() string {
if c.i.Containers == -1 {
return "N/A"
}
return fmt.Sprintf("%d", c.i.Containers)
}
func (c *imageContext) VirtualSize() string {
return units.HumanSize(float64(c.i.VirtualSize))
}
func (c *imageContext) SharedSize() string {
if c.i.SharedSize == -1 {
return "N/A"
}
return units.HumanSize(float64(c.i.SharedSize))
}
func (c *imageContext) UniqueSize() string {
if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
return "N/A"
}
return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize))
}

View File

@ -0,0 +1,327 @@
package formatter
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestImageContext(t *testing.T) {
imageID := stringid.GenerateRandomID()
unix := time.Now().Unix()
var ctx imageContext
cases := []struct {
imageCtx imageContext
expValue string
call func() string
}{
{imageContext{
i: types.ImageSummary{ID: imageID},
trunc: true,
}, stringid.TruncateID(imageID), ctx.ID},
{imageContext{
i: types.ImageSummary{ID: imageID},
trunc: false,
}, imageID, ctx.ID},
{imageContext{
i: types.ImageSummary{Size: 10, VirtualSize: 10},
trunc: true,
}, "10B", ctx.Size},
{imageContext{
i: types.ImageSummary{Created: unix},
trunc: true,
}, time.Unix(unix, 0).String(), ctx.CreatedAt},
// FIXME
// {imageContext{
// i: types.ImageSummary{Created: unix},
// trunc: true,
// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
{imageContext{
i: types.ImageSummary{},
repo: "busybox",
}, "busybox", ctx.Repository},
{imageContext{
i: types.ImageSummary{},
tag: "latest",
}, "latest", ctx.Tag},
{imageContext{
i: types.ImageSummary{},
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", 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)
}
}
}
func TestImageContextWrite(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -1).Unix()
expectedTime := time.Unix(unixTime, 0).String()
cases := []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: NewImageFormat("table", false, false),
},
},
`REPOSITORY TAG IMAGE ID CREATED SIZE
image tag1 imageID1 24 hours ago 0B
image tag2 imageID2 24 hours ago 0B
<none> <none> imageID3 24 hours ago 0B
`,
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Repository}}", false, false),
},
},
"REPOSITORY\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Repository}}", false, true),
},
Digest: true,
},
`REPOSITORY DIGEST
image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
image <none>
<none> <none>
`,
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Repository}}", true, false),
},
},
"REPOSITORY\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table", true, false),
},
},
"imageID1\nimageID2\nimageID3\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table", false, true),
},
Digest: true,
},
`REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0B
image tag2 <none> imageID2 24 hours ago 0B
<none> <none> <none> imageID3 24 hours ago 0B
`,
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table", true, true),
},
Digest: true,
},
"imageID1\nimageID2\nimageID3\n",
},
// Raw Format
{
ImageContext{
Context: Context{
Format: NewImageFormat("raw", false, false),
},
},
fmt.Sprintf(`repository: image
tag: tag1
image_id: imageID1
created_at: %s
virtual_size: 0B
repository: image
tag: tag2
image_id: imageID2
created_at: %s
virtual_size: 0B
repository: <none>
tag: <none>
image_id: imageID3
created_at: %s
virtual_size: 0B
`, expectedTime, expectedTime, expectedTime),
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("raw", false, true),
},
Digest: true,
},
fmt.Sprintf(`repository: image
tag: tag1
digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
image_id: imageID1
created_at: %s
virtual_size: 0B
repository: image
tag: tag2
digest: <none>
image_id: imageID2
created_at: %s
virtual_size: 0B
repository: <none>
tag: <none>
digest: <none>
image_id: imageID3
created_at: %s
virtual_size: 0B
`, expectedTime, expectedTime, expectedTime),
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("raw", true, false),
},
},
`image_id: imageID1
image_id: imageID2
image_id: imageID3
`,
},
// Custom Format
{
ImageContext{
Context: Context{
Format: NewImageFormat("{{.Repository}}", false, false),
},
},
"image\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("{{.Repository}}", false, true),
},
Digest: true,
},
"image\nimage\n<none>\n",
},
}
for _, testcase := range cases {
images := []types.ImageSummary{
{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("")
testcase.context.Output = out
err := ImageWrite(testcase.context, images)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestImageContextWriteWithNoImage(t *testing.T) {
out := bytes.NewBufferString("")
images := []types.ImageSummary{}
contexts := []struct {
context ImageContext
expected string
}{
{
ImageContext{
Context: Context{
Format: NewImageFormat("{{.Repository}}", false, false),
Output: out,
},
},
"",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Repository}}", false, false),
Output: out,
},
},
"REPOSITORY\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("{{.Repository}}", false, true),
Output: out,
},
},
"",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Repository}}", false, true),
Output: out,
},
},
"REPOSITORY DIGEST\n",
},
}
for _, context := range contexts {
ImageWrite(context.context, images)
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}

View File

@ -0,0 +1,129 @@
package formatter
import (
"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"
)
// NewNetworkFormat returns a Format for rendering using a network Context
func NewNetworkFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return defaultNetworkTableFormat
case RawFormatKey:
if quiet {
return `network_id: {{.ID}}`
}
return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
}
return Format(source)
}
// NetworkWrite writes the context
func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
render := func(format func(subContext subContext) error) error {
for _, network := range networks {
networkCtx := &networkContext{trunc: ctx.Trunc, n: network}
if err := format(networkCtx); err != nil {
return err
}
}
return nil
}
networkCtx := networkContext{}
networkCtx.header = networkHeaderContext{
"ID": networkIDHeader,
"Name": nameHeader,
"Driver": driverHeader,
"Scope": scopeHeader,
"IPv6": ipv6Header,
"Internal": internalHeader,
"Labels": labelsHeader,
"CreatedAt": createdAtHeader,
}
return ctx.Write(&networkCtx, render)
}
type networkHeaderContext map[string]string
func (c networkHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
type networkContext struct {
HeaderContext
trunc bool
n types.NetworkResource
}
func (c *networkContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *networkContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.n.ID)
}
return c.n.ID
}
func (c *networkContext) Name() string {
return c.n.Name
}
func (c *networkContext) Driver() string {
return c.n.Driver
}
func (c *networkContext) Scope() string {
return c.n.Scope
}
func (c *networkContext) IPv6() string {
return fmt.Sprintf("%v", c.n.EnableIPv6)
}
func (c *networkContext) Internal() string {
return fmt.Sprintf("%v", c.n.Internal)
}
func (c *networkContext) Labels() string {
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 {
if c.n.Labels == nil {
return ""
}
return c.n.Labels[name]
}
func (c *networkContext) CreatedAt() string {
return c.n.Created.String()
}

View File

@ -0,0 +1,213 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestNetworkContext(t *testing.T) {
networkID := stringid.GenerateRandomID()
var ctx networkContext
cases := []struct {
networkCtx networkContext
expValue string
call func() string
}{
{networkContext{
n: types.NetworkResource{ID: networkID},
trunc: false,
}, networkID, ctx.ID},
{networkContext{
n: types.NetworkResource{ID: networkID},
trunc: true,
}, stringid.TruncateID(networkID), ctx.ID},
{networkContext{
n: types.NetworkResource{Name: "network_name"},
}, "network_name", ctx.Name},
{networkContext{
n: types.NetworkResource{Driver: "driver_name"},
}, "driver_name", ctx.Driver},
{networkContext{
n: types.NetworkResource{EnableIPv6: true},
}, "true", ctx.IPv6},
{networkContext{
n: types.NetworkResource{EnableIPv6: false},
}, "false", ctx.IPv6},
{networkContext{
n: types.NetworkResource{Internal: true},
}, "true", ctx.Internal},
{networkContext{
n: types.NetworkResource{Internal: false},
}, "false", ctx.Internal},
{networkContext{
n: types.NetworkResource{},
}, "", ctx.Labels},
{networkContext{
n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
}, "label1=value1,label2=value2", 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)
}
}
}
func TestNetworkContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewNetworkFormat("table", false)},
`NETWORK ID NAME DRIVER SCOPE
networkID1 foobar_baz foo local
networkID2 foobar_bar bar local
`,
},
{
Context{Format: NewNetworkFormat("table", true)},
`networkID1
networkID2
`,
},
{
Context{Format: NewNetworkFormat("table {{.Name}}", false)},
`NAME
foobar_baz
foobar_bar
`,
},
{
Context{Format: NewNetworkFormat("table {{.Name}}", true)},
`NAME
foobar_baz
foobar_bar
`,
},
// Raw Format
{
Context{Format: NewNetworkFormat("raw", false)},
`network_id: networkID1
name: foobar_baz
driver: foo
scope: local
network_id: networkID2
name: foobar_bar
driver: bar
scope: local
`,
},
{
Context{Format: NewNetworkFormat("raw", true)},
`network_id: networkID1
network_id: networkID2
`,
},
// Custom Format
{
Context{Format: NewNetworkFormat("{{.Name}}", false)},
`foobar_baz
foobar_bar
`,
},
// Custom Format with CreatedAt
{
Context{Format: NewNetworkFormat("{{.Name}} {{.CreatedAt}}", false)},
`foobar_baz 2016-01-01 00:00:00 +0000 UTC
foobar_bar 2017-01-01 00:00:00 +0000 UTC
`,
},
}
timestamp1, _ := time.Parse("2006-01-02", "2016-01-01")
timestamp2, _ := time.Parse("2006-01-02", "2017-01-01")
for _, testcase := range cases {
networks := []types.NetworkResource{
{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1},
{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := NetworkWrite(testcase.context, networks)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestNetworkContextWriteJSON(t *testing.T) {
networks := []types.NetworkResource{
{ID: "networkID1", Name: "foobar_baz"},
{ID: "networkID2", Name: "foobar_bar"},
}
expectedJSONs := []map[string]interface{}{
{"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"},
{"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"},
}
out := bytes.NewBufferString("")
err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestNetworkContextWriteJSONField(t *testing.T) {
networks := []types.NetworkResource{
{ID: "networkID1", Name: "foobar_baz"},
{ID: "networkID2", Name: "foobar_bar"},
}
out := bytes.NewBufferString("")
err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, networks[i].ID, s)
}
}

View File

@ -0,0 +1,292 @@
package formatter
import (
"fmt"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/inspect"
units "github.com/docker/go-units"
)
const (
defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}"
nodeInspectPrettyTemplate Format = `ID: {{.ID}}
{{- if .Name }}
Name: {{.Name}}
{{- end }}
{{- if .Labels }}
Labels:
{{- range $k, $v := .Labels }}
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{ end }}
Hostname: {{.Hostname}}
Joined at: {{.CreatedAt}}
Status:
State: {{.StatusState}}
{{- if .HasStatusMessage}}
Message: {{.StatusMessage}}
{{- end}}
Availability: {{.SpecAvailability}}
{{- if .Status.Addr}}
Address: {{.StatusAddr}}
{{- end}}
{{- if .HasManagerStatus}}
Manager Status:
Address: {{.ManagerStatusAddr}}
Raft Status: {{.ManagerStatusReachability}}
{{- if .IsManagerStatusLeader}}
Leader: Yes
{{- else}}
Leader: No
{{- end}}
{{- end}}
Platform:
Operating System: {{.PlatformOS}}
Architecture: {{.PlatformArchitecture}}
Resources:
CPUs: {{.ResourceNanoCPUs}}
Memory: {{.ResourceMemory}}
{{- if .HasEnginePlugins}}
Plugins:
{{- range $k, $v := .EnginePlugins }}
{{ $k }}:{{if $v }} {{ $v }}{{ end }}
{{- end }}
{{- end }}
Engine Version: {{.EngineVersion}}
{{- if .EngineLabels}}
Engine Labels:
{{- range $k, $v := .EngineLabels }}
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{- end }}
`
nodeIDHeader = "ID"
selfHeader = ""
hostnameHeader = "HOSTNAME"
availabilityHeader = "AVAILABILITY"
managerStatusHeader = "MANAGER STATUS"
)
// NewNodeFormat returns a Format for rendering using a node Context
func NewNodeFormat(source string, quiet bool) Format {
switch source {
case PrettyFormatKey:
return nodeInspectPrettyTemplate
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return defaultNodeTableFormat
case RawFormatKey:
if quiet {
return `node_id: {{.ID}}`
}
return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n`
}
return Format(source)
}
// NodeWrite writes the context
func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error {
render := func(format func(subContext subContext) error) error {
for _, node := range nodes {
nodeCtx := &nodeContext{n: node, info: info}
if err := format(nodeCtx); err != nil {
return err
}
}
return nil
}
nodeCtx := nodeContext{}
nodeCtx.header = nodeHeaderContext{
"ID": nodeIDHeader,
"Self": selfHeader,
"Hostname": hostnameHeader,
"Status": statusHeader,
"Availability": availabilityHeader,
"ManagerStatus": managerStatusHeader,
}
return ctx.Write(&nodeCtx, render)
}
type nodeHeaderContext map[string]string
type nodeContext struct {
HeaderContext
n swarm.Node
info types.Info
}
func (c *nodeContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *nodeContext) ID() string {
return c.n.ID
}
func (c *nodeContext) Self() bool {
return c.n.ID == c.info.Swarm.NodeID
}
func (c *nodeContext) Hostname() string {
return c.n.Description.Hostname
}
func (c *nodeContext) Status() string {
return command.PrettyPrint(string(c.n.Status.State))
}
func (c *nodeContext) Availability() string {
return command.PrettyPrint(string(c.n.Spec.Availability))
}
func (c *nodeContext) ManagerStatus() string {
reachability := ""
if c.n.ManagerStatus != nil {
if c.n.ManagerStatus.Leader {
reachability = "Leader"
} else {
reachability = string(c.n.ManagerStatus.Reachability)
}
}
return command.PrettyPrint(reachability)
}
// NodeInspectWrite renders the context for a list of services
func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error {
if ctx.Format != nodeInspectPrettyTemplate {
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
}
render := func(format func(subContext subContext) error) error {
for _, ref := range refs {
nodeI, _, err := getRef(ref)
if err != nil {
return err
}
node, ok := nodeI.(swarm.Node)
if !ok {
return fmt.Errorf("got wrong object to inspect :%v", ok)
}
if err := format(&nodeInspectContext{Node: node}); err != nil {
return err
}
}
return nil
}
return ctx.Write(&nodeInspectContext{}, render)
}
type nodeInspectContext struct {
swarm.Node
subContext
}
func (ctx *nodeInspectContext) ID() string {
return ctx.Node.ID
}
func (ctx *nodeInspectContext) Name() string {
return ctx.Node.Spec.Name
}
func (ctx *nodeInspectContext) Labels() map[string]string {
return ctx.Node.Spec.Labels
}
func (ctx *nodeInspectContext) Hostname() string {
return ctx.Node.Description.Hostname
}
func (ctx *nodeInspectContext) CreatedAt() string {
return command.PrettyPrint(ctx.Node.CreatedAt)
}
func (ctx *nodeInspectContext) StatusState() string {
return command.PrettyPrint(ctx.Node.Status.State)
}
func (ctx *nodeInspectContext) HasStatusMessage() bool {
return ctx.Node.Status.Message != ""
}
func (ctx *nodeInspectContext) StatusMessage() string {
return command.PrettyPrint(ctx.Node.Status.Message)
}
func (ctx *nodeInspectContext) SpecAvailability() string {
return command.PrettyPrint(ctx.Node.Spec.Availability)
}
func (ctx *nodeInspectContext) HasStatusAddr() bool {
return ctx.Node.Status.Addr != ""
}
func (ctx *nodeInspectContext) StatusAddr() string {
return ctx.Node.Status.Addr
}
func (ctx *nodeInspectContext) HasManagerStatus() bool {
return ctx.Node.ManagerStatus != nil
}
func (ctx *nodeInspectContext) ManagerStatusAddr() string {
return ctx.Node.ManagerStatus.Addr
}
func (ctx *nodeInspectContext) ManagerStatusReachability() string {
return command.PrettyPrint(ctx.Node.ManagerStatus.Reachability)
}
func (ctx *nodeInspectContext) IsManagerStatusLeader() bool {
return ctx.Node.ManagerStatus.Leader
}
func (ctx *nodeInspectContext) PlatformOS() string {
return ctx.Node.Description.Platform.OS
}
func (ctx *nodeInspectContext) PlatformArchitecture() string {
return ctx.Node.Description.Platform.Architecture
}
func (ctx *nodeInspectContext) ResourceNanoCPUs() int {
if ctx.Node.Description.Resources.NanoCPUs == 0 {
return int(0)
}
return int(ctx.Node.Description.Resources.NanoCPUs) / 1e9
}
func (ctx *nodeInspectContext) ResourceMemory() string {
if ctx.Node.Description.Resources.MemoryBytes == 0 {
return ""
}
return units.BytesSize(float64(ctx.Node.Description.Resources.MemoryBytes))
}
func (ctx *nodeInspectContext) HasEnginePlugins() bool {
return len(ctx.Node.Description.Engine.Plugins) > 0
}
func (ctx *nodeInspectContext) EnginePlugins() map[string]string {
pluginMap := map[string][]string{}
for _, p := range ctx.Node.Description.Engine.Plugins {
pluginMap[p.Type] = append(pluginMap[p.Type], p.Name)
}
pluginNamesByType := map[string]string{}
for k, v := range pluginMap {
pluginNamesByType[k] = strings.Join(v, ", ")
}
return pluginNamesByType
}
func (ctx *nodeInspectContext) EngineLabels() map[string]string {
return ctx.Node.Description.Engine.Labels
}
func (ctx *nodeInspectContext) EngineVersion() string {
return ctx.Node.Description.Engine.EngineVersion
}

View File

@ -0,0 +1,188 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestNodeContext(t *testing.T) {
nodeID := stringid.GenerateRandomID()
var ctx nodeContext
cases := []struct {
nodeCtx nodeContext
expValue string
call func() string
}{
{nodeContext{
n: swarm.Node{ID: nodeID},
}, nodeID, ctx.ID},
{nodeContext{
n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}},
}, "node_hostname", ctx.Hostname},
{nodeContext{
n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}},
}, "Foo", ctx.Status},
{nodeContext{
n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}},
}, "Drain", ctx.Availability},
{nodeContext{
n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}},
}, "Leader", ctx.ManagerStatus},
}
for _, c := range cases {
ctx = c.nodeCtx
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)
}
}
}
func TestNodeContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewNodeFormat("table", false)},
`ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
nodeID1 foobar_baz Foo Drain Leader
nodeID2 foobar_bar Bar Active Reachable
`,
},
{
Context{Format: NewNodeFormat("table", true)},
`nodeID1
nodeID2
`,
},
{
Context{Format: NewNodeFormat("table {{.Hostname}}", false)},
`HOSTNAME
foobar_baz
foobar_bar
`,
},
{
Context{Format: NewNodeFormat("table {{.Hostname}}", true)},
`HOSTNAME
foobar_baz
foobar_bar
`,
},
// Raw Format
{
Context{Format: NewNodeFormat("raw", false)},
`node_id: nodeID1
hostname: foobar_baz
status: Foo
availability: Drain
manager_status: Leader
node_id: nodeID2
hostname: foobar_bar
status: Bar
availability: Active
manager_status: Reachable
`,
},
{
Context{Format: NewNodeFormat("raw", true)},
`node_id: nodeID1
node_id: nodeID2
`,
},
// Custom Format
{
Context{Format: NewNodeFormat("{{.Hostname}}", false)},
`foobar_baz
foobar_bar
`,
},
}
for _, testcase := range cases {
nodes := []swarm.Node{
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}, Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, ManagerStatus: &swarm.ManagerStatus{Leader: true}},
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}, Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, ManagerStatus: &swarm.ManagerStatus{Leader: false, Reachability: swarm.Reachability("Reachable")}},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := NodeWrite(testcase.context, nodes, types.Info{})
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestNodeContextWriteJSON(t *testing.T) {
nodes := []swarm.Node{
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}},
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
}
expectedJSONs := []map[string]interface{}{
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false},
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false},
}
out := bytes.NewBufferString("")
err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, types.Info{})
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestNodeContextWriteJSONField(t *testing.T) {
nodes := []swarm.Node{
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}},
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
}
out := bytes.NewBufferString("")
err := NodeWrite(Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{})
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, nodes[i].ID, s)
}
}

View File

@ -0,0 +1,95 @@
package formatter
import (
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
)
const (
defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}"
pluginIDHeader = "ID"
descriptionHeader = "DESCRIPTION"
enabledHeader = "ENABLED"
)
// NewPluginFormat returns a Format for rendering using a plugin Context
func NewPluginFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return defaultPluginTableFormat
case RawFormatKey:
if quiet {
return `plugin_id: {{.ID}}`
}
return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n`
}
return Format(source)
}
// PluginWrite writes the context
func PluginWrite(ctx Context, plugins []*types.Plugin) error {
render := func(format func(subContext subContext) error) error {
for _, plugin := range plugins {
pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin}
if err := format(pluginCtx); err != nil {
return err
}
}
return nil
}
pluginCtx := pluginContext{}
pluginCtx.header = map[string]string{
"ID": pluginIDHeader,
"Name": nameHeader,
"Description": descriptionHeader,
"Enabled": enabledHeader,
"PluginReference": imageHeader,
}
return ctx.Write(&pluginCtx, render)
}
type pluginContext struct {
HeaderContext
trunc bool
p types.Plugin
}
func (c *pluginContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *pluginContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.p.ID)
}
return c.p.ID
}
func (c *pluginContext) Name() string {
return c.p.Name
}
func (c *pluginContext) Description() string {
desc := strings.Replace(c.p.Config.Description, "\n", "", -1)
desc = strings.Replace(desc, "\r", "", -1)
if c.trunc {
desc = stringutils.Ellipsis(desc, 45)
}
return desc
}
func (c *pluginContext) Enabled() bool {
return c.p.Enabled
}
func (c *pluginContext) PluginReference() string {
return c.p.PluginReference
}

View File

@ -0,0 +1,182 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestPluginContext(t *testing.T) {
pluginID := stringid.GenerateRandomID()
var ctx pluginContext
cases := []struct {
pluginCtx pluginContext
expValue string
call func() string
}{
{pluginContext{
p: types.Plugin{ID: pluginID},
trunc: false,
}, pluginID, ctx.ID},
{pluginContext{
p: types.Plugin{ID: pluginID},
trunc: true,
}, stringid.TruncateID(pluginID), ctx.ID},
{pluginContext{
p: types.Plugin{Name: "plugin_name"},
}, "plugin_name", ctx.Name},
{pluginContext{
p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}},
}, "plugin_description", ctx.Description},
}
for _, c := range cases {
ctx = c.pluginCtx
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)
}
}
}
func TestPluginContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewPluginFormat("table", false)},
`ID NAME DESCRIPTION ENABLED
pluginID1 foobar_baz description 1 true
pluginID2 foobar_bar description 2 false
`,
},
{
Context{Format: NewPluginFormat("table", true)},
`pluginID1
pluginID2
`,
},
{
Context{Format: NewPluginFormat("table {{.Name}}", false)},
`NAME
foobar_baz
foobar_bar
`,
},
{
Context{Format: NewPluginFormat("table {{.Name}}", true)},
`NAME
foobar_baz
foobar_bar
`,
},
// Raw Format
{
Context{Format: NewPluginFormat("raw", false)},
`plugin_id: pluginID1
name: foobar_baz
description: description 1
enabled: true
plugin_id: pluginID2
name: foobar_bar
description: description 2
enabled: false
`,
},
{
Context{Format: NewPluginFormat("raw", true)},
`plugin_id: pluginID1
plugin_id: pluginID2
`,
},
// Custom Format
{
Context{Format: NewPluginFormat("{{.Name}}", false)},
`foobar_baz
foobar_bar
`,
},
}
for _, testcase := range cases {
plugins := []*types.Plugin{
{ID: "pluginID1", Name: "foobar_baz", Config: types.PluginConfig{Description: "description 1"}, Enabled: true},
{ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := PluginWrite(testcase.context, plugins)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestPluginContextWriteJSON(t *testing.T) {
plugins := []*types.Plugin{
{ID: "pluginID1", Name: "foobar_baz"},
{ID: "pluginID2", Name: "foobar_bar"},
}
expectedJSONs := []map[string]interface{}{
{"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""},
{"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""},
}
out := bytes.NewBufferString("")
err := PluginWrite(Context{Format: "{{json .}}", Output: out}, plugins)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestPluginContextWriteJSONField(t *testing.T) {
plugins := []*types.Plugin{
{ID: "pluginID1", Name: "foobar_baz"},
{ID: "pluginID2", Name: "foobar_bar"},
}
out := bytes.NewBufferString("")
err := PluginWrite(Context{Format: "{{json .ID}}", Output: out}, plugins)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, plugins[i].ID, s)
}
}

View File

@ -0,0 +1,66 @@
package formatter
import (
"encoding/json"
"reflect"
"unicode"
"github.com/pkg/errors"
)
func marshalJSON(x interface{}) ([]byte, error) {
m, err := marshalMap(x)
if err != nil {
return nil, err
}
return json.Marshal(m)
}
// marshalMap marshals x to map[string]interface{}
func marshalMap(x interface{}) (map[string]interface{}, error) {
val := reflect.ValueOf(x)
if val.Kind() != reflect.Ptr {
return nil, errors.Errorf("expected a pointer to a struct, got %v", val.Kind())
}
if val.IsNil() {
return nil, errors.Errorf("expected a pointer to a struct, got nil pointer")
}
valElem := val.Elem()
if valElem.Kind() != reflect.Struct {
return nil, errors.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind())
}
typ := val.Type()
m := make(map[string]interface{})
for i := 0; i < val.NumMethod(); i++ {
k, v, err := marshalForMethod(typ.Method(i), val.Method(i))
if err != nil {
return nil, err
}
if k != "" {
m[k] = v
}
}
return m, nil
}
var unmarshallableNames = map[string]struct{}{"FullHeader": {}}
// marshalForMethod returns the map key and the map value for marshalling the method.
// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()")
func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) {
if val.Kind() != reflect.Func {
return "", nil, errors.Errorf("expected func, got %v", val.Kind())
}
name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut()
_, blackListed := unmarshallableNames[name]
// FIXME: In text/template, (numOut == 2) is marshallable,
// if the type of the second param is error.
marshallable := unicode.IsUpper(rune(name[0])) && !blackListed &&
numIn == 0 && numOut == 1
if !marshallable {
return "", nil, nil
}
result := val.Call(make([]reflect.Value, numIn))
intf := result[0].Interface()
return name, intf, nil
}

View File

@ -0,0 +1,66 @@
package formatter
import (
"reflect"
"testing"
)
type dummy struct {
}
func (d *dummy) Func1() string {
return "Func1"
}
func (d *dummy) func2() string {
return "func2(should not be marshalled)"
}
func (d *dummy) Func3() (string, int) {
return "Func3(should not be marshalled)", -42
}
func (d *dummy) Func4() int {
return 4
}
type dummyType string
func (d *dummy) Func5() dummyType {
return dummyType("Func5")
}
func (d *dummy) FullHeader() string {
return "FullHeader(should not be marshalled)"
}
var dummyExpected = map[string]interface{}{
"Func1": "Func1",
"Func4": 4,
"Func5": dummyType("Func5"),
}
func TestMarshalMap(t *testing.T) {
d := dummy{}
m, err := marshalMap(&d)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(dummyExpected, m) {
t.Fatalf("expected %+v, got %+v",
dummyExpected, m)
}
}
func TestMarshalMapBad(t *testing.T) {
if _, err := marshalMap(nil); err == nil {
t.Fatal("expected an error (argument is nil)")
}
if _, err := marshalMap(dummy{}); err == nil {
t.Fatal("expected an error (argument is non-pointer)")
}
x := 42
if _, err := marshalMap(&x); err == nil {
t.Fatal("expected an error (argument is a pointer to non-struct)")
}
}

View File

@ -0,0 +1,101 @@
package formatter
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/api/types/swarm"
units "github.com/docker/go-units"
)
const (
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
secretIDHeader = "ID"
secretNameHeader = "NAME"
secretCreatedHeader = "CREATED"
secretUpdatedHeader = "UPDATED"
)
// NewSecretFormat returns a Format for rendering using a network Context
func NewSecretFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return defaultSecretTableFormat
}
return Format(source)
}
// SecretWrite writes the context
func SecretWrite(ctx Context, secrets []swarm.Secret) error {
render := func(format func(subContext subContext) error) error {
for _, secret := range secrets {
secretCtx := &secretContext{s: secret}
if err := format(secretCtx); err != nil {
return err
}
}
return nil
}
return ctx.Write(newSecretContext(), render)
}
func newSecretContext() *secretContext {
sCtx := &secretContext{}
sCtx.header = map[string]string{
"ID": secretIDHeader,
"Name": nameHeader,
"CreatedAt": secretCreatedHeader,
"UpdatedAt": secretUpdatedHeader,
"Labels": labelsHeader,
}
return sCtx
}
type secretContext struct {
HeaderContext
s swarm.Secret
}
func (c *secretContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *secretContext) ID() string {
return c.s.ID
}
func (c *secretContext) Name() string {
return c.s.Spec.Annotations.Name
}
func (c *secretContext) CreatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago"
}
func (c *secretContext) UpdatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago"
}
func (c *secretContext) Labels() string {
mapLabels := c.s.Spec.Annotations.Labels
if mapLabels == nil {
return ""
}
var joinLabels []string
for k, v := range mapLabels {
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
func (c *secretContext) Label(name string) string {
if c.s.Spec.Annotations.Labels == nil {
return ""
}
return c.s.Spec.Annotations.Labels[name]
}

View File

@ -0,0 +1,63 @@
package formatter
import (
"bytes"
"testing"
"time"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
)
func TestSecretContextFormatWrite(t *testing.T) {
// Check default output format (verbose and non-verbose mode) for table headers
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{Context{Format: NewSecretFormat("table", false)},
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`},
{Context{Format: NewSecretFormat("table {{.Name}}", true)},
`NAME
passwords
id_rsa
`},
{Context{Format: NewSecretFormat("{{.ID}}-{{.Name}}", false)},
`1-passwords
2-id_rsa
`},
}
secrets := []swarm.Secret{
{ID: "1",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "passwords"}}},
{ID: "2",
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
}
for _, testcase := range cases {
out := bytes.NewBufferString("")
testcase.context.Output = out
if err := SecretWrite(testcase.context, secrets); err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}

View File

@ -0,0 +1,535 @@
package formatter
import (
"fmt"
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command/inspect"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/pkg/errors"
)
const serviceInspectPrettyTemplate Format = `
ID: {{.ID}}
Name: {{.Name}}
{{- if .Labels }}
Labels:
{{- range $k, $v := .Labels }}
{{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{ end }}
Service Mode:
{{- if .IsModeGlobal }} Global
{{- else if .IsModeReplicated }} Replicated
{{- if .ModeReplicatedReplicas }}
Replicas: {{ .ModeReplicatedReplicas }}
{{- end }}{{ end }}
{{- if .HasUpdateStatus }}
UpdateStatus:
State: {{ .UpdateStatusState }}
{{- if .HasUpdateStatusStarted }}
Started: {{ .UpdateStatusStarted }}
{{- end }}
{{- if .UpdateIsCompleted }}
Completed: {{ .UpdateStatusCompleted }}
{{- end }}
Message: {{ .UpdateStatusMessage }}
{{- end }}
Placement:
{{- if .TaskPlacementConstraints }}
Constraints: {{ .TaskPlacementConstraints }}
{{- end }}
{{- if .TaskPlacementPreferences }}
Preferences: {{ .TaskPlacementPreferences }}
{{- end }}
{{- if .HasUpdateConfig }}
UpdateConfig:
Parallelism: {{ .UpdateParallelism }}
{{- if .HasUpdateDelay}}
Delay: {{ .UpdateDelay }}
{{- end }}
On failure: {{ .UpdateOnFailure }}
{{- if .HasUpdateMonitor}}
Monitoring Period: {{ .UpdateMonitor }}
{{- end }}
Max failure ratio: {{ .UpdateMaxFailureRatio }}
Update order: {{ .UpdateOrder }}
{{- end }}
{{- if .HasRollbackConfig }}
RollbackConfig:
Parallelism: {{ .RollbackParallelism }}
{{- if .HasRollbackDelay}}
Delay: {{ .RollbackDelay }}
{{- end }}
On failure: {{ .RollbackOnFailure }}
{{- if .HasRollbackMonitor}}
Monitoring Period: {{ .RollbackMonitor }}
{{- end }}
Max failure ratio: {{ .RollbackMaxFailureRatio }}
Rollback order: {{ .RollbackOrder }}
{{- end }}
ContainerSpec:
Image: {{ .ContainerImage }}
{{- if .ContainerArgs }}
Args: {{ range $arg := .ContainerArgs }}{{ $arg }} {{ end }}
{{- end -}}
{{- if .ContainerEnv }}
Env: {{ range $env := .ContainerEnv }}{{ $env }} {{ end }}
{{- end -}}
{{- if .ContainerWorkDir }}
Dir: {{ .ContainerWorkDir }}
{{- end -}}
{{- if .ContainerUser }}
User: {{ .ContainerUser }}
{{- end }}
{{- if .ContainerMounts }}
Mounts:
{{- end }}
{{- range $mount := .ContainerMounts }}
Target = {{ $mount.Target }}
Source = {{ $mount.Source }}
ReadOnly = {{ $mount.ReadOnly }}
Type = {{ $mount.Type }}
{{- end -}}
{{- if .HasResources }}
Resources:
{{- if .HasResourceReservations }}
Reservations:
{{- if gt .ResourceReservationNanoCPUs 0.0 }}
CPU: {{ .ResourceReservationNanoCPUs }}
{{- end }}
{{- if .ResourceReservationMemory }}
Memory: {{ .ResourceReservationMemory }}
{{- end }}{{ end }}
{{- if .HasResourceLimits }}
Limits:
{{- if gt .ResourceLimitsNanoCPUs 0.0 }}
CPU: {{ .ResourceLimitsNanoCPUs }}
{{- end }}
{{- if .ResourceLimitMemory }}
Memory: {{ .ResourceLimitMemory }}
{{- end }}{{ end }}{{ end }}
{{- if .Networks }}
Networks:
{{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }}
Endpoint Mode: {{ .EndpointMode }}
{{- if .Ports }}
Ports:
{{- range $port := .Ports }}
PublishedPort = {{ $port.PublishedPort }}
Protocol = {{ $port.Protocol }}
TargetPort = {{ $port.TargetPort }}
PublishMode = {{ $port.PublishMode }}
{{- end }} {{ end -}}
`
// NewServiceFormat returns a Format for rendering using a Context
func NewServiceFormat(source string) Format {
switch source {
case PrettyFormatKey:
return serviceInspectPrettyTemplate
default:
return Format(strings.TrimPrefix(source, RawFormatKey))
}
}
func resolveNetworks(service swarm.Service, getNetwork inspect.GetRefFunc) map[string]string {
networkNames := make(map[string]string)
for _, network := range service.Spec.TaskTemplate.Networks {
if resolved, _, err := getNetwork(network.Target); err == nil {
if resolvedNetwork, ok := resolved.(types.NetworkResource); ok {
networkNames[resolvedNetwork.ID] = resolvedNetwork.Name
}
}
}
return networkNames
}
// ServiceInspectWrite renders the context for a list of services
func ServiceInspectWrite(ctx Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error {
if ctx.Format != serviceInspectPrettyTemplate {
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
}
render := func(format func(subContext subContext) error) error {
for _, ref := range refs {
serviceI, _, err := getRef(ref)
if err != nil {
return err
}
service, ok := serviceI.(swarm.Service)
if !ok {
return errors.Errorf("got wrong object to inspect")
}
if err := format(&serviceInspectContext{Service: service, networkNames: resolveNetworks(service, getNetwork)}); err != nil {
return err
}
}
return nil
}
return ctx.Write(&serviceInspectContext{}, render)
}
type serviceInspectContext struct {
swarm.Service
subContext
// networkNames is a map from network IDs (as found in
// Networks[x].Target) to network names.
networkNames map[string]string
}
func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) {
return marshalJSON(ctx)
}
func (ctx *serviceInspectContext) ID() string {
return ctx.Service.ID
}
func (ctx *serviceInspectContext) Name() string {
return ctx.Service.Spec.Name
}
func (ctx *serviceInspectContext) Labels() map[string]string {
return ctx.Service.Spec.Labels
}
func (ctx *serviceInspectContext) IsModeGlobal() bool {
return ctx.Service.Spec.Mode.Global != nil
}
func (ctx *serviceInspectContext) IsModeReplicated() bool {
return ctx.Service.Spec.Mode.Replicated != nil
}
func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 {
return ctx.Service.Spec.Mode.Replicated.Replicas
}
func (ctx *serviceInspectContext) HasUpdateStatus() bool {
return ctx.Service.UpdateStatus != nil && ctx.Service.UpdateStatus.State != ""
}
func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState {
return ctx.Service.UpdateStatus.State
}
func (ctx *serviceInspectContext) HasUpdateStatusStarted() bool {
return ctx.Service.UpdateStatus.StartedAt != nil
}
func (ctx *serviceInspectContext) UpdateStatusStarted() string {
return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.StartedAt)) + " ago"
}
func (ctx *serviceInspectContext) UpdateIsCompleted() bool {
return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted && ctx.Service.UpdateStatus.CompletedAt != nil
}
func (ctx *serviceInspectContext) UpdateStatusCompleted() string {
return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.CompletedAt)) + " ago"
}
func (ctx *serviceInspectContext) UpdateStatusMessage() string {
return ctx.Service.UpdateStatus.Message
}
func (ctx *serviceInspectContext) TaskPlacementConstraints() []string {
if ctx.Service.Spec.TaskTemplate.Placement != nil {
return ctx.Service.Spec.TaskTemplate.Placement.Constraints
}
return nil
}
func (ctx *serviceInspectContext) TaskPlacementPreferences() []string {
if ctx.Service.Spec.TaskTemplate.Placement == nil {
return nil
}
var strings []string
for _, pref := range ctx.Service.Spec.TaskTemplate.Placement.Preferences {
if pref.Spread != nil {
strings = append(strings, "spread="+pref.Spread.SpreadDescriptor)
}
}
return strings
}
func (ctx *serviceInspectContext) HasUpdateConfig() bool {
return ctx.Service.Spec.UpdateConfig != nil
}
func (ctx *serviceInspectContext) UpdateParallelism() uint64 {
return ctx.Service.Spec.UpdateConfig.Parallelism
}
func (ctx *serviceInspectContext) HasUpdateDelay() bool {
return ctx.Service.Spec.UpdateConfig.Delay.Nanoseconds() > 0
}
func (ctx *serviceInspectContext) UpdateDelay() time.Duration {
return ctx.Service.Spec.UpdateConfig.Delay
}
func (ctx *serviceInspectContext) UpdateOnFailure() string {
return ctx.Service.Spec.UpdateConfig.FailureAction
}
func (ctx *serviceInspectContext) UpdateOrder() string {
return ctx.Service.Spec.UpdateConfig.Order
}
func (ctx *serviceInspectContext) HasUpdateMonitor() bool {
return ctx.Service.Spec.UpdateConfig.Monitor.Nanoseconds() > 0
}
func (ctx *serviceInspectContext) UpdateMonitor() time.Duration {
return ctx.Service.Spec.UpdateConfig.Monitor
}
func (ctx *serviceInspectContext) UpdateMaxFailureRatio() float32 {
return ctx.Service.Spec.UpdateConfig.MaxFailureRatio
}
func (ctx *serviceInspectContext) HasRollbackConfig() bool {
return ctx.Service.Spec.RollbackConfig != nil
}
func (ctx *serviceInspectContext) RollbackParallelism() uint64 {
return ctx.Service.Spec.RollbackConfig.Parallelism
}
func (ctx *serviceInspectContext) HasRollbackDelay() bool {
return ctx.Service.Spec.RollbackConfig.Delay.Nanoseconds() > 0
}
func (ctx *serviceInspectContext) RollbackDelay() time.Duration {
return ctx.Service.Spec.RollbackConfig.Delay
}
func (ctx *serviceInspectContext) RollbackOnFailure() string {
return ctx.Service.Spec.RollbackConfig.FailureAction
}
func (ctx *serviceInspectContext) HasRollbackMonitor() bool {
return ctx.Service.Spec.RollbackConfig.Monitor.Nanoseconds() > 0
}
func (ctx *serviceInspectContext) RollbackMonitor() time.Duration {
return ctx.Service.Spec.RollbackConfig.Monitor
}
func (ctx *serviceInspectContext) RollbackMaxFailureRatio() float32 {
return ctx.Service.Spec.RollbackConfig.MaxFailureRatio
}
func (ctx *serviceInspectContext) RollbackOrder() string {
return ctx.Service.Spec.RollbackConfig.Order
}
func (ctx *serviceInspectContext) ContainerImage() string {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image
}
func (ctx *serviceInspectContext) ContainerArgs() []string {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Args
}
func (ctx *serviceInspectContext) ContainerEnv() []string {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Env
}
func (ctx *serviceInspectContext) ContainerWorkDir() string {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Dir
}
func (ctx *serviceInspectContext) ContainerUser() string {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.User
}
func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount {
return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts
}
func (ctx *serviceInspectContext) HasResources() bool {
return ctx.Service.Spec.TaskTemplate.Resources != nil
}
func (ctx *serviceInspectContext) HasResourceReservations() bool {
if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Reservations == nil {
return false
}
return ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes > 0
}
func (ctx *serviceInspectContext) ResourceReservationNanoCPUs() float64 {
if ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs == 0 {
return float64(0)
}
return float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs) / 1e9
}
func (ctx *serviceInspectContext) ResourceReservationMemory() string {
if ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes == 0 {
return ""
}
return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes))
}
func (ctx *serviceInspectContext) HasResourceLimits() bool {
if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Limits == nil {
return false
}
return ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes > 0
}
func (ctx *serviceInspectContext) ResourceLimitsNanoCPUs() float64 {
return float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs) / 1e9
}
func (ctx *serviceInspectContext) ResourceLimitMemory() string {
if ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes == 0 {
return ""
}
return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes))
}
func (ctx *serviceInspectContext) Networks() []string {
var out []string
for _, n := range ctx.Service.Spec.TaskTemplate.Networks {
if name, ok := ctx.networkNames[n.Target]; ok {
out = append(out, name)
} else {
out = append(out, n.Target)
}
}
return out
}
func (ctx *serviceInspectContext) EndpointMode() string {
if ctx.Service.Spec.EndpointSpec == nil {
return ""
}
return string(ctx.Service.Spec.EndpointSpec.Mode)
}
func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
return ctx.Service.Endpoint.Ports
}
const (
defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}"
serviceIDHeader = "ID"
modeHeader = "MODE"
replicasHeader = "REPLICAS"
)
// NewServiceListFormat returns a Format for rendering using a service Context
func NewServiceListFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return defaultServiceTableFormat
case RawFormatKey:
if quiet {
return `id: {{.ID}}`
}
return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\nports: {{.Ports}}\n`
}
return Format(source)
}
// ServiceListInfo stores the information about mode and replicas to be used by template
type ServiceListInfo struct {
Mode string
Replicas string
}
// ServiceListWrite writes the context
func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error {
render := func(format func(subContext subContext) error) error {
for _, service := range services {
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
if err := format(serviceCtx); err != nil {
return err
}
}
return nil
}
serviceCtx := serviceContext{}
serviceCtx.header = map[string]string{
"ID": serviceIDHeader,
"Name": nameHeader,
"Mode": modeHeader,
"Replicas": replicasHeader,
"Image": imageHeader,
"Ports": portsHeader,
}
return ctx.Write(&serviceCtx, render)
}
type serviceContext struct {
HeaderContext
service swarm.Service
mode string
replicas string
}
func (c *serviceContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *serviceContext) ID() string {
return stringid.TruncateID(c.service.ID)
}
func (c *serviceContext) Name() string {
return c.service.Spec.Name
}
func (c *serviceContext) Mode() string {
return c.mode
}
func (c *serviceContext) Replicas() string {
return c.replicas
}
func (c *serviceContext) Image() string {
image := c.service.Spec.TaskTemplate.ContainerSpec.Image
if ref, err := reference.ParseNormalizedNamed(image); err == nil {
// update image string for display, (strips any digest)
if nt, ok := ref.(reference.NamedTagged); ok {
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
image = reference.FamiliarString(namedTagged)
}
}
}
return image
}
func (c *serviceContext) Ports() string {
if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil {
return ""
}
ports := []string{}
for _, pConfig := range c.service.Spec.EndpointSpec.Ports {
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
pConfig.PublishedPort,
pConfig.TargetPort,
pConfig.Protocol,
))
}
}
return strings.Join(ports, ",")
}

View File

@ -0,0 +1,239 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
)
func TestServiceContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewServiceListFormat("table", false)},
`ID NAME MODE REPLICAS IMAGE PORTS
id_baz baz global 2/4 *:80->8080/tcp
id_bar bar replicated 2/4 *:80->8080/tcp
`,
},
{
Context{Format: NewServiceListFormat("table", true)},
`id_baz
id_bar
`,
},
{
Context{Format: NewServiceListFormat("table {{.Name}}", false)},
`NAME
baz
bar
`,
},
{
Context{Format: NewServiceListFormat("table {{.Name}}", true)},
`NAME
baz
bar
`,
},
// Raw Format
{
Context{Format: NewServiceListFormat("raw", false)},
`id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp
`,
},
{
Context{Format: NewServiceListFormat("raw", true)},
`id: id_baz
id: id_bar
`,
},
// Custom Format
{
Context{Format: NewServiceListFormat("{{.Name}}", false)},
`baz
bar
`,
},
}
for _, testcase := range cases {
services := []swarm.Service{
{
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
},
{
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
},
}
info := map[string]ServiceListInfo{
"id_baz": {
Mode: "global",
Replicas: "2/4",
},
"id_bar": {
Mode: "replicated",
Replicas: "2/4",
},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := ServiceListWrite(testcase.context, services, info)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestServiceContextWriteJSON(t *testing.T) {
services := []swarm.Service{
{
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
},
{
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
},
}
info := map[string]ServiceListInfo{
"id_baz": {
Mode: "global",
Replicas: "2/4",
},
"id_bar": {
Mode: "replicated",
Replicas: "2/4",
},
}
expectedJSONs := []map[string]interface{}{
{"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
{"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
}
out := bytes.NewBufferString("")
err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestServiceContextWriteJSONField(t *testing.T) {
services := []swarm.Service{
{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
}
info := map[string]ServiceListInfo{
"id_baz": {
Mode: "global",
Replicas: "2/4",
},
"id_bar": {
Mode: "replicated",
Replicas: "2/4",
},
}
out := bytes.NewBufferString("")
err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, services[i].Spec.Name, s)
}
}

View File

@ -0,0 +1,220 @@
package formatter
import (
"fmt"
"sync"
units "github.com/docker/go-units"
)
const (
winOSType = "windows"
defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
containerHeader = "CONTAINER"
cpuPercHeader = "CPU %"
netIOHeader = "NET I/O"
blockIOHeader = "BLOCK I/O"
memPercHeader = "MEM %" // Used only on Linux
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
pidsHeader = "PIDS" // Used only on Linux
)
// StatsEntry represents represents the statistics data collected from a container
type StatsEntry struct {
Container string
Name string
ID string
CPUPercentage float64
Memory float64 // On Windows this is the private working set
MemoryLimit float64 // Not used on Windows
MemoryPercentage float64 // Not used on Windows
NetworkRx float64
NetworkTx float64
BlockRead float64
BlockWrite float64
PidsCurrent uint64 // Not used on Windows
IsInvalid bool
}
// ContainerStats represents an entity to store containers statistics synchronously
type ContainerStats struct {
mutex sync.Mutex
StatsEntry
err error
}
// GetError returns the container statistics error.
// This is used to determine whether the statistics are valid or not
func (cs *ContainerStats) GetError() error {
cs.mutex.Lock()
defer cs.mutex.Unlock()
return cs.err
}
// SetErrorAndReset zeroes all the container statistics and store the error.
// It is used when receiving time out error during statistics collecting to reduce lock overhead
func (cs *ContainerStats) SetErrorAndReset(err error) {
cs.mutex.Lock()
defer cs.mutex.Unlock()
cs.CPUPercentage = 0
cs.Memory = 0
cs.MemoryPercentage = 0
cs.MemoryLimit = 0
cs.NetworkRx = 0
cs.NetworkTx = 0
cs.BlockRead = 0
cs.BlockWrite = 0
cs.PidsCurrent = 0
cs.err = err
cs.IsInvalid = true
}
// SetError sets container statistics error
func (cs *ContainerStats) SetError(err error) {
cs.mutex.Lock()
defer cs.mutex.Unlock()
cs.err = err
if err != nil {
cs.IsInvalid = true
}
}
// SetStatistics set the container statistics
func (cs *ContainerStats) SetStatistics(s StatsEntry) {
cs.mutex.Lock()
defer cs.mutex.Unlock()
s.Container = cs.Container
cs.StatsEntry = s
}
// GetStatistics returns container statistics with other meta data such as the container name
func (cs *ContainerStats) GetStatistics() StatsEntry {
cs.mutex.Lock()
defer cs.mutex.Unlock()
return cs.StatsEntry
}
// NewStatsFormat returns a format for rendering an CStatsContext
func NewStatsFormat(source, osType string) Format {
if source == TableFormatKey {
if osType == winOSType {
return Format(winDefaultStatsTableFormat)
}
return Format(defaultStatsTableFormat)
}
return Format(source)
}
// NewContainerStats returns a new ContainerStats entity and sets in it the given name
func NewContainerStats(container, osType string) *ContainerStats {
return &ContainerStats{
StatsEntry: StatsEntry{Container: container},
}
}
// ContainerStatsWrite renders the context for a list of containers statistics
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error {
render := func(format func(subContext subContext) error) error {
for _, cstats := range containerStats {
containerStatsCtx := &containerStatsContext{
s: cstats,
os: osType,
}
if err := format(containerStatsCtx); err != nil {
return err
}
}
return nil
}
memUsage := memUseHeader
if osType == winOSType {
memUsage = winMemUseHeader
}
containerStatsCtx := containerStatsContext{}
containerStatsCtx.header = map[string]string{
"Container": containerHeader,
"Name": nameHeader,
"ID": containerIDHeader,
"CPUPerc": cpuPercHeader,
"MemUsage": memUsage,
"MemPerc": memPercHeader,
"NetIO": netIOHeader,
"BlockIO": blockIOHeader,
"PIDs": pidsHeader,
}
containerStatsCtx.os = osType
return ctx.Write(&containerStatsCtx, render)
}
type containerStatsContext struct {
HeaderContext
s StatsEntry
os string
}
func (c *containerStatsContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *containerStatsContext) Container() string {
return c.s.Container
}
func (c *containerStatsContext) Name() string {
if len(c.s.Name) > 1 {
return c.s.Name[1:]
}
return "--"
}
func (c *containerStatsContext) ID() string {
return c.s.ID
}
func (c *containerStatsContext) CPUPerc() string {
if c.s.IsInvalid {
return fmt.Sprintf("--")
}
return fmt.Sprintf("%.2f%%", c.s.CPUPercentage)
}
func (c *containerStatsContext) MemUsage() string {
if c.s.IsInvalid {
return fmt.Sprintf("-- / --")
}
if c.os == winOSType {
return fmt.Sprintf("%s", units.BytesSize(c.s.Memory))
}
return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
}
func (c *containerStatsContext) MemPerc() string {
if c.s.IsInvalid || c.os == winOSType {
return fmt.Sprintf("--")
}
return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage)
}
func (c *containerStatsContext) NetIO() string {
if c.s.IsInvalid {
return fmt.Sprintf("--")
}
return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3))
}
func (c *containerStatsContext) BlockIO() string {
if c.s.IsInvalid {
return fmt.Sprintf("--")
}
return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3))
}
func (c *containerStatsContext) PIDs() string {
if c.s.IsInvalid || c.os == winOSType {
return fmt.Sprintf("--")
}
return fmt.Sprintf("%d", c.s.PidsCurrent)
}

View File

@ -0,0 +1,266 @@
package formatter
import (
"bytes"
"testing"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestContainerStatsContext(t *testing.T) {
containerID := stringid.GenerateRandomID()
var ctx containerStatsContext
tt := []struct {
stats StatsEntry
osType string
expValue string
expHeader string
call func() string
}{
{StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container},
{StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc},
{StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc},
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO},
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO},
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO},
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO},
{StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc},
{StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc},
{StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc},
{StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage},
{StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage},
{StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage},
{StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs},
{StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs},
{StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs},
}
for _, te := range tt {
ctx = containerStatsContext{s: te.stats, os: te.osType}
if v := te.call(); v != te.expValue {
t.Fatalf("Expected %q, got %q", te.expValue, v)
}
}
}
func TestContainerStatsContextWrite(t *testing.T) {
tt := []struct {
context Context
expected string
}{
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
{
Context{Format: "table {{.MemUsage}}"},
`MEM USAGE / LIMIT
20B / 20B
-- / --
`,
},
{
Context{Format: "{{.Container}} {{.ID}} {{.Name}}"},
`container1 abcdef foo
container2 --
`,
},
{
Context{Format: "{{.Container}} {{.CPUPerc}}"},
`container1 20.00%
container2 --
`,
},
}
for _, te := range tt {
stats := []StatsEntry{
{
Container: "container1",
ID: "abcdef",
Name: "/foo",
CPUPercentage: 20,
Memory: 20,
MemoryLimit: 20,
MemoryPercentage: 20,
NetworkRx: 20,
NetworkTx: 20,
BlockRead: 20,
BlockWrite: 20,
PidsCurrent: 2,
IsInvalid: false,
},
{
Container: "container2",
CPUPercentage: 30,
Memory: 30,
MemoryLimit: 30,
MemoryPercentage: 30,
NetworkRx: 30,
NetworkTx: 30,
BlockRead: 30,
BlockWrite: 30,
PidsCurrent: 3,
IsInvalid: true,
},
}
var out bytes.Buffer
te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "linux")
if err != nil {
assert.EqualError(t, err, te.expected)
} else {
assert.Equal(t, te.expected, out.String())
}
}
}
func TestContainerStatsContextWriteWindows(t *testing.T) {
tt := []struct {
context Context
expected string
}{
{
Context{Format: "table {{.MemUsage}}"},
`PRIV WORKING SET
20B
-- / --
`,
},
{
Context{Format: "{{.Container}} {{.CPUPerc}}"},
`container1 20.00%
container2 --
`,
},
{
Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"},
`container1 -- --
container2 -- --
`,
},
}
for _, te := range tt {
stats := []StatsEntry{
{
Container: "container1",
CPUPercentage: 20,
Memory: 20,
MemoryLimit: 20,
MemoryPercentage: 20,
NetworkRx: 20,
NetworkTx: 20,
BlockRead: 20,
BlockWrite: 20,
PidsCurrent: 2,
IsInvalid: false,
},
{
Container: "container2",
CPUPercentage: 30,
Memory: 30,
MemoryLimit: 30,
MemoryPercentage: 30,
NetworkRx: 30,
NetworkTx: 30,
BlockRead: 30,
BlockWrite: 30,
PidsCurrent: 3,
IsInvalid: true,
},
}
var out bytes.Buffer
te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "windows")
if err != nil {
assert.EqualError(t, err, te.expected)
} else {
assert.Equal(t, te.expected, out.String())
}
}
}
func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
var out bytes.Buffer
contexts := []struct {
context Context
expected string
}{
{
Context{
Format: "{{.Container}}",
Output: &out,
},
"",
},
{
Context{
Format: "table {{.Container}}",
Output: &out,
},
"CONTAINER\n",
},
{
Context{
Format: "table {{.Container}}\t{{.CPUPerc}}",
Output: &out,
},
"CONTAINER CPU %\n",
},
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "linux")
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}
func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
var out bytes.Buffer
contexts := []struct {
context Context
expected string
}{
{
Context{
Format: "{{.Container}}",
Output: &out,
},
"",
},
{
Context{
Format: "table {{.Container}}\t{{.MemUsage}}",
Output: &out,
},
"CONTAINER PRIV WORKING SET\n",
},
{
Context{
Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
Output: &out,
},
"CONTAINER CPU % PRIV WORKING SET\n",
},
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "windows")
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}

View File

@ -0,0 +1,150 @@
package formatter
import (
"fmt"
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
const (
defaultTaskTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Image}}\t{{.Node}}\t{{.DesiredState}}\t{{.CurrentState}}\t{{.Error}}\t{{.Ports}}"
nodeHeader = "NODE"
taskIDHeader = "ID"
desiredStateHeader = "DESIRED STATE"
currentStateHeader = "CURRENT STATE"
errorHeader = "ERROR"
maxErrLength = 30
)
// NewTaskFormat returns a Format for rendering using a task Context
func NewTaskFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return defaultTaskTableFormat
case RawFormatKey:
if quiet {
return `id: {{.ID}}`
}
return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n`
}
return Format(source)
}
// TaskWrite writes the context
func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error {
render := func(format func(subContext subContext) error) error {
for _, task := range tasks {
taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]}
if err := format(taskCtx); err != nil {
return err
}
}
return nil
}
taskCtx := taskContext{}
taskCtx.header = taskHeaderContext{
"ID": taskIDHeader,
"Name": nameHeader,
"Image": imageHeader,
"Node": nodeHeader,
"DesiredState": desiredStateHeader,
"CurrentState": currentStateHeader,
"Error": errorHeader,
"Ports": portsHeader,
}
return ctx.Write(&taskCtx, render)
}
type taskHeaderContext map[string]string
type taskContext struct {
HeaderContext
trunc bool
task swarm.Task
name string
node string
}
func (c *taskContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *taskContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.task.ID)
}
return c.task.ID
}
func (c *taskContext) Name() string {
return c.name
}
func (c *taskContext) Image() string {
image := c.task.Spec.ContainerSpec.Image
if c.trunc {
ref, err := reference.ParseNormalizedNamed(image)
if err == nil {
// update image string for display, (strips any digest)
if nt, ok := ref.(reference.NamedTagged); ok {
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
image = reference.FamiliarString(namedTagged)
}
}
}
}
return image
}
func (c *taskContext) Node() string {
return c.node
}
func (c *taskContext) DesiredState() string {
return command.PrettyPrint(c.task.DesiredState)
}
func (c *taskContext) CurrentState() string {
return fmt.Sprintf("%s %s ago",
command.PrettyPrint(c.task.Status.State),
strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))),
)
}
func (c *taskContext) Error() string {
// Trim and quote the error message.
taskErr := c.task.Status.Err
if c.trunc && len(taskErr) > maxErrLength {
taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1])
}
if len(taskErr) > 0 {
taskErr = fmt.Sprintf("\"%s\"", taskErr)
}
return taskErr
}
func (c *taskContext) Ports() string {
if len(c.task.Status.PortStatus.Ports) == 0 {
return ""
}
ports := []string{}
for _, pConfig := range c.task.Status.PortStatus.Ports {
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
pConfig.PublishedPort,
pConfig.TargetPort,
pConfig.Protocol,
))
}
return strings.Join(ports, ",")
}

View File

@ -0,0 +1,107 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
)
func TestTaskContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
{
Context{Format: NewTaskFormat("table", true)},
`taskID1
taskID2
`,
},
{
Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)},
`NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2
`,
},
{
Context{Format: NewTaskFormat("table {{.Name}}", true)},
`NAME
foobar_baz
foobar_bar
`,
},
{
Context{Format: NewTaskFormat("raw", true)},
`id: taskID1
id: taskID2
`,
},
{
Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)},
`foobar_baz foo1
foobar_bar foo2
`,
},
}
for _, testcase := range cases {
tasks := []swarm.Task{
{ID: "taskID1"},
{ID: "taskID2"},
}
names := map[string]string{
"taskID1": "foobar_baz",
"taskID2": "foobar_bar",
}
nodes := map[string]string{
"taskID1": "foo1",
"taskID2": "foo2",
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := TaskWrite(testcase.context, tasks, names, nodes)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestTaskContextWriteJSONField(t *testing.T) {
tasks := []swarm.Task{
{ID: "taskID1"},
{ID: "taskID2"},
}
names := map[string]string{
"taskID1": "foobar_baz",
"taskID2": "foobar_bar",
}
out := bytes.NewBufferString("")
err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{})
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, tasks[i].ID, s)
}
}

View File

@ -0,0 +1,131 @@
package formatter
import (
"fmt"
"strings"
"github.com/docker/docker/api/types"
units "github.com/docker/go-units"
)
const (
defaultVolumeQuietFormat = "{{.Name}}"
defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
volumeNameHeader = "VOLUME NAME"
mountpointHeader = "MOUNTPOINT"
linksHeader = "LINKS"
// Status header ?
)
// NewVolumeFormat returns a format for use with a volume Context
func NewVolumeFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultVolumeQuietFormat
}
return defaultVolumeTableFormat
case RawFormatKey:
if quiet {
return `name: {{.Name}}`
}
return `name: {{.Name}}\ndriver: {{.Driver}}\n`
}
return Format(source)
}
// VolumeWrite writes formatted volumes using the Context
func VolumeWrite(ctx Context, volumes []*types.Volume) error {
render := func(format func(subContext subContext) error) error {
for _, volume := range volumes {
if err := format(&volumeContext{v: *volume}); err != nil {
return err
}
}
return nil
}
return ctx.Write(newVolumeContext(), render)
}
type volumeHeaderContext map[string]string
func (c volumeHeaderContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
return h
}
type volumeContext struct {
HeaderContext
v types.Volume
}
func newVolumeContext() *volumeContext {
volumeCtx := volumeContext{}
volumeCtx.header = volumeHeaderContext{
"Name": volumeNameHeader,
"Driver": driverHeader,
"Scope": scopeHeader,
"Mountpoint": mountpointHeader,
"Labels": labelsHeader,
"Links": linksHeader,
"Size": sizeHeader,
}
return &volumeCtx
}
func (c *volumeContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *volumeContext) Name() string {
return c.v.Name
}
func (c *volumeContext) Driver() string {
return c.v.Driver
}
func (c *volumeContext) Scope() string {
return c.v.Scope
}
func (c *volumeContext) Mountpoint() string {
return c.v.Mountpoint
}
func (c *volumeContext) Labels() string {
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 {
if c.v.Labels == nil {
return ""
}
return c.v.Labels[name]
}
func (c *volumeContext) Links() string {
if c.v.UsageData == nil {
return "N/A"
}
return fmt.Sprintf("%d", c.v.UsageData.RefCount)
}
func (c *volumeContext) Size() string {
if c.v.UsageData == nil {
return "N/A"
}
return units.HumanSize(float64(c.v.UsageData.Size))
}

View File

@ -0,0 +1,183 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestVolumeContext(t *testing.T) {
volumeName := stringid.GenerateRandomID()
var ctx volumeContext
cases := []struct {
volumeCtx volumeContext
expValue string
call func() string
}{
{volumeContext{
v: types.Volume{Name: volumeName},
}, volumeName, ctx.Name},
{volumeContext{
v: types.Volume{Driver: "driver_name"},
}, "driver_name", ctx.Driver},
{volumeContext{
v: types.Volume{Scope: "local"},
}, "local", ctx.Scope},
{volumeContext{
v: types.Volume{Mountpoint: "mountpoint"},
}, "mountpoint", ctx.Mountpoint},
{volumeContext{
v: types.Volume{},
}, "", ctx.Labels},
{volumeContext{
v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
}, "label1=value1,label2=value2", 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)
}
}
}
func TestVolumeContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewVolumeFormat("table", false)},
`DRIVER VOLUME NAME
foo foobar_baz
bar foobar_bar
`,
},
{
Context{Format: NewVolumeFormat("table", true)},
`foobar_baz
foobar_bar
`,
},
{
Context{Format: NewVolumeFormat("table {{.Name}}", false)},
`VOLUME NAME
foobar_baz
foobar_bar
`,
},
{
Context{Format: NewVolumeFormat("table {{.Name}}", true)},
`VOLUME NAME
foobar_baz
foobar_bar
`,
},
// Raw Format
{
Context{Format: NewVolumeFormat("raw", false)},
`name: foobar_baz
driver: foo
name: foobar_bar
driver: bar
`,
},
{
Context{Format: NewVolumeFormat("raw", true)},
`name: foobar_baz
name: foobar_bar
`,
},
// Custom Format
{
Context{Format: NewVolumeFormat("{{.Name}}", false)},
`foobar_baz
foobar_bar
`,
},
}
for _, testcase := range cases {
volumes := []*types.Volume{
{Name: "foobar_baz", Driver: "foo"},
{Name: "foobar_bar", Driver: "bar"},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := VolumeWrite(testcase.context, volumes)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
func TestVolumeContextWriteJSON(t *testing.T) {
volumes := []*types.Volume{
{Driver: "foo", Name: "foobar_baz"},
{Driver: "bar", Name: "foobar_bar"},
}
expectedJSONs := []map[string]interface{}{
{"Driver": "foo", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A"},
{"Driver": "bar", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A"},
}
out := bytes.NewBufferString("")
err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedJSONs[i], m)
}
}
func TestVolumeContextWriteJSONField(t *testing.T) {
volumes := []*types.Volume{
{Driver: "foo", Name: "foobar_baz"},
{Driver: "bar", Name: "foobar_bar"},
}
out := bytes.NewBufferString("")
err := VolumeWrite(Context{Format: "{{json .Name}}", Output: out}, volumes)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, volumes[i].Name, s)
}
}

View File

@ -0,0 +1,28 @@
package idresolver
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
nodeInspectFunc func(string) (swarm.Node, []byte, error)
serviceInspectFunc func(string) (swarm.Service, []byte, error)
}
func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) {
if cli.nodeInspectFunc != nil {
return cli.nodeInspectFunc(nodeID)
}
return swarm.Node{}, []byte{}, nil
}
func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) {
if cli.serviceInspectFunc != nil {
return cli.serviceInspectFunc(serviceID)
}
return swarm.Service{}, []byte{}, nil
}

View File

@ -0,0 +1,70 @@
package idresolver
import (
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/pkg/errors"
)
// 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, types.ServiceInspectOptions{})
if err != nil {
return id, nil
}
return service.Spec.Annotations.Name, nil
default:
return "", errors.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
}

View File

@ -0,0 +1,144 @@
package idresolver
import (
"testing"
"github.com/docker/docker/api/types/swarm"
// Import builders to get the builder function as package function
. "github.com/docker/docker/cli/internal/test/builders"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestResolveError(t *testing.T) {
cli := &fakeClient{
nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) {
return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node")
},
}
idResolver := New(cli, false)
_, err := idResolver.Resolve(context.Background(), struct{}{}, "nodeID")
assert.EqualError(t, err, "unsupported type")
}
func TestResolveWithNoResolveOption(t *testing.T) {
resolved := false
cli := &fakeClient{
nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) {
resolved = true
return swarm.Node{}, []byte{}, nil
},
serviceInspectFunc: func(serviceID string) (swarm.Service, []byte, error) {
resolved = true
return swarm.Service{}, []byte{}, nil
},
}
idResolver := New(cli, true)
id, err := idResolver.Resolve(context.Background(), swarm.Node{}, "nodeID")
assert.NoError(t, err)
assert.Equal(t, "nodeID", id)
assert.False(t, resolved)
}
func TestResolveWithCache(t *testing.T) {
inspectCounter := 0
cli := &fakeClient{
nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) {
inspectCounter++
return *Node(NodeName("node-foo")), []byte{}, nil
},
}
idResolver := New(cli, false)
ctx := context.Background()
for i := 0; i < 2; i++ {
id, err := idResolver.Resolve(ctx, swarm.Node{}, "nodeID")
assert.NoError(t, err)
assert.Equal(t, "node-foo", id)
}
assert.Equal(t, 1, inspectCounter)
}
func TestResolveNode(t *testing.T) {
testCases := []struct {
nodeID string
nodeInspectFunc func(string) (swarm.Node, []byte, error)
expectedID string
}{
{
nodeID: "nodeID",
nodeInspectFunc: func(string) (swarm.Node, []byte, error) {
return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node")
},
expectedID: "nodeID",
},
{
nodeID: "nodeID",
nodeInspectFunc: func(string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-foo")), []byte{}, nil
},
expectedID: "node-foo",
},
{
nodeID: "nodeID",
nodeInspectFunc: func(string) (swarm.Node, []byte, error) {
return *Node(NodeName(""), Hostname("node-hostname")), []byte{}, nil
},
expectedID: "node-hostname",
},
}
ctx := context.Background()
for _, tc := range testCases {
cli := &fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
}
idResolver := New(cli, false)
id, err := idResolver.Resolve(ctx, swarm.Node{}, tc.nodeID)
assert.NoError(t, err)
assert.Equal(t, tc.expectedID, id)
}
}
func TestResolveService(t *testing.T) {
testCases := []struct {
serviceID string
serviceInspectFunc func(string) (swarm.Service, []byte, error)
expectedID string
}{
{
serviceID: "serviceID",
serviceInspectFunc: func(string) (swarm.Service, []byte, error) {
return swarm.Service{}, []byte{}, errors.Errorf("error inspecting service")
},
expectedID: "serviceID",
},
{
serviceID: "serviceID",
serviceInspectFunc: func(string) (swarm.Service, []byte, error) {
return *Service(ServiceName("service-foo")), []byte{}, nil
},
expectedID: "service-foo",
},
}
ctx := context.Background()
for _, tc := range testCases {
cli := &fakeClient{
serviceInspectFunc: tc.serviceInspectFunc,
}
idResolver := New(cli, false)
id, err := idResolver.Resolve(ctx, swarm.Service{}, tc.serviceID)
assert.NoError(t, err)
assert.Equal(t, tc.expectedID, id)
}
}

530
cli/command/image/build.go Normal file
View File

@ -0,0 +1,530 @@
package image
import (
"archive/tar"
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/builder/dockerignore"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/image/build"
"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/stringid"
"github.com/docker/docker/pkg/urlutil"
runconfigopts "github.com/docker/docker/runconfig/opts"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type buildOptions struct {
context string
dockerfileName string
tags opts.ListOpts
labels opts.ListOpts
buildArgs opts.ListOpts
extraHosts opts.ListOpts
ulimits *opts.UlimitOpt
memory opts.MemBytes
memorySwap opts.MemSwapBytes
shmSize opts.MemBytes
cpuShares int64
cpuPeriod int64
cpuQuota int64
cpuSetCpus string
cpuSetMems string
cgroupParent string
isolation string
quiet bool
noCache bool
rm bool
forceRm bool
pull bool
cacheFrom []string
compress bool
securityOpt []string
networkMode string
squash bool
target string
}
// 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(opts.ValidateEnv),
ulimits: opts.NewUlimitOpt(&ulimits),
labels: opts.NewListOpts(opts.ValidateEnv),
extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
}
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.VarP(&options.memory, "memory", "m", "Memory limit")
flags.Var(&options.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm")
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.Var(&options.labels, "label", "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")
flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip")
flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build")
flags.SetAnnotation("network", "version", []string{"1.25"})
flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
flags.StringVar(&options.target, "target", "", "Set the target build stage to build.")
command.AddTrustVerificationFlags(flags)
flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
flags.SetAnnotation("squash", "experimental", nil)
flags.SetAnnotation("squash", "version", []string{"1.25"})
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 suppress 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
dockerfileCtx io.ReadCloser
err error
contextDir string
tempDir string
relDockerfile string
progBuff io.Writer
buildBuff io.Writer
)
specifiedContext := options.context
progBuff = dockerCli.Out()
buildBuff = dockerCli.Out()
if options.quiet {
progBuff = bytes.NewBuffer(nil)
buildBuff = bytes.NewBuffer(nil)
}
if options.dockerfileName == "-" {
if specifiedContext == "-" {
return errors.New("invalid argument: can't use stdin for both build context and dockerfile")
}
dockerfileCtx = dockerCli.In()
}
switch {
case specifiedContext == "-":
buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
case isLocalDir(specifiedContext):
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
case urlutil.IsGitURL(specifiedContext):
tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)
case urlutil.IsURL(specifiedContext):
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
default:
return errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
}
if err != nil {
if options.quiet && urlutil.IsURL(specifiedContext) {
fmt.Fprintln(dockerCli.Err(), progBuff)
}
return errors.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 errors.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 := build.ValidateContextDirectory(contextDir, excludes); err != nil {
return errors.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
// if necessary, after it parses the Dockerfile. Ignore errors here, as
// they will have been caught by validateContextDirectory above.
// Excludes are used instead of includes to maintain the order of files
// in the archive.
if keep, _ := fileutils.Matches(".dockerignore", excludes); keep {
excludes = append(excludes, "!.dockerignore")
}
if keep, _ := fileutils.Matches(relDockerfile, excludes); keep && dockerfileCtx == nil {
excludes = append(excludes, "!"+relDockerfile)
}
compression := archive.Uncompressed
if options.compress {
compression = archive.Gzip
}
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
Compression: compression,
ExcludePatterns: excludes,
})
if err != nil {
return err
}
}
// replace Dockerfile if added dynamically
if dockerfileCtx != nil {
buildCtx, relDockerfile, err = addDockerfileToBuildContext(dockerfileCtx, buildCtx)
if err != nil {
return err
}
}
ctx := context.Background()
var resolvedTags []*resolvedTag
if command.IsTrusted() {
translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
return TrustedReference(ctx, dockerCli, ref, nil)
}
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls.
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &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")
authConfigs, _ := dockerCli.GetAllCredentials()
buildOptions := types.ImageBuildOptions{
Memory: options.memory.Value(),
MemorySwap: options.memorySwap.Value(),
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: options.shmSize.Value(),
Ulimits: options.ulimits.GetList(),
BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()),
AuthConfigs: authConfigs,
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
CacheFrom: options.cacheFrom,
SecurityOpt: options.securityOpt,
NetworkMode: options.networkMode,
Squash: options.squash,
ExtraHosts: options.extraHosts.GetAll(),
Target: options.target,
}
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
if err != nil {
if options.quiet {
fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
}
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}
}
return err
}
// 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.Out(), "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 := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil {
return err
}
}
}
return nil
}
func addDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) {
file, err := ioutil.ReadAll(dockerfileCtx)
dockerfileCtx.Close()
if err != nil {
return nil, "", err
}
now := time.Now()
hdrTmpl := &tar.Header{
Mode: 0600,
Uid: 0,
Gid: 0,
ModTime: now,
Typeflag: tar.TypeReg,
AccessTime: now,
ChangeTime: now,
}
randomName := ".dockerfile." + stringid.GenerateRandomID()[:20]
buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{
// Add the dockerfile with a random filename
randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
return hdrTmpl, file, nil
},
// Update .dockerignore to include the random filename
".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
if h == nil {
h = hdrTmpl
}
b := &bytes.Buffer{}
if content != nil {
if _, err := b.ReadFrom(content); err != nil {
return nil, nil, err
}
} else {
b.WriteString(".dockerignore")
}
b.WriteString("\n" + randomName + "\n")
return h, b.Bytes(), nil
},
})
return buildCtx, randomName, nil
}
func isLocalDir(c string) bool {
_, err := os.Stat(c)
return err == 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.ParseNormalizedNamed(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"
var ref reference.Named
ref, err = reference.ParseNormalizedNamed(matches[1])
if err != nil {
return nil, nil, err
}
ref = reference.TagNameOnly(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", reference.FamiliarString(trustedRef)))
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
}

View File

@ -0,0 +1,275 @@
package build
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/gitutils"
"github.com/docker/docker/pkg/httputils"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/pkg/errors"
)
const (
// DefaultDockerfileName is the Default filename with Docker commands, read by docker build
DefaultDockerfileName string = "Dockerfile"
)
// ValidateContextDirectory checks if all the contents of the directory
// can be read and returns an error if some files can't be read
// symlinks which point to non-existing files don't trigger an error
func ValidateContextDirectory(srcPath string, excludes []string) error {
contextRoot, err := getContextRoot(srcPath)
if err != nil {
return err
}
return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error {
if err != nil {
if os.IsPermission(err) {
return errors.Errorf("can't stat '%s'", filePath)
}
if os.IsNotExist(err) {
return nil
}
return err
}
// skip this directory/file if it's not in the path, it won't get added to the context
if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil {
return err
} else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil {
return err
} else if skip {
if f.IsDir() {
return filepath.SkipDir
}
return nil
}
// skip checking if symlinks point to non-existing files, such symlinks can be useful
// also skip named pipes, because they hanging on open
if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 {
return nil
}
if !f.IsDir() {
currentFile, err := os.Open(filePath)
if err != nil && os.IsPermission(err) {
return errors.Errorf("no permission to read from '%s'", filePath)
}
currentFile.Close()
}
return nil
})
}
// GetContextFromReader will read the contents of the given reader as either a
// Dockerfile or tar archive. Returns a tar archive used as a context and a
// path to the Dockerfile inside the tar.
func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
buf := bufio.NewReader(r)
magic, err := buf.Peek(archive.HeaderSize)
if err != nil && err != io.EOF {
return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err)
}
if archive.IsArchive(magic) {
return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
}
if dockerfileName == "-" {
return nil, "", errors.New("build context is not an archive")
}
// Input should be read as a Dockerfile.
tmpDir, err := ioutil.TempDir("", "docker-build-context-")
if err != nil {
return nil, "", errors.Errorf("unable to create temporary context directory: %v", err)
}
f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName))
if err != nil {
return nil, "", err
}
_, err = io.Copy(f, buf)
if err != nil {
f.Close()
return nil, "", err
}
if err := f.Close(); err != nil {
return nil, "", err
}
if err := r.Close(); err != nil {
return nil, "", err
}
tar, err := archive.Tar(tmpDir, archive.Uncompressed)
if err != nil {
return nil, "", err
}
return ioutils.NewReadCloserWrapper(tar, func() error {
err := tar.Close()
os.RemoveAll(tmpDir)
return err
}), DefaultDockerfileName, nil
}
// GetContextFromGitURL uses a Git URL as context for a `docker build`. The
// git repo is cloned into a temporary directory used as the context directory.
// Returns the absolute path to the temporary context directory, the relative
// path of the dockerfile in that context directory, and a non-nil error on
// success.
func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) {
if _, err := exec.LookPath("git"); err != nil {
return "", "", errors.Errorf("unable to find 'git': %v", err)
}
if absContextDir, err = gitutils.Clone(gitURL); err != nil {
return "", "", errors.Errorf("unable to 'git clone' to temporary context directory: %v", err)
}
return getDockerfileRelPath(absContextDir, dockerfileName)
}
// GetContextFromURL uses a remote URL as context for a `docker build`. The
// remote resource is downloaded as either a Dockerfile or a tar archive.
// Returns the tar archive used for the context and a path of the
// dockerfile inside the tar.
func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) {
response, err := httputils.Download(remoteURL)
if err != nil {
return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err)
}
progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true)
// Pass the response body through a progress reader.
progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL))
return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
}
// GetContextFromLocalDir uses the given local directory as context for a
// `docker build`. Returns the absolute path to the local context directory,
// the relative path of the dockerfile in that context directory, and a non-nil
// error on success.
func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) {
// When using a local context directory, when the Dockerfile is specified
// with the `-f/--file` option then it is considered relative to the
// current directory and not the context directory.
if dockerfileName != "" && dockerfileName != "-" {
if dockerfileName, err = filepath.Abs(dockerfileName); err != nil {
return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err)
}
}
return getDockerfileRelPath(localDir, dockerfileName)
}
// getDockerfileRelPath uses the given context directory for a `docker build`
// and returns the absolute path to the context directory, the relative path of
// the dockerfile in that context directory, and a non-nil error on success.
func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) {
if absContextDir, err = filepath.Abs(givenContextDir); err != nil {
return "", "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err)
}
// The context dir might be a symbolic link, so follow it to the actual
// target directory.
//
// FIXME. We use isUNC (always false on non-Windows platforms) to workaround
// an issue in golang. On Windows, EvalSymLinks does not work on UNC file
// paths (those starting with \\). This hack means that when using links
// on UNC paths, they will not be followed.
if !isUNC(absContextDir) {
absContextDir, err = filepath.EvalSymlinks(absContextDir)
if err != nil {
return "", "", errors.Errorf("unable to evaluate symlinks in context path: %v", err)
}
}
stat, err := os.Lstat(absContextDir)
if err != nil {
return "", "", errors.Errorf("unable to stat context directory %q: %v", absContextDir, err)
}
if !stat.IsDir() {
return "", "", errors.Errorf("context must be a directory: %s", absContextDir)
}
absDockerfile := givenDockerfile
if absDockerfile == "" {
// No -f/--file was specified so use the default relative to the
// context directory.
absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName)
// Just to be nice ;-) look for 'dockerfile' too but only
// use it if we found it, otherwise ignore this check
if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) {
altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName))
if _, err = os.Lstat(altPath); err == nil {
absDockerfile = altPath
}
}
} else if absDockerfile == "-" {
absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName)
}
// If not already an absolute path, the Dockerfile path should be joined to
// the base directory.
if !filepath.IsAbs(absDockerfile) {
absDockerfile = filepath.Join(absContextDir, absDockerfile)
}
// Evaluate symlinks in the path to the Dockerfile too.
//
// FIXME. We use isUNC (always false on non-Windows platforms) to workaround
// an issue in golang. On Windows, EvalSymLinks does not work on UNC file
// paths (those starting with \\). This hack means that when using links
// on UNC paths, they will not be followed.
if givenDockerfile != "-" {
if !isUNC(absDockerfile) {
absDockerfile, err = filepath.EvalSymlinks(absDockerfile)
if err != nil {
return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err)
}
}
if _, err := os.Lstat(absDockerfile); err != nil {
if os.IsNotExist(err) {
return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile)
}
return "", "", errors.Errorf("unable to stat Dockerfile: %v", err)
}
}
if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil {
return "", "", errors.Errorf("unable to get relative Dockerfile path: %v", err)
}
if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
return "", "", errors.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir)
}
return absContextDir, relDockerfile, nil
}
// isUNC returns true if the path is UNC (one starting \\). It always returns
// false on Linux.
func isUNC(path string) bool {
return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`)
}

View File

@ -0,0 +1,383 @@
package build
import (
"archive/tar"
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/docker/docker/pkg/archive"
)
const dockerfileContents = "FROM busybox"
var prepareEmpty = func(t *testing.T) (string, func()) {
return "", func() {}
}
var prepareNoFiles = func(t *testing.T) (string, func()) {
return createTestTempDir(t, "", "builder-context-test")
}
var prepareOneFile = func(t *testing.T) (string, func()) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777)
return contextDir, cleanup
}
func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) (string, func()), excludes []string) {
contextDir, cleanup := prepare(t)
defer cleanup()
err := ValidateContextDirectory(contextDir, excludes)
if err != nil {
t.Fatalf("Error should be nil, got: %s", err)
}
}
func TestGetContextFromLocalDirNoDockerfile(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
if err == nil {
t.Fatalf("Error should not be nil")
}
if absContextDir != "" {
t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
}
if relDockerfile != "" {
t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
}
}
func TestGetContextFromLocalDirNotExistingDir(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
fakePath := filepath.Join(contextDir, "fake")
absContextDir, relDockerfile, err := GetContextFromLocalDir(fakePath, "")
if err == nil {
t.Fatalf("Error should not be nil")
}
if absContextDir != "" {
t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
}
if relDockerfile != "" {
t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
}
}
func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
fakePath := filepath.Join(contextDir, "fake")
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, fakePath)
if err == nil {
t.Fatalf("Error should not be nil")
}
if absContextDir != "" {
t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
}
if relDockerfile != "" {
t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
}
}
func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) {
contextDir, dirCleanup := createTestTempDir(t, "", "builder-context-test")
defer dirCleanup()
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777)
chdirCleanup := chdir(t, contextDir)
defer chdirCleanup()
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
if err != nil {
t.Fatalf("Error when getting context from local dir: %s", err)
}
if absContextDir != contextDir {
t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir)
}
if relDockerfile != DefaultDockerfileName {
t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile)
}
}
func TestGetContextFromLocalDirWithDockerfile(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777)
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
if err != nil {
t.Fatalf("Error when getting context from local dir: %s", err)
}
if absContextDir != contextDir {
t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir)
}
if relDockerfile != DefaultDockerfileName {
t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile)
}
}
func TestGetContextFromLocalDirLocalFile(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777)
testFilename := createTestTempFile(t, contextDir, "tmpTest", "test", 0777)
absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "")
if err == nil {
t.Fatalf("Error should not be nil")
}
if absContextDir != "" {
t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir)
}
if relDockerfile != "" {
t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile)
}
}
func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
chdirCleanup := chdir(t, contextDir)
defer chdirCleanup()
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777)
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName)
if err != nil {
t.Fatalf("Error when getting context from local dir: %s", err)
}
if absContextDir != contextDir {
t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir)
}
if relDockerfile != DefaultDockerfileName {
t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile)
}
}
func TestGetContextFromReaderString(t *testing.T) {
tarArchive, relDockerfile, err := GetContextFromReader(ioutil.NopCloser(strings.NewReader(dockerfileContents)), "")
if err != nil {
t.Fatalf("Error when executing GetContextFromReader: %s", err)
}
tarReader := tar.NewReader(tarArchive)
_, err = tarReader.Next()
if err != nil {
t.Fatalf("Error when reading tar archive: %s", err)
}
buff := new(bytes.Buffer)
buff.ReadFrom(tarReader)
contents := buff.String()
_, err = tarReader.Next()
if err != io.EOF {
t.Fatalf("Tar stream too long: %s", err)
}
if err = tarArchive.Close(); err != nil {
t.Fatalf("Error when closing tar stream: %s", err)
}
if dockerfileContents != contents {
t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents)
}
if relDockerfile != DefaultDockerfileName {
t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile)
}
}
func TestGetContextFromReaderTar(t *testing.T) {
contextDir, cleanup := createTestTempDir(t, "", "builder-context-test")
defer cleanup()
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777)
tarStream, err := archive.Tar(contextDir, archive.Uncompressed)
if err != nil {
t.Fatalf("Error when creating tar: %s", err)
}
tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName)
if err != nil {
t.Fatalf("Error when executing GetContextFromReader: %s", err)
}
tarReader := tar.NewReader(tarArchive)
header, err := tarReader.Next()
if err != nil {
t.Fatalf("Error when reading tar archive: %s", err)
}
if header.Name != DefaultDockerfileName {
t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name)
}
buff := new(bytes.Buffer)
buff.ReadFrom(tarReader)
contents := buff.String()
_, err = tarReader.Next()
if err != io.EOF {
t.Fatalf("Tar stream too long: %s", err)
}
if err = tarArchive.Close(); err != nil {
t.Fatalf("Error when closing tar stream: %s", err)
}
if dockerfileContents != contents {
t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents)
}
if relDockerfile != DefaultDockerfileName {
t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile)
}
}
func TestValidateContextDirectoryEmptyContext(t *testing.T) {
// This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81.
// The test will ultimately end up calling filepath.Abs(""). On Windows,
// golang will error. On Linux, golang will return /. Due to there being
// drive letters on Windows, this is probably the correct behaviour for
// Windows.
if runtime.GOOS == "windows" {
t.Skip("Invalid test on Windows")
}
testValidateContextDirectory(t, prepareEmpty, []string{})
}
func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) {
testValidateContextDirectory(t, prepareNoFiles, []string{})
}
func TestValidateContextDirectoryWithOneFile(t *testing.T) {
testValidateContextDirectory(t, prepareOneFile, []string{})
}
func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) {
testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName})
}
// createTestTempDir creates a temporary directory for testing.
// It returns the created path and a cleanup function which is meant to be used as deferred call.
// When an error occurs, it terminates the test.
func createTestTempDir(t *testing.T, dir, prefix string) (string, func()) {
path, err := ioutil.TempDir(dir, prefix)
if err != nil {
t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err)
}
return path, func() {
err = os.RemoveAll(path)
if err != nil {
t.Fatalf("Error when removing directory %s: %s", path, err)
}
}
}
// createTestTempSubdir creates a temporary directory for testing.
// It returns the created path but doesn't provide a cleanup function,
// so createTestTempSubdir should be used only for creating temporary subdirectories
// whose parent directories are properly cleaned up.
// When an error occurs, it terminates the test.
func createTestTempSubdir(t *testing.T, dir, prefix string) string {
path, err := ioutil.TempDir(dir, prefix)
if err != nil {
t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err)
}
return path
}
// createTestTempFile creates a temporary file within dir with specific contents and permissions.
// When an error occurs, it terminates the test
func createTestTempFile(t *testing.T, dir, filename, contents string, perm os.FileMode) string {
filePath := filepath.Join(dir, filename)
err := ioutil.WriteFile(filePath, []byte(contents), perm)
if err != nil {
t.Fatalf("Error when creating %s file: %s", filename, err)
}
return filePath
}
// chdir changes current working directory to dir.
// It returns a function which changes working directory back to the previous one.
// This function is meant to be executed as a deferred call.
// When an error occurs, it terminates the test.
func chdir(t *testing.T, dir string) func() {
workingDirectory, err := os.Getwd()
if err != nil {
t.Fatalf("Error when retrieving working directory: %s", err)
}
err = os.Chdir(dir)
if err != nil {
t.Fatalf("Error when changing directory to %s: %s", dir, err)
}
return func() {
err = os.Chdir(workingDirectory)
if err != nil {
t.Fatalf("Error when changing back to working directory (%s): %s", workingDirectory, err)
}
}
}

View File

@ -0,0 +1,11 @@
// +build !windows
package build
import (
"path/filepath"
)
func getContextRoot(srcPath string) (string, error) {
return filepath.Join(srcPath, "."), nil
}

View File

@ -0,0 +1,17 @@
// +build windows
package build
import (
"path/filepath"
"github.com/docker/docker/pkg/longpath"
)
func getContextRoot(srcPath string) (string, error) {
cr, err := filepath.Abs(srcPath)
if err != nil {
return "", err
}
return longpath.AddPrefix(cr), nil
}

33
cli/command/image/cmd.go Normal file
View File

@ -0,0 +1,33 @@
package image
import (
"github.com/spf13/cobra"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
)
// NewImageCommand returns a cobra command for `image` subcommands
func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "image",
Short: "Manage images",
Args: cli.NoArgs,
RunE: dockerCli.ShowHelp,
}
cmd.AddCommand(
NewBuildCommand(dockerCli),
NewHistoryCommand(dockerCli),
NewImportCommand(dockerCli),
NewLoadCommand(dockerCli),
NewPullCommand(dockerCli),
NewPushCommand(dockerCli),
NewSaveCommand(dockerCli),
NewTagCommand(dockerCli),
newListCommand(dockerCli),
newRemoveCommand(dockerCli),
newInspectCommand(dockerCli),
NewPruneCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,64 @@
package image
import (
"golang.org/x/net/context"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/formatter"
"github.com/spf13/cobra"
)
type historyOptions struct {
image string
human bool
quiet bool
noTrunc bool
format string
}
// 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")
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
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
}
format := opts.format
if len(format) == 0 {
format = formatter.TableFormatKey
}
historyCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human),
Trunc: !opts.noTrunc,
}
return formatter.HistoryWrite(historyCtx, opts.human, history)
}

View File

@ -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)
}

View File

@ -0,0 +1,44 @@
package image
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
refs []string
}
// newInspectCommand creates a new cobra.Command for `docker image inspect`
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
Use: "inspect [OPTIONS] IMAGE [IMAGE...]",
Short: "Display detailed information on one or more images",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.refs = 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 *command.DockerCli, opts inspectOptions) error {
client := dockerCli.Client()
ctx := context.Background()
getRefFunc := func(ref string) (interface{}, []byte, error) {
return client.ImageInspectWithRaw(ctx, ref)
}
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)
}

96
cli/command/image/list.go Normal file
View File

@ -0,0 +1,96 @@
package image
import (
"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/cli/command/formatter"
"github.com/docker/docker/opts"
"github.com/spf13/cobra"
)
type imagesOptions struct {
matchName string
quiet bool
all bool
noTrunc bool
showDigests bool
format string
filter opts.FilterOpt
}
// NewImagesCommand creates a new `docker images` command
func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := imagesOptions{filter: opts.NewFilterOpt()}
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.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := *NewImagesCommand(dockerCli)
cmd.Aliases = []string{"images", "list"}
cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]"
return &cmd
}
func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
ctx := context.Background()
filters := opts.filter.Value()
if opts.matchName != "" {
filters.Add("reference", opts.matchName)
}
options := types.ImageListOptions{
All: opts.all,
Filters: filters,
}
images, err := dockerCli.Client().ImageList(ctx, options)
if err != nil {
return err
}
format := opts.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
format = dockerCli.ConfigFile().ImagesFormat
} else {
format = formatter.TableFormatKey
}
}
imageCtx := formatter.ImageContext{
Context: formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests),
Trunc: !opts.noTrunc,
},
Digest: opts.showDigests,
}
return formatter.ImageWrite(imageCtx, images)
}

77
cli/command/image/load.go Normal file
View File

@ -0,0 +1,77 @@
package image
import (
"io"
"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/pkg/system"
"github.com/pkg/errors"
"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 != "" {
// We use system.OpenSequential to use sequential file access on Windows, avoiding
// depleting the standby list un-necessarily. On Linux, this equates to a regular os.Open.
file, err := system.OpenSequential(opts.input)
if err != nil {
return err
}
defer file.Close()
input = file
}
// To avoid getting stuck, verify that a tar file is given either in
// the input flag or through stdin and if not display an error message and exit.
if opts.input == "" && dockerCli.In().IsTerminal() {
return errors.Errorf("requested load from stdin, but stdin is empty")
}
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
}

View File

@ -0,0 +1,95 @@
package image
import (
"fmt"
"golang.org/x/net/context"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/opts"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
type pruneOptions struct {
force bool
all bool
filter opts.FilterOpt
}
// NewPruneCommand returns a new cobra prune command for images
func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := pruneOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "prune [OPTIONS]",
Short: "Remove unused images",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(dockerCli, opts)
if err != nil {
return err
}
if output != "" {
fmt.Fprintln(dockerCli.Out(), output)
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil
},
Tags: map[string]string{"version": "1.25"},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation")
flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones")
flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')")
return cmd
}
const (
allImageWarning = `WARNING! This will remove all images without at least one container associated to them.
Are you sure you want to continue?`
danglingWarning = `WARNING! This will remove all dangling images.
Are you sure you want to continue?`
)
func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := opts.filter.Value()
pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all))
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
warning := danglingWarning
if opts.all {
warning = allImageWarning
}
if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return
}
report, err := dockerCli.Client().ImagesPrune(context.Background(), pruneFilters)
if err != nil {
return
}
if len(report.ImagesDeleted) > 0 {
output = "Deleted Images:\n"
for _, st := range report.ImagesDeleted {
if st.Untagged != "" {
output += fmt.Sprintln("untagged:", st.Untagged)
} else {
output += fmt.Sprintln("deleted:", st.Deleted)
}
}
spaceReclaimed = report.SpaceReclaimed
}
return
}
// RunPrune calls the Image Prune API
// This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
}

85
cli/command/image/pull.go Normal file
View File

@ -0,0 +1,85 @@
package image
import (
"fmt"
"strings"
"github.com/docker/distribution/reference"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
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.AddTrustVerificationFlags(flags)
return cmd
}
func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
distributionRef, err := reference.ParseNormalizedNamed(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.TagNameOnly(distributionRef)
if tagged, ok := distributionRef.(reference.Tagged); ok {
fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag())
}
}
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
if err != nil {
return err
}
ctx := context.Background()
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull")
// Check if reference has a digest
_, isCanonical := distributionRef.(reference.Canonical)
if command.IsTrusted() && !isCanonical {
err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege)
} else {
err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all)
}
if err != nil {
if strings.Contains(err.Error(), "when fetching 'plugin'") {
return errors.New(err.Error() + " - Use `docker plugin install`")
}
return err
}
return nil
}

61
cli/command/image/push.go Normal file
View File

@ -0,0 +1,61 @@
package image
import (
"golang.org/x/net/context"
"github.com/docker/distribution/reference"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/pkg/jsonmessage"
"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.AddTrustSigningFlags(flags)
return cmd
}
func runPush(dockerCli *command.DockerCli, remote string) error {
ref, err := reference.ParseNormalizedNamed(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 := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
if command.IsTrusted() {
return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
}
responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege)
if err != nil {
return err
}
defer responseBody.Close()
return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
}

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