Move api/client -> cli/command
Using
gomvpkg
-from github.com/docker/docker/api/client
-to github.com/docker/docker/cli/command
-vcs_mv_cmd 'git mv {{.Src}} {{.Dst}}'
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
130
command/container/attach.go
Normal file
130
command/container/attach.go
Normal file
@ -0,0 +1,130 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type attachOptions struct {
|
||||
noStdin bool
|
||||
proxy bool
|
||||
detachKeys string
|
||||
|
||||
container string
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts attachOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "attach [OPTIONS] CONTAINER",
|
||||
Short: "Attach to a running container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runAttach(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN")
|
||||
flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process")
|
||||
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
c, err := client.ContainerInspect(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c.State.Running {
|
||||
return fmt.Errorf("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
|
||||
if c.State.Paused {
|
||||
return fmt.Errorf("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
|
||||
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||
}
|
||||
|
||||
options := types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: !opts.noStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||
}
|
||||
|
||||
var in io.ReadCloser
|
||||
if options.Stdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
|
||||
if opts.proxy && !c.Config.Tty {
|
||||
sigc := ForwardAllSignals(ctx, dockerCli, opts.container)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
|
||||
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
|
||||
// ContainerAttach returns an ErrPersistEOF (connection closed)
|
||||
// means server met an error and put it in Hijacked connection
|
||||
// keep the error and read detailed error message from hijacked connection later
|
||||
return errAttach
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
if c.Config.Tty && dockerCli.Out().IsTerminal() {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTtyTo(ctx, client, opts.container, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil {
|
||||
logrus.Debugf("Error monitoring TTY size: %s", err)
|
||||
}
|
||||
}
|
||||
if err := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
|
||||
_, status, err := getExitCode(dockerCli, ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
76
command/container/commit.go
Normal file
76
command/container/commit.go
Normal file
@ -0,0 +1,76 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
dockeropts "github.com/docker/docker/opts"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type commitOptions struct {
|
||||
container string
|
||||
reference string
|
||||
|
||||
pause bool
|
||||
comment string
|
||||
author string
|
||||
changes dockeropts.ListOpts
|
||||
}
|
||||
|
||||
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||
func NewCommitCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts commitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]",
|
||||
Short: "Create a new image from a container's changes",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
if len(args) > 1 {
|
||||
opts.reference = args[1]
|
||||
}
|
||||
return runCommit(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.BoolVarP(&opts.pause, "pause", "p", true, "Pause container during commit")
|
||||
flags.StringVarP(&opts.comment, "message", "m", "", "Commit message")
|
||||
flags.StringVarP(&opts.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith <hannibal@a-team.com>\")")
|
||||
|
||||
opts.changes = dockeropts.NewListOpts(nil)
|
||||
flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCommit(dockerCli *command.DockerCli, opts *commitOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
name := opts.container
|
||||
reference := opts.reference
|
||||
|
||||
options := types.ContainerCommitOptions{
|
||||
Reference: reference,
|
||||
Comment: opts.comment,
|
||||
Author: opts.author,
|
||||
Changes: opts.changes.GetAll(),
|
||||
Pause: opts.pause,
|
||||
}
|
||||
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, name, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
return nil
|
||||
}
|
||||
303
command/container/cp.go
Normal file
303
command/container/cp.go
Normal file
@ -0,0 +1,303 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type copyOptions struct {
|
||||
source string
|
||||
destination string
|
||||
followLink bool
|
||||
}
|
||||
|
||||
type copyDirection int
|
||||
|
||||
const (
|
||||
fromContainer copyDirection = (1 << iota)
|
||||
toContainer
|
||||
acrossContainers = fromContainer | toContainer
|
||||
)
|
||||
|
||||
type cpConfig struct {
|
||||
followLink bool
|
||||
}
|
||||
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts copyOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
|
||||
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
|
||||
Short: "Copy files/folders between a container and the local filesystem",
|
||||
Long: strings.Join([]string{
|
||||
"Copy files/folders between a container and the local filesystem\n",
|
||||
"\nUse '-' as the source to read a tar archive from stdin\n",
|
||||
"and extract it to a directory destination in a container.\n",
|
||||
"Use '-' as the destination to stream a tar archive of a\n",
|
||||
"container source to stdout.",
|
||||
}, ""),
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "" {
|
||||
return fmt.Errorf("source can not be empty")
|
||||
}
|
||||
if args[1] == "" {
|
||||
return fmt.Errorf("destination can not be empty")
|
||||
}
|
||||
opts.source = args[0]
|
||||
opts.destination = args[1]
|
||||
return runCopy(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCopy(dockerCli *command.DockerCli, opts copyOptions) error {
|
||||
srcContainer, srcPath := splitCpArg(opts.source)
|
||||
dstContainer, dstPath := splitCpArg(opts.destination)
|
||||
|
||||
var direction copyDirection
|
||||
if srcContainer != "" {
|
||||
direction |= fromContainer
|
||||
}
|
||||
if dstContainer != "" {
|
||||
direction |= toContainer
|
||||
}
|
||||
|
||||
cpParam := &cpConfig{
|
||||
followLink: opts.followLink,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch direction {
|
||||
case fromContainer:
|
||||
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam)
|
||||
case toContainer:
|
||||
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam)
|
||||
case acrossContainers:
|
||||
// Copying between containers isn't supported.
|
||||
return fmt.Errorf("copying between containers is not supported")
|
||||
default:
|
||||
// User didn't specify any container.
|
||||
return fmt.Errorf("must specify at least one container source")
|
||||
}
|
||||
}
|
||||
|
||||
func statContainerPath(ctx context.Context, dockerCli *command.DockerCli, containerName, path string) (types.ContainerPathStat, error) {
|
||||
return dockerCli.Client().ContainerStatPath(ctx, containerName, path)
|
||||
}
|
||||
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
||||
}
|
||||
|
||||
func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
|
||||
if dstPath != "-" {
|
||||
// Get an absolute destination path.
|
||||
dstPath, err = resolveLocalPath(dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
if cpParam.followLink {
|
||||
srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath)
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if !system.IsAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
linkTarget = filepath.Join(srcParent, linkTarget)
|
||||
}
|
||||
|
||||
linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
|
||||
srcPath = linkTarget
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
// Send the response to STDOUT.
|
||||
_, err = io.Copy(os.Stdout, content)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare source copy info.
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
RebaseName: rebaseName,
|
||||
}
|
||||
|
||||
preArchive := content
|
||||
if len(srcInfo.RebaseName) != 0 {
|
||||
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
|
||||
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
|
||||
}
|
||||
// See comments in the implementation of `archive.CopyTo` for exactly what
|
||||
// goes into deciding how and whether the source archive needs to be
|
||||
// altered for the correct copy behavior.
|
||||
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||
}
|
||||
|
||||
func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) {
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath)
|
||||
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dstStat.LinkTarget
|
||||
if !system.IsAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
}
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versa) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
if err == nil {
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||
}
|
||||
|
||||
var (
|
||||
content io.Reader
|
||||
resolvedDstPath string
|
||||
)
|
||||
|
||||
if srcPath == "-" {
|
||||
// Use STDIN.
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
if !dstInfo.IsDir {
|
||||
return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcArchive, err := archive.TarResource(srcInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcArchive.Close()
|
||||
|
||||
// With the stat info about the local source as well as the
|
||||
// destination, we have enough information to know whether we need to
|
||||
// alter the archive that we upload so that when the server extracts
|
||||
// it to the specified directory in the container we get the desired
|
||||
// copy behavior.
|
||||
|
||||
// See comments in the implementation of `archive.PrepareArchiveCopy`
|
||||
// for exactly what goes into deciding how and whether the source
|
||||
// archive needs to be altered for the correct copy behavior when it is
|
||||
// extracted. This function also infers from the source and destination
|
||||
// info which directory to extract to, which may be the parent of the
|
||||
// destination that the user specified.
|
||||
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer preparedArchive.Close()
|
||||
|
||||
resolvedDstPath = dstDir
|
||||
content = preparedArchive
|
||||
}
|
||||
|
||||
options := types.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: false,
|
||||
}
|
||||
|
||||
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
|
||||
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
|
||||
// absolute path:
|
||||
// `/path/to/file:name.txt` or `./file:name.txt`
|
||||
//
|
||||
// This is apparently how `scp` handles this as well:
|
||||
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
|
||||
//
|
||||
// We can't simply check for a filepath separator because container names may
|
||||
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
|
||||
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
|
||||
// client, a `:` could be part of an absolute Windows path, in which case it
|
||||
// is immediately proceeded by a backslash.
|
||||
func splitCpArg(arg string) (container, path string) {
|
||||
if system.IsAbs(arg) {
|
||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
parts := strings.SplitN(arg, ":", 2)
|
||||
|
||||
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
|
||||
// Either there's no `:` in the arg
|
||||
// OR it's an explicit local relative path like `./file:name.txt`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
217
command/container/create.go
Normal file
217
command/container/create.go
Normal file
@ -0,0 +1,217 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
// FIXME migrate to docker/distribution/reference
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts createOptions
|
||||
var copts *runconfigopts.ContainerOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||
Short: "Create a new container",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
copts.Image = args[0]
|
||||
if len(args) > 1 {
|
||||
copts.Args = args[1:]
|
||||
}
|
||||
return runCreate(dockerCli, cmd.Flags(), &opts, copts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddTrustedFlags(flags, true)
|
||||
copts = runconfigopts.AddFlags(flags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *runconfigopts.ContainerOptions) error {
|
||||
config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts)
|
||||
if err != nil {
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
response, err := createContainer(context.Background(), dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error {
|
||||
ref, err := reference.ParseNamed(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
|
||||
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := types.ImageCreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
return jsonmessage.DisplayJSONMessagesStream(
|
||||
responseBody,
|
||||
out,
|
||||
dockerCli.Out().FD(),
|
||||
dockerCli.Out().IsTerminal(),
|
||||
nil)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
path string
|
||||
file *os.File
|
||||
written bool
|
||||
}
|
||||
|
||||
func (cid *cidFile) Close() error {
|
||||
cid.file.Close()
|
||||
|
||||
if !cid.written {
|
||||
if err := os.Remove(cid.path); err != nil {
|
||||
return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cid *cidFile) Write(id string) error {
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return fmt.Errorf("Failed to write the container ID to the file: %s", err)
|
||||
}
|
||||
cid.written = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCIDFile(path string) (*cidFile, error) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create the container ID file: %s", err)
|
||||
}
|
||||
|
||||
return &cidFile{path: path, file: f}, nil
|
||||
}
|
||||
|
||||
func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) {
|
||||
stderr := dockerCli.Err()
|
||||
|
||||
var containerIDFile *cidFile
|
||||
if cidfile != "" {
|
||||
var err error
|
||||
if containerIDFile, err = newCIDFile(cidfile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer containerIDFile.Close()
|
||||
}
|
||||
|
||||
var trustedRef reference.Canonical
|
||||
_, ref, err := reference.ParseIDOrReference(config.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ref != nil {
|
||||
ref = reference.WithDefaultTag(ref)
|
||||
|
||||
if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
|
||||
var err error
|
||||
trustedRef, err = dockerCli.TrustedReference(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Image = trustedRef.String()
|
||||
}
|
||||
}
|
||||
|
||||
//create the container
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name)
|
||||
|
||||
//if image not found try to pull it
|
||||
if err != nil {
|
||||
if apiclient.IsErrImageNotFound(err) && ref != nil {
|
||||
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", ref.String())
|
||||
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
if err := dockerCli.TagTrusted(ctx, trustedRef, ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Retry
|
||||
var retryErr error
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name)
|
||||
if retryErr != nil {
|
||||
return nil, retryErr
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
|
||||
}
|
||||
if containerIDFile != nil {
|
||||
if err = containerIDFile.Write(response.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
58
command/container/diff.go
Normal file
58
command/container/diff.go
Normal file
@ -0,0 +1,58 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type diffOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts diffOptions
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
Short: "Inspect changes on a container's filesystem",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runDiff(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error {
|
||||
if opts.container == "" {
|
||||
return fmt.Errorf("Container name cannot be empty")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, change := range changes {
|
||||
var kind string
|
||||
switch change.Kind {
|
||||
case archive.ChangeModify:
|
||||
kind = "C"
|
||||
case archive.ChangeAdd:
|
||||
kind = "A"
|
||||
case archive.ChangeDelete:
|
||||
kind = "D"
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "%s %s\n", kind, change.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
192
command/container/exec.go
Normal file
192
command/container/exec.go
Normal file
@ -0,0 +1,192 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type execOptions struct {
|
||||
detachKeys string
|
||||
interactive bool
|
||||
tty bool
|
||||
detach bool
|
||||
user string
|
||||
privileged bool
|
||||
}
|
||||
|
||||
// NewExecCommand creats a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts execOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
|
||||
Short: "Run a command in a running container",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
container := args[0]
|
||||
execCmd := args[1:]
|
||||
return runExec(dockerCli, &opts, container, execCmd)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
|
||||
flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
|
||||
flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
|
||||
flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||
flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||
flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error {
|
||||
execConfig, err := parseExec(opts, container, execCmd)
|
||||
// just in case the ParseExec does not exit
|
||||
if container == "" || err != nil {
|
||||
return cli.StatusError{StatusCode: 1}
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||
}
|
||||
|
||||
// Send client escape keys
|
||||
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
|
||||
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execID := response.ID
|
||||
if execID == "" {
|
||||
fmt.Fprintf(dockerCli.Out(), "exec ID empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
//Temp struct for execStart so that we don't need to transfer all the execConfig
|
||||
if !execConfig.Detach {
|
||||
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Detach: execConfig.Detach,
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
|
||||
if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil {
|
||||
return err
|
||||
}
|
||||
// For now don't print this - wait for when we support exec wait()
|
||||
// fmt.Fprintf(dockerCli.Out(), "%s\n", execID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
in io.ReadCloser
|
||||
errCh chan error
|
||||
)
|
||||
|
||||
if execConfig.AttachStdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
if execConfig.AttachStdout {
|
||||
out = dockerCli.Out()
|
||||
}
|
||||
if execConfig.AttachStderr {
|
||||
if execConfig.Tty {
|
||||
stderr = dockerCli.Out()
|
||||
} else {
|
||||
stderr = dockerCli.Err()
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Close()
|
||||
errCh = promise.Go(func() error {
|
||||
return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp)
|
||||
})
|
||||
|
||||
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
logrus.Debugf("Error hijack: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var status int
|
||||
if _, status, err = getExecExitCode(ctx, client, execID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getExecExitCode perform an inspect on the exec command. It returns
|
||||
// the running state and the exit code.
|
||||
func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) {
|
||||
resp, err := client.ContainerExecInspect(ctx, execID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if err != apiclient.ErrConnectionFailed {
|
||||
return false, -1, err
|
||||
}
|
||||
return false, -1, nil
|
||||
}
|
||||
|
||||
return resp.Running, resp.ExitCode, nil
|
||||
}
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(opts *execOptions, container string, execCmd []string) (*types.ExecConfig, error) {
|
||||
execConfig := &types.ExecConfig{
|
||||
User: opts.user,
|
||||
Privileged: opts.privileged,
|
||||
Tty: opts.tty,
|
||||
Cmd: execCmd,
|
||||
Detach: opts.detach,
|
||||
// container is not used here
|
||||
}
|
||||
|
||||
// If -d is not set, attach to everything by default
|
||||
if !opts.detach {
|
||||
execConfig.AttachStdout = true
|
||||
execConfig.AttachStderr = true
|
||||
if opts.interactive {
|
||||
execConfig.AttachStdin = true
|
||||
}
|
||||
}
|
||||
|
||||
return execConfig, nil
|
||||
}
|
||||
117
command/container/exec_test.go
Normal file
117
command/container/exec_test.go
Normal file
@ -0,0 +1,117 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
type arguments struct {
|
||||
options execOptions
|
||||
container string
|
||||
execCmd []string
|
||||
}
|
||||
|
||||
func TestParseExec(t *testing.T) {
|
||||
valids := map[*arguments]*types.ExecConfig{
|
||||
&arguments{
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
&arguments{
|
||||
execCmd: []string{"command1", "command2"},
|
||||
}: {
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
&arguments{
|
||||
options: execOptions{
|
||||
interactive: true,
|
||||
tty: true,
|
||||
user: "uid",
|
||||
},
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
&arguments{
|
||||
options: execOptions{
|
||||
detach: true,
|
||||
},
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
&arguments{
|
||||
options: execOptions{
|
||||
tty: true,
|
||||
interactive: true,
|
||||
detach: true,
|
||||
},
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
Detach: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
}
|
||||
|
||||
for valid, expectedExecConfig := range valids {
|
||||
execConfig, err := parseExec(&valid.options, valid.container, valid.execCmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !compareExecConfig(expectedExecConfig, execConfig) {
|
||||
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
|
||||
if config1.AttachStderr != config2.AttachStderr {
|
||||
return false
|
||||
}
|
||||
if config1.AttachStdin != config2.AttachStdin {
|
||||
return false
|
||||
}
|
||||
if config1.AttachStdout != config2.AttachStdout {
|
||||
return false
|
||||
}
|
||||
if config1.Detach != config2.Detach {
|
||||
return false
|
||||
}
|
||||
if config1.Privileged != config2.Privileged {
|
||||
return false
|
||||
}
|
||||
if config1.Tty != config2.Tty {
|
||||
return false
|
||||
}
|
||||
if config1.User != config2.User {
|
||||
return false
|
||||
}
|
||||
if len(config1.Cmd) != len(config2.Cmd) {
|
||||
return false
|
||||
}
|
||||
for index, value := range config1.Cmd {
|
||||
if value != config2.Cmd[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
59
command/container/export.go
Normal file
59
command/container/export.go
Normal file
@ -0,0 +1,59 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type exportOptions struct {
|
||||
container string
|
||||
output string
|
||||
}
|
||||
|
||||
// NewExportCommand creates a new `docker export` command
|
||||
func NewExportCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts exportOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "export [OPTIONS] CONTAINER",
|
||||
Short: "Export a container's filesystem as a tar archive",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runExport(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(dockerCli *command.DockerCli, opts exportOptions) error {
|
||||
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||
return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
|
||||
}
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
responseBody, err := clnt.ContainerExport(context.Background(), opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if opts.output == "" {
|
||||
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
return command.CopyToFile(opts.output, responseBody)
|
||||
}
|
||||
121
command/container/hijack.go
Normal file
121
command/container/hijack.go
Normal file
@ -0,0 +1,121 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type streams interface {
|
||||
In() *command.InStream
|
||||
Out() *command.OutStream
|
||||
}
|
||||
|
||||
// holdHijackedConnection handles copying input to and output from streams to the
|
||||
// connection
|
||||
func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
|
||||
var (
|
||||
err error
|
||||
restoreOnce sync.Once
|
||||
)
|
||||
if inputStream != nil && tty {
|
||||
if err := setRawTerminal(streams); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
restoreOnce.Do(func() {
|
||||
restoreTerminal(streams, inputStream)
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
receiveStdout := make(chan error, 1)
|
||||
if outputStream != nil || errorStream != nil {
|
||||
go func() {
|
||||
// When TTY is ON, use regular copy
|
||||
if tty && outputStream != nil {
|
||||
_, err = io.Copy(outputStream, resp.Reader)
|
||||
// we should restore the terminal as soon as possible once connection end
|
||||
// so any following print messages will be in normal type.
|
||||
if inputStream != nil {
|
||||
restoreOnce.Do(func() {
|
||||
restoreTerminal(streams, inputStream)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
|
||||
}
|
||||
|
||||
logrus.Debug("[hijack] End of stdout")
|
||||
receiveStdout <- err
|
||||
}()
|
||||
}
|
||||
|
||||
stdinDone := make(chan struct{})
|
||||
go func() {
|
||||
if inputStream != nil {
|
||||
io.Copy(resp.Conn, inputStream)
|
||||
// we should restore the terminal as soon as possible once connection end
|
||||
// so any following print messages will be in normal type.
|
||||
if tty {
|
||||
restoreOnce.Do(func() {
|
||||
restoreTerminal(streams, inputStream)
|
||||
})
|
||||
}
|
||||
logrus.Debug("[hijack] End of stdin")
|
||||
}
|
||||
|
||||
if err := resp.CloseWrite(); err != nil {
|
||||
logrus.Debugf("Couldn't send EOF: %s", err)
|
||||
}
|
||||
close(stdinDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-receiveStdout:
|
||||
if err != nil {
|
||||
logrus.Debugf("Error receiveStdout: %s", err)
|
||||
return err
|
||||
}
|
||||
case <-stdinDone:
|
||||
if outputStream != nil || errorStream != nil {
|
||||
select {
|
||||
case err := <-receiveStdout:
|
||||
if err != nil {
|
||||
logrus.Debugf("Error receiveStdout: %s", err)
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRawTerminal(streams streams) error {
|
||||
if err := streams.In().SetRawTerminal(); err != nil {
|
||||
return err
|
||||
}
|
||||
return streams.Out().SetRawTerminal()
|
||||
}
|
||||
|
||||
func restoreTerminal(streams streams, in io.Closer) error {
|
||||
streams.In().RestoreTerminal()
|
||||
streams.Out().RestoreTerminal()
|
||||
// WARNING: DO NOT REMOVE THE OS CHECK !!!
|
||||
// For some reason this Close call blocks on darwin..
|
||||
// As the client exists right after, simply discard the close
|
||||
// until we find a better solution.
|
||||
if in != nil && runtime.GOOS != "darwin" {
|
||||
return in.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
command/container/kill.go
Normal file
53
command/container/kill.go
Normal file
@ -0,0 +1,53 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type killOptions struct {
|
||||
signal string
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewKillCommand creates a new cobra.Command for `docker kill`
|
||||
func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts killOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "kill [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Kill one or more running containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runKill(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "KILL", "Signal to send to the container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKill(dockerCli *command.DockerCli, opts *killOptions) error {
|
||||
var errs []string
|
||||
ctx := context.Background()
|
||||
for _, name := range opts.containers {
|
||||
if err := dockerCli.Client().ContainerKill(ctx, name, opts.signal); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
87
command/container/logs.go
Normal file
87
command/container/logs.go
Normal file
@ -0,0 +1,87 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var validDrivers = map[string]bool{
|
||||
"json-file": true,
|
||||
"journald": true,
|
||||
}
|
||||
|
||||
type logsOptions struct {
|
||||
follow bool
|
||||
since string
|
||||
timestamps bool
|
||||
details bool
|
||||
tail string
|
||||
|
||||
container string
|
||||
}
|
||||
|
||||
// NewLogsCommand creates a new cobra.Command for `docker logs`
|
||||
func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts logsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs [OPTIONS] CONTAINER",
|
||||
Short: "Fetch the logs of a container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runLogs(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
|
||||
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp")
|
||||
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
|
||||
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
|
||||
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !validDrivers[c.HostConfig.LogConfig.Type] {
|
||||
return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type)
|
||||
}
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Since: opts.since,
|
||||
Timestamps: opts.timestamps,
|
||||
Follow: opts.follow,
|
||||
Tail: opts.tail,
|
||||
Details: opts.details,
|
||||
}
|
||||
responseBody, err := dockerCli.Client().ContainerLogs(ctx, opts.container, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if c.Config.Tty {
|
||||
_, err = io.Copy(dockerCli.Out(), responseBody)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody)
|
||||
}
|
||||
return err
|
||||
}
|
||||
48
command/container/pause.go
Normal file
48
command/container/pause.go
Normal file
@ -0,0 +1,48 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type pauseOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewPauseCommand creates a new cobra.Command for `docker pause`
|
||||
func NewPauseCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts pauseOptions
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "pause CONTAINER [CONTAINER...]",
|
||||
Short: "Pause all processes within one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runPause(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, container := range opts.containers {
|
||||
if err := dockerCli.Client().ContainerPause(ctx, container); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
78
command/container/port.go
Normal file
78
command/container/port.go
Normal file
@ -0,0 +1,78 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type portOptions struct {
|
||||
container string
|
||||
|
||||
port string
|
||||
}
|
||||
|
||||
// NewPortCommand creates a new cobra.Command for `docker port`
|
||||
func NewPortCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts portOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "port CONTAINER [PRIVATE_PORT[/PROTO]]",
|
||||
Short: "List port mappings or a specific mapping for the container",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
if len(args) > 1 {
|
||||
opts.port = args[1]
|
||||
}
|
||||
return runPort(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPort(dockerCli *command.DockerCli, opts *portOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.port != "" {
|
||||
port := opts.port
|
||||
proto := "tcp"
|
||||
parts := strings.SplitN(port, "/", 2)
|
||||
|
||||
if len(parts) == 2 && len(parts[1]) != 0 {
|
||||
port = parts[0]
|
||||
proto = parts[1]
|
||||
}
|
||||
natPort := port + "/" + proto
|
||||
newP, err := nat.NewPort(proto, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil {
|
||||
for _, frontend := range frontends {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s:%s\n", frontend.HostIP, frontend.HostPort)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Error: No public port '%s' published for %s", natPort, opts.container)
|
||||
}
|
||||
|
||||
for from, frontends := range c.NetworkSettings.Ports {
|
||||
for _, frontend := range frontends {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
142
command/container/ps.go
Normal file
142
command/container/ps.go
Normal file
@ -0,0 +1,142 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/docker/utils/templates"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
quiet bool
|
||||
size bool
|
||||
all bool
|
||||
noTrunc bool
|
||||
nLatest bool
|
||||
last int
|
||||
format string
|
||||
filter []string
|
||||
}
|
||||
|
||||
// NewPsCommand creates a new cobra.Command for `docker ps`
|
||||
func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts psOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ps [OPTIONS]",
|
||||
Short: "List containers",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runPs(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display numeric IDs")
|
||||
flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes")
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||
flags.BoolVarP(&opts.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
|
||||
flags.IntVarP(&opts.last, "last", "n", -1, "Show n last created containers (includes all states)")
|
||||
flags.StringVarP(&opts.format, "format", "", "", "Pretty-print containers using a Go template")
|
||||
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type preProcessor struct {
|
||||
types.Container
|
||||
opts *types.ContainerListOptions
|
||||
}
|
||||
|
||||
// Size sets the size option when called by a template execution.
|
||||
func (p *preProcessor) Size() bool {
|
||||
p.opts.Size = true
|
||||
return true
|
||||
}
|
||||
|
||||
func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) {
|
||||
|
||||
options := &types.ContainerListOptions{
|
||||
All: opts.all,
|
||||
Limit: opts.last,
|
||||
Size: opts.size,
|
||||
Filter: filters.NewArgs(),
|
||||
}
|
||||
|
||||
if opts.nLatest && opts.last == -1 {
|
||||
options.Limit = 1
|
||||
}
|
||||
|
||||
for _, f := range opts.filter {
|
||||
var err error
|
||||
options.Filter, err = filters.ParseFlag(f, options.Filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Currently only used with Size, so we can determine if the user
|
||||
// put {{.Size}} in their format.
|
||||
pre := &preProcessor{opts: options}
|
||||
tmpl, err := templates.Parse(opts.format)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// This shouldn't error out but swallowing the error makes it harder
|
||||
// to track down if preProcessor issues come up. Ref #24696
|
||||
if err := tmpl.Execute(ioutil.Discard, pre); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func runPs(dockerCli *command.DockerCli, opts *psOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
listOptions, err := buildContainerListOptions(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containers, err := dockerCli.Client().ContainerList(ctx, *listOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f := opts.format
|
||||
if len(f) == 0 {
|
||||
if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
|
||||
f = dockerCli.ConfigFile().PsFormat
|
||||
} else {
|
||||
f = "table"
|
||||
}
|
||||
}
|
||||
|
||||
psCtx := formatter.ContainerContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: f,
|
||||
Quiet: opts.quiet,
|
||||
Trunc: !opts.noTrunc,
|
||||
},
|
||||
Size: listOptions.Size,
|
||||
Containers: containers,
|
||||
}
|
||||
|
||||
psCtx.Write()
|
||||
|
||||
return nil
|
||||
}
|
||||
74
command/container/ps_test.go
Normal file
74
command/container/ps_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package container
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildContainerListOptions(t *testing.T) {
|
||||
|
||||
contexts := []struct {
|
||||
psOpts *psOptions
|
||||
expectedAll bool
|
||||
expectedSize bool
|
||||
expectedLimit int
|
||||
expectedFilters map[string]string
|
||||
}{
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
all: true,
|
||||
size: true,
|
||||
last: 5,
|
||||
filter: []string{"foo=bar", "baz=foo"},
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
all: true,
|
||||
size: true,
|
||||
last: -1,
|
||||
nLatest: true,
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 1,
|
||||
expectedFilters: make(map[string]string),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range contexts {
|
||||
options, err := buildContainerListOptions(c.psOpts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.expectedAll != options.All {
|
||||
t.Fatalf("Expected All to be %t but got %t", c.expectedAll, options.All)
|
||||
}
|
||||
|
||||
if c.expectedSize != options.Size {
|
||||
t.Fatalf("Expected Size to be %t but got %t", c.expectedSize, options.Size)
|
||||
}
|
||||
|
||||
if c.expectedLimit != options.Limit {
|
||||
t.Fatalf("Expected Limit to be %d but got %d", c.expectedLimit, options.Limit)
|
||||
}
|
||||
|
||||
f := options.Filter
|
||||
|
||||
if f.Len() != len(c.expectedFilters) {
|
||||
t.Fatalf("Expected %d filters but got %d", len(c.expectedFilters), f.Len())
|
||||
}
|
||||
|
||||
for k, v := range c.expectedFilters {
|
||||
f := options.Filter
|
||||
if !f.ExactMatch(k, v) {
|
||||
t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
command/container/rename.go
Normal file
51
command/container/rename.go
Normal file
@ -0,0 +1,51 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type renameOptions struct {
|
||||
oldName string
|
||||
newName string
|
||||
}
|
||||
|
||||
// NewRenameCommand creates a new cobra.Command for `docker rename`
|
||||
func NewRenameCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts renameOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rename CONTAINER NEW_NAME",
|
||||
Short: "Rename a container",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.oldName = args[0]
|
||||
opts.newName = args[1]
|
||||
return runRename(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRename(dockerCli *command.DockerCli, opts *renameOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
oldName := strings.TrimSpace(opts.oldName)
|
||||
newName := strings.TrimSpace(opts.newName)
|
||||
|
||||
if oldName == "" || newName == "" {
|
||||
return fmt.Errorf("Error: Neither old nor new names may be empty")
|
||||
}
|
||||
|
||||
if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "%s\n", err)
|
||||
return fmt.Errorf("Error: failed to rename container named %s", oldName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
command/container/restart.go
Normal file
55
command/container/restart.go
Normal file
@ -0,0 +1,55 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type restartOptions struct {
|
||||
nSeconds int
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewRestartCommand creates a new cobra.Command for `docker restart`
|
||||
func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts restartOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "restart [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Restart one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runRestart(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.IntVarP(&opts.nSeconds, "time", "t", 10, "Seconds to wait for stop before killing the container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error {
|
||||
ctx := context.Background()
|
||||
var errs []string
|
||||
for _, name := range opts.containers {
|
||||
timeout := time.Duration(opts.nSeconds) * time.Second
|
||||
if err := dockerCli.Client().ContainerRestart(ctx, name, &timeout); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
76
command/container/rm.go
Normal file
76
command/container/rm.go
Normal file
@ -0,0 +1,76 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type rmOptions struct {
|
||||
rmVolumes bool
|
||||
rmLink bool
|
||||
force bool
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewRmCommand creates a new cobra.Command for `docker rm`
|
||||
func NewRmCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts rmOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Remove one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runRm(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.rmVolumes, "volumes", "v", false, "Remove the volumes associated with the container")
|
||||
flags.BoolVarP(&opts.rmLink, "link", "l", false, "Remove the specified link")
|
||||
flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of a running container (uses SIGKILL)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRm(dockerCli *command.DockerCli, opts *rmOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, name := range opts.containers {
|
||||
if name == "" {
|
||||
return fmt.Errorf("Container name cannot be empty")
|
||||
}
|
||||
name = strings.Trim(name, "/")
|
||||
|
||||
if err := removeContainer(dockerCli, ctx, name, opts.rmVolumes, opts.rmLink, opts.force); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", name)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeContainer(dockerCli *command.DockerCli, ctx context.Context, container string, removeVolumes, removeLinks, force bool) error {
|
||||
options := types.ContainerRemoveOptions{
|
||||
RemoveVolumes: removeVolumes,
|
||||
RemoveLinks: removeLinks,
|
||||
Force: force,
|
||||
}
|
||||
if err := dockerCli.Client().ContainerRemove(ctx, container, options); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
288
command/container/run.go
Normal file
288
command/container/run.go
Normal file
@ -0,0 +1,288 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
opttypes "github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/libnetwork/resolvconf/dns"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type runOptions struct {
|
||||
detach bool
|
||||
sigProxy bool
|
||||
name string
|
||||
detachKeys string
|
||||
}
|
||||
|
||||
// NewRunCommand create a new `docker run` command
|
||||
func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts runOptions
|
||||
var copts *runconfigopts.ContainerOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||
Short: "Run a command in a new container",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
copts.Image = args[0]
|
||||
if len(args) > 1 {
|
||||
copts.Args = args[1:]
|
||||
}
|
||||
return runRun(dockerCli, cmd.Flags(), &opts, copts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
// These are flags not stored in Config/HostConfig
|
||||
flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID")
|
||||
flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process")
|
||||
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
|
||||
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddTrustedFlags(flags, true)
|
||||
copts = runconfigopts.AddFlags(flags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *runconfigopts.ContainerOptions) error {
|
||||
stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In()
|
||||
client := dockerCli.Client()
|
||||
// TODO: pass this as an argument
|
||||
cmdPath := "run"
|
||||
|
||||
var (
|
||||
flAttach *opttypes.ListOpts
|
||||
ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d")
|
||||
ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm")
|
||||
)
|
||||
|
||||
config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts)
|
||||
|
||||
// just in case the Parse does not exit
|
||||
if err != nil {
|
||||
reportError(stderr, cmdPath, err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
|
||||
if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||
return ErrConflictRestartPolicyAndAutoRemove
|
||||
}
|
||||
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
|
||||
fmt.Fprintf(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n")
|
||||
}
|
||||
|
||||
if len(hostConfig.DNS) > 0 {
|
||||
// check the DNS settings passed via --dns against
|
||||
// localhost regexp to warn if they are trying to
|
||||
// set a DNS to a localhost address
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if dns.IsLocalhost(dnsIP) {
|
||||
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.ArgsEscaped = false
|
||||
|
||||
if !opts.detach {
|
||||
if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if fl := flags.Lookup("attach"); fl != nil {
|
||||
flAttach = fl.Value.(*opttypes.ListOpts)
|
||||
if flAttach.Len() != 0 {
|
||||
return ErrConflictAttachDetach
|
||||
}
|
||||
}
|
||||
|
||||
config.AttachStdin = false
|
||||
config.AttachStdout = false
|
||||
config.AttachStderr = false
|
||||
config.StdinOnce = false
|
||||
}
|
||||
|
||||
// Disable sigProxy when in TTY mode
|
||||
if config.Tty {
|
||||
opts.sigProxy = false
|
||||
}
|
||||
|
||||
// Telling the Windows daemon the initial size of the tty during start makes
|
||||
// a far better user experience rather than relying on subsequent resizes
|
||||
// to cause things to catch up.
|
||||
if runtime.GOOS == "windows" {
|
||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
|
||||
}
|
||||
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
|
||||
createResponse, err := createContainer(ctx, dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name)
|
||||
if err != nil {
|
||||
reportError(stderr, cmdPath, err.Error(), true)
|
||||
return runStartContainerErr(err)
|
||||
}
|
||||
if opts.sigProxy {
|
||||
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
var (
|
||||
waitDisplayID chan struct{}
|
||||
errCh chan error
|
||||
)
|
||||
if !config.AttachStdout && !config.AttachStderr {
|
||||
// Make this asynchronous to allow the client to write to stdin before having to read the ID
|
||||
waitDisplayID = make(chan struct{})
|
||||
go func() {
|
||||
defer close(waitDisplayID)
|
||||
fmt.Fprintf(stdout, "%s\n", createResponse.ID)
|
||||
}()
|
||||
}
|
||||
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
|
||||
if attach {
|
||||
var (
|
||||
out, cerr io.Writer
|
||||
in io.ReadCloser
|
||||
)
|
||||
if config.AttachStdin {
|
||||
in = stdin
|
||||
}
|
||||
if config.AttachStdout {
|
||||
out = stdout
|
||||
}
|
||||
if config.AttachStderr {
|
||||
if config.Tty {
|
||||
cerr = stdout
|
||||
} else {
|
||||
cerr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||
}
|
||||
|
||||
options := types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: config.AttachStdin,
|
||||
Stdout: config.AttachStdout,
|
||||
Stderr: config.AttachStderr,
|
||||
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||
}
|
||||
|
||||
resp, errAttach := client.ContainerAttach(ctx, createResponse.ID, options)
|
||||
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
|
||||
// ContainerAttach returns an ErrPersistEOF (connection closed)
|
||||
// means server met an error and put it in Hijacked connection
|
||||
// keep the error and read detailed error message from hijacked connection later
|
||||
return errAttach
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
errCh = promise.Go(func() error {
|
||||
errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp)
|
||||
if errHijack == nil {
|
||||
return errAttach
|
||||
}
|
||||
return errHijack
|
||||
})
|
||||
}
|
||||
|
||||
statusChan, err := waitExitOrRemoved(dockerCli, context.Background(), createResponse.ID, hostConfig.AutoRemove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error waiting container's exit code: %v", err)
|
||||
}
|
||||
|
||||
//start the container
|
||||
if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
|
||||
// If we have holdHijackedConnection, we should notify
|
||||
// holdHijackedConnection we are going to exit and wait
|
||||
// to avoid the terminal are not restored.
|
||||
if attach {
|
||||
cancelFun()
|
||||
<-errCh
|
||||
}
|
||||
|
||||
reportError(stderr, cmdPath, err.Error(), false)
|
||||
if hostConfig.AutoRemove {
|
||||
// wait container to be removed
|
||||
<-statusChan
|
||||
}
|
||||
return runStartContainerErr(err)
|
||||
}
|
||||
|
||||
if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil {
|
||||
fmt.Fprintf(stderr, "Error monitoring TTY size: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if errCh != nil {
|
||||
if err := <-errCh; err != nil {
|
||||
logrus.Debugf("Error hijack: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Detached mode: wait for the id to be displayed and return.
|
||||
if !config.AttachStdout && !config.AttachStderr {
|
||||
// Detached mode
|
||||
<-waitDisplayID
|
||||
return nil
|
||||
}
|
||||
|
||||
status := <-statusChan
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportError is a utility method that prints a user-friendly message
|
||||
// containing the error that occurred during parsing and a suggestion to get help
|
||||
func reportError(stderr io.Writer, name string, str string, withHelp bool) {
|
||||
if withHelp {
|
||||
str += ".\nSee '" + os.Args[0] + " " + name + " --help'"
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s: %s.\n", os.Args[0], str)
|
||||
}
|
||||
|
||||
// if container start fails with 'not found'/'no such' error, return 127
|
||||
// if container start fails with 'permission denied' error, return 126
|
||||
// return 125 for generic docker daemon failures
|
||||
func runStartContainerErr(err error) error {
|
||||
trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
|
||||
statusError := cli.StatusError{StatusCode: 125}
|
||||
if strings.Contains(trimmedErr, "executable file not found") ||
|
||||
strings.Contains(trimmedErr, "no such file or directory") ||
|
||||
strings.Contains(trimmedErr, "system cannot find the file specified") {
|
||||
statusError = cli.StatusError{StatusCode: 127}
|
||||
} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) {
|
||||
statusError = cli.StatusError{StatusCode: 126}
|
||||
}
|
||||
|
||||
return statusError
|
||||
}
|
||||
161
command/container/start.go
Normal file
161
command/container/start.go
Normal file
@ -0,0 +1,161 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type startOptions struct {
|
||||
attach bool
|
||||
openStdin bool
|
||||
detachKeys string
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewStartCommand creates a new cobra.Command for `docker start`
|
||||
func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts startOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "start [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Start one or more stopped containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runStart(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals")
|
||||
flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN")
|
||||
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
|
||||
if opts.attach || opts.openStdin {
|
||||
// We're going to attach to a container.
|
||||
// 1. Ensure we only have one container.
|
||||
if len(opts.containers) > 1 {
|
||||
return fmt.Errorf("You cannot start and attach multiple containers at once.")
|
||||
}
|
||||
|
||||
// 2. Attach to the container.
|
||||
container := opts.containers[0]
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We always use c.ID instead of container to maintain consistency during `docker start`
|
||||
if !c.Config.Tty {
|
||||
sigc := ForwardAllSignals(ctx, dockerCli, c.ID)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||
}
|
||||
|
||||
options := types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: opts.openStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||
}
|
||||
|
||||
var in io.ReadCloser
|
||||
|
||||
if options.Stdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
|
||||
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options)
|
||||
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
|
||||
// ContainerAttach return an ErrPersistEOF (connection closed)
|
||||
// means server met an error and already put it in Hijacked connection,
|
||||
// we would keep the error and read the detailed error message from hijacked connection
|
||||
return errAttach
|
||||
}
|
||||
defer resp.Close()
|
||||
cErr := promise.Go(func() error {
|
||||
errHijack := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp)
|
||||
if errHijack == nil {
|
||||
return errAttach
|
||||
}
|
||||
return errHijack
|
||||
})
|
||||
|
||||
// 3. We should open a channel for receiving status code of the container
|
||||
// no matter it's detached, removed on daemon side(--rm) or exit normally.
|
||||
statusChan, statusErr := waitExitOrRemoved(dockerCli, context.Background(), c.ID, c.HostConfig.AutoRemove)
|
||||
|
||||
// 4. Start the container.
|
||||
if err := dockerCli.Client().ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
|
||||
cancelFun()
|
||||
<-cErr
|
||||
if c.HostConfig.AutoRemove && statusErr == nil {
|
||||
// wait container to be removed
|
||||
<-statusChan
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Wait for attachment to break.
|
||||
if c.Config.Tty && dockerCli.Out().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
|
||||
}
|
||||
}
|
||||
if attchErr := <-cErr; attchErr != nil {
|
||||
return attchErr
|
||||
}
|
||||
|
||||
if statusErr != nil {
|
||||
return fmt.Errorf("can't get container's exit code: %v", statusErr)
|
||||
}
|
||||
|
||||
if status := <-statusChan; status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
} else {
|
||||
// We're not going to attach to anything.
|
||||
// Start as many containers as we want.
|
||||
return startContainersWithoutAttachments(dockerCli, ctx, opts.containers)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startContainersWithoutAttachments(dockerCli *command.DockerCli, ctx context.Context, containers []string) error {
|
||||
var failedContainers []string
|
||||
for _, container := range containers {
|
||||
if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "%s\n", err)
|
||||
failedContainers = append(failedContainers, container)
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedContainers) > 0 {
|
||||
return fmt.Errorf("Error: failed to start containers: %v", strings.Join(failedContainers, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
233
command/container/stats.go
Normal file
233
command/container/stats.go
Normal file
@ -0,0 +1,233 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/system"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type statsOptions struct {
|
||||
all bool
|
||||
noStream bool
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewStatsCommand creates a new cobra.Command for `docker stats`
|
||||
func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts statsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "stats [OPTIONS] [CONTAINER...]",
|
||||
Short: "Display a live stream of container(s) resource usage statistics",
|
||||
Args: cli.RequiresMinArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runStats(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
|
||||
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runStats displays a live stream of resource usage statistics for one or more containers.
|
||||
// This shows real-time information on CPU usage, memory usage, and network I/O.
|
||||
func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
showAll := len(opts.containers) == 0
|
||||
closeChan := make(chan error)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// monitorContainerEvents watches for container creation and removal (only
|
||||
// used when calling `docker stats` without arguments).
|
||||
monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) {
|
||||
f := filters.NewArgs()
|
||||
f.Add("type", "container")
|
||||
options := types.EventsOptions{
|
||||
Filters: f,
|
||||
}
|
||||
resBody, err := dockerCli.Client().Events(ctx, options)
|
||||
// Whether we successfully subscribed to events or not, we can now
|
||||
// unblock the main goroutine.
|
||||
close(started)
|
||||
if err != nil {
|
||||
closeChan <- err
|
||||
return
|
||||
}
|
||||
defer resBody.Close()
|
||||
|
||||
system.DecodeEvents(resBody, func(event events.Message, err error) error {
|
||||
if err != nil {
|
||||
closeChan <- err
|
||||
return nil
|
||||
}
|
||||
c <- event
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// waitFirst is a WaitGroup to wait first stat data's reach for each container
|
||||
waitFirst := &sync.WaitGroup{}
|
||||
|
||||
cStats := stats{}
|
||||
// getContainerList simulates creation event for all previously existing
|
||||
// containers (only used when calling `docker stats` without arguments).
|
||||
getContainerList := func() {
|
||||
options := types.ContainerListOptions{
|
||||
All: opts.all,
|
||||
}
|
||||
cs, err := dockerCli.Client().ContainerList(ctx, options)
|
||||
if err != nil {
|
||||
closeChan <- err
|
||||
}
|
||||
for _, container := range cs {
|
||||
s := &containerStats{Name: container.ID[:12]}
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if showAll {
|
||||
// If no names were specified, start a long running goroutine which
|
||||
// monitors container events. We make sure we're subscribed before
|
||||
// retrieving the list of running containers to avoid a race where we
|
||||
// would "miss" a creation.
|
||||
started := make(chan struct{})
|
||||
eh := system.InitEventHandler()
|
||||
eh.Handle("create", func(e events.Message) {
|
||||
if opts.all {
|
||||
s := &containerStats{Name: e.ID[:12]}
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
eh.Handle("start", func(e events.Message) {
|
||||
s := &containerStats{Name: e.ID[:12]}
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
}
|
||||
})
|
||||
|
||||
eh.Handle("die", func(e events.Message) {
|
||||
if !opts.all {
|
||||
cStats.remove(e.ID[:12])
|
||||
}
|
||||
})
|
||||
|
||||
eventChan := make(chan events.Message)
|
||||
go eh.Watch(eventChan)
|
||||
go monitorContainerEvents(started, eventChan)
|
||||
defer close(eventChan)
|
||||
<-started
|
||||
|
||||
// Start a short-lived goroutine to retrieve the initial list of
|
||||
// containers.
|
||||
getContainerList()
|
||||
} else {
|
||||
// Artificially send creation events for the containers we were asked to
|
||||
// monitor (same code path than we use when monitoring all containers).
|
||||
for _, name := range opts.containers {
|
||||
s := &containerStats{Name: name}
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
}
|
||||
}
|
||||
|
||||
// We don't expect any asynchronous errors: closeChan can be closed.
|
||||
close(closeChan)
|
||||
|
||||
// Do a quick pause to detect any error with the provided list of
|
||||
// container names.
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
var errs []string
|
||||
cStats.mu.Lock()
|
||||
for _, c := range cStats.cs {
|
||||
c.mu.Lock()
|
||||
if c.err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
cStats.mu.Unlock()
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// before print to screen, make sure each container get at least one valid stat data
|
||||
waitFirst.Wait()
|
||||
|
||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||
printHeader := func() {
|
||||
if !opts.noStream {
|
||||
fmt.Fprint(dockerCli.Out(), "\033[2J")
|
||||
fmt.Fprint(dockerCli.Out(), "\033[H")
|
||||
}
|
||||
io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n")
|
||||
}
|
||||
|
||||
for range time.Tick(500 * time.Millisecond) {
|
||||
printHeader()
|
||||
toRemove := []string{}
|
||||
cStats.mu.Lock()
|
||||
for _, s := range cStats.cs {
|
||||
if err := s.Display(w); err != nil && !opts.noStream {
|
||||
logrus.Debugf("stats: got error for %s: %v", s.Name, err)
|
||||
if err == io.EOF {
|
||||
toRemove = append(toRemove, s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
cStats.mu.Unlock()
|
||||
for _, name := range toRemove {
|
||||
cStats.remove(name)
|
||||
}
|
||||
if len(cStats.cs) == 0 && !showAll {
|
||||
return nil
|
||||
}
|
||||
w.Flush()
|
||||
if opts.noStream {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case err, ok := <-closeChan:
|
||||
if ok {
|
||||
if err != nil {
|
||||
// this is suppressing "unexpected EOF" in the cli when the
|
||||
// daemon restarts so it shutdowns cleanly
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
// just skip
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
238
command/container/stats_helpers.go
Normal file
238
command/container/stats_helpers.go
Normal file
@ -0,0 +1,238 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-units"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type containerStats struct {
|
||||
Name string
|
||||
CPUPercentage float64
|
||||
Memory float64
|
||||
MemoryLimit float64
|
||||
MemoryPercentage float64
|
||||
NetworkRx float64
|
||||
NetworkTx float64
|
||||
BlockRead float64
|
||||
BlockWrite float64
|
||||
PidsCurrent uint64
|
||||
mu sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
mu sync.Mutex
|
||||
cs []*containerStats
|
||||
}
|
||||
|
||||
func (s *stats) add(cs *containerStats) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, exists := s.isKnownContainer(cs.Name); !exists {
|
||||
s.cs = append(s.cs, cs)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *stats) remove(id string) {
|
||||
s.mu.Lock()
|
||||
if i, exists := s.isKnownContainer(id); exists {
|
||||
s.cs = append(s.cs[:i], s.cs[i+1:]...)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *stats) isKnownContainer(cid string) (int, bool) {
|
||||
for i, c := range s.cs {
|
||||
if c.Name == cid {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
|
||||
logrus.Debugf("collecting stats for %s", s.Name)
|
||||
var (
|
||||
getFirst bool
|
||||
previousCPU uint64
|
||||
previousSystem uint64
|
||||
u = make(chan error, 1)
|
||||
)
|
||||
|
||||
defer func() {
|
||||
// if error happens and we get nothing of stats, release wait group whatever
|
||||
if !getFirst {
|
||||
getFirst = true
|
||||
waitFirst.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
responseBody, err := cli.ContainerStats(ctx, s.Name, streamStats)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
s.err = err
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
dec := json.NewDecoder(responseBody)
|
||||
go func() {
|
||||
for {
|
||||
var v *types.StatsJSON
|
||||
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
dec = json.NewDecoder(io.MultiReader(dec.Buffered(), responseBody))
|
||||
u <- err
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
var memPercent = 0.0
|
||||
var cpuPercent = 0.0
|
||||
|
||||
// MemoryStats.Limit will never be 0 unless the container is not running and we haven't
|
||||
// got any data from cgroup
|
||||
if v.MemoryStats.Limit != 0 {
|
||||
memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0
|
||||
}
|
||||
|
||||
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
|
||||
previousSystem = v.PreCPUStats.SystemUsage
|
||||
cpuPercent = calculateCPUPercent(previousCPU, previousSystem, v)
|
||||
blkRead, blkWrite := calculateBlockIO(v.BlkioStats)
|
||||
s.mu.Lock()
|
||||
s.CPUPercentage = cpuPercent
|
||||
s.Memory = float64(v.MemoryStats.Usage)
|
||||
s.MemoryLimit = float64(v.MemoryStats.Limit)
|
||||
s.MemoryPercentage = memPercent
|
||||
s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks)
|
||||
s.BlockRead = float64(blkRead)
|
||||
s.BlockWrite = float64(blkWrite)
|
||||
s.PidsCurrent = v.PidsStats.Current
|
||||
s.mu.Unlock()
|
||||
u <- nil
|
||||
if !streamStats {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-time.After(2 * time.Second):
|
||||
// zero out the values if we have not received an update within
|
||||
// the specified duration.
|
||||
s.mu.Lock()
|
||||
s.CPUPercentage = 0
|
||||
s.Memory = 0
|
||||
s.MemoryPercentage = 0
|
||||
s.MemoryLimit = 0
|
||||
s.NetworkRx = 0
|
||||
s.NetworkTx = 0
|
||||
s.BlockRead = 0
|
||||
s.BlockWrite = 0
|
||||
s.PidsCurrent = 0
|
||||
s.err = errors.New("timeout waiting for stats")
|
||||
s.mu.Unlock()
|
||||
// if this is the first stat you get, release WaitGroup
|
||||
if !getFirst {
|
||||
getFirst = true
|
||||
waitFirst.Done()
|
||||
}
|
||||
case err := <-u:
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
s.err = err
|
||||
s.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
s.err = nil
|
||||
// if this is the first stat you get, release WaitGroup
|
||||
if !getFirst {
|
||||
getFirst = true
|
||||
waitFirst.Done()
|
||||
}
|
||||
}
|
||||
if !streamStats {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *containerStats) Display(w io.Writer) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// NOTE: if you change this format, you must also change the err format below!
|
||||
format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n"
|
||||
if s.err != nil {
|
||||
format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n"
|
||||
errStr := "--"
|
||||
fmt.Fprintf(w, format,
|
||||
s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr,
|
||||
)
|
||||
err := s.err
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, format,
|
||||
s.Name,
|
||||
s.CPUPercentage,
|
||||
units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit),
|
||||
s.MemoryPercentage,
|
||||
units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx),
|
||||
units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite),
|
||||
s.PidsCurrent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
|
||||
var (
|
||||
cpuPercent = 0.0
|
||||
// calculate the change for the cpu usage of the container in between readings
|
||||
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU)
|
||||
// calculate the change for the entire system between readings
|
||||
systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem)
|
||||
)
|
||||
|
||||
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
||||
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0
|
||||
}
|
||||
return cpuPercent
|
||||
}
|
||||
|
||||
func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) {
|
||||
for _, bioEntry := range blkio.IoServiceBytesRecursive {
|
||||
switch strings.ToLower(bioEntry.Op) {
|
||||
case "read":
|
||||
blkRead = blkRead + bioEntry.Value
|
||||
case "write":
|
||||
blkWrite = blkWrite + bioEntry.Value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) {
|
||||
var rx, tx float64
|
||||
|
||||
for _, v := range network {
|
||||
rx += float64(v.RxBytes)
|
||||
tx += float64(v.TxBytes)
|
||||
}
|
||||
return rx, tx
|
||||
}
|
||||
45
command/container/stats_unit_test.go
Normal file
45
command/container/stats_unit_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
func TestDisplay(t *testing.T) {
|
||||
c := &containerStats{
|
||||
Name: "app",
|
||||
CPUPercentage: 30.0,
|
||||
Memory: 100 * 1024 * 1024.0,
|
||||
MemoryLimit: 2048 * 1024 * 1024.0,
|
||||
MemoryPercentage: 100.0 / 2048.0 * 100.0,
|
||||
NetworkRx: 100 * 1024 * 1024,
|
||||
NetworkTx: 800 * 1024 * 1024,
|
||||
BlockRead: 100 * 1024 * 1024,
|
||||
BlockWrite: 800 * 1024 * 1024,
|
||||
PidsCurrent: 1,
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := c.Display(&b); err != nil {
|
||||
t.Fatalf("c.Display() gave error: %s", err)
|
||||
}
|
||||
got := b.String()
|
||||
want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\t1\n"
|
||||
if got != want {
|
||||
t.Fatalf("c.Display() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculBlockIO(t *testing.T) {
|
||||
blkio := types.BlkioStats{
|
||||
IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}},
|
||||
}
|
||||
blkRead, blkWrite := calculateBlockIO(blkio)
|
||||
if blkRead != 5801 {
|
||||
t.Fatalf("blkRead = %d, want 5801", blkRead)
|
||||
}
|
||||
if blkWrite != 579 {
|
||||
t.Fatalf("blkWrite = %d, want 579", blkWrite)
|
||||
}
|
||||
}
|
||||
56
command/container/stop.go
Normal file
56
command/container/stop.go
Normal file
@ -0,0 +1,56 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type stopOptions struct {
|
||||
time int
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewStopCommand creates a new cobra.Command for `docker stop`
|
||||
func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts stopOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Stop one or more running containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runStop(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.IntVarP(&opts.time, "time", "t", 10, "Seconds to wait for stop before killing it")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStop(dockerCli *command.DockerCli, opts *stopOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, container := range opts.containers {
|
||||
timeout := time.Duration(opts.time) * time.Second
|
||||
if err := dockerCli.Client().ContainerStop(ctx, container, &timeout); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
58
command/container/top.go
Normal file
58
command/container/top.go
Normal file
@ -0,0 +1,58 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type topOptions struct {
|
||||
container string
|
||||
|
||||
args []string
|
||||
}
|
||||
|
||||
// NewTopCommand creates a new cobra.Command for `docker top`
|
||||
func NewTopCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts topOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "top CONTAINER [ps OPTIONS]",
|
||||
Short: "Display the running processes of a container",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
opts.args = args[1:]
|
||||
return runTop(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runTop(dockerCli *command.DockerCli, opts *topOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))
|
||||
|
||||
for _, proc := range procList.Processes {
|
||||
fmt.Fprintln(w, strings.Join(proc, "\t"))
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
103
command/container/tty.go
Normal file
103
command/container/tty.go
Normal file
@ -0,0 +1,103 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
gosignal "os/signal"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// resizeTtyTo resizes tty to specific height and width
|
||||
func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width int, isExec bool) {
|
||||
if height == 0 && width == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
options := types.ResizeOptions{
|
||||
Height: height,
|
||||
Width: width,
|
||||
}
|
||||
|
||||
var err error
|
||||
if isExec {
|
||||
err = client.ContainerExecResize(ctx, id, options)
|
||||
} else {
|
||||
err = client.ContainerResize(ctx, id, options)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logrus.Debugf("Error resize: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MonitorTtySize updates the container tty size when the terminal tty changes size
|
||||
func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isExec bool) error {
|
||||
resizeTty := func() {
|
||||
height, width := cli.Out().GetTtySize()
|
||||
resizeTtyTo(ctx, cli.Client(), id, height, width, isExec)
|
||||
}
|
||||
|
||||
resizeTty()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
go func() {
|
||||
prevH, prevW := cli.Out().GetTtySize()
|
||||
for {
|
||||
time.Sleep(time.Millisecond * 250)
|
||||
h, w := cli.Out().GetTtySize()
|
||||
|
||||
if prevW != w || prevH != h {
|
||||
resizeTty()
|
||||
}
|
||||
prevH = h
|
||||
prevW = w
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
gosignal.Notify(sigchan, signal.SIGWINCH)
|
||||
go func() {
|
||||
for range sigchan {
|
||||
resizeTty()
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForwardAllSignals forwards signals to the container
|
||||
func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal {
|
||||
sigc := make(chan os.Signal, 128)
|
||||
signal.CatchAll(sigc)
|
||||
go func() {
|
||||
for s := range sigc {
|
||||
if s == signal.SIGCHLD || s == signal.SIGPIPE {
|
||||
continue
|
||||
}
|
||||
var sig string
|
||||
for sigStr, sigN := range signal.SignalMap {
|
||||
if sigN == s {
|
||||
sig = sigStr
|
||||
break
|
||||
}
|
||||
}
|
||||
if sig == "" {
|
||||
fmt.Fprintf(cli.Err(), "Unsupported signal: %v. Discarding.\n", s)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil {
|
||||
logrus.Debugf("Error sending signal: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return sigc
|
||||
}
|
||||
49
command/container/unpause.go
Normal file
49
command/container/unpause.go
Normal file
@ -0,0 +1,49 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type unpauseOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewUnpauseCommand creates a new cobra.Command for `docker unpause`
|
||||
func NewUnpauseCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts unpauseOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "unpause CONTAINER [CONTAINER...]",
|
||||
Short: "Unpause all processes within one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runUnpause(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, container := range opts.containers {
|
||||
if err := dockerCli.Client().ContainerUnpause(ctx, container); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
157
command/container/update.go
Normal file
157
command/container/update.go
Normal file
@ -0,0 +1,157 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type updateOptions struct {
|
||||
blkioWeight uint16
|
||||
cpuPeriod int64
|
||||
cpuQuota int64
|
||||
cpusetCpus string
|
||||
cpusetMems string
|
||||
cpuShares int64
|
||||
memoryString string
|
||||
memoryReservation string
|
||||
memorySwap string
|
||||
kernelMemory string
|
||||
restartPolicy string
|
||||
|
||||
nFlag int
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewUpdateCommand creates a new cobra.Command for `docker update`
|
||||
func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts updateOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Update configuration of one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
opts.nFlag = cmd.Flags().NFlag()
|
||||
return runUpdate(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000")
|
||||
flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period")
|
||||
flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota")
|
||||
flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
|
||||
flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
|
||||
flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
|
||||
flags.StringVarP(&opts.memoryString, "memory", "m", "", "Memory limit")
|
||||
flags.StringVar(&opts.memoryReservation, "memory-reservation", "", "Memory soft limit")
|
||||
flags.StringVar(&opts.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
|
||||
flags.StringVar(&opts.kernelMemory, "kernel-memory", "", "Kernel memory limit")
|
||||
flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error {
|
||||
var err error
|
||||
|
||||
if opts.nFlag == 0 {
|
||||
return fmt.Errorf("You must provide one or more flags when using this command.")
|
||||
}
|
||||
|
||||
var memory int64
|
||||
if opts.memoryString != "" {
|
||||
memory, err = units.RAMInBytes(opts.memoryString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var memoryReservation int64
|
||||
if opts.memoryReservation != "" {
|
||||
memoryReservation, err = units.RAMInBytes(opts.memoryReservation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var memorySwap int64
|
||||
if opts.memorySwap != "" {
|
||||
if opts.memorySwap == "-1" {
|
||||
memorySwap = -1
|
||||
} else {
|
||||
memorySwap, err = units.RAMInBytes(opts.memorySwap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var kernelMemory int64
|
||||
if opts.kernelMemory != "" {
|
||||
kernelMemory, err = units.RAMInBytes(opts.kernelMemory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var restartPolicy containertypes.RestartPolicy
|
||||
if opts.restartPolicy != "" {
|
||||
restartPolicy, err = runconfigopts.ParseRestartPolicy(opts.restartPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resources := containertypes.Resources{
|
||||
BlkioWeight: opts.blkioWeight,
|
||||
CpusetCpus: opts.cpusetCpus,
|
||||
CpusetMems: opts.cpusetMems,
|
||||
CPUShares: opts.cpuShares,
|
||||
Memory: memory,
|
||||
MemoryReservation: memoryReservation,
|
||||
MemorySwap: memorySwap,
|
||||
KernelMemory: kernelMemory,
|
||||
CPUPeriod: opts.cpuPeriod,
|
||||
CPUQuota: opts.cpuQuota,
|
||||
}
|
||||
|
||||
updateConfig := containertypes.UpdateConfig{
|
||||
Resources: resources,
|
||||
RestartPolicy: restartPolicy,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var (
|
||||
warns []string
|
||||
errs []string
|
||||
)
|
||||
for _, container := range opts.containers {
|
||||
r, err := dockerCli.Client().ContainerUpdate(ctx, container, updateConfig)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", container)
|
||||
}
|
||||
warns = append(warns, r.Warnings...)
|
||||
}
|
||||
if len(warns) > 0 {
|
||||
fmt.Fprintf(dockerCli.Out(), "%s", strings.Join(warns, "\n"))
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
92
command/container/utils.go
Normal file
92
command/container/utils.go
Normal file
@ -0,0 +1,92 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/system"
|
||||
clientapi "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, containerID string, waitRemove bool) (chan int, error) {
|
||||
if len(containerID) == 0 {
|
||||
// containerID can never be empty
|
||||
panic("Internal Error: waitExitOrRemoved needs a containerID as parameter")
|
||||
}
|
||||
|
||||
statusChan := make(chan int)
|
||||
exitCode := 125
|
||||
|
||||
eventProcessor := func(e events.Message, err error) error {
|
||||
if err != nil {
|
||||
statusChan <- exitCode
|
||||
return fmt.Errorf("failed to decode event: %v", err)
|
||||
}
|
||||
|
||||
stopProcessing := false
|
||||
switch e.Status {
|
||||
case "die":
|
||||
if v, ok := e.Actor.Attributes["exitCode"]; ok {
|
||||
code, cerr := strconv.Atoi(v)
|
||||
if cerr != nil {
|
||||
logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr)
|
||||
} else {
|
||||
exitCode = code
|
||||
}
|
||||
}
|
||||
if !waitRemove {
|
||||
stopProcessing = true
|
||||
}
|
||||
case "detach":
|
||||
exitCode = 0
|
||||
stopProcessing = true
|
||||
case "destroy":
|
||||
stopProcessing = true
|
||||
}
|
||||
|
||||
if stopProcessing {
|
||||
statusChan <- exitCode
|
||||
// stop the loop processing
|
||||
return fmt.Errorf("done")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get events via Events API
|
||||
f := filters.NewArgs()
|
||||
f.Add("type", "container")
|
||||
f.Add("container", containerID)
|
||||
options := types.EventsOptions{
|
||||
Filters: f,
|
||||
}
|
||||
resBody, err := dockerCli.Client().Events(ctx, options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get events from daemon: %v", err)
|
||||
}
|
||||
|
||||
go system.DecodeEvents(resBody, eventProcessor)
|
||||
|
||||
return statusChan, nil
|
||||
}
|
||||
|
||||
// getExitCode performs an inspect on the container. It returns
|
||||
// the running state and the exit code.
|
||||
func getExitCode(dockerCli *command.DockerCli, ctx context.Context, containerID string) (bool, int, error) {
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if err != clientapi.ErrConnectionFailed {
|
||||
return false, -1, err
|
||||
}
|
||||
return false, -1, nil
|
||||
}
|
||||
return c.State.Running, c.State.ExitCode, nil
|
||||
}
|
||||
50
command/container/wait.go
Normal file
50
command/container/wait.go
Normal file
@ -0,0 +1,50 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type waitOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewWaitCommand creates a new cobra.Command for `docker wait`
|
||||
func NewWaitCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts waitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "wait CONTAINER [CONTAINER...]",
|
||||
Short: "Block until one or more containers stop, then print their exit codes",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runWait(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWait(dockerCli *command.DockerCli, opts *waitOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, container := range opts.containers {
|
||||
status, err := dockerCli.Client().ContainerWait(ctx, container)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(dockerCli.Out(), "%d\n", status)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user