diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 24ba98d..ccffb4c 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -27,48 +27,53 @@ }, { "ImportPath": "github.com/docker/docker/pkg/homedir", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/ioutils", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/mflag", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/parsers", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/plugins", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/proxy", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/reexec", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/stringid", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/docker/pkg/term", - "Comment": "v1.4.1-3479-ga9172f5", - "Rev": "a9172f572e13086859c652e2d581950e910d63d4" + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/api/client", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" }, { "ImportPath": "github.com/docker/libcontainer/user", diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/attach.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/attach.go new file mode 100644 index 0000000..5404e82 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/attach.go @@ -0,0 +1,83 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/url" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/signal" +) + +// CmdAttach attaches to a running container. +// +// Usage: docker attach [OPTIONS] CONTAINER +func (cli *DockerCli) CmdAttach(args ...string) error { + var ( + cmd = cli.Subcmd("attach", "CONTAINER", "Attach to a running container", true) + noStdin = cmd.Bool([]string{"#nostdin", "-no-stdin"}, false, "Do not attach STDIN") + proxy = cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy all received signals to the process") + ) + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + name := cmd.Arg(0) + + stream, _, _, err := cli.call("GET", "/containers/"+name+"/json", nil, nil) + if err != nil { + return err + } + + var c types.ContainerJSON + if err := json.NewDecoder(stream).Decode(&c); err != nil { + return err + } + + if !c.State.Running { + return fmt.Errorf("You cannot attach to a stopped container, start it first") + } + + if err := cli.CheckTtyInput(!*noStdin, c.Config.Tty); err != nil { + return err + } + + if c.Config.Tty && cli.isTerminalOut { + if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil { + logrus.Debugf("Error monitoring TTY size: %s", err) + } + } + + var in io.ReadCloser + + v := url.Values{} + v.Set("stream", "1") + if !*noStdin && c.Config.OpenStdin { + v.Set("stdin", "1") + in = cli.in + } + + v.Set("stdout", "1") + v.Set("stderr", "1") + + if *proxy && !c.Config.Tty { + sigc := cli.forwardAllSignals(cmd.Arg(0)) + defer signal.StopCatch(sigc) + } + + if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), c.Config.Tty, in, cli.out, cli.err, nil, nil); err != nil { + return err + } + + _, status, err := getExitCode(cli, cmd.Arg(0)) + if err != nil { + return err + } + if status != 0 { + return StatusError{StatusCode: status} + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/build.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/build.go new file mode 100644 index 0000000..23424c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/build.go @@ -0,0 +1,310 @@ +package client + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/docker/docker/api" + "github.com/docker/docker/graph/tags" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/jsonmessage" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/progressreader" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/units" + "github.com/docker/docker/pkg/urlutil" + "github.com/docker/docker/registry" + "github.com/docker/docker/utils" +) + +const ( + tarHeaderSize = 512 +) + +// CmdBuild builds a new image from the source code at a given path. +// +// If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN. +// +// Usage: docker build [OPTIONS] PATH | URL | - +func (cli *DockerCli) CmdBuild(args ...string) error { + cmd := cli.Subcmd("build", "PATH | URL | -", "Build a new image from the source code at PATH", true) + tag := cmd.String([]string{"t", "-tag"}, "", "Repository name (and optionally a tag) for the image") + suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers") + noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image") + rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") + forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") + pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") + dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") + flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit") + flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Total memory (memory + swap), '-1' to disable swap") + flCPUShares := cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)") + flCpuPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) period") + flCpuQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") + flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") + flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") + flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") + + cmd.Require(flag.Exact, 1) + cmd.ParseFlags(args, true) + + var ( + context archive.Archive + isRemote bool + err error + ) + + _, err = exec.LookPath("git") + hasGit := err == nil + if cmd.Arg(0) == "-" { + // As a special case, 'docker build -' will build from either an empty context with the + // contents of stdin as a Dockerfile, or a tar-ed context from stdin. + buf := bufio.NewReader(cli.in) + magic, err := buf.Peek(tarHeaderSize) + if err != nil && err != io.EOF { + return fmt.Errorf("failed to peek context header from STDIN: %v", err) + } + if !archive.IsArchive(magic) { + dockerfile, err := ioutil.ReadAll(buf) + if err != nil { + return fmt.Errorf("failed to read Dockerfile from STDIN: %v", err) + } + + // -f option has no meaning when we're reading it from stdin, + // so just use our default Dockerfile name + *dockerfileName = api.DefaultDockerfileName + context, err = archive.Generate(*dockerfileName, string(dockerfile)) + } else { + context = ioutil.NopCloser(buf) + } + } else if urlutil.IsURL(cmd.Arg(0)) && (!urlutil.IsGitURL(cmd.Arg(0)) || !hasGit) { + isRemote = true + } else { + root := cmd.Arg(0) + if urlutil.IsGitURL(root) { + root, err = utils.GitClone(root) + if err != nil { + return err + } + defer os.RemoveAll(root) + } + if _, err := os.Stat(root); err != nil { + return err + } + + absRoot, err := filepath.Abs(root) + if err != nil { + return err + } + + filename := *dockerfileName // path to Dockerfile + + if *dockerfileName == "" { + // No -f/--file was specified so use the default + *dockerfileName = api.DefaultDockerfileName + filename = filepath.Join(absRoot, *dockerfileName) + + // Just to be nice ;-) look for 'dockerfile' too but only + // use it if we found it, otherwise ignore this check + if _, err = os.Lstat(filename); os.IsNotExist(err) { + tmpFN := path.Join(absRoot, strings.ToLower(*dockerfileName)) + if _, err = os.Lstat(tmpFN); err == nil { + *dockerfileName = strings.ToLower(*dockerfileName) + filename = tmpFN + } + } + } + + origDockerfile := *dockerfileName // used for error msg + if filename, err = filepath.Abs(filename); err != nil { + return err + } + + // Verify that 'filename' is within the build context + filename, err = symlink.FollowSymlinkInScope(filename, absRoot) + if err != nil { + return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", origDockerfile, root) + } + + // Now reset the dockerfileName to be relative to the build context + *dockerfileName, err = filepath.Rel(absRoot, filename) + if err != nil { + return err + } + // And canonicalize dockerfile name to a platform-independent one + *dockerfileName, err = archive.CanonicalTarNameForPath(*dockerfileName) + if err != nil { + return fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", *dockerfileName, err) + } + + if _, err = os.Lstat(filename); os.IsNotExist(err) { + return fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile) + } + var includes = []string{"."} + + excludes, err := utils.ReadDockerIgnore(path.Join(root, ".dockerignore")) + if err != nil { + return err + } + + // If .dockerignore mentions .dockerignore or the Dockerfile + // then make sure we send both files over to the daemon + // because Dockerfile is, obviously, needed no matter what, and + // .dockerignore is needed to know if either one needs to be + // removed. The deamon will remove them for us, if needed, after it + // parses the Dockerfile. + keepThem1, _ := fileutils.Matches(".dockerignore", excludes) + keepThem2, _ := fileutils.Matches(*dockerfileName, excludes) + if keepThem1 || keepThem2 { + includes = append(includes, ".dockerignore", *dockerfileName) + } + + if err := utils.ValidateContextDirectory(root, excludes); err != nil { + return fmt.Errorf("Error checking context: '%s'.", err) + } + options := &archive.TarOptions{ + Compression: archive.Uncompressed, + ExcludePatterns: excludes, + IncludeFiles: includes, + } + context, err = archive.TarWithOptions(root, options) + if err != nil { + return err + } + } + + // windows: show error message about modified file permissions + // FIXME: this is not a valid warning when the daemon is running windows. should be removed once docker engine for windows can build. + if runtime.GOOS == "windows" { + fmt.Fprintln(cli.err, `SECURITY WARNING: You are building a Docker image from Windows against a Linux Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) + } + + var body io.Reader + // Setup an upload progress bar + // FIXME: ProgressReader shouldn't be this annoying to use + if context != nil { + sf := streamformatter.NewStreamFormatter() + body = progressreader.New(progressreader.Config{ + In: context, + Out: cli.out, + Formatter: sf, + NewLines: true, + ID: "", + Action: "Sending build context to Docker daemon", + }) + } + + var memory int64 + if *flMemoryString != "" { + parsedMemory, err := units.RAMInBytes(*flMemoryString) + if err != nil { + return err + } + memory = parsedMemory + } + + var memorySwap int64 + if *flMemorySwap != "" { + if *flMemorySwap == "-1" { + memorySwap = -1 + } else { + parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) + if err != nil { + return err + } + memorySwap = parsedMemorySwap + } + } + // Send the build context + v := &url.Values{} + + //Check if the given image name can be resolved + if *tag != "" { + repository, tag := parsers.ParseRepositoryTag(*tag) + if err := registry.ValidateRepositoryName(repository); err != nil { + return err + } + if len(tag) > 0 { + if err := tags.ValidateTagName(tag); err != nil { + return err + } + } + } + + v.Set("t", *tag) + + if *suppressOutput { + v.Set("q", "1") + } + if isRemote { + v.Set("remote", cmd.Arg(0)) + } + if *noCache { + v.Set("nocache", "1") + } + if *rm { + v.Set("rm", "1") + } else { + v.Set("rm", "0") + } + + if *forceRm { + v.Set("forcerm", "1") + } + + if *pull { + v.Set("pull", "1") + } + + v.Set("cpusetcpus", *flCPUSetCpus) + v.Set("cpusetmems", *flCPUSetMems) + v.Set("cpushares", strconv.FormatInt(*flCPUShares, 10)) + v.Set("cpuquota", strconv.FormatInt(*flCpuQuota, 10)) + v.Set("cpuperiod", strconv.FormatInt(*flCpuPeriod, 10)) + v.Set("memory", strconv.FormatInt(memory, 10)) + v.Set("memswap", strconv.FormatInt(memorySwap, 10)) + v.Set("cgroupparent", *flCgroupParent) + + v.Set("dockerfile", *dockerfileName) + + headers := http.Header(make(map[string][]string)) + buf, err := json.Marshal(cli.configFile.AuthConfigs) + if err != nil { + return err + } + headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) + + if context != nil { + headers.Set("Content-Type", "application/tar") + } + sopts := &streamOpts{ + rawTerminal: true, + in: body, + out: cli.out, + headers: headers, + } + err = cli.stream("POST", fmt.Sprintf("/build?%s", v.Encode()), sopts) + if jerr, ok := err.(*jsonmessage.JSONError); ok { + // If no error code is set, default to 1 + if jerr.Code == 0 { + jerr.Code = 1 + } + return StatusError{Status: jerr.Message, StatusCode: jerr.Code} + } + return err +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/cli.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/cli.go new file mode 100644 index 0000000..65564fe --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/cli.go @@ -0,0 +1,203 @@ +package client + +import ( + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "path/filepath" + "reflect" + "strings" + "text/template" + + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/homedir" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/utils" +) + +// DockerCli represents the docker command line client. +// Instances of the client can be returned from NewDockerCli. +type DockerCli struct { + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string + + // configFile has the client configuration file + configFile *cliconfig.ConfigFile + // in holds the input stream and closer (io.ReadCloser) for the client. + in io.ReadCloser + // out holds the output stream (io.Writer) for the client. + out io.Writer + // err holds the error stream (io.Writer) for the client. + err io.Writer + // keyFile holds the key file as a string. + keyFile string + // tlsConfig holds the TLS configuration for the client, and will + // set the scheme to https in NewDockerCli if present. + tlsConfig *tls.Config + // scheme holds the scheme of the client i.e. https. + scheme string + // inFd holds the file descriptor of the client's STDIN (if valid). + inFd uintptr + // outFd holds file descriptor of the client's STDOUT (if valid). + outFd uintptr + // isTerminalIn indicates whether the client's STDIN is a TTY + isTerminalIn bool + // isTerminalOut dindicates whether the client's STDOUT is a TTY + isTerminalOut bool + // transport holds the client transport instance. + transport *http.Transport +} + +var funcMap = template.FuncMap{ + "json": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, +} + +func (cli *DockerCli) Out() io.Writer { + return cli.out +} + +func (cli *DockerCli) Err() io.Writer { + return cli.err +} + +func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) { + camelArgs := make([]string, len(args)) + for i, s := range args { + if len(s) == 0 { + return nil, false + } + camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) + } + methodName := "Cmd" + strings.Join(camelArgs, "") + method := reflect.ValueOf(cli).MethodByName(methodName) + if !method.IsValid() { + return nil, false + } + return method.Interface().(func(...string) error), true +} + +// Cmd executes the specified command. +func (cli *DockerCli) Cmd(args ...string) error { + if len(args) > 1 { + method, exists := cli.getMethod(args[:2]...) + if exists { + return method(args[2:]...) + } + } + if len(args) > 0 { + method, exists := cli.getMethod(args[0]) + if !exists { + return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'.", args[0]) + } + return method(args[1:]...) + } + return cli.CmdHelp() +} + +// Subcmd is a subcommand of the main "docker" command. +// A subcommand represents an action that can be performed +// from the Docker command line client. +// +// To see all available subcommands, run "docker --help". +func (cli *DockerCli) Subcmd(name, signature, description string, exitOnError bool) *flag.FlagSet { + var errorHandling flag.ErrorHandling + if exitOnError { + errorHandling = flag.ExitOnError + } else { + errorHandling = flag.ContinueOnError + } + flags := flag.NewFlagSet(name, errorHandling) + if signature != "" { + signature = " " + signature + } + flags.Usage = func() { + flags.ShortUsage() + flags.PrintDefaults() + } + flags.ShortUsage = func() { + options := "" + if flags.FlagCountUndeprecated() > 0 { + options = " [OPTIONS]" + } + fmt.Fprintf(flags.Out(), "\nUsage: docker %s%s%s\n\n%s\n", name, options, signature, description) + } + return flags +} + +// CheckTtyInput checks if we are trying to attach to a container tty +// from a non-tty client input stream, and if so, returns an error. +func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { + // In order to attach to a container tty, input stream for the client must + // be a tty itself: redirecting or piping the client standard input is + // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. + if ttyMode && attachStdin && !cli.isTerminalIn { + return errors.New("cannot enable tty mode on non tty input") + } + return nil +} + +// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. +// The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config +// is set the client scheme will be set to https. +// The client will be given a 32-second timeout (see https://github.com/docker/docker/pull/8035). +func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, addr string, tlsConfig *tls.Config) *DockerCli { + var ( + inFd uintptr + outFd uintptr + isTerminalIn = false + isTerminalOut = false + scheme = "http" + ) + + if tlsConfig != nil { + scheme = "https" + } + if in != nil { + inFd, isTerminalIn = term.GetFdInfo(in) + } + + if out != nil { + outFd, isTerminalOut = term.GetFdInfo(out) + } + + if err == nil { + err = out + } + + // The transport is created here for reuse during the client session. + tr := &http.Transport{ + TLSClientConfig: tlsConfig, + } + utils.ConfigureTCPTransport(tr, proto, addr) + + configFile, e := cliconfig.Load(filepath.Join(homedir.Get(), ".docker")) + if e != nil { + fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) + } + + return &DockerCli{ + proto: proto, + addr: addr, + configFile: configFile, + in: in, + out: out, + err: err, + keyFile: keyFile, + inFd: inFd, + outFd: outFd, + isTerminalIn: isTerminalIn, + isTerminalOut: isTerminalOut, + tlsConfig: tlsConfig, + scheme: scheme, + transport: tr, + } +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/client.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/client.go new file mode 100644 index 0000000..3170881 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/client.go @@ -0,0 +1,17 @@ +// Package client provides a command-line interface for Docker. +// +// Run "docker help SUBCOMMAND" or "docker SUBCOMMAND --help" to see more information on any Docker subcommand, including the full list of options supported for the subcommand. +// See https://docs.docker.com/installation/ for instructions on installing Docker. +package client + +import "fmt" + +// An StatusError reports an unsuccessful exit by a command. +type StatusError struct { + Status string + StatusCode int +} + +func (e StatusError) Error() string { + return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/commit.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/commit.go new file mode 100644 index 0000000..09816dc --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/commit.go @@ -0,0 +1,80 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" +) + +// CmdCommit creates a new image from a container's changes. +// +// Usage: docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] +func (cli *DockerCli) CmdCommit(args ...string) error { + cmd := cli.Subcmd("commit", "CONTAINER [REPOSITORY[:TAG]]", "Create a new image from a container's changes", true) + flPause := cmd.Bool([]string{"p", "-pause"}, true, "Pause container during commit") + flComment := cmd.String([]string{"m", "-message"}, "", "Commit message") + flAuthor := cmd.String([]string{"a", "#author", "-author"}, "", "Author (e.g., \"John Hannibal Smith \")") + flChanges := opts.NewListOpts(nil) + cmd.Var(&flChanges, []string{"c", "-change"}, "Apply Dockerfile instruction to the created image") + // FIXME: --run is deprecated, it will be replaced with inline Dockerfile commands. + flConfig := cmd.String([]string{"#run", "#-run"}, "", "This option is deprecated and will be removed in a future version in favor of inline Dockerfile-compatible commands") + cmd.Require(flag.Max, 2) + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + var ( + name = cmd.Arg(0) + repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1)) + ) + + //Check if the given image name can be resolved + if repository != "" { + if err := registry.ValidateRepositoryName(repository); err != nil { + return err + } + } + + v := url.Values{} + v.Set("container", name) + v.Set("repo", repository) + v.Set("tag", tag) + v.Set("comment", *flComment) + v.Set("author", *flAuthor) + for _, change := range flChanges.GetAll() { + v.Add("changes", change) + } + + if *flPause != true { + v.Set("pause", "0") + } + + var ( + config *runconfig.Config + response types.ContainerCommitResponse + ) + + if *flConfig != "" { + config = &runconfig.Config{} + if err := json.Unmarshal([]byte(*flConfig), config); err != nil { + return err + } + } + stream, _, _, err := cli.call("POST", "/commit?"+v.Encode(), config, nil) + if err != nil { + return err + } + + if err := json.NewDecoder(stream).Decode(&response); err != nil { + return err + } + + fmt.Fprintln(cli.out, response.ID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/cp.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/cp.go new file mode 100644 index 0000000..e40af89 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/cp.go @@ -0,0 +1,57 @@ +package client + +import ( + "fmt" + "io" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/archive" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdCp copies files/folders from a path on the container to a directory on the host running the command. +// +// If HOSTDIR is '-', the data is written as a tar file to STDOUT. +// +// Usage: docker cp CONTAINER:PATH HOSTDIR +func (cli *DockerCli) CmdCp(args ...string) error { + cmd := cli.Subcmd("cp", "CONTAINER:PATH HOSTDIR|-", "Copy files/folders from a PATH on the container to a HOSTDIR on the host\nrunning the command. Use '-' to write the data as a tar file to STDOUT.", true) + cmd.Require(flag.Exact, 2) + + cmd.ParseFlags(args, true) + + // deal with path name with `:` + info := strings.SplitN(cmd.Arg(0), ":", 2) + + if len(info) != 2 { + return fmt.Errorf("Error: Path not specified") + } + + cfg := &types.CopyConfig{ + Resource: info[1], + } + stream, _, statusCode, err := cli.call("POST", "/containers/"+info[0]+"/copy", cfg, nil) + if stream != nil { + defer stream.Close() + } + if statusCode == 404 { + return fmt.Errorf("No such container: %v", info[0]) + } + if err != nil { + return err + } + + hostPath := cmd.Arg(1) + if statusCode == 200 { + if hostPath == "-" { + _, err = io.Copy(cli.out, stream) + } else { + err = archive.Untar(stream, hostPath, &archive.TarOptions{NoLchown: true}) + } + if err != nil { + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/create.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/create.go new file mode 100644 index 0000000..0d0f66a --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/create.go @@ -0,0 +1,160 @@ +package client + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/url" + "os" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/graph/tags" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" + "github.com/docker/docker/utils" +) + +func (cli *DockerCli) pullImage(image string) error { + return cli.pullImageCustomOut(image, cli.out) +} + +func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { + v := url.Values{} + repos, tag := parsers.ParseRepositoryTag(image) + // pull only the image tagged 'latest' if no tag was specified + if tag == "" { + tag = tags.DEFAULTTAG + } + v.Set("fromImage", repos) + v.Set("tag", tag) + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(repos) + if err != nil { + return err + } + + // Resolve the Auth config relevant for this server + authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) + buf, err := json.Marshal(authConfig) + if err != nil { + return err + } + + registryAuthHeader := []string{ + base64.URLEncoding.EncodeToString(buf), + } + sopts := &streamOpts{ + rawTerminal: true, + out: out, + headers: map[string][]string{"X-Registry-Auth": registryAuthHeader}, + } + if err := cli.stream("POST", "/images/create?"+v.Encode(), sopts); err != nil { + return err + } + return nil +} + +type cidFile struct { + path string + file *os.File + written bool +} + +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 (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runconfig.HostConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { + containerValues := url.Values{} + if name != "" { + containerValues.Set("name", name) + } + + mergedConfig := runconfig.MergeConfigs(config, hostConfig) + + var containerIDFile *cidFile + if cidfile != "" { + var err error + if containerIDFile, err = newCIDFile(cidfile); err != nil { + return nil, err + } + defer containerIDFile.Close() + } + + //create the container + stream, _, statusCode, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil) + //if image not found try to pull it + if statusCode == 404 && strings.Contains(err.Error(), config.Image) { + repo, tag := parsers.ParseRepositoryTag(config.Image) + if tag == "" { + tag = tags.DEFAULTTAG + } + fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", utils.ImageReference(repo, tag)) + + // we don't want to write to stdout anything apart from container.ID + if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil { + return nil, err + } + // Retry + if stream, _, _, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil); err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } + + var response types.ContainerCreateResponse + if err := json.NewDecoder(stream).Decode(&response); err != nil { + return nil, err + } + for _, warning := range response.Warnings { + fmt.Fprintf(cli.err, "WARNING: %s\n", warning) + } + if containerIDFile != nil { + if err = containerIDFile.Write(response.ID); err != nil { + return nil, err + } + } + return &response, nil +} + +// CmdCreate creates a new container from a given image. +// +// Usage: docker create [OPTIONS] IMAGE [COMMAND] [ARG...] +func (cli *DockerCli) CmdCreate(args ...string) error { + cmd := cli.Subcmd("create", "IMAGE [COMMAND] [ARG...]", "Create a new container", true) + + // These are flags not stored in Config/HostConfig + var ( + flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") + ) + + config, hostConfig, cmd, err := runconfig.Parse(cmd, args) + if err != nil { + cmd.ReportError(err.Error(), true) + os.Exit(1) + } + if config.Image == "" { + cmd.Usage() + return nil + } + response, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName) + if err != nil { + return err + } + fmt.Fprintf(cli.out, "%s\n", response.ID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/diff.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/diff.go new file mode 100644 index 0000000..16528d7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/diff.go @@ -0,0 +1,52 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/archive" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdDiff shows changes on a container's filesystem. +// +// Each changed file is printed on a separate line, prefixed with a single +// character that indicates the status of the file: C (modified), A (added), +// or D (deleted). +// +// Usage: docker diff CONTAINER +func (cli *DockerCli) CmdDiff(args ...string) error { + cmd := cli.Subcmd("diff", "CONTAINER", "Inspect changes on a container's filesystem", true) + cmd.Require(flag.Exact, 1) + cmd.ParseFlags(args, true) + + if cmd.Arg(0) == "" { + return fmt.Errorf("Container name cannot be empty") + } + + rdr, _, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil, nil) + if err != nil { + return err + } + + changes := []types.ContainerChange{} + if err := json.NewDecoder(rdr).Decode(&changes); 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(cli.out, "%s %s\n", kind, change.Path) + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/events.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/events.go new file mode 100644 index 0000000..d3e2350 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/events.go @@ -0,0 +1,62 @@ +package client + +import ( + "net/url" + "time" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/timeutils" +) + +// CmdEvents prints a live stream of real time events from the server. +// +// Usage: docker events [OPTIONS] +func (cli *DockerCli) CmdEvents(args ...string) error { + cmd := cli.Subcmd("events", "", "Get real time events from the server", true) + since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp") + until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + cmd.Require(flag.Exact, 0) + + cmd.ParseFlags(args, true) + + var ( + v = url.Values{} + eventFilterArgs = filters.Args{} + ) + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + for _, f := range flFilter.GetAll() { + var err error + eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs) + if err != nil { + return err + } + } + ref := time.Now() + if *since != "" { + v.Set("since", timeutils.GetTimestamp(*since, ref)) + } + if *until != "" { + v.Set("until", timeutils.GetTimestamp(*until, ref)) + } + if len(eventFilterArgs) > 0 { + filterJSON, err := filters.ToParam(eventFilterArgs) + if err != nil { + return err + } + v.Set("filters", filterJSON) + } + sopts := &streamOpts{ + rawTerminal: true, + out: cli.out, + } + if err := cli.stream("GET", "/events?"+v.Encode(), sopts); err != nil { + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/exec.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/exec.go new file mode 100644 index 0000000..be03cba --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/exec.go @@ -0,0 +1,131 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/runconfig" +) + +// CmdExec runs a command in a running container. +// +// Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...] +func (cli *DockerCli) CmdExec(args ...string) error { + cmd := cli.Subcmd("exec", "CONTAINER COMMAND [ARG...]", "Run a command in a running container", true) + + execConfig, err := runconfig.ParseExec(cmd, args) + // just in case the ParseExec does not exit + if execConfig.Container == "" || err != nil { + return StatusError{StatusCode: 1} + } + + stream, _, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, nil) + if err != nil { + return err + } + + var response types.ContainerExecCreateResponse + if err := json.NewDecoder(stream).Decode(&response); err != nil { + return err + } + + execID := response.ID + + if execID == "" { + fmt.Fprintf(cli.out, "exec ID empty") + return nil + } + + //Temp struct for execStart so that we don't need to transfer all the execConfig + execStartCheck := &types.ExecStartCheck{ + Detach: execConfig.Detach, + Tty: execConfig.Tty, + } + + if !execConfig.Detach { + if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil { + return err + } + } else { + if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execStartCheck, nil)); err != nil { + return err + } + // For now don't print this - wait for when we support exec wait() + // fmt.Fprintf(cli.out, "%s\n", execID) + return nil + } + + // Interactive exec requested. + var ( + out, stderr io.Writer + in io.ReadCloser + hijacked = make(chan io.Closer) + errCh chan error + ) + + // Block the return until the chan gets closed + defer func() { + logrus.Debugf("End of CmdExec(), Waiting for hijack to finish.") + if _, ok := <-hijacked; ok { + fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)") + } + }() + + if execConfig.AttachStdin { + in = cli.in + } + if execConfig.AttachStdout { + out = cli.out + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + errCh = promise.Go(func() error { + return cli.hijack("POST", "/exec/"+execID+"/start", execConfig.Tty, in, out, stderr, hijacked, execConfig) + }) + + // Acknowledge the hijack before starting + select { + case closer := <-hijacked: + // Make sure that hijack gets closed when returning. (result + // in closing hijack chan and freeing server's goroutines. + if closer != nil { + defer closer.Close() + } + case err := <-errCh: + if err != nil { + logrus.Debugf("Error hijack: %s", err) + return err + } + } + + if execConfig.Tty && cli.isTerminalIn { + if err := cli.monitorTtySize(execID, true); err != nil { + fmt.Fprintf(cli.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(cli, execID); err != nil { + return err + } + + if status != 0 { + return StatusError{StatusCode: status} + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/export.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/export.go new file mode 100644 index 0000000..42b0834 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/export.go @@ -0,0 +1,46 @@ +package client + +import ( + "errors" + "io" + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdExport exports a filesystem as a tar archive. +// +// The tar archive is streamed to STDOUT by default or written to a file. +// +// Usage: docker export [OPTIONS] CONTAINER +func (cli *DockerCli) CmdExport(args ...string) error { + cmd := cli.Subcmd("export", "CONTAINER", "Export a filesystem as a tar archive (streamed to STDOUT by default)", true) + outfile := cmd.String([]string{"o", "-output"}, "", "Write to a file, instead of STDOUT") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + var ( + output io.Writer = cli.out + err error + ) + if *outfile != "" { + output, err = os.Create(*outfile) + if err != nil { + return err + } + } else if cli.isTerminalOut { + return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") + } + + image := cmd.Arg(0) + sopts := &streamOpts{ + rawTerminal: true, + out: output, + } + if err := cli.stream("GET", "/containers/"+image+"/export", sopts); err != nil { + return err + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/help.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/help.go new file mode 100644 index 0000000..8e1dc85 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/help.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdHelp displays information on a Docker command. +// +// If more than one command is specified, information is only shown for the first command. +// +// Usage: docker help COMMAND or docker COMMAND --help +func (cli *DockerCli) CmdHelp(args ...string) error { + if len(args) > 1 { + method, exists := cli.getMethod(args[:2]...) + if exists { + method("--help") + return nil + } + } + if len(args) > 0 { + method, exists := cli.getMethod(args[0]) + if !exists { + return fmt.Errorf("docker: '%s' is not a docker command. See 'docker --help'.", args[0]) + } + method("--help") + return nil + } + + flag.Usage() + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/hijack.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/hijack.go new file mode 100644 index 0000000..becb8a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/hijack.go @@ -0,0 +1,257 @@ +package client + +import ( + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api" + "github.com/docker/docker/autogen/dockerversion" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/docker/pkg/term" +) + +type tlsClientCon struct { + *tls.Conn + rawConn net.Conn +} + +func (c *tlsClientCon) CloseWrite() error { + // Go standard tls.Conn doesn't provide the CloseWrite() method so we do it + // on its underlying connection. + if cwc, ok := c.rawConn.(interface { + CloseWrite() error + }); ok { + return cwc.CloseWrite() + } + return nil +} + +func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) { + return tlsDialWithDialer(new(net.Dialer), network, addr, config) +} + +// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in +// order to return our custom tlsClientCon struct which holds both the tls.Conn +// object _and_ its underlying raw connection. The rationale for this is that +// we need to be able to close the write end of the connection when attaching, +// which tls.Conn does not provide. +func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) { + // We want the Timeout and Deadline values from dialer to cover the + // whole process: TCP connection and TLS handshake. This means that we + // also need to start our own timers now. + timeout := dialer.Timeout + + if !dialer.Deadline.IsZero() { + deadlineTimeout := dialer.Deadline.Sub(time.Now()) + if timeout == 0 || deadlineTimeout < timeout { + timeout = deadlineTimeout + } + } + + var errChannel chan error + + if timeout != 0 { + errChannel = make(chan error, 2) + time.AfterFunc(timeout, func() { + errChannel <- errors.New("") + }) + } + + rawConn, err := dialer.Dial(network, addr) + if err != nil { + return nil, err + } + // When we set up a TCP connection for hijack, there could be long periods + // of inactivity (a long running command with no output) that in certain + // network setups may cause ECONNTIMEOUT, leaving the client in an unknown + // state. Setting TCP KeepAlive on the socket connection will prohibit + // ECONNTIMEOUT unless the socket connection truly is broken + if tcpConn, ok := rawConn.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + + colonPos := strings.LastIndex(addr, ":") + if colonPos == -1 { + colonPos = len(addr) + } + hostname := addr[:colonPos] + + // If no ServerName is set, infer the ServerName + // from the hostname we're connecting to. + if config.ServerName == "" { + // Make a copy to avoid polluting argument or default. + c := *config + c.ServerName = hostname + config = &c + } + + conn := tls.Client(rawConn, config) + + if timeout == 0 { + err = conn.Handshake() + } else { + go func() { + errChannel <- conn.Handshake() + }() + + err = <-errChannel + } + + if err != nil { + rawConn.Close() + return nil, err + } + + // This is Docker difference with standard's crypto/tls package: returned a + // wrapper which holds both the TLS and raw connections. + return &tlsClientCon{conn, rawConn}, nil +} + +func (cli *DockerCli) dial() (net.Conn, error) { + if cli.tlsConfig != nil && cli.proto != "unix" { + // Notice this isn't Go standard's tls.Dial function + return tlsDial(cli.proto, cli.addr, cli.tlsConfig) + } + return net.Dial(cli.proto, cli.addr) +} + +func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error { + defer func() { + if started != nil { + close(started) + } + }() + + params, err := cli.encodeData(data) + if err != nil { + return err + } + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params) + if err != nil { + return err + } + + // Add CLI Config's HTTP Headers BEFORE we set the Docker headers + // then the user can't change OUR headers + for k, v := range cli.configFile.HttpHeaders { + req.Header.Set(k, v) + } + + req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION+" ("+runtime.GOOS+")") + req.Header.Set("Content-Type", "text/plain") + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") + req.Host = cli.addr + + dial, err := cli.dial() + // When we set up a TCP connection for hijack, there could be long periods + // of inactivity (a long running command with no output) that in certain + // network setups may cause ECONNTIMEOUT, leaving the client in an unknown + // state. Setting TCP KeepAlive on the socket connection will prohibit + // ECONNTIMEOUT unless the socket connection truly is broken + if tcpConn, ok := dial.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?") + } + return err + } + clientconn := httputil.NewClientConn(dial, nil) + defer clientconn.Close() + + // Server hijacks the connection, error 'connection closed' expected + clientconn.Do(req) + + rwc, br := clientconn.Hijack() + defer rwc.Close() + + if started != nil { + started <- rwc + } + + var receiveStdout chan error + + var oldState *term.State + + if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" { + oldState, err = term.SetRawTerminal(cli.inFd) + if err != nil { + return err + } + defer term.RestoreTerminal(cli.inFd, oldState) + } + + if stdout != nil || stderr != nil { + receiveStdout = promise.Go(func() (err error) { + defer func() { + if in != nil { + if setRawTerminal && cli.isTerminalIn { + term.RestoreTerminal(cli.inFd, oldState) + } + // 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 runtime.GOOS != "darwin" { + in.Close() + } + } + }() + + // When TTY is ON, use regular copy + if setRawTerminal && stdout != nil { + _, err = io.Copy(stdout, br) + } else { + _, err = stdcopy.StdCopy(stdout, stderr, br) + } + logrus.Debugf("[hijack] End of stdout") + return err + }) + } + + sendStdin := promise.Go(func() error { + if in != nil { + io.Copy(rwc, in) + logrus.Debugf("[hijack] End of stdin") + } + + if conn, ok := rwc.(interface { + CloseWrite() error + }); ok { + if err := conn.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) + } + } + // Discard errors due to pipe interruption + return nil + }) + + if stdout != nil || stderr != nil { + if err := <-receiveStdout; err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + } + + if !cli.isTerminalIn { + if err := <-sendStdin; err != nil { + logrus.Debugf("Error sendStdin: %s", err) + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/history.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/history.go new file mode 100644 index 0000000..d934580 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/history.go @@ -0,0 +1,73 @@ +package client + +import ( + "encoding/json" + "fmt" + "text/tabwriter" + "time" + + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/pkg/units" +) + +// CmdHistory shows the history of an image. +// +// Usage: docker history [OPTIONS] IMAGE +func (cli *DockerCli) CmdHistory(args ...string) error { + cmd := cli.Subcmd("history", "IMAGE", "Show the history of an image", true) + human := cmd.Bool([]string{"H", "-human"}, true, "Print sizes and dates in human readable format") + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") + noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") + cmd.Require(flag.Exact, 1) + cmd.ParseFlags(args, true) + + rdr, _, _, err := cli.call("GET", "/images/"+cmd.Arg(0)+"/history", nil, nil) + if err != nil { + return err + } + + history := []types.ImageHistory{} + if err := json.NewDecoder(rdr).Decode(&history); err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + if !*quiet { + fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT") + } + + for _, entry := range history { + if *noTrunc { + fmt.Fprintf(w, entry.ID) + } else { + fmt.Fprintf(w, stringid.TruncateID(entry.ID)) + } + if !*quiet { + if *human { + fmt.Fprintf(w, "\t%s ago\t", units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0)))) + } else { + fmt.Fprintf(w, "\t%s\t", time.Unix(entry.Created, 0).Format(time.RFC3339)) + } + + if *noTrunc { + fmt.Fprintf(w, "%s\t", entry.CreatedBy) + } else { + fmt.Fprintf(w, "%s\t", stringutils.Truncate(entry.CreatedBy, 45)) + } + + if *human { + fmt.Fprintf(w, "%s\t", units.HumanSize(float64(entry.Size))) + } else { + fmt.Fprintf(w, "%d\t", entry.Size) + } + + fmt.Fprintf(w, "%s", entry.Comment) + } + fmt.Fprintf(w, "\n") + } + w.Flush() + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/images.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/images.go new file mode 100644 index 0000000..6def371 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/images.go @@ -0,0 +1,126 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + "text/tabwriter" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/units" + "github.com/docker/docker/utils" +) + +// CmdImages lists the images in a specified repository, or all top-level images if no repository is specified. +// +// Usage: docker images [OPTIONS] [REPOSITORY] +func (cli *DockerCli) CmdImages(args ...string) error { + cmd := cli.Subcmd("images", "[REPOSITORY]", "List images", true) + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") + all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)") + noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") + showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests") + + flFilter := opts.NewListOpts(nil) + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + cmd.Require(flag.Max, 1) + cmd.ParseFlags(args, true) + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + imageFilterArgs := filters.Args{} + for _, f := range flFilter.GetAll() { + var err error + imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs) + if err != nil { + return err + } + } + + matchName := cmd.Arg(0) + v := url.Values{} + if len(imageFilterArgs) > 0 { + filterJSON, err := filters.ToParam(imageFilterArgs) + if err != nil { + return err + } + v.Set("filters", filterJSON) + } + + if cmd.NArg() == 1 { + // FIXME rename this parameter, to not be confused with the filters flag + v.Set("filter", matchName) + } + if *all { + v.Set("all", "1") + } + + rdr, _, _, err := cli.call("GET", "/images/json?"+v.Encode(), nil, nil) + if err != nil { + return err + } + + images := []types.Image{} + if err := json.NewDecoder(rdr).Decode(&images); err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + if !*quiet { + if *showDigests { + fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tVIRTUAL SIZE") + } else { + fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE") + } + } + + for _, image := range images { + ID := image.ID + if !*noTrunc { + ID = stringid.TruncateID(ID) + } + + repoTags := image.RepoTags + repoDigests := image.RepoDigests + + if len(repoTags) == 1 && repoTags[0] == ":" && len(repoDigests) == 1 && repoDigests[0] == "@" { + // dangling image - clear out either repoTags or repoDigsts so we only show it once below + repoDigests = []string{} + } + + // combine the tags and digests lists + tagsAndDigests := append(repoTags, repoDigests...) + for _, repoAndRef := range tagsAndDigests { + repo, ref := parsers.ParseRepositoryTag(repoAndRef) + // default tag and digest to none - if there's a value, it'll be set below + tag := "" + digest := "" + if utils.DigestReference(ref) { + digest = ref + } else { + tag = ref + } + + if !*quiet { + if *showDigests { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.VirtualSize))) + } else { + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.VirtualSize))) + } + } else { + fmt.Fprintln(w, ID) + } + } + } + + if !*quiet { + w.Flush() + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/import.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/import.go new file mode 100644 index 0000000..48c5689 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/import.go @@ -0,0 +1,64 @@ +package client + +import ( + "fmt" + "io" + "net/url" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" +) + +// CmdImport creates an empty filesystem image, imports the contents of the tarball into the image, and optionally tags the image. +// +// The URL argument is the address of a tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) file. If the URL is '-', then the tar file is read from STDIN. +// +// Usage: docker import [OPTIONS] URL [REPOSITORY[:TAG]] +func (cli *DockerCli) CmdImport(args ...string) error { + cmd := cli.Subcmd("import", "URL|- [REPOSITORY[:TAG]]", "Create an empty filesystem image and import the contents of the\ntarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then\noptionally tag it.", true) + flChanges := opts.NewListOpts(nil) + cmd.Var(&flChanges, []string{"c", "-change"}, "Apply Dockerfile instruction to the created image") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var ( + v = url.Values{} + src = cmd.Arg(0) + repository = cmd.Arg(1) + ) + + v.Set("fromSrc", src) + v.Set("repo", repository) + for _, change := range flChanges.GetAll() { + v.Add("changes", change) + } + if cmd.NArg() == 3 { + fmt.Fprintf(cli.err, "[DEPRECATED] The format 'URL|- [REPOSITORY [TAG]]' has been deprecated. Please use URL|- [REPOSITORY[:TAG]]\n") + v.Set("tag", cmd.Arg(2)) + } + + if repository != "" { + //Check if the given image name can be resolved + repo, _ := parsers.ParseRepositoryTag(repository) + if err := registry.ValidateRepositoryName(repo); err != nil { + return err + } + } + + var in io.Reader + + if src == "-" { + in = cli.in + } + + sopts := &streamOpts{ + rawTerminal: true, + in: in, + out: cli.out, + } + + return cli.stream("POST", "/images/create?"+v.Encode(), sopts) +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/info.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/info.go new file mode 100644 index 0000000..5214742 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/info.go @@ -0,0 +1,91 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/ioutils" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/units" +) + +// CmdInfo displays system-wide information. +// +// Usage: docker info +func (cli *DockerCli) CmdInfo(args ...string) error { + cmd := cli.Subcmd("info", "", "Display system-wide information", true) + cmd.Require(flag.Exact, 0) + cmd.ParseFlags(args, true) + + rdr, _, _, err := cli.call("GET", "/info", nil, nil) + if err != nil { + return err + } + + info := &types.Info{} + if err := json.NewDecoder(rdr).Decode(info); err != nil { + return fmt.Errorf("Error reading remote info: %v", err) + } + + fmt.Fprintf(cli.out, "Containers: %d\n", info.Containers) + fmt.Fprintf(cli.out, "Images: %d\n", info.Images) + ioutils.FprintfIfNotEmpty(cli.out, "Storage Driver: %s\n", info.Driver) + if info.DriverStatus != nil { + for _, pair := range info.DriverStatus { + fmt.Fprintf(cli.out, " %s: %s\n", pair[0], pair[1]) + } + } + ioutils.FprintfIfNotEmpty(cli.out, "Execution Driver: %s\n", info.ExecutionDriver) + ioutils.FprintfIfNotEmpty(cli.out, "Logging Driver: %s\n", info.LoggingDriver) + ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion) + ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem) + fmt.Fprintf(cli.out, "CPUs: %d\n", info.NCPU) + fmt.Fprintf(cli.out, "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) + ioutils.FprintfIfNotEmpty(cli.out, "Name: %s\n", info.Name) + ioutils.FprintfIfNotEmpty(cli.out, "ID: %s\n", info.ID) + + if info.Debug { + fmt.Fprintf(cli.out, "Debug mode (server): %v\n", info.Debug) + fmt.Fprintf(cli.out, "File Descriptors: %d\n", info.NFd) + fmt.Fprintf(cli.out, "Goroutines: %d\n", info.NGoroutines) + fmt.Fprintf(cli.out, "System Time: %s\n", info.SystemTime) + fmt.Fprintf(cli.out, "EventsListeners: %d\n", info.NEventsListener) + fmt.Fprintf(cli.out, "Init SHA1: %s\n", info.InitSha1) + fmt.Fprintf(cli.out, "Init Path: %s\n", info.InitPath) + fmt.Fprintf(cli.out, "Docker Root Dir: %s\n", info.DockerRootDir) + } + + ioutils.FprintfIfNotEmpty(cli.out, "Http Proxy: %s\n", info.HttpProxy) + ioutils.FprintfIfNotEmpty(cli.out, "Https Proxy: %s\n", info.HttpsProxy) + ioutils.FprintfIfNotEmpty(cli.out, "No Proxy: %s\n", info.NoProxy) + + if info.IndexServerAddress != "" { + u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username + if len(u) > 0 { + fmt.Fprintf(cli.out, "Username: %v\n", u) + fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress) + } + } + if !info.MemoryLimit { + fmt.Fprintf(cli.err, "WARNING: No memory limit support\n") + } + if !info.SwapLimit { + fmt.Fprintf(cli.err, "WARNING: No swap limit support\n") + } + if !info.IPv4Forwarding { + fmt.Fprintf(cli.err, "WARNING: IPv4 forwarding is disabled.\n") + } + if info.Labels != nil { + fmt.Fprintln(cli.out, "Labels:") + for _, attribute := range info.Labels { + fmt.Fprintf(cli.out, " %s\n", attribute) + } + } + + if info.ExperimentalBuild { + fmt.Fprintf(cli.out, "Experimental: true\n") + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/inspect.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/inspect.go new file mode 100644 index 0000000..734d239 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/inspect.go @@ -0,0 +1,124 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "text/template" + + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdInspect displays low-level information on one or more containers or images. +// +// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...] +func (cli *DockerCli) CmdInspect(args ...string) error { + cmd := cli.Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container or image", true) + tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var tmpl *template.Template + if *tmplStr != "" { + var err error + if tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr); err != nil { + return StatusError{StatusCode: 64, + Status: "Template parsing error: " + err.Error()} + } + } + + indented := new(bytes.Buffer) + indented.WriteString("[\n") + status := 0 + isImage := false + + for _, name := range cmd.Args() { + obj, _, err := readBody(cli.call("GET", "/containers/"+name+"/json", nil, nil)) + if err != nil { + obj, _, err = readBody(cli.call("GET", "/images/"+name+"/json", nil, nil)) + isImage = true + if err != nil { + if strings.Contains(err.Error(), "No such") { + fmt.Fprintf(cli.err, "Error: No such image or container: %s\n", name) + } else { + fmt.Fprintf(cli.err, "%s", err) + } + status = 1 + continue + } + } + + if tmpl == nil { + if err = json.Indent(indented, obj, "", " "); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 + continue + } + } else { + rdr := bytes.NewReader(obj) + dec := json.NewDecoder(rdr) + + if isImage { + inspPtr := types.ImageInspect{} + if err := dec.Decode(&inspPtr); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 + continue + } + if err := tmpl.Execute(cli.out, inspPtr); err != nil { + rdr.Seek(0, 0) + var raw interface{} + if err := dec.Decode(&raw); err != nil { + return err + } + if err = tmpl.Execute(cli.out, raw); err != nil { + return err + } + } + } else { + inspPtr := types.ContainerJSON{} + if err := dec.Decode(&inspPtr); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 + continue + } + if err := tmpl.Execute(cli.out, inspPtr); err != nil { + rdr.Seek(0, 0) + var raw interface{} + if err := dec.Decode(&raw); err != nil { + return err + } + if err = tmpl.Execute(cli.out, raw); err != nil { + return err + } + } + } + cli.out.Write([]byte{'\n'}) + } + indented.WriteString(",") + } + + if indented.Len() > 1 { + // Remove trailing ',' + indented.Truncate(indented.Len() - 1) + } + indented.WriteString("]\n") + + if tmpl == nil { + // Note that we will always write "[]" when "-f" isn't specified, + // to make sure the output would always be array, see + // https://github.com/docker/docker/pull/9500#issuecomment-65846734 + if _, err := io.Copy(cli.out, indented); err != nil { + return err + } + } + + if status != 0 { + return StatusError{StatusCode: status} + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/kill.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/kill.go new file mode 100644 index 0000000..becff3b --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/kill.go @@ -0,0 +1,32 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdKill kills one or more running container using SIGKILL or a specified signal. +// +// Usage: docker kill [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdKill(args ...string) error { + cmd := cli.Subcmd("kill", "CONTAINER [CONTAINER...]", "Kill a running container using SIGKILL or a specified signal", true) + signal := cmd.String([]string{"s", "-signal"}, "KILL", "Signal to send to the container") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errNames []string + for _, name := range cmd.Args() { + if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/kill?signal=%s", name, *signal), nil, nil)); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to kill containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/load.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/load.go new file mode 100644 index 0000000..8dd8bb5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/load.go @@ -0,0 +1,41 @@ +package client + +import ( + "io" + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdLoad loads an image from a tar archive. +// +// The tar archive is read from STDIN by default, or from a tar archive file. +// +// Usage: docker load [OPTIONS] +func (cli *DockerCli) CmdLoad(args ...string) error { + cmd := cli.Subcmd("load", "", "Load an image from a tar archive on STDIN", true) + infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN") + cmd.Require(flag.Exact, 0) + + cmd.ParseFlags(args, true) + + var ( + input io.Reader = cli.in + err error + ) + if *infile != "" { + input, err = os.Open(*infile) + if err != nil { + return err + } + } + sopts := &streamOpts{ + rawTerminal: true, + in: input, + out: cli.out, + } + if err := cli.stream("POST", "/images/load", sopts); err != nil { + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/login.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/login.go new file mode 100644 index 0000000..832c5bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/login.go @@ -0,0 +1,144 @@ +package client + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cliconfig" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/registry" +) + +// CmdLogin logs in or registers a user to a Docker registry service. +// +// If no server is specified, the user will be logged into or registered to the registry's index server. +// +// Usage: docker login SERVER +func (cli *DockerCli) CmdLogin(args ...string) error { + cmd := cli.Subcmd("login", "[SERVER]", "Register or log in to a Docker registry server, if no server is\nspecified \""+registry.IndexServerAddress()+"\" is the default.", true) + cmd.Require(flag.Max, 1) + + var username, password, email string + + cmd.StringVar(&username, []string{"u", "-username"}, "", "Username") + cmd.StringVar(&password, []string{"p", "-password"}, "", "Password") + cmd.StringVar(&email, []string{"e", "-email"}, "", "Email") + + cmd.ParseFlags(args, true) + + serverAddress := registry.IndexServerAddress() + if len(cmd.Args()) > 0 { + serverAddress = cmd.Arg(0) + } + + promptDefault := func(prompt string, configDefault string) { + if configDefault == "" { + fmt.Fprintf(cli.out, "%s: ", prompt) + } else { + fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault) + } + } + + readInput := func(in io.Reader, out io.Writer) string { + reader := bufio.NewReader(in) + line, _, err := reader.ReadLine() + if err != nil { + fmt.Fprintln(out, err.Error()) + os.Exit(1) + } + return string(line) + } + + authconfig, ok := cli.configFile.AuthConfigs[serverAddress] + if !ok { + authconfig = cliconfig.AuthConfig{} + } + + if username == "" { + promptDefault("Username", authconfig.Username) + username = readInput(cli.in, cli.out) + username = strings.Trim(username, " ") + if username == "" { + username = authconfig.Username + } + } + // Assume that a different username means they may not want to use + // the password or email from the config file, so prompt them + if username != authconfig.Username { + if password == "" { + oldState, err := term.SaveState(cli.inFd) + if err != nil { + return err + } + fmt.Fprintf(cli.out, "Password: ") + term.DisableEcho(cli.inFd, oldState) + + password = readInput(cli.in, cli.out) + fmt.Fprint(cli.out, "\n") + + term.RestoreTerminal(cli.inFd, oldState) + if password == "" { + return fmt.Errorf("Error : Password Required") + } + } + + if email == "" { + promptDefault("Email", authconfig.Email) + email = readInput(cli.in, cli.out) + if email == "" { + email = authconfig.Email + } + } + } else { + // However, if they don't override the username use the + // password or email from the cmd line if specified. IOW, allow + // then to change/override them. And if not specified, just + // use what's in the config file + if password == "" { + password = authconfig.Password + } + if email == "" { + email = authconfig.Email + } + } + authconfig.Username = username + authconfig.Password = password + authconfig.Email = email + authconfig.ServerAddress = serverAddress + cli.configFile.AuthConfigs[serverAddress] = authconfig + + stream, _, statusCode, err := cli.call("POST", "/auth", cli.configFile.AuthConfigs[serverAddress], nil) + if statusCode == 401 { + delete(cli.configFile.AuthConfigs, serverAddress) + if err2 := cli.configFile.Save(); err2 != nil { + fmt.Fprintf(cli.out, "WARNING: could not save config file: %v\n", err2) + } + return err + } + if err != nil { + return err + } + + var response types.AuthResponse + if err := json.NewDecoder(stream).Decode(&response); err != nil { + // Upon error, remove entry + delete(cli.configFile.AuthConfigs, serverAddress) + return err + } + + if err := cli.configFile.Save(); err != nil { + return fmt.Errorf("Error saving config file: %v", err) + } + fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s\n", cli.configFile.Filename()) + + if response.Status != "" { + fmt.Fprintf(cli.out, "%s\n", response.Status) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/logout.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/logout.go new file mode 100644 index 0000000..a571af2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/logout.go @@ -0,0 +1,36 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/registry" +) + +// CmdLogout logs a user out from a Docker registry. +// +// If no server is specified, the user will be logged out from the registry's index server. +// +// Usage: docker logout [SERVER] +func (cli *DockerCli) CmdLogout(args ...string) error { + cmd := cli.Subcmd("logout", "[SERVER]", "Log out from a Docker registry, if no server is\nspecified \""+registry.IndexServerAddress()+"\" is the default.", true) + cmd.Require(flag.Max, 1) + + cmd.ParseFlags(args, true) + serverAddress := registry.IndexServerAddress() + if len(cmd.Args()) > 0 { + serverAddress = cmd.Arg(0) + } + + if _, ok := cli.configFile.AuthConfigs[serverAddress]; !ok { + fmt.Fprintf(cli.out, "Not logged in to %s\n", serverAddress) + } else { + fmt.Fprintf(cli.out, "Remove login credentials for %s\n", serverAddress) + delete(cli.configFile.AuthConfigs, serverAddress) + + if err := cli.configFile.Save(); err != nil { + return fmt.Errorf("Failed to save docker config: %v", err) + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/logs.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/logs.go new file mode 100644 index 0000000..0309144 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/logs.go @@ -0,0 +1,69 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/timeutils" +) + +// CmdLogs fetches the logs of a given container. +// +// docker logs [OPTIONS] CONTAINER +func (cli *DockerCli) CmdLogs(args ...string) error { + var ( + cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container", true) + follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") + since = cmd.String([]string{"-since"}, "", "Show logs since timestamp") + times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") + tail = cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the logs") + ) + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + name := cmd.Arg(0) + + stream, _, _, err := cli.call("GET", "/containers/"+name+"/json", nil, nil) + if err != nil { + return err + } + + var c types.ContainerJSON + if err := json.NewDecoder(stream).Decode(&c); err != nil { + return err + } + + if logType := c.HostConfig.LogConfig.Type; logType != "json-file" { + return fmt.Errorf("\"logs\" command is supported only for \"json-file\" logging driver (got: %s)", logType) + } + + v := url.Values{} + v.Set("stdout", "1") + v.Set("stderr", "1") + + if *since != "" { + v.Set("since", timeutils.GetTimestamp(*since, time.Now())) + } + + if *times { + v.Set("timestamps", "1") + } + + if *follow { + v.Set("follow", "1") + } + v.Set("tail", *tail) + + sopts := &streamOpts{ + rawTerminal: c.Config.Tty, + out: cli.out, + err: cli.err, + } + + return cli.stream("GET", "/containers/"+name+"/logs?"+v.Encode(), sopts) +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/pause.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/pause.go new file mode 100644 index 0000000..ee690c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/pause.go @@ -0,0 +1,30 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdPause pauses all processes within one or more containers. +// +// Usage: docker pause CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdPause(args ...string) error { + cmd := cli.Subcmd("pause", "CONTAINER [CONTAINER...]", "Pause all processes within a container", true) + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + var errNames []string + for _, name := range cmd.Args() { + if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/pause", name), nil, nil)); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to pause containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/port.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/port.go new file mode 100644 index 0000000..1721bc1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/port.go @@ -0,0 +1,64 @@ +package client + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/docker/docker/nat" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdPort lists port mappings for a container. +// If a private port is specified, it also shows the public-facing port that is NATed to the private port. +// +// Usage: docker port CONTAINER [PRIVATE_PORT[/PROTO]] +func (cli *DockerCli) CmdPort(args ...string) error { + cmd := cli.Subcmd("port", "CONTAINER [PRIVATE_PORT[/PROTO]]", "List port mappings for the CONTAINER, or lookup the public-facing port that\nis NAT-ed to the PRIVATE_PORT", true) + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + stream, _, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil) + if err != nil { + return err + } + + var c struct { + NetworkSettings struct { + Ports nat.PortMap + } + } + + if err := json.NewDecoder(stream).Decode(&c); err != nil { + return err + } + + if cmd.NArg() == 2 { + var ( + port = cmd.Arg(1) + proto = "tcp" + parts = strings.SplitN(port, "/", 2) + ) + + if len(parts) == 2 && len(parts[1]) != 0 { + port = parts[0] + proto = parts[1] + } + natPort := port + "/" + proto + if frontends, exists := c.NetworkSettings.Ports[nat.Port(port+"/"+proto)]; exists && frontends != nil { + for _, frontend := range frontends { + fmt.Fprintf(cli.out, "%s:%s\n", frontend.HostIp, frontend.HostPort) + } + return nil + } + return fmt.Errorf("Error: No public port '%s' published for %s", natPort, cmd.Arg(0)) + } + + for from, frontends := range c.NetworkSettings.Ports { + for _, frontend := range frontends { + fmt.Fprintf(cli.out, "%s -> %s:%s\n", from, frontend.HostIp, frontend.HostPort) + } + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/ps.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/ps.go new file mode 100644 index 0000000..5d241db --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/ps.go @@ -0,0 +1,175 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + "text/tabwriter" + "time" + + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/pkg/units" +) + +// CmdPs outputs a list of Docker containers. +// +// Usage: docker ps [OPTIONS] +func (cli *DockerCli) CmdPs(args ...string) error { + var ( + err error + + psFilterArgs = filters.Args{} + v = url.Values{} + + cmd = cli.Subcmd("ps", "", "List containers", true) + quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") + size = cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes") + all = cmd.Bool([]string{"a", "-all"}, false, "Show all containers (default shows just running)") + noTrunc = cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") + nLatest = cmd.Bool([]string{"l", "-latest"}, false, "Show the latest created container, include non-running") + since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running") + before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name") + last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running") + flFilter = opts.NewListOpts(nil) + ) + cmd.Require(flag.Exact, 0) + + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + + cmd.ParseFlags(args, true) + if *last == -1 && *nLatest { + *last = 1 + } + + if *all { + v.Set("all", "1") + } + + if *last != -1 { + v.Set("limit", strconv.Itoa(*last)) + } + + if *since != "" { + v.Set("since", *since) + } + + if *before != "" { + v.Set("before", *before) + } + + if *size { + v.Set("size", "1") + } + + // Consolidate all filter flags, and sanity check them. + // They'll get processed in the daemon/server. + for _, f := range flFilter.GetAll() { + if psFilterArgs, err = filters.ParseFlag(f, psFilterArgs); err != nil { + return err + } + } + + if len(psFilterArgs) > 0 { + filterJSON, err := filters.ToParam(psFilterArgs) + if err != nil { + return err + } + + v.Set("filters", filterJSON) + } + + rdr, _, _, err := cli.call("GET", "/containers/json?"+v.Encode(), nil, nil) + if err != nil { + return err + } + + containers := []types.Container{} + if err := json.NewDecoder(rdr).Decode(&containers); err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + if !*quiet { + fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") + + if *size { + fmt.Fprintln(w, "\tSIZE") + } else { + fmt.Fprint(w, "\n") + } + } + + stripNamePrefix := func(ss []string) []string { + for i, s := range ss { + ss[i] = s[1:] + } + + return ss + } + + for _, container := range containers { + ID := container.ID + + if !*noTrunc { + ID = stringid.TruncateID(ID) + } + + if *quiet { + fmt.Fprintln(w, ID) + + continue + } + + var ( + names = stripNamePrefix(container.Names) + command = strconv.Quote(container.Command) + ) + + if !*noTrunc { + command = stringutils.Truncate(command, 20) + + // only display the default name for the container with notrunc is passed + for _, name := range names { + if len(strings.Split(name, "/")) == 1 { + names = []string{name} + break + } + } + } + + image := container.Image + if image == "" { + image = "" + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", ID, image, command, + units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(container.Created), 0))), + container.Status, api.DisplayablePorts(container.Ports), strings.Join(names, ",")) + + if *size { + if container.SizeRootFs > 0 { + fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(float64(container.SizeRw)), units.HumanSize(float64(container.SizeRootFs))) + } else { + fmt.Fprintf(w, "%s\n", units.HumanSize(float64(container.SizeRw))) + } + + continue + } + + fmt.Fprint(w, "\n") + } + + if !*quiet { + w.Flush() + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/pull.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/pull.go new file mode 100644 index 0000000..4be30b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/pull.go @@ -0,0 +1,47 @@ +package client + +import ( + "fmt" + "net/url" + + "github.com/docker/docker/graph/tags" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" + "github.com/docker/docker/utils" +) + +// CmdPull pulls an image or a repository from the registry. +// +// Usage: docker pull [OPTIONS] IMAGENAME[:TAG|@DIGEST] +func (cli *DockerCli) CmdPull(args ...string) error { + cmd := cli.Subcmd("pull", "NAME[:TAG|@DIGEST]", "Pull an image or a repository from the registry", true) + allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + var ( + v = url.Values{} + remote = cmd.Arg(0) + newRemote = remote + ) + taglessRemote, tag := parsers.ParseRepositoryTag(remote) + if tag == "" && !*allTags { + newRemote = utils.ImageReference(taglessRemote, tags.DEFAULTTAG) + } + if tag != "" && *allTags { + return fmt.Errorf("tag can't be used with --all-tags/-a") + } + + v.Set("fromImage", newRemote) + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(taglessRemote) + if err != nil { + return err + } + + _, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") + return err +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/push.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/push.go new file mode 100644 index 0000000..dc4266c --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/push.go @@ -0,0 +1,49 @@ +package client + +import ( + "fmt" + "net/url" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" +) + +// CmdPush pushes an image or repository to the registry. +// +// Usage: docker push NAME[:TAG] +func (cli *DockerCli) CmdPush(args ...string) error { + cmd := cli.Subcmd("push", "NAME[:TAG]", "Push an image or a repository to the registry", true) + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + name := cmd.Arg(0) + + remote, tag := parsers.ParseRepositoryTag(name) + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(remote) + if err != nil { + return err + } + // Resolve the Auth config relevant for this server + authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) + // If we're not using a custom registry, we know the restrictions + // applied to repository names and can warn the user in advance. + // Custom repositories can have different rules, and we must also + // allow pushing by image ID. + if repoInfo.Official { + username := authConfig.Username + if username == "" { + username = "" + } + return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to / (ex: %s/%s)", username, repoInfo.LocalName) + } + + v := url.Values{} + v.Set("tag", tag) + + _, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push") + return err +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/rename.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/rename.go new file mode 100644 index 0000000..ebe1696 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/rename.go @@ -0,0 +1,25 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdRename renames a container. +// +// Usage: docker rename OLD_NAME NEW_NAME +func (cli *DockerCli) CmdRename(args ...string) error { + cmd := cli.Subcmd("rename", "OLD_NAME NEW_NAME", "Rename a container", true) + cmd.Require(flag.Exact, 2) + cmd.ParseFlags(args, true) + + oldName := cmd.Arg(0) + newName := cmd.Arg(1) + + if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/rename?name=%s", oldName, newName), nil, nil)); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + return fmt.Errorf("Error: failed to rename container named %s", oldName) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/restart.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/restart.go new file mode 100644 index 0000000..c769fb6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/restart.go @@ -0,0 +1,38 @@ +package client + +import ( + "fmt" + "net/url" + "strconv" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdRestart restarts one or more running containers. +// +// Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdRestart(args ...string) error { + cmd := cli.Subcmd("restart", "CONTAINER [CONTAINER...]", "Restart a running container", true) + nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing the container") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + v := url.Values{} + v.Set("t", strconv.Itoa(*nSeconds)) + + var errNames []string + for _, name := range cmd.Args() { + _, _, err := readBody(cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to restart containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/rm.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/rm.go new file mode 100644 index 0000000..e6f3aea --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/rm.go @@ -0,0 +1,54 @@ +package client + +import ( + "fmt" + "net/url" + "strings" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdRm removes one or more containers. +// +// Usage: docker rm [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdRm(args ...string) error { + cmd := cli.Subcmd("rm", "CONTAINER [CONTAINER...]", "Remove one or more containers", true) + v := cmd.Bool([]string{"v", "-volumes"}, false, "Remove the volumes associated with the container") + link := cmd.Bool([]string{"l", "#link", "-link"}, false, "Remove the specified link") + force := cmd.Bool([]string{"f", "-force"}, false, "Force the removal of a running container (uses SIGKILL)") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + val := url.Values{} + if *v { + val.Set("v", "1") + } + if *link { + val.Set("link", "1") + } + + if *force { + val.Set("force", "1") + } + + var errNames []string + for _, name := range cmd.Args() { + if name == "" { + return fmt.Errorf("Container name cannot be empty") + } + name = strings.Trim(name, "/") + + _, _, err := readBody(cli.call("DELETE", "/containers/"+name+"?"+val.Encode(), nil, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to remove containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/rmi.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/rmi.go new file mode 100644 index 0000000..0f773f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/rmi.go @@ -0,0 +1,59 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdRmi removes all images with the specified name(s). +// +// Usage: docker rmi [OPTIONS] IMAGE [IMAGE...] +func (cli *DockerCli) CmdRmi(args ...string) error { + var ( + cmd = cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images", true) + force = cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image") + noprune = cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents") + ) + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + v := url.Values{} + if *force { + v.Set("force", "1") + } + if *noprune { + v.Set("noprune", "1") + } + + var errNames []string + for _, name := range cmd.Args() { + rdr, _, _, err := cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil, nil) + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + dels := []types.ImageDelete{} + if err := json.NewDecoder(rdr).Decode(&dels); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + continue + } + + for _, del := range dels { + if del.Deleted != "" { + fmt.Fprintf(cli.out, "Deleted: %s\n", del.Deleted) + } else { + fmt.Fprintf(cli.out, "Untagged: %s\n", del.Untagged) + } + } + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to remove images: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/run.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/run.go new file mode 100644 index 0000000..c4eb528 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/run.go @@ -0,0 +1,247 @@ +package client + +import ( + "fmt" + "io" + "net/url" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/runconfig" + "github.com/docker/libnetwork/resolvconf/dns" +) + +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 +} + +// CmdRun runs a command in a new container. +// +// Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] +func (cli *DockerCli) CmdRun(args ...string) error { + cmd := cli.Subcmd("run", "IMAGE [COMMAND] [ARG...]", "Run a command in a new container", true) + + // These are flags not stored in Config/HostConfig + var ( + flAutoRemove = cmd.Bool([]string{"-rm"}, false, "Automatically remove the container when it exits") + flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Run container in background and print container ID") + flSigProxy = cmd.Bool([]string{"-sig-proxy"}, true, "Proxy received signals to the process") + flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") + flAttach *opts.ListOpts + + ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d") + ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm") + ErrConflictDetachAutoRemove = fmt.Errorf("Conflicting options: --rm and -d") + ) + + config, hostConfig, cmd, err := runconfig.Parse(cmd, args) + // just in case the Parse does not exit + if err != nil { + cmd.ReportError(err.Error(), true) + os.Exit(1) + } + + 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(cli.err, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) + break + } + } + } + if config.Image == "" { + cmd.Usage() + return nil + } + + if !*flDetach { + if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil { + return err + } + } else { + if fl := cmd.Lookup("-attach"); fl != nil { + flAttach = fl.Value.(*opts.ListOpts) + if flAttach.Len() != 0 { + return ErrConflictAttachDetach + } + } + if *flAutoRemove { + return ErrConflictDetachAutoRemove + } + + config.AttachStdin = false + config.AttachStdout = false + config.AttachStderr = false + config.StdinOnce = false + } + + // Disable flSigProxy when in TTY mode + sigProxy := *flSigProxy + if config.Tty { + sigProxy = false + } + + createResponse, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName) + if err != nil { + return err + } + if sigProxy { + sigc := cli.forwardAllSignals(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(cli.out, "%s\n", createResponse.ID) + }() + } + if *flAutoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) { + return ErrConflictRestartPolicyAndAutoRemove + } + // We need to instantiate the chan because the select needs it. It can + // be closed but can't be uninitialized. + hijacked := make(chan io.Closer) + // Block the return until the chan gets closed + defer func() { + logrus.Debugf("End of CmdRun(), Waiting for hijack to finish.") + if _, ok := <-hijacked; ok { + fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)") + } + }() + if config.AttachStdin || config.AttachStdout || config.AttachStderr { + var ( + out, stderr io.Writer + in io.ReadCloser + v = url.Values{} + ) + v.Set("stream", "1") + if config.AttachStdin { + v.Set("stdin", "1") + in = cli.in + } + if config.AttachStdout { + v.Set("stdout", "1") + out = cli.out + } + if config.AttachStderr { + v.Set("stderr", "1") + if config.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + errCh = promise.Go(func() error { + return cli.hijack("POST", "/containers/"+createResponse.ID+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked, nil) + }) + } else { + close(hijacked) + } + // Acknowledge the hijack before starting + select { + case closer := <-hijacked: + // Make sure that the hijack gets closed when returning (results + // in closing the hijack chan and freeing server's goroutines) + if closer != nil { + defer closer.Close() + } + case err := <-errCh: + if err != nil { + logrus.Debugf("Error hijack: %s", err) + return err + } + } + + defer func() { + if *flAutoRemove { + if _, _, err = readBody(cli.call("DELETE", "/containers/"+createResponse.ID+"?v=1", nil, nil)); err != nil { + fmt.Fprintf(cli.err, "Error deleting container: %s\n", err) + } + } + }() + + //start the container + if _, _, err = readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil { + return err + } + + if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut { + if err := cli.monitorTtySize(createResponse.ID, false); err != nil { + fmt.Fprintf(cli.err, "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 + } + + var status int + + // Attached mode + if *flAutoRemove { + // Autoremove: wait for the container to finish, retrieve + // the exit code and remove the container + if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/wait", nil, nil)); err != nil { + return err + } + if _, status, err = getExitCode(cli, createResponse.ID); err != nil { + return err + } + } else { + // No Autoremove: Simply retrieve the exit code + if !config.Tty { + // In non-TTY mode, we can't detach, so we must wait for container exit + if status, err = waitForExit(cli, createResponse.ID); err != nil { + return err + } + } else { + // In TTY mode, there is a race: if the process dies too slowly, the state could + // be updated after the getExitCode call and result in the wrong exit code being reported + if _, status, err = getExitCode(cli, createResponse.ID); err != nil { + return err + } + } + } + if status != 0 { + return StatusError{StatusCode: status} + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/save.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/save.go new file mode 100644 index 0000000..a04cbcf --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/save.go @@ -0,0 +1,57 @@ +package client + +import ( + "errors" + "io" + "net/url" + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdSave saves one or more images to a tar archive. +// +// The tar archive is written to STDOUT by default, or written to a file. +// +// Usage: docker save [OPTIONS] IMAGE [IMAGE...] +func (cli *DockerCli) CmdSave(args ...string) error { + cmd := cli.Subcmd("save", "IMAGE [IMAGE...]", "Save an image(s) to a tar archive (streamed to STDOUT by default)", true) + outfile := cmd.String([]string{"o", "-output"}, "", "Write to an file, instead of STDOUT") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var ( + output io.Writer = cli.out + err error + ) + if *outfile != "" { + output, err = os.Create(*outfile) + if err != nil { + return err + } + } else if cli.isTerminalOut { + return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") + } + + sopts := &streamOpts{ + rawTerminal: true, + out: output, + } + + if len(cmd.Args()) == 1 { + image := cmd.Arg(0) + if err := cli.stream("GET", "/images/"+image+"/get", sopts); err != nil { + return err + } + } else { + v := url.Values{} + for _, arg := range cmd.Args() { + v.Add("names", arg) + } + if err := cli.stream("GET", "/images/get?"+v.Encode(), sopts); err != nil { + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/search.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/search.go new file mode 100644 index 0000000..e606d47 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/search.go @@ -0,0 +1,84 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + "sort" + "strings" + "text/tabwriter" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/registry" +) + +// ByStars sorts search results in ascending order by number of stars. +type ByStars []registry.SearchResult + +func (r ByStars) Len() int { return len(r) } +func (r ByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r ByStars) Less(i, j int) bool { return r[i].StarCount < r[j].StarCount } + +// CmdSearch searches the Docker Hub for images. +// +// Usage: docker search [OPTIONS] TERM +func (cli *DockerCli) CmdSearch(args ...string) error { + cmd := cli.Subcmd("search", "TERM", "Search the Docker Hub for images", true) + noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") + trusted := cmd.Bool([]string{"#t", "#trusted", "#-trusted"}, false, "Only show trusted builds") + automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds") + stars := cmd.Uint([]string{"s", "#stars", "-stars"}, 0, "Only displays with at least x stars") + cmd.Require(flag.Exact, 1) + + cmd.ParseFlags(args, true) + + name := cmd.Arg(0) + v := url.Values{} + v.Set("term", name) + + // Resolve the Repository name from fqn to hostname + name + taglessRemote, _ := parsers.ParseRepositoryTag(name) + repoInfo, err := registry.ParseRepositoryInfo(taglessRemote) + if err != nil { + return err + } + + rdr, _, err := cli.clientRequestAttemptLogin("GET", "/images/search?"+v.Encode(), nil, nil, repoInfo.Index, "search") + if err != nil { + return err + } + + results := ByStars{} + if err := json.NewDecoder(rdr).Decode(&results); err != nil { + return err + } + + sort.Sort(sort.Reverse(results)) + + w := tabwriter.NewWriter(cli.out, 10, 1, 3, ' ', 0) + fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") + for _, res := range results { + if ((*automated || *trusted) && (!res.IsTrusted && !res.IsAutomated)) || (int(*stars) > res.StarCount) { + continue + } + desc := strings.Replace(res.Description, "\n", " ", -1) + desc = strings.Replace(desc, "\r", " ", -1) + if !*noTrunc && len(desc) > 45 { + desc = stringutils.Truncate(desc, 42) + "..." + } + fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount) + if res.IsOfficial { + fmt.Fprint(w, "[OK]") + + } + fmt.Fprint(w, "\t") + if res.IsAutomated || res.IsTrusted { + fmt.Fprint(w, "[OK]") + } + fmt.Fprint(w, "\n") + } + w.Flush() + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/start.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/start.go new file mode 100644 index 0000000..22786ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/start.go @@ -0,0 +1,167 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/url" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" +) + +func (cli *DockerCli) forwardAllSignals(cid string) chan os.Signal { + sigc := make(chan os.Signal, 128) + signal.CatchAll(sigc) + go func() { + for s := range sigc { + if s == signal.SIGCHLD { + 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) + } + if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/kill?signal=%s", cid, sig), nil, nil)); err != nil { + logrus.Debugf("Error sending signal: %s", err) + } + } + }() + return sigc +} + +// CmdStart starts one or more stopped containers. +// +// Usage: docker start [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdStart(args ...string) error { + var ( + cErr chan error + tty bool + + cmd = cli.Subcmd("start", "CONTAINER [CONTAINER...]", "Start one or more stopped containers", true) + attach = cmd.Bool([]string{"a", "-attach"}, false, "Attach STDOUT/STDERR and forward signals") + openStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN") + ) + + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + if *attach || *openStdin { + if cmd.NArg() > 1 { + return fmt.Errorf("You cannot start and attach multiple containers at once.") + } + + stream, _, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil) + if err != nil { + return err + } + + var c types.ContainerJSON + if err := json.NewDecoder(stream).Decode(&c); err != nil { + return err + } + + tty = c.Config.Tty + + if !tty { + sigc := cli.forwardAllSignals(cmd.Arg(0)) + defer signal.StopCatch(sigc) + } + + var in io.ReadCloser + + v := url.Values{} + v.Set("stream", "1") + + if *openStdin && c.Config.OpenStdin { + v.Set("stdin", "1") + in = cli.in + } + + v.Set("stdout", "1") + v.Set("stderr", "1") + + hijacked := make(chan io.Closer) + // Block the return until the chan gets closed + defer func() { + logrus.Debugf("CmdStart() returned, defer waiting for hijack to finish.") + if _, ok := <-hijacked; ok { + fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)") + } + cli.in.Close() + }() + cErr = promise.Go(func() error { + return cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, hijacked, nil) + }) + + // Acknowledge the hijack before starting + select { + case closer := <-hijacked: + // Make sure that the hijack gets closed when returning (results + // in closing the hijack chan and freeing server's goroutines) + if closer != nil { + defer closer.Close() + } + case err := <-cErr: + if err != nil { + return err + } + } + } + + var encounteredError error + var errNames []string + for _, name := range cmd.Args() { + _, _, err := readBody(cli.call("POST", "/containers/"+name+"/start", nil, nil)) + if err != nil { + if !*attach && !*openStdin { + // attach and openStdin is false means it could be starting multiple containers + // when a container start failed, show the error message and start next + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + encounteredError = err + } + } else { + if !*attach && !*openStdin { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + } + + if len(errNames) > 0 { + encounteredError = fmt.Errorf("Error: failed to start containers: %v", errNames) + } + if encounteredError != nil { + return encounteredError + } + + if *openStdin || *attach { + if tty && cli.isTerminalOut { + if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil { + fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err) + } + } + if attchErr := <-cErr; attchErr != nil { + return attchErr + } + _, status, err := getExitCode(cli, cmd.Arg(0)) + if err != nil { + return err + } + if status != 0 { + return StatusError{StatusCode: status} + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/stats.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/stats.go new file mode 100644 index 0000000..3dd05b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/stats.go @@ -0,0 +1,198 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/url" + "sort" + "strings" + "sync" + "text/tabwriter" + "time" + + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/units" +) + +type containerStats struct { + Name string + CPUPercentage float64 + Memory float64 + MemoryLimit float64 + MemoryPercentage float64 + NetworkRx float64 + NetworkTx float64 + mu sync.RWMutex + err error +} + +func (s *containerStats) Collect(cli *DockerCli, streamStats bool) { + v := url.Values{} + if streamStats { + v.Set("stream", "1") + } else { + v.Set("stream", "0") + } + stream, _, _, err := cli.call("GET", "/containers/"+s.Name+"/stats?"+v.Encode(), nil, nil) + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + return + } + defer stream.Close() + var ( + previousCPU uint64 + previousSystem uint64 + dec = json.NewDecoder(stream) + u = make(chan error, 1) + ) + go func() { + for { + var v *types.Stats + if err := dec.Decode(&v); err != nil { + u <- err + return + } + var ( + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + cpuPercent = 0.0 + ) + previousCPU = v.PreCpuStats.CpuUsage.TotalUsage + previousSystem = v.PreCpuStats.SystemUsage + cpuPercent = calculateCPUPercent(previousCPU, previousSystem, v) + s.mu.Lock() + s.CPUPercentage = cpuPercent + s.Memory = float64(v.MemoryStats.Usage) + s.MemoryLimit = float64(v.MemoryStats.Limit) + s.MemoryPercentage = memPercent + s.NetworkRx = float64(v.Network.RxBytes) + s.NetworkTx = float64(v.Network.TxBytes) + 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.mu.Unlock() + case err := <-u: + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + return + } + } + if !streamStats { + return + } + } +} + +func (s *containerStats) Display(w io.Writer) error { + s.mu.RLock() + defer s.mu.RUnlock() + if s.err != nil { + return s.err + } + fmt.Fprintf(w, "%s\t%.2f%%\t%s/%s\t%.2f%%\t%s/%s\n", + s.Name, + s.CPUPercentage, + units.HumanSize(s.Memory), units.HumanSize(s.MemoryLimit), + s.MemoryPercentage, + units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx)) + return nil +} + +// CmdStats 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. +// +// Usage: docker stats CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdStats(args ...string) error { + cmd := cli.Subcmd("stats", "CONTAINER [CONTAINER...]", "Display a live stream of one or more containers' resource usage statistics", true) + noStream := cmd.Bool([]string{"-no-stream"}, false, "Disable streaming stats and only pull the first result") + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + names := cmd.Args() + sort.Strings(names) + var ( + cStats []*containerStats + w = tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + ) + printHeader := func() { + if !*noStream { + fmt.Fprint(cli.out, "\033[2J") + fmt.Fprint(cli.out, "\033[H") + } + io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE/LIMIT\tMEM %\tNET I/O\n") + } + for _, n := range names { + s := &containerStats{Name: n} + cStats = append(cStats, s) + go s.Collect(cli, !*noStream) + } + // do a quick pause so that any failed connections for containers that do not exist are able to be + // evicted before we display the initial or default values. + time.Sleep(1500 * time.Millisecond) + var errs []string + for _, c := range cStats { + c.mu.Lock() + if c.err != nil { + errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err)) + } + c.mu.Unlock() + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, ", ")) + } + for range time.Tick(500 * time.Millisecond) { + printHeader() + toRemove := []int{} + for i, s := range cStats { + if err := s.Display(w); err != nil && !*noStream { + toRemove = append(toRemove, i) + } + } + for j := len(toRemove) - 1; j >= 0; j-- { + i := toRemove[j] + cStats = append(cStats[:i], cStats[i+1:]...) + } + if len(cStats) == 0 { + return nil + } + w.Flush() + if *noStream { + break + } + } + return nil +} + +func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.Stats) float64 { + var ( + cpuPercent = 0.0 + // calculate the change for the cpu usage of the container in between readings + cpuDelta = float64(v.CpuStats.CpuUsage.TotalUsage - previousCPU) + // calculate the change for the entire system between readings + systemDelta = float64(v.CpuStats.SystemUsage - previousSystem) + ) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CpuStats.CpuUsage.PercpuUsage)) * 100.0 + } + return cpuPercent +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/stats_unit_test.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/stats_unit_test.go new file mode 100644 index 0000000..0831dbc --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/stats_unit_test.go @@ -0,0 +1,29 @@ +package client + +import ( + "bytes" + "sync" + "testing" +) + +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, + mu: sync.RWMutex{}, + } + 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%\t104.9 MB/2.147 GB\t4.88%\t104.9 MB/838.9 MB\n" + if got != want { + t.Fatalf("c.Display() = %q, want %q", got, want) + } +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/stop.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/stop.go new file mode 100644 index 0000000..9551911 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/stop.go @@ -0,0 +1,40 @@ +package client + +import ( + "fmt" + "net/url" + "strconv" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdStop stops one or more running containers. +// +// A running container is stopped by first sending SIGTERM and then SIGKILL if the container fails to stop within a grace period (the default is 10 seconds). +// +// Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdStop(args ...string) error { + cmd := cli.Subcmd("stop", "CONTAINER [CONTAINER...]", "Stop a running container by sending SIGTERM and then SIGKILL after a\ngrace period", true) + nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing it") + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + v := url.Values{} + v.Set("t", strconv.Itoa(*nSeconds)) + + var errNames []string + for _, name := range cmd.Args() { + _, _, err := readBody(cli.call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to stop containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/tag.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/tag.go new file mode 100644 index 0000000..56541f8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/tag.go @@ -0,0 +1,41 @@ +package client + +import ( + "net/url" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" +) + +// CmdTag tags an image into a repository. +// +// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] +func (cli *DockerCli) CmdTag(args ...string) error { + cmd := cli.Subcmd("tag", "IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]", "Tag an image into a repository", true) + force := cmd.Bool([]string{"f", "#force", "-force"}, false, "Force") + cmd.Require(flag.Exact, 2) + + cmd.ParseFlags(args, true) + + var ( + repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1)) + v = url.Values{} + ) + + //Check if the given image name can be resolved + if err := registry.ValidateRepositoryName(repository); err != nil { + return err + } + v.Set("repo", repository) + v.Set("tag", tag) + + if *force { + v.Set("force", "1") + } + + if _, _, err := readBody(cli.call("POST", "/images/"+cmd.Arg(0)+"/tag?"+v.Encode(), nil, nil)); err != nil { + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/top.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/top.go new file mode 100644 index 0000000..a5ffee0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/top.go @@ -0,0 +1,46 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + "text/tabwriter" + + "github.com/docker/docker/api/types" + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdTop displays the running processes of a container. +// +// Usage: docker top CONTAINER +func (cli *DockerCli) CmdTop(args ...string) error { + cmd := cli.Subcmd("top", "CONTAINER [ps OPTIONS]", "Display the running processes of a container", true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + val := url.Values{} + if cmd.NArg() > 1 { + val.Set("ps_args", strings.Join(cmd.Args()[1:], " ")) + } + + stream, _, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/top?"+val.Encode(), nil, nil) + if err != nil { + return err + } + + procList := types.ContainerProcessList{} + if err := json.NewDecoder(stream).Decode(&procList); err != nil { + return err + } + + w := tabwriter.NewWriter(cli.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 +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/unpause.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/unpause.go new file mode 100644 index 0000000..428dfec --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/unpause.go @@ -0,0 +1,30 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdUnpause unpauses all processes within a container, for one or more containers. +// +// Usage: docker unpause CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdUnpause(args ...string) error { + cmd := cli.Subcmd("unpause", "CONTAINER [CONTAINER...]", "Unpause all processes within a container", true) + cmd.Require(flag.Min, 1) + cmd.ParseFlags(args, true) + + var errNames []string + for _, name := range cmd.Args() { + if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/unpause", name), nil, nil)); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%s\n", name) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to unpause containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/utils.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/utils.go new file mode 100644 index 0000000..b9d8f27 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/utils.go @@ -0,0 +1,345 @@ +package client + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + gosignal "os/signal" + "runtime" + "strconv" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/autogen/dockerversion" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/registry" +) + +var ( + errConnectionRefused = errors.New("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?") +) + +// HTTPClient creates a new HTTP client with the cli's client transport instance. +func (cli *DockerCli) HTTPClient() *http.Client { + return &http.Client{Transport: cli.transport} +} + +func (cli *DockerCli) encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} + +func (cli *DockerCli) clientRequest(method, path string, in io.Reader, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { + expectedPayload := (method == "POST" || method == "PUT") + if expectedPayload && in == nil { + in = bytes.NewReader([]byte{}) + } + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), in) + if err != nil { + return nil, nil, -1, err + } + + // Add CLI Config's HTTP Headers BEFORE we set the Docker headers + // then the user can't change OUR headers + for k, v := range cli.configFile.HttpHeaders { + req.Header.Set(k, v) + } + + req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION+" ("+runtime.GOOS+")") + req.URL.Host = cli.addr + req.URL.Scheme = cli.scheme + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + resp, err := cli.HTTPClient().Do(req) + statusCode := -1 + if resp != nil { + statusCode = resp.StatusCode + } + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return nil, nil, statusCode, errConnectionRefused + } + + if cli.tlsConfig == nil { + return nil, nil, statusCode, fmt.Errorf("%v. Are you trying to connect to a TLS-enabled daemon without TLS?", err) + } + return nil, nil, statusCode, fmt.Errorf("An error occurred trying to connect: %v", err) + } + + if statusCode < 200 || statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, nil, statusCode, err + } + if len(body) == 0 { + return nil, nil, statusCode, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(statusCode), req.URL) + } + return nil, nil, statusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body)) + } + + return resp.Body, resp.Header, statusCode, nil +} + +func (cli *DockerCli) clientRequestAttemptLogin(method, path string, in io.Reader, out io.Writer, index *registry.IndexInfo, cmdName string) (io.ReadCloser, int, error) { + cmdAttempt := func(authConfig cliconfig.AuthConfig) (io.ReadCloser, int, error) { + buf, err := json.Marshal(authConfig) + if err != nil { + return nil, -1, err + } + registryAuthHeader := []string{ + base64.URLEncoding.EncodeToString(buf), + } + + // begin the request + body, hdr, statusCode, err := cli.clientRequest(method, path, in, map[string][]string{ + "X-Registry-Auth": registryAuthHeader, + }) + if err == nil && out != nil { + // If we are streaming output, complete the stream since + // errors may not appear until later. + err = cli.streamBody(body, hdr.Get("Content-Type"), true, out, nil) + } + if err != nil { + // Since errors in a stream appear after status 200 has been written, + // we may need to change the status code. + if strings.Contains(err.Error(), "Authentication is required") || + strings.Contains(err.Error(), "Status 401") || + strings.Contains(err.Error(), "status code 401") { + statusCode = http.StatusUnauthorized + } + } + return body, statusCode, err + } + + // Resolve the Auth config relevant for this server + authConfig := registry.ResolveAuthConfig(cli.configFile, index) + body, statusCode, err := cmdAttempt(authConfig) + if statusCode == http.StatusUnauthorized { + fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) + if err = cli.CmdLogin(index.GetAuthConfigKey()); err != nil { + return nil, -1, err + } + authConfig = registry.ResolveAuthConfig(cli.configFile, index) + return cmdAttempt(authConfig) + } + return body, statusCode, err +} + +func (cli *DockerCli) call(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { + params, err := cli.encodeData(data) + if err != nil { + return nil, nil, -1, err + } + + if data != nil { + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + body, hdr, statusCode, err := cli.clientRequest(method, path, params, headers) + return body, hdr, statusCode, err +} + +type streamOpts struct { + rawTerminal bool + in io.Reader + out io.Writer + err io.Writer + headers map[string][]string +} + +func (cli *DockerCli) stream(method, path string, opts *streamOpts) error { + body, hdr, _, err := cli.clientRequest(method, path, opts.in, opts.headers) + if err != nil { + return err + } + return cli.streamBody(body, hdr.Get("Content-Type"), opts.rawTerminal, opts.out, opts.err) +} + +func (cli *DockerCli) streamBody(body io.ReadCloser, contentType string, rawTerminal bool, stdout, stderr io.Writer) error { + defer body.Close() + + if api.MatchesContentType(contentType, "application/json") { + return jsonmessage.DisplayJSONMessagesStream(body, stdout, cli.outFd, cli.isTerminalOut) + } + if stdout != nil || stderr != nil { + // When TTY is ON, use regular copy + var err error + if rawTerminal { + _, err = io.Copy(stdout, body) + } else { + _, err = stdcopy.StdCopy(stdout, stderr, body) + } + logrus.Debugf("[stream] End of stdout") + return err + } + return nil +} + +func (cli *DockerCli) resizeTty(id string, isExec bool) { + height, width := cli.getTtySize() + if height == 0 && width == 0 { + return + } + v := url.Values{} + v.Set("h", strconv.Itoa(height)) + v.Set("w", strconv.Itoa(width)) + + path := "" + if !isExec { + path = "/containers/" + id + "/resize?" + } else { + path = "/exec/" + id + "/resize?" + } + + if _, _, err := readBody(cli.call("POST", path+v.Encode(), nil, nil)); err != nil { + logrus.Debugf("Error resize: %s", err) + } +} + +func waitForExit(cli *DockerCli, containerID string) (int, error) { + stream, _, _, err := cli.call("POST", "/containers/"+containerID+"/wait", nil, nil) + if err != nil { + return -1, err + } + + var res types.ContainerWaitResponse + if err := json.NewDecoder(stream).Decode(&res); err != nil { + return -1, err + } + + return res.StatusCode, nil +} + +// getExitCode perform an inspect on the container. It returns +// the running state and the exit code. +func getExitCode(cli *DockerCli, containerID string) (bool, int, error) { + stream, _, _, err := cli.call("GET", "/containers/"+containerID+"/json", nil, nil) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != errConnectionRefused { + return false, -1, err + } + return false, -1, nil + } + + var c types.ContainerJSON + if err := json.NewDecoder(stream).Decode(&c); err != nil { + return false, -1, err + } + + return c.State.Running, c.State.ExitCode, nil +} + +// getExecExitCode perform an inspect on the exec command. It returns +// the running state and the exit code. +func getExecExitCode(cli *DockerCli, execID string) (bool, int, error) { + stream, _, _, err := cli.call("GET", "/exec/"+execID+"/json", nil, nil) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != errConnectionRefused { + return false, -1, err + } + return false, -1, nil + } + + //TODO: Should we reconsider having a type in api/types? + //this is a response to exex/id/json not container + var c struct { + Running bool + ExitCode int + } + + if err := json.NewDecoder(stream).Decode(&c); err != nil { + return false, -1, err + } + + return c.Running, c.ExitCode, nil +} + +func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { + cli.resizeTty(id, isExec) + + if runtime.GOOS == "windows" { + go func() { + prevH, prevW := cli.getTtySize() + for { + time.Sleep(time.Millisecond * 250) + h, w := cli.getTtySize() + + if prevW != w || prevH != h { + cli.resizeTty(id, isExec) + } + prevH = h + prevW = w + } + }() + } else { + sigchan := make(chan os.Signal, 1) + gosignal.Notify(sigchan, signal.SIGWINCH) + go func() { + for range sigchan { + cli.resizeTty(id, isExec) + } + }() + } + return nil +} + +func (cli *DockerCli) getTtySize() (int, int) { + if !cli.isTerminalOut { + return 0, 0 + } + ws, err := term.GetWinsize(cli.outFd) + if err != nil { + logrus.Debugf("Error getting size: %s", err) + if ws == nil { + return 0, 0 + } + } + return int(ws.Height), int(ws.Width) +} + +func readBody(stream io.ReadCloser, hdr http.Header, statusCode int, err error) ([]byte, int, error) { + if stream != nil { + defer stream.Close() + } + if err != nil { + return nil, statusCode, err + } + body, err := ioutil.ReadAll(stream) + if err != nil { + return nil, -1, err + } + return body, statusCode, nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/version.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/version.go new file mode 100644 index 0000000..a111f8f --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/version.go @@ -0,0 +1,61 @@ +package client + +import ( + "encoding/json" + "fmt" + "runtime" + + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/autogen/dockerversion" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" +) + +// CmdVersion shows Docker version information. +// +// Available version information is shown for: client Docker version, client API version, client Go version, client Git commit, client OS/Arch, server Docker version, server API version, server Go version, server Git commit, and server OS/Arch. +// +// Usage: docker version +func (cli *DockerCli) CmdVersion(args ...string) error { + cmd := cli.Subcmd("version", "", "Show the Docker version information.", true) + cmd.Require(flag.Exact, 0) + + cmd.ParseFlags(args, true) + + if dockerversion.VERSION != "" { + fmt.Fprintf(cli.out, "Client version: %s\n", dockerversion.VERSION) + } + fmt.Fprintf(cli.out, "Client API version: %s\n", api.APIVERSION) + fmt.Fprintf(cli.out, "Go version (client): %s\n", runtime.Version()) + if dockerversion.GITCOMMIT != "" { + fmt.Fprintf(cli.out, "Git commit (client): %s\n", dockerversion.GITCOMMIT) + } + fmt.Fprintf(cli.out, "OS/Arch (client): %s/%s\n", runtime.GOOS, runtime.GOARCH) + if utils.ExperimentalBuild() { + fmt.Fprintf(cli.out, "Experimental (client): true\n") + } + + stream, _, _, err := cli.call("GET", "/version", nil, nil) + if err != nil { + return err + } + + var v types.Version + if err := json.NewDecoder(stream).Decode(&v); err != nil { + fmt.Fprintf(cli.err, "Error reading remote version: %s\n", err) + return err + } + + fmt.Fprintf(cli.out, "Server version: %s\n", v.Version) + if v.ApiVersion != "" { + fmt.Fprintf(cli.out, "Server API version: %s\n", v.ApiVersion) + } + fmt.Fprintf(cli.out, "Go version (server): %s\n", v.GoVersion) + fmt.Fprintf(cli.out, "Git commit (server): %s\n", v.GitCommit) + fmt.Fprintf(cli.out, "OS/Arch (server): %s/%s\n", v.Os, v.Arch) + if v.Experimental { + fmt.Fprintf(cli.out, "Experimental (server): true\n") + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/api/client/wait.go b/Godeps/_workspace/src/github.com/docker/docker/api/client/wait.go new file mode 100644 index 0000000..bfec19e --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/api/client/wait.go @@ -0,0 +1,34 @@ +package client + +import ( + "fmt" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CmdWait blocks until a container stops, then prints its exit code. +// +// If more than one container is specified, this will wait synchronously on each container. +// +// Usage: docker wait CONTAINER [CONTAINER...] +func (cli *DockerCli) CmdWait(args ...string) error { + cmd := cli.Subcmd("wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.", true) + cmd.Require(flag.Min, 1) + + cmd.ParseFlags(args, true) + + var errNames []string + for _, name := range cmd.Args() { + status, err := waitForExit(cli, name) + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + errNames = append(errNames, name) + } else { + fmt.Fprintf(cli.out, "%d\n", status) + } + } + if len(errNames) > 0 { + return fmt.Errorf("Error: failed to wait containers: %v", errNames) + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/fmt.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/fmt.go new file mode 100644 index 0000000..801132f --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/fmt.go @@ -0,0 +1,14 @@ +package ioutils + +import ( + "fmt" + "io" +) + +// FprintfIfNotEmpty prints the string value if it's not empty +func FprintfIfNotEmpty(w io.Writer, format, value string) (int, error) { + if value != "" { + return fmt.Fprintf(w, format, value) + } + return 0, nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/fmt_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/fmt_test.go new file mode 100644 index 0000000..8968863 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/fmt_test.go @@ -0,0 +1,17 @@ +package ioutils + +import "testing" + +func TestFprintfIfNotEmpty(t *testing.T) { + wc := NewWriteCounter(&NopWriter{}) + n, _ := FprintfIfNotEmpty(wc, "foo%s", "") + + if wc.Count != 0 || n != 0 { + t.Errorf("Wrong count: %v vs. %v vs. 0", wc.Count, n) + } + + n, _ = FprintfIfNotEmpty(wc, "foo%s", "bar") + if wc.Count != 6 || n != 6 { + t.Errorf("Wrong count: %v vs. %v vs. 6", wc.Count, n) + } +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/readers_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/readers_test.go index b4cbfd9..d220487 100644 --- a/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/readers_test.go +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/ioutils/readers_test.go @@ -43,10 +43,9 @@ func TestReaderErrWrapperReadOnError(t *testing.T) { } func TestReaderErrWrapperRead(t *testing.T) { - called := false reader := strings.NewReader("a string reader.") wrapper := NewReaderErrWrapper(reader, func() { - called = true // Should not be called + t.Fatalf("readErrWrapper should not have called the anonymous function on failure") }) // Read 20 byte (should be ok with the string above) num, err := wrapper.Read(make([]byte, 20)) diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/mflag/flag.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/mflag/flag.go index 11dfb75..9626a2f 100644 --- a/Godeps/_workspace/src/github.com/docker/docker/pkg/mflag/flag.go +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/mflag/flag.go @@ -289,7 +289,8 @@ type FlagSet struct { // Usage is the function called when an error occurs while parsing flags. // The field is a function (not a method) that may be changed to point to // a custom error handler. - Usage func() + Usage func() + ShortUsage func() name string parsed bool @@ -511,6 +512,12 @@ func (f *FlagSet) PrintDefaults() { if runtime.GOOS != "windows" && home == "/" { home = "" } + + // Add a blank line between cmd description and list of options + if f.FlagCount() > 0 { + fmt.Fprintln(writer, "") + } + f.VisitAll(func(flag *Flag) { format := " -%s=%s" names := []string{} @@ -560,10 +567,16 @@ func defaultUsage(f *FlagSet) { // Usage prints to standard error a usage message documenting all defined command-line flags. // The function is a variable that may be changed to point to a custom function. var Usage = func() { - fmt.Fprintf(CommandLine.output, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(CommandLine.Out(), "Usage of %s:\n", os.Args[0]) PrintDefaults() } +// Usage prints to standard error a usage message documenting the standard command layout +// The function is a variable that may be changed to point to a custom function. +var ShortUsage = func() { + fmt.Fprintf(CommandLine.output, "Usage of %s:\n", os.Args[0]) +} + // FlagCount returns the number of flags that have been defined. func (f *FlagSet) FlagCount() int { return len(sortFlags(f.formal)) } @@ -1067,12 +1080,15 @@ func (cmd *FlagSet) ParseFlags(args []string, withHelp bool) error { return err } if help != nil && *help { + cmd.SetOutput(os.Stdout) cmd.Usage() - // just in case Usage does not exit os.Exit(0) } if str := cmd.CheckArgs(); str != "" { + cmd.SetOutput(os.Stderr) cmd.ReportError(str, withHelp) + cmd.ShortUsage() + os.Exit(1) } return nil } @@ -1080,13 +1096,12 @@ func (cmd *FlagSet) ParseFlags(args []string, withHelp bool) error { func (cmd *FlagSet) ReportError(str string, withHelp bool) { if withHelp { if os.Args[0] == cmd.Name() { - str += ". See '" + os.Args[0] + " --help'" + str += ".\nSee '" + os.Args[0] + " --help'" } else { - str += ". See '" + os.Args[0] + " " + cmd.Name() + " --help'" + str += ".\nSee '" + os.Args[0] + " " + cmd.Name() + " --help'" } } - fmt.Fprintf(cmd.Out(), "docker: %s\n", str) - os.Exit(1) + fmt.Fprintf(cmd.Out(), "docker: %s.\n", str) } // Parsed reports whether f.Parse has been called. diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client.go index 00ca105..890fd7c 100644 --- a/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client.go +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client.go @@ -31,6 +31,10 @@ type Client struct { } func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) error { + return c.callWithRetry(serviceMethod, args, ret, true) +} + +func (c *Client) callWithRetry(serviceMethod string, args interface{}, ret interface{}, retry bool) error { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(args); err != nil { return err @@ -50,20 +54,25 @@ func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) e for { resp, err := c.http.Do(req) if err != nil { + if !retry { + return err + } + timeOff := backoff(retries) - if timeOff+time.Since(start) > defaultTimeOut { + if abort(start, timeOff) { return err } retries++ - logrus.Warn("Unable to connect to plugin: %s, retrying in %ds\n", c.addr, timeOff) + logrus.Warnf("Unable to connect to plugin: %s, retrying in %v", c.addr, timeOff) time.Sleep(timeOff) continue } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { remoteErr, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil + return fmt.Errorf("Plugin Error: %s", err) } return fmt.Errorf("Plugin Error: %s", remoteErr) } @@ -73,7 +82,7 @@ func (c *Client) Call(serviceMethod string, args interface{}, ret interface{}) e } func backoff(retries int) time.Duration { - b, max := float64(1), float64(defaultTimeOut) + b, max := 1, defaultTimeOut for b < max && retries > 0 { b *= 2 retries-- @@ -81,7 +90,11 @@ func backoff(retries int) time.Duration { if b > max { b = max } - return time.Duration(b) + return time.Duration(b) * time.Second +} + +func abort(start time.Time, timeOff time.Duration) bool { + return timeOff+time.Since(start) > time.Duration(defaultTimeOut)*time.Second } func configureTCPTransport(tr *http.Transport, proto, addr string) { diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client_test.go index b414ecb..0f7cd34 100644 --- a/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client_test.go +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/client_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "reflect" "testing" + "time" ) var ( @@ -27,7 +28,7 @@ func teardownRemotePluginServer() { func TestFailedConnection(t *testing.T) { c := NewClient("tcp://127.0.0.1:1") - err := c.Call("Service.Method", nil, nil) + err := c.callWithRetry("Service.Method", nil, nil, false) if err == nil { t.Fatal("Unexpected successful connection") } @@ -61,3 +62,44 @@ func TestEchoInputOutput(t *testing.T) { t.Fatalf("Expected %v, was %v\n", m, output) } } + +func TestBackoff(t *testing.T) { + cases := []struct { + retries int + expTimeOff time.Duration + }{ + {0, time.Duration(1)}, + {1, time.Duration(2)}, + {2, time.Duration(4)}, + {4, time.Duration(16)}, + {6, time.Duration(30)}, + {10, time.Duration(30)}, + } + + for _, c := range cases { + s := c.expTimeOff * time.Second + if d := backoff(c.retries); d != s { + t.Fatalf("Retry %v, expected %v, was %v\n", c.retries, s, d) + } + } +} + +func TestAbortRetry(t *testing.T) { + cases := []struct { + timeOff time.Duration + expAbort bool + }{ + {time.Duration(1), false}, + {time.Duration(2), false}, + {time.Duration(10), false}, + {time.Duration(30), true}, + {time.Duration(40), true}, + } + + for _, c := range cases { + s := c.timeOff * time.Second + if a := abort(time.Now(), s); a != c.expAbort { + t.Fatalf("Duration %v, expected %v, was %v\n", c.timeOff, s, a) + } + } +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/discovery_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/discovery_test.go index b6e66e2..a818dc3 100644 --- a/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/discovery_test.go +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/plugins/discovery_test.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "net" "os" - "path" "path/filepath" "reflect" "testing" @@ -66,6 +65,7 @@ func TestFileSpecPlugin(t *testing.T) { if err != nil { t.Fatal(err) } + defer os.RemoveAll(tmpdir) cases := []struct { path string @@ -79,9 +79,6 @@ func TestFileSpecPlugin(t *testing.T) { } for _, c := range cases { - if err = os.MkdirAll(path.Dir(c.path), 0755); err != nil { - t.Fatal(err) - } if err = ioutil.WriteFile(c.path, []byte(c.addr), 0644); err != nil { t.Fatal(err) } @@ -103,6 +100,5 @@ func TestFileSpecPlugin(t *testing.T) { if p.Addr != c.addr { t.Fatalf("Expected plugin addr `%s`, got %s\n", c.addr, p.Addr) } - os.Remove(c.path) } } diff --git a/client/client.go b/client/client.go index 4bc86da..bb2aedc 100644 --- a/client/client.go +++ b/client/client.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "reflect" "strings" @@ -11,7 +12,7 @@ import ( ) // CallFunc provides environment specific call utility to invoke backend functions from UI -type CallFunc func(string, string, interface{}, map[string][]string) (io.ReadCloser, int, error) +type CallFunc func(string, string, interface{}, map[string][]string) (io.ReadCloser, http.Header, int, error) // NetworkCli is the UI object for network subcmds type NetworkCli struct { @@ -96,7 +97,7 @@ func (cli *NetworkCli) Subcmd(chain, name, signature, description string, exitOn return flags } -func readBody(stream io.ReadCloser, statusCode int, err error) ([]byte, int, error) { +func readBody(stream io.ReadCloser, hdr http.Header, statusCode int, err error) ([]byte, int, error) { if stream != nil { defer stream.Close() } diff --git a/client/client_test.go b/client/client_test.go index 2ba610d..06b8b2a 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "os" "strings" "testing" @@ -24,7 +25,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -var callbackFunc func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) +var callbackFunc func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) var mockNwJSON, mockNwListJSON, mockServiceJSON, mockServiceListJSON []byte var mockNwName = "test" var mockNwID = "2a3456789" @@ -45,7 +46,9 @@ func setupMockHTTPCallback() { srvList = append(srvList, ep) mockServiceListJSON, _ = json.Marshal(srvList) - callbackFunc = func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + dummyHTTPHdr := http.Header{} + + callbackFunc = func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { var rsp string switch method { case "GET": @@ -74,7 +77,7 @@ func setupMockHTTPCallback() { } else if strings.HasSuffix(path, "services/"+mockServiceID) { rsp = string(mockServiceJSON) } else if strings.Contains(path, "containers") { - return nopCloser{bytes.NewBufferString("")}, 400, fmt.Errorf("Bad Request") + return nopCloser{bytes.NewBufferString("")}, dummyHTTPHdr, 400, fmt.Errorf("Bad Request") } case "POST": var data []byte @@ -90,7 +93,7 @@ func setupMockHTTPCallback() { case "DELETE": rsp = "" } - return nopCloser{bytes.NewBufferString(rsp)}, 200, nil + return nopCloser{bytes.NewBufferString(rsp)}, dummyHTTPHdr, 200, nil } } diff --git a/cmd/dnet/dnet.go b/cmd/dnet/dnet.go index 18a2b1b..c0d497d 100644 --- a/cmd/dnet/dnet.go +++ b/cmd/dnet/dnet.go @@ -218,16 +218,16 @@ func newDnetConnection(val string) (*dnetConnection, error) { return &dnetConnection{protoAddrParts[0], protoAddrParts[1]}, nil } -func (d *dnetConnection) httpCall(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { +func (d *dnetConnection) httpCall(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { var in io.Reader in, err := encodeData(data) if err != nil { - return nil, -1, err + return nil, nil, -1, err } req, err := http.NewRequest(method, fmt.Sprintf("%s", path), in) if err != nil { - return nil, -1, err + return nil, nil, -1, err } setupRequestHeaders(method, data, req, headers) @@ -242,18 +242,18 @@ func (d *dnetConnection) httpCall(method, path string, data interface{}, headers statusCode = resp.StatusCode } if err != nil { - return nil, statusCode, fmt.Errorf("error when trying to connect: %v", err) + return nil, nil, statusCode, fmt.Errorf("error when trying to connect: %v", err) } if statusCode < 200 || statusCode >= 400 { body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, statusCode, err + return nil, nil, statusCode, err } - return nil, statusCode, fmt.Errorf("error : %s", bytes.TrimSpace(body)) + return nil, nil, statusCode, fmt.Errorf("error : %s", bytes.TrimSpace(body)) } - return resp.Body, statusCode, nil + return resp.Body, resp.Header, statusCode, nil } func setupRequestHeaders(method string, data interface{}, req *http.Request, headers map[string][]string) {