diff --git a/components/engine/api/client/attach.go b/components/engine/api/client/attach.go index 9ebd02d760..f0979e2bf8 100644 --- a/components/engine/api/client/attach.go +++ b/components/engine/api/client/attach.go @@ -1,10 +1,8 @@ package client import ( - "encoding/json" "fmt" "io" - "net/url" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" @@ -25,18 +23,11 @@ func (cli *DockerCli) CmdAttach(args ...string) error { cmd.ParseFlags(args, true) - serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil) + c, err := cli.client.ContainerInspect(cmd.Arg(0)) if err != nil { return err } - defer serverResp.body.Close() - - var c types.ContainerJSON - if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil { - return err - } - if !c.State.Running { return fmt.Errorf("You cannot attach to a stopped container, start it first") } @@ -55,28 +46,35 @@ func (cli *DockerCli) CmdAttach(args ...string) error { } } - var in io.ReadCloser + options := types.ContainerAttachOptions{ + ContainerID: cmd.Arg(0), + Stream: true, + Stdin: !*noStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + } - v := url.Values{} - v.Set("stream", "1") - if !*noStdin && c.Config.OpenStdin { - v.Set("stdin", "1") + var in io.ReadCloser + if options.Stdin { in = cli.in } - v.Set("stdout", "1") - v.Set("stderr", "1") - if *proxy && !c.Config.Tty { - sigc := cli.forwardAllSignals(cmd.Arg(0)) + sigc := cli.forwardAllSignals(options.ContainerID) 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 { + resp, err := cli.client.ContainerAttach(options) + if err != nil { + return err + } + defer resp.Close() + + if err := cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp); err != nil { return err } - _, status, err := getExitCode(cli, cmd.Arg(0)) + _, status, err := getExitCode(cli, options.ContainerID) if err != nil { return err } diff --git a/components/engine/api/client/build.go b/components/engine/api/client/build.go index 8de9c6837d..8af15b2465 100644 --- a/components/engine/api/client/build.go +++ b/components/engine/api/client/build.go @@ -3,23 +3,19 @@ package client import ( "archive/tar" "bufio" - "encoding/base64" - "encoding/json" "fmt" "io" "io/ioutil" - "net/http" - "net/url" "os" "os/exec" "path/filepath" "regexp" "runtime" - "strconv" "strings" "github.com/docker/distribution/reference" "github.com/docker/docker/api" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/archive" @@ -33,7 +29,6 @@ import ( "github.com/docker/docker/pkg/units" "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/registry" - "github.com/docker/docker/runconfig" tagpkg "github.com/docker/docker/tag" "github.com/docker/docker/utils" ) @@ -207,108 +202,55 @@ func (cli *DockerCli) CmdBuild(args ...string) error { } } - // Send the build context - v := url.Values{ - "t": flTags.GetAll(), - } - if *suppressOutput { - v.Set("q", "1") - } + var remoteContext string 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") + remoteContext = cmd.Arg(0) } - if *forceRm { - v.Set("forcerm", "1") + options := types.ImageBuildOptions{ + Context: body, + Memory: memory, + MemorySwap: memorySwap, + Tags: flTags.GetAll(), + SuppressOutput: *suppressOutput, + RemoteContext: remoteContext, + NoCache: *noCache, + Remove: *rm, + ForceRemove: *forceRm, + PullParent: *pull, + Isolation: *isolation, + CPUSetCPUs: *flCPUSetCpus, + CPUSetMems: *flCPUSetMems, + CPUShares: *flCPUShares, + CPUQuota: *flCPUQuota, + CPUPeriod: *flCPUPeriod, + CgroupParent: *flCgroupParent, + ShmSize: *flShmSize, + Dockerfile: relDockerfile, + Ulimits: flUlimits.GetList(), + BuildArgs: flBuildArg.GetAll(), + AuthConfigs: cli.configFile.AuthConfigs, } - if *pull { - v.Set("pull", "1") + response, err := cli.client.ImageBuild(options) + if err != nil { + return err } - if !runconfig.IsolationLevel.IsDefault(runconfig.IsolationLevel(*isolation)) { - v.Set("isolation", *isolation) - } - - 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) - - if *flShmSize != "" { - parsedShmSize, err := units.RAMInBytes(*flShmSize) - if err != nil { - return err + err = jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut) + if err != nil { + if jerr, ok := err.(*jsonmessage.JSONError); ok { + // If no error code is set, default to 1 + if jerr.Code == 0 { + jerr.Code = 1 + } + return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} } - v.Set("shmsize", strconv.FormatInt(parsedShmSize, 10)) } - v.Set("dockerfile", relDockerfile) - - ulimitsVar := flUlimits.GetList() - ulimitsJSON, err := json.Marshal(ulimitsVar) - if err != nil { - return err - } - v.Set("ulimits", string(ulimitsJSON)) - - // collect all the build-time environment variables for the container - buildArgs := runconfig.ConvertKVStringsToMap(flBuildArg.GetAll()) - buildArgsJSON, err := json.Marshal(buildArgs) - if err != nil { - return err - } - v.Set("buildargs", string(buildArgsJSON)) - - 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)) - headers.Set("Content-Type", "application/tar") - - sopts := &streamOpts{ - rawTerminal: true, - in: body, - out: cli.out, - headers: headers, - } - - serverResp, err := cli.stream("POST", fmt.Sprintf("/build?%s", v.Encode()), sopts) - // Windows: show error message about modified file permissions. - if runtime.GOOS == "windows" { - h, err := httputils.ParseServerHeader(serverResp.header.Get("Server")) - if err == nil { - if h.OS != "windows" { - fmt.Fprintln(cli.err, `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) - } - } - } - - if jerr, ok := err.(*jsonmessage.JSONError); ok { - // If no error code is set, default to 1 - if jerr.Code == 0 { - jerr.Code = 1 - } - return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} - } - - if err != nil { - return err + if response.OSType == "windows" { + fmt.Fprintln(cli.err, `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) } // Since the build was successful, now we must tag any of the resolved diff --git a/components/engine/api/client/cli.go b/components/engine/api/client/cli.go index 834c47a4d3..19c635d776 100644 --- a/components/engine/api/client/cli.go +++ b/components/engine/api/client/cli.go @@ -1,19 +1,17 @@ package client import ( - "crypto/tls" "errors" "fmt" "io" - "net/http" - "net/url" "os" - "strings" + "runtime" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/cli" "github.com/docker/docker/cliconfig" + "github.com/docker/docker/dockerversion" "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/sockets" "github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/tlsconfig" ) @@ -24,13 +22,6 @@ type DockerCli struct { // initializing closure init func() error - // proto holds the client protocol i.e. unix. - proto string - // addr holds the client address. - addr string - // basePath holds the path to prepend to the requests - basePath string - // configFile has the client configuration file configFile *cliconfig.ConfigFile // in holds the input stream and closer (io.ReadCloser) for the client. @@ -41,11 +32,6 @@ type DockerCli struct { 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). @@ -54,8 +40,8 @@ type DockerCli struct { isTerminalIn bool // isTerminalOut indicates whether the client's STDOUT is a TTY isTerminalOut bool - // transport holds the client transport instance. - transport *http.Transport + // client is the http client that performs all API operations + client apiClient } // Initialize calls the init function that will setup the configuration for the client @@ -98,50 +84,29 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF } cli.init = func() error { - clientFlags.PostParse() + configFile, e := cliconfig.Load(cliconfig.ConfigDir()) + if e != nil { + fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e) + } + cli.configFile = configFile - hosts := clientFlags.Common.Hosts - - switch len(hosts) { - case 0: - hosts = []string{os.Getenv("DOCKER_HOST")} - case 1: - // only accept one host to talk to - default: - return errors.New("Please specify only one -H") + host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions) + if err != nil { + return err } - defaultHost := opts.DefaultTCPHost - if clientFlags.Common.TLSOptions != nil { - defaultHost = opts.DefaultTLSHost + customHeaders := cli.configFile.HTTPHeaders + if customHeaders == nil { + customHeaders = map[string]string{} } + customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" - var e error - if hosts[0], e = opts.ParseHost(defaultHost, hosts[0]); e != nil { - return e - } - - protoAddrParts := strings.SplitN(hosts[0], "://", 2) - cli.proto, cli.addr = protoAddrParts[0], protoAddrParts[1] - - if cli.proto == "tcp" { - // error is checked in pkg/parsers already - parsed, _ := url.Parse("tcp://" + cli.addr) - cli.addr = parsed.Host - cli.basePath = parsed.Path - } - - if clientFlags.Common.TLSOptions != nil { - cli.scheme = "https" - var e error - cli.tlsConfig, e = tlsconfig.Client(*clientFlags.Common.TLSOptions) - if e != nil { - return e - } - } else { - cli.scheme = "http" + client, err := lib.NewClient(host, clientFlags.Common.TLSOptions, customHeaders) + if err != nil { + return err } + cli.client = client if cli.in != nil { cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) @@ -150,20 +115,27 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out) } - // The transport is created here for reuse during the client session. - cli.transport = &http.Transport{ - TLSClientConfig: cli.tlsConfig, - } - sockets.ConfigureTCPTransport(cli.transport, cli.proto, cli.addr) - - configFile, e := cliconfig.Load(cliconfig.ConfigDir()) - if e != nil { - fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e) - } - cli.configFile = configFile - return nil } return cli } + +func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) { + switch len(hosts) { + case 0: + host = os.Getenv("DOCKER_HOST") + case 1: + host = hosts[0] + default: + return "", errors.New("Please specify only one -H") + } + + defaultHost := opts.DefaultTCPHost + if tlsOptions != nil { + defaultHost = opts.DefaultTLSHost + } + + host, err = opts.ParseHost(defaultHost, host) + return +} diff --git a/components/engine/api/client/client.go b/components/engine/api/client/client.go index 4cfce5f684..52dd4e5cd3 100644 --- a/components/engine/api/client/client.go +++ b/components/engine/api/client/client.go @@ -3,3 +3,74 @@ // 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 ( + "io" + + "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" +) + +// apiClient is an interface that clients that talk with a docker server must implement. +type apiClient interface { + ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error) + ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) + ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error) + ContainerDiff(containerID string) ([]types.ContainerChange, error) + ContainerExecAttach(execID string, config runconfig.ExecConfig) (types.HijackedResponse, error) + ContainerExecCreate(config runconfig.ExecConfig) (types.ContainerExecCreateResponse, error) + ContainerExecInspect(execID string) (types.ContainerExecInspect, error) + ContainerExecResize(options types.ResizeOptions) error + ContainerExecStart(execID string, config types.ExecStartCheck) error + ContainerExport(containerID string) (io.ReadCloser, error) + ContainerInspect(containerID string) (types.ContainerJSON, error) + ContainerInspectWithRaw(containerID string, getSize bool) (types.ContainerJSON, []byte, error) + ContainerKill(containerID, signal string) error + ContainerList(options types.ContainerListOptions) ([]types.Container, error) + ContainerLogs(options types.ContainerLogsOptions) (io.ReadCloser, error) + ContainerPause(containerID string) error + ContainerRemove(options types.ContainerRemoveOptions) error + ContainerRename(containerID, newContainerName string) error + ContainerResize(options types.ResizeOptions) error + ContainerRestart(containerID string, timeout int) error + ContainerStatPath(containerID, path string) (types.ContainerPathStat, error) + ContainerStats(containerID string, stream bool) (io.ReadCloser, error) + ContainerStart(containerID string) error + ContainerStop(containerID string, timeout int) error + ContainerTop(containerID string, arguments []string) (types.ContainerProcessList, error) + ContainerUnpause(containerID string) error + ContainerWait(containerID string) (int, error) + CopyFromContainer(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) + CopyToContainer(options types.CopyToContainerOptions) error + Events(options types.EventsOptions) (io.ReadCloser, error) + ImageBuild(options types.ImageBuildOptions) (types.ImageBuildResponse, error) + ImageCreate(options types.ImageCreateOptions) (io.ReadCloser, error) + ImageHistory(imageID string) ([]types.ImageHistory, error) + ImageImport(options types.ImageImportOptions) (io.ReadCloser, error) + ImageInspectWithRaw(imageID string, getSize bool) (types.ImageInspect, []byte, error) + ImageList(options types.ImageListOptions) ([]types.Image, error) + ImageLoad(input io.Reader) (io.ReadCloser, error) + ImagePull(options types.ImagePullOptions, privilegeFunc lib.RequestPrivilegeFunc) (io.ReadCloser, error) + ImagePush(options types.ImagePushOptions, privilegeFunc lib.RequestPrivilegeFunc) (io.ReadCloser, error) + ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, error) + ImageSearch(options types.ImageSearchOptions, privilegeFunc lib.RequestPrivilegeFunc) ([]registry.SearchResult, error) + ImageSave(imageIDs []string) (io.ReadCloser, error) + ImageTag(options types.ImageTagOptions) error + Info() (types.Info, error) + NetworkConnect(networkID, containerID string) error + NetworkCreate(options types.NetworkCreate) (types.NetworkCreateResponse, error) + NetworkDisconnect(networkID, containerID string) error + NetworkInspect(networkID string) (types.NetworkResource, error) + NetworkList() ([]types.NetworkResource, error) + NetworkRemove(networkID string) error + RegistryLogin(auth cliconfig.AuthConfig) (types.AuthResponse, error) + SystemVersion() (types.VersionResponse, error) + VolumeCreate(options types.VolumeCreateRequest) (types.Volume, error) + VolumeInspect(volumeID string) (types.Volume, error) + VolumeList(filter filters.Args) (types.VolumesListResponse, error) + VolumeRemove(volumeID string) error +} diff --git a/components/engine/api/client/commit.go b/components/engine/api/client/commit.go index 36fcb3c77e..b75520b243 100644 --- a/components/engine/api/client/commit.go +++ b/components/engine/api/client/commit.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "net/url" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" @@ -59,39 +58,27 @@ func (cli *DockerCli) CmdCommit(args ...string) error { } } - v := url.Values{} - v.Set("container", name) - v.Set("repo", repositoryName) - 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 - ) - + var config *runconfig.Config if *flConfig != "" { config = &runconfig.Config{} if err := json.Unmarshal([]byte(*flConfig), config); err != nil { return err } } - serverResp, err := cli.call("POST", "/commit?"+v.Encode(), config, nil) - if err != nil { - return err + + options := types.ContainerCommitOptions{ + ContainerID: name, + RepositoryName: repositoryName, + Tag: tag, + Comment: *flComment, + Author: *flAuthor, + Changes: flChanges.GetAll(), + Pause: *flPause, + Config: config, } - defer serverResp.body.Close() - - if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil { + response, err := cli.client.ContainerCommit(options) + if err != nil { return err } diff --git a/components/engine/api/client/cp.go b/components/engine/api/client/cp.go index bd97441ca5..ab92689710 100644 --- a/components/engine/api/client/cp.go +++ b/components/engine/api/client/cp.go @@ -1,12 +1,8 @@ package client import ( - "encoding/base64" - "encoding/json" "fmt" "io" - "net/http" - "net/url" "os" "path/filepath" "strings" @@ -129,38 +125,7 @@ func splitCpArg(arg string) (container, path string) { } func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) { - var stat types.ContainerPathStat - - query := make(url.Values, 1) - query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. - - urlStr := fmt.Sprintf("/containers/%s/archive?%s", containerName, query.Encode()) - - response, err := cli.call("HEAD", urlStr, nil, nil) - if err != nil { - return stat, err - } - defer response.body.Close() - - if response.statusCode != http.StatusOK { - return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) - } - - return getContainerPathStatFromHeader(response.header) -} - -func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) { - var stat types.ContainerPathStat - - encodedStat := header.Get("X-Docker-Container-Path-Stat") - statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) - - err := json.NewDecoder(statDecoder).Decode(&stat) - if err != nil { - err = fmt.Errorf("unable to decode container path stat header: %s", err) - } - - return stat, err + return cli.client.ContainerStatPath(containerName, path) } func resolveLocalPath(localPath string) (absPath string, err error) { @@ -200,39 +165,19 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, c } - query := make(url.Values, 1) - query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. - - urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode()) - - response, err := cli.call("GET", urlStr, nil, nil) + content, stat, err := cli.client.CopyFromContainer(srcContainer, srcPath) if err != nil { return err } - defer response.body.Close() - - if response.statusCode != http.StatusOK { - return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) - } + defer content.Close() if dstPath == "-" { // Send the response to STDOUT. - _, err = io.Copy(os.Stdout, response.body) + _, err = io.Copy(os.Stdout, content) return err } - // In order to get the copy behavior right, we need to know information - // about both the source and the destination. The response headers include - // stat info about the source that we can use in deciding exactly how to - // copy it locally. Along with the stat info about the local destination, - // we have everything we need to handle the multiple possibilities there - // can be when copying a file/dir from one location to another file/dir. - stat, err := getContainerPathStatFromHeader(response.header) - if err != nil { - return fmt.Errorf("unable to get resource stat from response: %s", err) - } - // Prepare source copy info. srcInfo := archive.CopyInfo{ Path: srcPath, @@ -241,10 +186,10 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, c RebaseName: rebaseName, } - preArchive := response.body + preArchive := content if len(srcInfo.RebaseName) != 0 { _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) - preArchive = archive.RebaseArchiveEntries(response.body, srcBase, srcInfo.RebaseName) + preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) } // See comments in the implementation of `archive.CopyTo` for exactly what // goes into deciding how and whether the source archive needs to be @@ -340,22 +285,12 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpP content = preparedArchive } - query := make(url.Values, 2) - query.Set("path", filepath.ToSlash(resolvedDstPath)) // Normalize the paths used in the API. - // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. - query.Set("noOverwriteDirNonDir", "true") - - urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode()) - - response, err := cli.stream("PUT", urlStr, &streamOpts{in: content}) - if err != nil { - return err - } - defer response.body.Close() - - if response.statusCode != http.StatusOK { - return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + options := types.CopyToContainerOptions{ + ContainerID: dstContainer, + Path: resolvedDstPath, + Content: content, + AllowOverwriteDirWithFile: false, } - return nil + return cli.client.CopyToContainer(options) } diff --git a/components/engine/api/client/create.go b/components/engine/api/client/create.go index 9443799e55..857ef90aac 100644 --- a/components/engine/api/client/create.go +++ b/components/engine/api/client/create.go @@ -1,17 +1,15 @@ package client import ( - "encoding/base64" - "encoding/json" "fmt" "io" - "net/url" "os" - "strings" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" tagpkg "github.com/docker/docker/tag" @@ -22,8 +20,6 @@ func (cli *DockerCli) pullImage(image string) error { } func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { - v := url.Values{} - ref, err := reference.ParseNamed(image) if err != nil { return err @@ -40,9 +36,6 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { tag = tagpkg.DefaultTag } - v.Set("fromImage", ref.Name()) - v.Set("tag", tag) - // Resolve the Repository name from fqn to RepositoryInfo repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { @@ -50,24 +43,24 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { } // Resolve the Auth config relevant for this server - authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) - buf, err := json.Marshal(authConfig) + encodedAuth, err := cli.encodeRegistryAuth(repoInfo.Index) if err != nil { return err } - registryAuthHeader := []string{ - base64.URLEncoding.EncodeToString(buf), + options := types.ImageCreateOptions{ + Parent: ref.Name(), + Tag: tag, + RegistryAuth: encodedAuth, } - 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 { + + responseBody, err := cli.client.ImageCreate(options) + if err != nil { return err } - return nil + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut) } type cidFile struct { @@ -90,11 +83,6 @@ func newCIDFile(path string) (*cidFile, error) { } 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 @@ -135,34 +123,32 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc } //create the container - serverResp, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil) + response, err := cli.client.ContainerCreate(mergedConfig, name) //if image not found try to pull it - if serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) { - fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.String()) + if err != nil { + if lib.IsErrImageNotFound(err) { + fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.String()) - // 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 - } - if trustedRef != nil && !isDigested { - if err := cli.tagTrusted(trustedRef, ref.(reference.NamedTagged)); err != nil { + // 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 serverResp, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil); err != nil { + if trustedRef != nil && !isDigested { + if err := cli.tagTrusted(trustedRef, ref.(reference.NamedTagged)); err != nil { + return nil, err + } + } + // Retry + var retryErr error + response, retryErr = cli.client.ContainerCreate(mergedConfig, name) + if retryErr != nil { + return nil, retryErr + } + } else { return nil, err } - } else if err != nil { - return nil, err } - defer serverResp.body.Close() - - var response types.ContainerCreateResponse - if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil { - return nil, err - } for _, warning := range response.Warnings { fmt.Fprintf(cli.err, "WARNING: %s\n", warning) } diff --git a/components/engine/api/client/diff.go b/components/engine/api/client/diff.go index 24350a04a0..f180e00172 100644 --- a/components/engine/api/client/diff.go +++ b/components/engine/api/client/diff.go @@ -1,10 +1,8 @@ package client import ( - "encoding/json" "fmt" - "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/pkg/archive" flag "github.com/docker/docker/pkg/mflag" @@ -27,18 +25,11 @@ func (cli *DockerCli) CmdDiff(args ...string) error { return fmt.Errorf("Container name cannot be empty") } - serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil, nil) + changes, err := cli.client.ContainerDiff(cmd.Arg(0)) if err != nil { return err } - defer serverResp.body.Close() - - changes := []types.ContainerChange{} - if err := json.NewDecoder(serverResp.body).Decode(&changes); err != nil { - return err - } - for _, change := range changes { var kind string switch change.Kind { diff --git a/components/engine/api/client/events.go b/components/engine/api/client/events.go index fa84eac518..a6702e130d 100644 --- a/components/engine/api/client/events.go +++ b/components/engine/api/client/events.go @@ -1,14 +1,12 @@ package client import ( - "net/url" - "time" - + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/jsonmessage" 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. @@ -24,10 +22,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error { cmd.ParseFlags(args, true) - var ( - v = url.Values{} - eventFilterArgs = filters.NewArgs() - ) + eventFilterArgs := filters.NewArgs() // Consolidate all filter flags, and sanity check them early. // They'll get process in the daemon/server. @@ -38,34 +33,18 @@ func (cli *DockerCli) CmdEvents(args ...string) error { return err } } - ref := time.Now() - if *since != "" { - ts, err := timeutils.GetTimestamp(*since, ref) - if err != nil { - return err - } - v.Set("since", ts) + + options := types.EventsOptions{ + Since: *since, + Until: *until, + Filters: eventFilterArgs, } - if *until != "" { - ts, err := timeutils.GetTimestamp(*until, ref) - if err != nil { - return err - } - v.Set("until", ts) - } - if eventFilterArgs.Len() > 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 { + + responseBody, err := cli.client.Events(options) + if err != nil { return err } - return nil + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) } diff --git a/components/engine/api/client/exec.go b/components/engine/api/client/exec.go index a9c836c943..06cdd2e4c8 100644 --- a/components/engine/api/client/exec.go +++ b/components/engine/api/client/exec.go @@ -1,7 +1,6 @@ package client import ( - "encoding/json" "fmt" "io" @@ -24,37 +23,29 @@ func (cli *DockerCli) CmdExec(args ...string) error { return Cli.StatusError{StatusCode: 1} } - serverResp, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, nil) + response, err := cli.client.ContainerExecCreate(*execConfig) if err != nil { return err } - defer serverResp.body.Close() - - var response types.ContainerExecCreateResponse - if err := json.NewDecoder(serverResp.body).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 { + execStartCheck := types.ExecStartCheck{ + Detach: execConfig.Detach, + Tty: execConfig.Tty, + } + + if err := cli.client.ContainerExecStart(execID, execStartCheck); err != nil { return err } // For now don't print this - wait for when we support exec wait() @@ -66,18 +57,9 @@ func (cli *DockerCli) CmdExec(args ...string) error { 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 } @@ -91,24 +73,15 @@ func (cli *DockerCli) CmdExec(args ...string) error { stderr = cli.err } } - errCh = promise.Go(func() error { - return cli.hijackWithContentType("POST", "/exec/"+execID+"/start", "application/json", 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 - } + resp, err := cli.client.ContainerExecAttach(execID, *execConfig) + if err != nil { + return err } + defer resp.Close() + errCh = promise.Go(func() error { + return cli.holdHijackedConnection(execConfig.Tty, in, out, stderr, resp) + }) if execConfig.Tty && cli.isTerminalIn { if err := cli.monitorTtySize(execID, true); err != nil { diff --git a/components/engine/api/client/export.go b/components/engine/api/client/export.go index 2763b7b045..ec94f6728e 100644 --- a/components/engine/api/client/export.go +++ b/components/engine/api/client/export.go @@ -2,6 +2,7 @@ package client import ( "errors" + "io" "os" Cli "github.com/docker/docker/cli" @@ -33,14 +34,12 @@ func (cli *DockerCli) CmdExport(args ...string) error { 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 { + responseBody, err := cli.client.ContainerExport(cmd.Arg(0)) + if err != nil { return err } + defer responseBody.Close() - return nil + _, err = io.Copy(output, responseBody) + return err } diff --git a/components/engine/api/client/hijack.go b/components/engine/api/client/hijack.go index bf152c6a9e..72f5ed43de 100644 --- a/components/engine/api/client/hijack.go +++ b/components/engine/api/client/hijack.go @@ -1,197 +1,21 @@ 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/dockerversion" + "github.com/docker/docker/api/types" "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 { - return cli.hijackWithContentType(method, path, "text/plain", setRawTerminal, in, stdout, stderr, started, data) -} - -func (cli *DockerCli) hijackWithContentType(method, path, contentType 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("%s/v%s%s", cli.basePath, api.Version, 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", contentType) - req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", "tcp") - req.Host = cli.addr - - dial, err := cli.dial() - if err != nil { - if strings.Contains(err.Error(), "connection refused") { - return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") - } - return 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 := dial.(*net.TCPConn); ok { - tcpConn.SetKeepAlive(true) - tcpConn.SetKeepAlivePeriod(30 * time.Second) - } - - 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 oldState *term.State - - if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" { +func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { + var ( + err error + oldState *term.State + ) + if inputStream != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" { oldState, err = term.SetRawTerminal(cli.inFd) if err != nil { return err @@ -200,22 +24,22 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se } receiveStdout := make(chan error, 1) - if stdout != nil || stderr != nil { + if outputStream != nil || errorStream != nil { go func() { defer func() { - if in != nil { + if inputStream != nil { if setRawTerminal && cli.isTerminalIn { term.RestoreTerminal(cli.inFd, oldState) } - in.Close() + inputStream.Close() } }() // When TTY is ON, use regular copy - if setRawTerminal && stdout != nil { - _, err = io.Copy(stdout, br) + if setRawTerminal && outputStream != nil { + _, err = io.Copy(outputStream, resp.Reader) } else { - _, err = stdcopy.StdCopy(stdout, stderr, br) + _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) } logrus.Debugf("[hijack] End of stdout") receiveStdout <- err @@ -224,17 +48,13 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se stdinDone := make(chan struct{}) go func() { - if in != nil { - io.Copy(rwc, in) + if inputStream != nil { + io.Copy(resp.Conn, inputStream) 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) - } + if err := resp.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) } close(stdinDone) }() @@ -246,7 +66,7 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se return err } case <-stdinDone: - if stdout != nil || stderr != nil { + if outputStream != nil || errorStream != nil { if err := <-receiveStdout; err != nil { logrus.Debugf("Error receiveStdout: %s", err) return err diff --git a/components/engine/api/client/history.go b/components/engine/api/client/history.go index f8a71c870d..fe092df08a 100644 --- a/components/engine/api/client/history.go +++ b/components/engine/api/client/history.go @@ -1,14 +1,12 @@ package client import ( - "encoding/json" "fmt" "strconv" "strings" "text/tabwriter" "time" - "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/stringid" @@ -28,18 +26,11 @@ func (cli *DockerCli) CmdHistory(args ...string) error { cmd.ParseFlags(args, true) - serverResp, err := cli.call("GET", "/images/"+cmd.Arg(0)+"/history", nil, nil) + history, err := cli.client.ImageHistory(cmd.Arg(0)) if err != nil { return err } - defer serverResp.body.Close() - - history := []types.ImageHistory{} - if err := json.NewDecoder(serverResp.body).Decode(&history); err != nil { - return err - } - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) if *quiet { diff --git a/components/engine/api/client/images.go b/components/engine/api/client/images.go index 26e7c9706e..895f1b0d56 100644 --- a/components/engine/api/client/images.go +++ b/components/engine/api/client/images.go @@ -1,9 +1,7 @@ package client import ( - "encoding/json" "fmt" - "net/url" "strings" "text/tabwriter" "time" @@ -45,36 +43,22 @@ func (cli *DockerCli) CmdImages(args ...string) error { } } - matchName := cmd.Arg(0) - v := url.Values{} - if imageFilterArgs.Len() > 0 { - filterJSON, err := filters.ToParam(imageFilterArgs) - if err != nil { - return err - } - v.Set("filters", filterJSON) - } - + var matchName string 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") + matchName = cmd.Arg(0) } - serverResp, err := cli.call("GET", "/images/json?"+v.Encode(), nil, nil) + options := types.ImageListOptions{ + MatchName: matchName, + All: *all, + Filters: imageFilterArgs, + } + + images, err := cli.client.ImageList(options) if err != nil { return err } - defer serverResp.body.Close() - - images := []types.Image{} - if err := json.NewDecoder(serverResp.body).Decode(&images); err != nil { - return err - } - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) if !*quiet { if *showDigests { diff --git a/components/engine/api/client/import.go b/components/engine/api/client/import.go index 2838debd98..2e9be55f57 100644 --- a/components/engine/api/client/import.go +++ b/components/engine/api/client/import.go @@ -3,12 +3,13 @@ package client import ( "fmt" "io" - "net/url" "os" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/jsonmessage" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/registry" @@ -29,20 +30,17 @@ func (cli *DockerCli) CmdImport(args ...string) error { cmd.ParseFlags(args, true) var ( - v = url.Values{} + in io.Reader + tag string src = cmd.Arg(0) + srcName = src repository = cmd.Arg(1) + changes = flChanges.GetAll() ) - v.Set("fromSrc", src) - v.Set("repo", repository) - v.Set("message", *message) - for _, change := range flChanges.GetAll() { - v.Add("changes", change) - } if cmd.NArg() == 3 { fmt.Fprintf(cli.err, "[DEPRECATED] The format 'file|URL|- [REPOSITORY [TAG]]' has been deprecated. Please use file|URL|- [REPOSITORY[:TAG]]\n") - v.Set("tag", cmd.Arg(2)) + tag = cmd.Arg(2) } if repository != "" { @@ -56,12 +54,10 @@ func (cli *DockerCli) CmdImport(args ...string) error { } } - var in io.Reader - if src == "-" { in = cli.in } else if !urlutil.IsURL(src) { - v.Set("fromSrc", "-") + srcName = "-" file, err := os.Open(src) if err != nil { return err @@ -71,12 +67,20 @@ func (cli *DockerCli) CmdImport(args ...string) error { } - sopts := &streamOpts{ - rawTerminal: true, - in: in, - out: cli.out, + options := types.ImageImportOptions{ + Source: in, + SourceName: srcName, + RepositoryName: repository, + Message: *message, + Tag: tag, + Changes: changes, } - _, err := cli.stream("POST", "/images/create?"+v.Encode(), sopts) - return err + responseBody, err := cli.client.ImageImport(options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) } diff --git a/components/engine/api/client/info.go b/components/engine/api/client/info.go index cf81418019..1abc20db2e 100644 --- a/components/engine/api/client/info.go +++ b/components/engine/api/client/info.go @@ -1,12 +1,9 @@ package client import ( - "encoding/json" "fmt" - "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" - "github.com/docker/docker/pkg/httputils" "github.com/docker/docker/pkg/ioutils" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/units" @@ -21,18 +18,11 @@ func (cli *DockerCli) CmdInfo(args ...string) error { cmd.ParseFlags(args, true) - serverResp, err := cli.call("GET", "/info", nil, nil) + info, err := cli.client.Info() if err != nil { return err } - defer serverResp.body.Close() - - info := &types.Info{} - if err := json.NewDecoder(serverResp.body).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, "Server Version: %s\n", info.ServerVersion) @@ -90,38 +80,36 @@ func (cli *DockerCli) CmdInfo(args ...string) error { } // Only output these warnings if the server does not support these features - if h, err := httputils.ParseServerHeader(serverResp.header.Get("Server")); err == nil { - if h.OS != "windows" { - 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.OomKillDisable { - fmt.Fprintf(cli.err, "WARNING: No oom kill disable support\n") - } - if !info.CPUCfsQuota { - fmt.Fprintf(cli.err, "WARNING: No cpu cfs quota support\n") - } - if !info.CPUCfsPeriod { - fmt.Fprintf(cli.err, "WARNING: No cpu cfs period support\n") - } - if !info.CPUShares { - fmt.Fprintf(cli.err, "WARNING: No cpu shares support\n") - } - if !info.CPUSet { - fmt.Fprintf(cli.err, "WARNING: No cpuset support\n") - } - if !info.IPv4Forwarding { - fmt.Fprintf(cli.err, "WARNING: IPv4 forwarding is disabled\n") - } - if !info.BridgeNfIptables { - fmt.Fprintf(cli.err, "WARNING: bridge-nf-call-iptables is disabled\n") - } - if !info.BridgeNfIP6tables { - fmt.Fprintf(cli.err, "WARNING: bridge-nf-call-ip6tables is disabled\n") - } + if info.OSType != "windows" { + 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.OomKillDisable { + fmt.Fprintf(cli.err, "WARNING: No oom kill disable support\n") + } + if !info.CPUCfsQuota { + fmt.Fprintf(cli.err, "WARNING: No cpu cfs quota support\n") + } + if !info.CPUCfsPeriod { + fmt.Fprintf(cli.err, "WARNING: No cpu cfs period support\n") + } + if !info.CPUShares { + fmt.Fprintf(cli.err, "WARNING: No cpu shares support\n") + } + if !info.CPUSet { + fmt.Fprintf(cli.err, "WARNING: No cpuset support\n") + } + if !info.IPv4Forwarding { + fmt.Fprintf(cli.err, "WARNING: IPv4 forwarding is disabled\n") + } + if !info.BridgeNfIptables { + fmt.Fprintf(cli.err, "WARNING: bridge-nf-call-iptables is disabled\n") + } + if !info.BridgeNfIP6tables { + fmt.Fprintf(cli.err, "WARNING: bridge-nf-call-ip6tables is disabled\n") } } diff --git a/components/engine/api/client/inspect.go b/components/engine/api/client/inspect.go index fe3666e3b9..375129bdaa 100644 --- a/components/engine/api/client/inspect.go +++ b/components/engine/api/client/inspect.go @@ -1,16 +1,12 @@ package client import ( - "bytes" "encoding/json" "fmt" - "io" - "net/http" - "net/url" - "strings" "text/template" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/inspect" + "github.com/docker/docker/api/client/lib" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -34,172 +30,104 @@ func (cli *DockerCli) CmdInspect(args ...string) error { cmd.ParseFlags(args, true) - var tmpl *template.Template - var err error - var obj []byte - var statusCode int - - if *tmplStr != "" { - if tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr); err != nil { - return Cli.StatusError{StatusCode: 64, - Status: "Template parsing error: " + err.Error()} - } - } - if *inspectType != "" && *inspectType != "container" && *inspectType != "image" { return fmt.Errorf("%q is not a valid value for --type", *inspectType) } - indented := new(bytes.Buffer) - indented.WriteString("[\n") - status := 0 - isImage := false - - v := url.Values{} - if *size { - v.Set("size", "1") + var elementSearcher inspectSearcher + switch *inspectType { + case "container": + elementSearcher = cli.inspectContainers(*size) + case "image": + elementSearcher = cli.inspectImages(*size) + default: + elementSearcher = cli.inspectAll(*size) } - for _, name := range cmd.Args() { - if *inspectType == "" || *inspectType == "container" { - obj, statusCode, err = readBody(cli.call("GET", "/containers/"+name+"/json?"+v.Encode(), nil, nil)) - if err != nil { - if err == errConnectionFailed { - return err - } - if *inspectType == "container" { - if statusCode == http.StatusNotFound { - fmt.Fprintf(cli.err, "Error: No such container: %s\n", name) - } else { - fmt.Fprintf(cli.err, "%s", err) + return cli.inspectElements(*tmplStr, cmd.Args(), elementSearcher) +} + +func (cli *DockerCli) inspectContainers(getSize bool) inspectSearcher { + return func(ref string) (interface{}, []byte, error) { + return cli.client.ContainerInspectWithRaw(ref, getSize) + } +} + +func (cli *DockerCli) inspectImages(getSize bool) inspectSearcher { + return func(ref string) (interface{}, []byte, error) { + return cli.client.ImageInspectWithRaw(ref, getSize) + } +} + +func (cli *DockerCli) inspectAll(getSize bool) inspectSearcher { + return func(ref string) (interface{}, []byte, error) { + c, rawContainer, err := cli.client.ContainerInspectWithRaw(ref, getSize) + if err != nil { + // Search for image with that id if a container doesn't exist. + if lib.IsErrContainerNotFound(err) { + i, rawImage, err := cli.client.ImageInspectWithRaw(ref, getSize) + if err != nil { + if lib.IsErrImageNotFound(err) { + return nil, nil, fmt.Errorf("Error: No such image or container: %s", ref) } - status = 1 - continue + return nil, nil, err } + return i, rawImage, err } + return nil, nil, err } + return c, rawContainer, err + } +} - if obj == nil && (*inspectType == "" || *inspectType == "image") { - obj, statusCode, err = readBody(cli.call("GET", "/images/"+name+"/json", nil, nil)) - isImage = true - if err != nil { - if err == errConnectionFailed { - return err - } - if statusCode == http.StatusNotFound { - if *inspectType == "" { - fmt.Fprintf(cli.err, "Error: No such image or container: %s\n", name) - } else { - fmt.Fprintf(cli.err, "Error: No such image: %s\n", name) - } - } else { - fmt.Fprintf(cli.err, "%s", err) - } - status = 1 - continue - } - } +type inspectSearcher func(ref string) (interface{}, []byte, error) - 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) - buf := bytes.NewBufferString("") - - if isImage { - inspPtr := types.ImageInspect{} - if err := dec.Decode(&inspPtr); err != nil { - fmt.Fprintf(cli.err, "Unable to read inspect data: %v\n", err) - status = 1 - break - } - if err := tmpl.Execute(buf, inspPtr); err != nil { - rdr.Seek(0, 0) - var ok bool - - if buf, ok = cli.decodeRawInspect(tmpl, dec); !ok { - fmt.Fprintf(cli.err, "Template parsing error: %v\n", err) - status = 1 - break - } - } - } else { - inspPtr := types.ContainerJSON{} - if err := dec.Decode(&inspPtr); err != nil { - fmt.Fprintf(cli.err, "Unable to read inspect data: %v\n", err) - status = 1 - break - } - if err := tmpl.Execute(buf, inspPtr); err != nil { - rdr.Seek(0, 0) - var ok bool - - if buf, ok = cli.decodeRawInspect(tmpl, dec); !ok { - fmt.Fprintf(cli.err, "Template parsing error: %v\n", err) - status = 1 - break - } - } - } - - cli.out.Write(buf.Bytes()) - cli.out.Write([]byte{'\n'}) - } - indented.WriteString(",") +func (cli *DockerCli) inspectElements(tmplStr string, references []string, searchByReference inspectSearcher) error { + elementInspector, err := cli.newInspectorWithTemplate(tmplStr) + if err != nil { + return Cli.StatusError{StatusCode: 64, Status: err.Error()} } - if indented.Len() > 1 { - // Remove trailing ',' - indented.Truncate(indented.Len() - 1) - } - indented.WriteString("]\n") + var inspectErr error + for _, ref := range references { + element, raw, err := searchByReference(ref) + if err != nil { + inspectErr = err + break + } - 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 err := elementInspector.Inspect(element, raw); err != nil { + inspectErr = err + break } } - if status != 0 { + if err := elementInspector.Flush(); err != nil { + cli.inspectErrorStatus(err) + } + + if status := cli.inspectErrorStatus(inspectErr); status != 0 { return Cli.StatusError{StatusCode: status} } return nil } -// decodeRawInspect executes the inspect template with a raw interface. -// This allows docker cli to parse inspect structs injected with Swarm fields. -// Unfortunately, go 1.4 doesn't fail executing invalid templates when the input is an interface. -// It doesn't allow to modify this behavior either, sending messages to the output. -// We assume that the template is invalid when there is a , if the template was valid -// we'd get or "" values. In that case we fail with the original error raised executing the -// template with the typed input. -// -// TODO: Go 1.5 allows to customize the error behavior, we can probably get rid of this as soon as -// we build Docker with that version: -// https://golang.org/pkg/text/template/#Template.Option -func (cli *DockerCli) decodeRawInspect(tmpl *template.Template, dec *json.Decoder) (*bytes.Buffer, bool) { - var raw interface{} - buf := bytes.NewBufferString("") - - if rawErr := dec.Decode(&raw); rawErr != nil { - fmt.Fprintf(cli.err, "Unable to read inspect data: %v\n", rawErr) - return buf, false +func (cli *DockerCli) inspectErrorStatus(err error) (status int) { + if err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 } + return +} - if rawErr := tmpl.Execute(buf, raw); rawErr != nil { - return buf, false +func (cli *DockerCli) newInspectorWithTemplate(tmplStr string) (inspect.Inspector, error) { + elementInspector := inspect.NewIndentedInspector(cli.out) + if tmplStr != "" { + tmpl, err := template.New("").Funcs(funcMap).Parse(tmplStr) + if err != nil { + return nil, fmt.Errorf("Template parsing error: %s", err) + } + elementInspector = inspect.NewTemplateInspector(cli.out, tmpl) } - - if strings.Contains(buf.String(), "") { - return buf, false - } - return buf, true + return elementInspector, nil } diff --git a/components/engine/api/client/inspect/inspector.go b/components/engine/api/client/inspect/inspector.go new file mode 100644 index 0000000000..eee0f5ddb7 --- /dev/null +++ b/components/engine/api/client/inspect/inspector.go @@ -0,0 +1,111 @@ +package inspect + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "text/template" +) + +// Inspector defines an interface to implement to process elements +type Inspector interface { + Inspect(typedElement interface{}, rawElement []byte) error + Flush() error +} + +// TemplateInspector uses a text template to inspect elements. +type TemplateInspector struct { + outputStream io.Writer + buffer *bytes.Buffer + tmpl *template.Template +} + +// NewTemplateInspector creates a new inspector with a template. +func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector { + return &TemplateInspector{ + outputStream: outputStream, + buffer: new(bytes.Buffer), + tmpl: tmpl, + } +} + +// Inspect executes the inspect template. +// It decodes the raw element into a map if the initial execution fails. +// This allows docker cli to parse inspect structs injected with Swarm fields. +func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error { + buffer := new(bytes.Buffer) + if err := i.tmpl.Execute(buffer, typedElement); err != nil { + if rawElement == nil { + return fmt.Errorf("Template parsing error: %v", err) + } + return i.tryRawInspectFallback(rawElement) + } + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} + +func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { + var raw interface{} + buffer := new(bytes.Buffer) + rdr := bytes.NewReader(rawElement) + dec := json.NewDecoder(rdr) + + if rawErr := dec.Decode(&raw); rawErr != nil { + return fmt.Errorf("unable to read inspect data: %v", rawErr) + } + + tmplMissingKey := i.tmpl.Option("missingkey=error") + if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { + return fmt.Errorf("Template parsing error: %v", rawErr) + } + + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} + +// Flush write the result of inspecting all elements into the output stream. +func (i *TemplateInspector) Flush() error { + _, err := io.Copy(i.outputStream, i.buffer) + return err +} + +// IndentedInspector uses a buffer to stop the indented representation of an element. +type IndentedInspector struct { + outputStream io.Writer + elements []interface{} +} + +// NewIndentedInspector generates a new IndentedInspector. +func NewIndentedInspector(outputStream io.Writer) Inspector { + return &IndentedInspector{ + outputStream: outputStream, + } +} + +// Inspect writes the raw element with an indented json format. +func (i *IndentedInspector) Inspect(typedElement interface{}, _ []byte) error { + i.elements = append(i.elements, typedElement) + return nil +} + +// Flush write the result of inspecting all elements into the output stream. +func (i *IndentedInspector) Flush() error { + if len(i.elements) == 0 { + _, err := io.WriteString(i.outputStream, "[]\n") + return err + } + + buffer, err := json.MarshalIndent(i.elements, "", " ") + if err != nil { + return err + } + + if _, err := io.Copy(i.outputStream, bytes.NewReader(buffer)); err != nil { + return err + } + _, err = io.WriteString(i.outputStream, "\n") + return err +} diff --git a/components/engine/api/client/kill.go b/components/engine/api/client/kill.go index c4c355c74b..e71304644f 100644 --- a/components/engine/api/client/kill.go +++ b/components/engine/api/client/kill.go @@ -19,7 +19,7 @@ func (cli *DockerCli) CmdKill(args ...string) error { 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 { + if err := cli.client.ContainerKill(name, *signal); err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { diff --git a/components/engine/api/client/lib/client.go b/components/engine/api/client/lib/client.go new file mode 100644 index 0000000000..3625a8170f --- /dev/null +++ b/components/engine/api/client/lib/client.go @@ -0,0 +1,102 @@ +package lib + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/api" + "github.com/docker/docker/pkg/sockets" + "github.com/docker/docker/pkg/tlsconfig" + "github.com/docker/docker/pkg/version" +) + +// Client is the API client that performs all operations +// against a docker server. +type Client struct { + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string + // basePath holds the path to prepend to the requests + basePath string + // scheme holds the scheme of the client i.e. https. + scheme string + // tlsConfig holds the tls configuration to use in hijacked requests. + tlsConfig *tls.Config + // httpClient holds the client transport instance. Exported to keep the old code running. + httpClient *http.Client + // version of the server to talk to. + version version.Version + // custom http headers configured by users + customHTTPHeaders map[string]string +} + +// NewClient initializes a new API client +// for the given host. It uses the tlsOptions +// to decide whether to use a secure connection or not. +// It also initializes the custom http headers to add to each request. +func NewClient(host string, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) { + return NewClientWithVersion(host, api.Version, tlsOptions, httpHeaders) +} + +// NewClientWithVersion initializes a new API client +// for the given host and API version. It uses the tlsOptions +// to decide whether to use a secure connection or not. +// It also initializes the custom http headers to add to each request. +func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) { + var ( + basePath string + tlsConfig *tls.Config + scheme = "http" + protoAddrParts = strings.SplitN(host, "://", 2) + proto, addr = protoAddrParts[0], protoAddrParts[1] + ) + + if proto == "tcp" { + parsed, err := url.Parse("tcp://" + addr) + if err != nil { + return nil, err + } + addr = parsed.Host + basePath = parsed.Path + } + + if tlsOptions != nil { + scheme = "https" + var err error + tlsConfig, err = tlsconfig.Client(*tlsOptions) + if err != nil { + return nil, err + } + } + + // The transport is created here for reuse during the client session. + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + sockets.ConfigureTCPTransport(transport, proto, addr) + + return &Client{ + proto: proto, + addr: addr, + basePath: basePath, + scheme: scheme, + tlsConfig: tlsConfig, + httpClient: &http.Client{Transport: transport}, + version: version, + customHTTPHeaders: httpHeaders, + }, nil +} + +// getAPIPath returns the versioned request path to call the api. +// It appends the query parameters to the path if they are not empty. +func (cli *Client) getAPIPath(p string, query url.Values) string { + apiPath := fmt.Sprintf("%s/v%s%s", cli.basePath, cli.version, p) + if len(query) > 0 { + apiPath += "?" + query.Encode() + } + return apiPath +} diff --git a/components/engine/api/client/lib/container_attach.go b/components/engine/api/client/lib/container_attach.go new file mode 100644 index 0000000000..439ec2d0d7 --- /dev/null +++ b/components/engine/api/client/lib/container_attach.go @@ -0,0 +1,30 @@ +package lib + +import ( + "net/url" + + "github.com/docker/docker/api/types" +) + +// ContainerAttach attaches a connection to a container in the server. +// It returns a types.HijackedConnection with the hijacked connection +// and the a reader to get output. It's up to the called to close +// the hijacked connection by calling types.HijackedResponse.Close. +func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error) { + query := url.Values{} + if options.Stream { + query.Set("stream", "1") + } + if options.Stdin { + query.Set("stdin", "1") + } + if options.Stdout { + query.Set("stdout", "1") + } + if options.Stderr { + query.Set("stderr", "1") + } + + headers := map[string][]string{"Content-Type": {"text/plain"}} + return cli.postHijacked("/containers/"+options.ContainerID+"/attach", query, nil, headers) +} diff --git a/components/engine/api/client/lib/container_commit.go b/components/engine/api/client/lib/container_commit.go new file mode 100644 index 0000000000..45cf166250 --- /dev/null +++ b/components/engine/api/client/lib/container_commit.go @@ -0,0 +1,37 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ContainerCommit applies changes into a container and creates a new tagged image. +func (cli *Client) ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) { + query := url.Values{} + query.Set("container", options.ContainerID) + query.Set("repo", options.RepositoryName) + query.Set("tag", options.Tag) + query.Set("comment", options.Comment) + query.Set("author", options.Author) + for _, change := range options.Changes { + query.Add("changes", change) + } + if options.Pause != true { + query.Set("pause", "0") + } + + var response types.ContainerCommitResponse + resp, err := cli.post("/commit", query, options.Config, nil) + if err != nil { + return response, err + } + defer ensureReaderClosed(resp) + + if err := json.NewDecoder(resp.body).Decode(&response); err != nil { + return response, err + } + + return response, nil +} diff --git a/components/engine/api/client/lib/container_create.go b/components/engine/api/client/lib/container_create.go new file mode 100644 index 0000000000..a5f24b09fc --- /dev/null +++ b/components/engine/api/client/lib/container_create.go @@ -0,0 +1,43 @@ +package lib + +import ( + "encoding/json" + "net/url" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/runconfig" +) + +// ContainerCreate creates a new container based in the given configuration. +// It can be associated with a name, but it's not mandatory. +func (cli *Client) ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error) { + var response types.ContainerCreateResponse + query := url.Values{} + if containerName != "" { + query.Set("name", containerName) + } + + serverResp, err := cli.post("/containers/create", query, config, nil) + if err != nil { + if serverResp != nil && serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) { + return response, imageNotFoundError{config.Image} + } + return response, err + } + + if serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) { + return response, imageNotFoundError{config.Image} + } + + if err != nil { + return response, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil { + return response, err + } + + return response, nil +} diff --git a/components/engine/api/client/lib/container_inspect.go b/components/engine/api/client/lib/container_inspect.go new file mode 100644 index 0000000000..41e413132e --- /dev/null +++ b/components/engine/api/client/lib/container_inspect.go @@ -0,0 +1,64 @@ +package lib + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ContainerInspect returns the container information. +func (cli *Client) ContainerInspect(containerID string) (types.ContainerJSON, error) { + serverResp, err := cli.get("/containers/"+containerID+"/json", nil, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ContainerJSON{}, containerNotFoundError{containerID} + } + return types.ContainerJSON{}, err + } + defer ensureReaderClosed(serverResp) + + var response types.ContainerJSON + err = json.NewDecoder(serverResp.body).Decode(&response) + return response, err +} + +// ContainerInspectWithRaw returns the container information and it's raw representation. +func (cli *Client) ContainerInspectWithRaw(containerID string, getSize bool) (types.ContainerJSON, []byte, error) { + query := url.Values{} + if getSize { + query.Set("size", "1") + } + serverResp, err := cli.get("/containers/"+containerID+"/json", query, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ContainerJSON{}, nil, containerNotFoundError{containerID} + } + return types.ContainerJSON{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return types.ContainerJSON{}, nil, err + } + + var response types.ContainerJSON + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} + +func (cli *Client) containerInspectWithResponse(containerID string, query url.Values) (types.ContainerJSON, *serverResponse, error) { + serverResp, err := cli.get("/containers/"+containerID+"/json", nil, nil) + if err != nil { + return types.ContainerJSON{}, serverResp, err + } + + var response types.ContainerJSON + err = json.NewDecoder(serverResp.body).Decode(&response) + return response, serverResp, err +} diff --git a/components/engine/api/client/lib/container_list.go b/components/engine/api/client/lib/container_list.go new file mode 100644 index 0000000000..5392894c76 --- /dev/null +++ b/components/engine/api/client/lib/container_list.go @@ -0,0 +1,54 @@ +package lib + +import ( + "encoding/json" + "net/url" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/parsers/filters" +) + +// ContainerList returns the list of containers in the docker host. +func (cli *Client) ContainerList(options types.ContainerListOptions) ([]types.Container, error) { + query := url.Values{} + + if options.All { + query.Set("all", "1") + } + + if options.Limit != -1 { + query.Set("limit", strconv.Itoa(options.Limit)) + } + + if options.Since != "" { + query.Set("since", options.Since) + } + + if options.Before != "" { + query.Set("before", options.Before) + } + + if options.Size { + query.Set("size", "1") + } + + if options.Filter.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filter) + if err != nil { + return nil, err + } + + query.Set("filters", filterJSON) + } + + resp, err := cli.get("/containers/json", query, nil) + if err != nil { + return nil, err + } + defer ensureReaderClosed(resp) + + var containers []types.Container + err = json.NewDecoder(resp.body).Decode(&containers) + return containers, err +} diff --git a/components/engine/api/client/lib/container_remove.go b/components/engine/api/client/lib/container_remove.go new file mode 100644 index 0000000000..aff5012b36 --- /dev/null +++ b/components/engine/api/client/lib/container_remove.go @@ -0,0 +1,26 @@ +package lib + +import ( + "net/url" + + "github.com/docker/docker/api/types" +) + +// ContainerRemove kills and removes a container from the docker host. +func (cli *Client) ContainerRemove(options types.ContainerRemoveOptions) error { + query := url.Values{} + if options.RemoveVolumes { + query.Set("v", "1") + } + if options.RemoveLinks { + query.Set("link", "1") + } + + if options.Force { + query.Set("force", "1") + } + + resp, err := cli.delete("/containers/"+options.ContainerID, query, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/container_rename.go b/components/engine/api/client/lib/container_rename.go new file mode 100644 index 0000000000..1aed00ada1 --- /dev/null +++ b/components/engine/api/client/lib/container_rename.go @@ -0,0 +1,12 @@ +package lib + +import "net/url" + +// ContainerRename changes the name of a given container. +func (cli *Client) ContainerRename(containerID, newContainerName string) error { + query := url.Values{} + query.Set("name", newContainerName) + resp, err := cli.post("/containers/"+containerID+"/rename", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/container_restart.go b/components/engine/api/client/lib/container_restart.go new file mode 100644 index 0000000000..74b5022b90 --- /dev/null +++ b/components/engine/api/client/lib/container_restart.go @@ -0,0 +1,17 @@ +package lib + +import ( + "net/url" + "strconv" +) + +// ContainerRestart stops and starts a container again. +// It makes the daemon to wait for the container to be up again for +// a specific amount of time, given the timeout. +func (cli *Client) ContainerRestart(containerID string, timeout int) error { + query := url.Values{} + query.Set("t", strconv.Itoa(timeout)) + resp, err := cli.post("/containers/"+containerID+"/restart", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/container_start.go b/components/engine/api/client/lib/container_start.go new file mode 100644 index 0000000000..2c65b5df30 --- /dev/null +++ b/components/engine/api/client/lib/container_start.go @@ -0,0 +1,8 @@ +package lib + +// ContainerStart sends a request to the docker daemon to start a container. +func (cli *Client) ContainerStart(containerID string) error { + resp, err := cli.post("/containers/"+containerID+"/start", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/container_stats.go b/components/engine/api/client/lib/container_stats.go new file mode 100644 index 0000000000..a18fae7a0b --- /dev/null +++ b/components/engine/api/client/lib/container_stats.go @@ -0,0 +1,22 @@ +package lib + +import ( + "io" + "net/url" +) + +// ContainerStats returns near realtime stats for a given container. +// It's up to the caller to close the io.ReadCloser returned. +func (cli *Client) ContainerStats(containerID string, stream bool) (io.ReadCloser, error) { + query := url.Values{} + query.Set("stream", "0") + if stream { + query.Set("stream", "1") + } + + resp, err := cli.get("/containers/"+containerID+"/stats", query, nil) + if err != nil { + return nil, err + } + return resp.body, err +} diff --git a/components/engine/api/client/lib/container_stop.go b/components/engine/api/client/lib/container_stop.go new file mode 100644 index 0000000000..3bb73ea75e --- /dev/null +++ b/components/engine/api/client/lib/container_stop.go @@ -0,0 +1,16 @@ +package lib + +import ( + "net/url" + "strconv" +) + +// ContainerStop stops a container without terminating the process. +// The process is blocked until the container stops or the timeout expires. +func (cli *Client) ContainerStop(containerID string, timeout int) error { + query := url.Values{} + query.Set("t", strconv.Itoa(timeout)) + resp, err := cli.post("/containers/"+containerID+"/stop", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/container_top.go b/components/engine/api/client/lib/container_top.go new file mode 100644 index 0000000000..abee1ed417 --- /dev/null +++ b/components/engine/api/client/lib/container_top.go @@ -0,0 +1,27 @@ +package lib + +import ( + "encoding/json" + "net/url" + "strings" + + "github.com/docker/docker/api/types" +) + +// ContainerTop shows process information from within a container. +func (cli *Client) ContainerTop(containerID string, arguments []string) (types.ContainerProcessList, error) { + var response types.ContainerProcessList + query := url.Values{} + if len(arguments) > 0 { + query.Set("ps_args", strings.Join(arguments, " ")) + } + + resp, err := cli.get("/containers/"+containerID+"/top", query, nil) + if err != nil { + return response, err + } + defer ensureReaderClosed(resp) + + err = json.NewDecoder(resp.body).Decode(&response) + return response, err +} diff --git a/components/engine/api/client/lib/container_unpause.go b/components/engine/api/client/lib/container_unpause.go new file mode 100644 index 0000000000..146cbd8fc4 --- /dev/null +++ b/components/engine/api/client/lib/container_unpause.go @@ -0,0 +1,8 @@ +package lib + +// ContainerUnpause resumes the process execution within a container +func (cli *Client) ContainerUnpause(containerID string) error { + resp, err := cli.post("/containers/"+containerID+"/unpause", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/copy.go b/components/engine/api/client/lib/copy.go new file mode 100644 index 0000000000..e26a7f79b0 --- /dev/null +++ b/components/engine/api/client/lib/copy.go @@ -0,0 +1,95 @@ +package lib + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types" +) + +// ContainerStatPath returns Stat information about a path inside the container filesystem. +func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerPathStat, error) { + query := url.Values{} + query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. + + urlStr := fmt.Sprintf("/containers/%s/archive", containerID) + response, err := cli.head(urlStr, query, nil) + if err != nil { + return types.ContainerPathStat{}, err + } + defer ensureReaderClosed(response) + return getContainerPathStatFromHeader(response.header) +} + +// CopyToContainer copies content into the container filesystem. +func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error { + query := url.Values{} + query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API. + // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. + if !options.AllowOverwriteDirWithFile { + query.Set("noOverwriteDirNonDir", "true") + } + + path := fmt.Sprintf("/containers/%s/archive", options.ContainerID) + + response, err := cli.putRaw(path, query, options.Content, nil) + if err != nil { + return err + } + defer ensureReaderClosed(response) + + if response.statusCode != http.StatusOK { + return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + } + + return nil +} + +// CopyFromContainer get the content from the container and return it as a Reader +// to manipulate it in the host. It's up to the caller to close the reader. +func (cli *Client) CopyFromContainer(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) { + query := make(url.Values, 1) + query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. + + apiPath := fmt.Sprintf("/containers/%s/archive", containerID) + response, err := cli.get(apiPath, query, nil) + if err != nil { + return nil, types.ContainerPathStat{}, err + } + + if response.statusCode != http.StatusOK { + return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) + } + + // In order to get the copy behavior right, we need to know information + // about both the source and the destination. The response headers include + // stat info about the source that we can use in deciding exactly how to + // copy it locally. Along with the stat info about the local destination, + // we have everything we need to handle the multiple possibilities there + // can be when copying a file/dir from one location to another file/dir. + stat, err := getContainerPathStatFromHeader(response.header) + if err != nil { + return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) + } + return response.body, stat, err +} + +func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) { + var stat types.ContainerPathStat + + encodedStat := header.Get("X-Docker-Container-Path-Stat") + statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) + + err := json.NewDecoder(statDecoder).Decode(&stat) + if err != nil { + err = fmt.Errorf("unable to decode container path stat header: %s", err) + } + + return stat, err +} diff --git a/components/engine/api/client/lib/diff.go b/components/engine/api/client/lib/diff.go new file mode 100644 index 0000000000..15954c5ed7 --- /dev/null +++ b/components/engine/api/client/lib/diff.go @@ -0,0 +1,25 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ContainerDiff shows differences in a container filesystem since it was started. +func (cli *Client) ContainerDiff(containerID string) ([]types.ContainerChange, error) { + var changes []types.ContainerChange + + serverResp, err := cli.get("/containers/"+containerID+"/changes", url.Values{}, nil) + if err != nil { + return changes, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&changes); err != nil { + return changes, err + } + + return changes, nil +} diff --git a/components/engine/api/client/lib/errors.go b/components/engine/api/client/lib/errors.go new file mode 100644 index 0000000000..95b88075da --- /dev/null +++ b/components/engine/api/client/lib/errors.go @@ -0,0 +1,94 @@ +package lib + +import ( + "errors" + "fmt" +) + +// ErrConnectionFailed is a error raised when the connection between the client and the server failed. +var ErrConnectionFailed = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?") + +// imageNotFoundError implements an error returned when an image is not in the docker host. +type imageNotFoundError struct { + imageID string +} + +// Error returns a string representation of an imageNotFoundError +func (i imageNotFoundError) Error() string { + return fmt.Sprintf("Error: No such image: %s", i.imageID) +} + +// IsErrImageNotFound returns true if the error is caused +// when an image is not found in the docker host. +func IsErrImageNotFound(err error) bool { + _, ok := err.(imageNotFoundError) + return ok +} + +// containerNotFoundError implements an error returned when a container is not in the docker host. +type containerNotFoundError struct { + containerID string +} + +// Error returns a string representation of an containerNotFoundError +func (e containerNotFoundError) Error() string { + return fmt.Sprintf("Error: No such container: %s", e.containerID) +} + +// IsErrContainerNotFound returns true if the error is caused +// when a container is not found in the docker host. +func IsErrContainerNotFound(err error) bool { + _, ok := err.(containerNotFoundError) + return ok +} + +// networkNotFoundError implements an error returned when a network is not in the docker host. +type networkNotFoundError struct { + networkID string +} + +// Error returns a string representation of an networkNotFoundError +func (e networkNotFoundError) Error() string { + return fmt.Sprintf("Error: No such network: %s", e.networkID) +} + +// IsErrNetworkNotFound returns true if the error is caused +// when a network is not found in the docker host. +func IsErrNetworkNotFound(err error) bool { + _, ok := err.(networkNotFoundError) + return ok +} + +// volumeNotFoundError implements an error returned when a volume is not in the docker host. +type volumeNotFoundError struct { + volumeID string +} + +// Error returns a string representation of an networkNotFoundError +func (e volumeNotFoundError) Error() string { + return fmt.Sprintf("Error: No such volume: %s", e.volumeID) +} + +// IsErrVolumeNotFound returns true if the error is caused +// when a volume is not found in the docker host. +func IsErrVolumeNotFound(err error) bool { + _, ok := err.(networkNotFoundError) + return ok +} + +// unauthorizedError represents an authorization error in a remote registry. +type unauthorizedError struct { + cause error +} + +// Error returns a string representation of an unauthorizedError +func (u unauthorizedError) Error() string { + return u.cause.Error() +} + +// IsErrUnauthorized returns true if the error is caused +// when an the remote registry authentication fails +func IsErrUnauthorized(err error) bool { + _, ok := err.(unauthorizedError) + return ok +} diff --git a/components/engine/api/client/lib/events.go b/components/engine/api/client/lib/events.go new file mode 100644 index 0000000000..1d63a845a0 --- /dev/null +++ b/components/engine/api/client/lib/events.go @@ -0,0 +1,46 @@ +package lib + +import ( + "io" + "net/url" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/timeutils" +) + +// Events returns a stream of events in the daemon in a ReadCloser. +// It's up to the caller to close the stream. +func (cli *Client) Events(options types.EventsOptions) (io.ReadCloser, error) { + query := url.Values{} + ref := time.Now() + + if options.Since != "" { + ts, err := timeutils.GetTimestamp(options.Since, ref) + if err != nil { + return nil, err + } + query.Set("since", ts) + } + if options.Until != "" { + ts, err := timeutils.GetTimestamp(options.Until, ref) + if err != nil { + return nil, err + } + query.Set("until", ts) + } + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) + if err != nil { + return nil, err + } + query.Set("filters", filterJSON) + } + + serverResponse, err := cli.get("/events", query, nil) + if err != nil { + return nil, err + } + return serverResponse.body, nil +} diff --git a/components/engine/api/client/lib/exec.go b/components/engine/api/client/lib/exec.go new file mode 100644 index 0000000000..4b042c71f2 --- /dev/null +++ b/components/engine/api/client/lib/exec.go @@ -0,0 +1,49 @@ +package lib + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/runconfig" +) + +// ContainerExecCreate creates a new exec configuration to run an exec process. +func (cli *Client) ContainerExecCreate(config runconfig.ExecConfig) (types.ContainerExecCreateResponse, error) { + var response types.ContainerExecCreateResponse + resp, err := cli.post("/containers/"+config.Container+"/exec", nil, config, nil) + if err != nil { + return response, err + } + defer ensureReaderClosed(resp) + err = json.NewDecoder(resp.body).Decode(&response) + return response, err +} + +// ContainerExecStart starts an exec process already create in the docker host. +func (cli *Client) ContainerExecStart(execID string, config types.ExecStartCheck) error { + resp, err := cli.post("/exec/"+execID+"/start", nil, config, nil) + ensureReaderClosed(resp) + return err +} + +// ContainerExecAttach attaches a connection to an exec process in the server. +// It returns a types.HijackedConnection with the hijacked connection +// and the a reader to get output. It's up to the called to close +// the hijacked connection by calling types.HijackedResponse.Close. +func (cli *Client) ContainerExecAttach(execID string, config runconfig.ExecConfig) (types.HijackedResponse, error) { + headers := map[string][]string{"Content-Type": {"application/json"}} + return cli.postHijacked("/exec/"+execID+"/start", nil, config, headers) +} + +// ContainerExecInspect returns information about a specific exec process on the docker host. +func (cli *Client) ContainerExecInspect(execID string) (types.ContainerExecInspect, error) { + var response types.ContainerExecInspect + resp, err := cli.get("/exec/"+execID+"/json", nil, nil) + if err != nil { + return response, err + } + defer ensureReaderClosed(resp) + + err = json.NewDecoder(resp.body).Decode(&response) + return response, err +} diff --git a/components/engine/api/client/lib/export.go b/components/engine/api/client/lib/export.go new file mode 100644 index 0000000000..07893c5b83 --- /dev/null +++ b/components/engine/api/client/lib/export.go @@ -0,0 +1,18 @@ +package lib + +import ( + "io" + "net/url" +) + +// ContainerExport retrieves the raw contents of a container +// and returns them as a io.ReadCloser. It's up to the caller +// to close the stream. +func (cli *Client) ContainerExport(containerID string) (io.ReadCloser, error) { + serverResp, err := cli.get("/containers/"+containerID+"/export", url.Values{}, nil) + if err != nil { + return nil, err + } + + return serverResp.body, nil +} diff --git a/components/engine/api/client/lib/hijack.go b/components/engine/api/client/lib/hijack.go new file mode 100644 index 0000000000..4fcb4f62b0 --- /dev/null +++ b/components/engine/api/client/lib/hijack.go @@ -0,0 +1,164 @@ +package lib + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "net/http/httputil" + "net/url" + "strings" + "time" + + "github.com/docker/docker/api/types" +) + +// tlsClientCon holds tls information and a dialed connection. +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 conn, ok := c.rawConn.(types.CloseWriter); ok { + return conn.CloseWrite() + } + return nil +} + +// postHijacked sends a POST request and hijacks the connection. +func (cli *Client) postHijacked(path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) { + bodyEncoded, err := encodeData(body) + if err != nil { + return types.HijackedResponse{}, err + } + + req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) + if err != nil { + return types.HijackedResponse{}, err + } + req.Host = cli.addr + + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") + + conn, err := dial(cli.proto, cli.addr, cli.tlsConfig) + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") + } + return types.HijackedResponse{}, 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 := conn.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + + clientconn := httputil.NewClientConn(conn, nil) + defer clientconn.Close() + + // Server hijacks the connection, error 'connection closed' expected + clientconn.Do(req) + + rwc, br := clientconn.Hijack() + + return types.HijackedResponse{rwc, br}, 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 dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) { + if tlsConfig != nil && proto != "unix" { + // Notice this isn't Go standard's tls.Dial function + return tlsDial(proto, addr, tlsConfig) + } + return net.Dial(proto, addr) +} diff --git a/components/engine/api/client/lib/history.go b/components/engine/api/client/lib/history.go new file mode 100644 index 0000000000..e729f70eff --- /dev/null +++ b/components/engine/api/client/lib/history.go @@ -0,0 +1,23 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageHistory returns the changes in an image in history format. +func (cli *Client) ImageHistory(imageID string) ([]types.ImageHistory, error) { + var history []types.ImageHistory + serverResp, err := cli.get("/images/"+imageID+"/history", url.Values{}, nil) + if err != nil { + return history, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&history); err != nil { + return history, err + } + return history, nil +} diff --git a/components/engine/api/client/lib/image_build.go b/components/engine/api/client/lib/image_build.go new file mode 100644 index 0000000000..46ad161472 --- /dev/null +++ b/components/engine/api/client/lib/image_build.go @@ -0,0 +1,113 @@ +package lib + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "net/url" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/units" + "github.com/docker/docker/runconfig" +) + +// ImageBuild sends request to the daemon to build images. +// The Body in the response implement an io.ReadCloser and it's up to the caller to +// close it. +func (cli *Client) ImageBuild(options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + query, err := imageBuildOptionsToQuery(options) + if err != nil { + return types.ImageBuildResponse{}, err + } + + headers := http.Header(make(map[string][]string)) + buf, err := json.Marshal(options.AuthConfigs) + if err != nil { + return types.ImageBuildResponse{}, err + } + headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) + headers.Set("Content-Type", "application/tar") + + serverResp, err := cli.postRaw("/build", query, options.Context, headers) + if err != nil { + return types.ImageBuildResponse{}, err + } + + var osType string + if h, err := httputils.ParseServerHeader(serverResp.header.Get("Server")); err == nil { + osType = h.OS + } + + return types.ImageBuildResponse{ + Body: serverResp.body, + OSType: osType, + }, nil +} + +func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { + query := url.Values{ + "t": options.Tags, + } + if options.SuppressOutput { + query.Set("q", "1") + } + if options.RemoteContext != "" { + query.Set("remote", options.RemoteContext) + } + if options.NoCache { + query.Set("nocache", "1") + } + if options.Remove { + query.Set("rm", "1") + } else { + query.Set("rm", "0") + } + + if options.ForceRemove { + query.Set("forcerm", "1") + } + + if options.PullParent { + query.Set("pull", "1") + } + + if !runconfig.IsolationLevel.IsDefault(runconfig.IsolationLevel(options.Isolation)) { + query.Set("isolation", options.Isolation) + } + + query.Set("cpusetcpus", options.CPUSetCPUs) + query.Set("cpusetmems", options.CPUSetMems) + query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10)) + query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10)) + query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10)) + query.Set("memory", strconv.FormatInt(options.Memory, 10)) + query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10)) + query.Set("cgroupparent", options.CgroupParent) + + if options.ShmSize != "" { + parsedShmSize, err := units.RAMInBytes(options.ShmSize) + if err != nil { + return query, err + } + query.Set("shmsize", strconv.FormatInt(parsedShmSize, 10)) + } + + query.Set("dockerfile", options.Dockerfile) + + ulimitsJSON, err := json.Marshal(options.Ulimits) + if err != nil { + return query, err + } + query.Set("ulimits", string(ulimitsJSON)) + + buildArgs := runconfig.ConvertKVStringsToMap(options.BuildArgs) + buildArgsJSON, err := json.Marshal(buildArgs) + if err != nil { + return query, err + } + query.Set("buildargs", string(buildArgsJSON)) + + return query, nil +} diff --git a/components/engine/api/client/lib/image_create.go b/components/engine/api/client/lib/image_create.go new file mode 100644 index 0000000000..75d4b75e4a --- /dev/null +++ b/components/engine/api/client/lib/image_create.go @@ -0,0 +1,26 @@ +package lib + +import ( + "io" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageCreate creates a new image based in the parent options. +// It returns the JSON content in the response body. +func (cli *Client) ImageCreate(options types.ImageCreateOptions) (io.ReadCloser, error) { + query := url.Values{} + query.Set("fromImage", options.Parent) + query.Set("tag", options.Tag) + resp, err := cli.tryImageCreate(query, options.RegistryAuth) + if err != nil { + return nil, err + } + return resp.body, nil +} + +func (cli *Client) tryImageCreate(query url.Values, registryAuth string) (*serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post("/images/create", query, nil, headers) +} diff --git a/components/engine/api/client/lib/image_import.go b/components/engine/api/client/lib/image_import.go new file mode 100644 index 0000000000..873010f3e5 --- /dev/null +++ b/components/engine/api/client/lib/image_import.go @@ -0,0 +1,27 @@ +package lib + +import ( + "io" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageImport creates a new image based in the source options. +// It returns the JSON content in the response body. +func (cli *Client) ImageImport(options types.ImageImportOptions) (io.ReadCloser, error) { + query := url.Values{} + query.Set("fromSrc", options.SourceName) + query.Set("repo", options.RepositoryName) + query.Set("tag", options.Tag) + query.Set("message", options.Message) + for _, change := range options.Changes { + query.Add("changes", change) + } + + resp, err := cli.postRaw("/images/create", query, options.Source, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/components/engine/api/client/lib/image_inspect.go b/components/engine/api/client/lib/image_inspect.go new file mode 100644 index 0000000000..f4089cba0b --- /dev/null +++ b/components/engine/api/client/lib/image_inspect.go @@ -0,0 +1,37 @@ +package lib + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageInspectWithRaw returns the image information and it's raw representation. +func (cli *Client) ImageInspectWithRaw(imageID string, getSize bool) (types.ImageInspect, []byte, error) { + query := url.Values{} + if getSize { + query.Set("size", "1") + } + serverResp, err := cli.get("/images/"+imageID+"/json", query, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ImageInspect{}, nil, imageNotFoundError{imageID} + } + return types.ImageInspect{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return types.ImageInspect{}, nil, err + } + + var response types.ImageInspect + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +} diff --git a/components/engine/api/client/lib/image_list.go b/components/engine/api/client/lib/image_list.go new file mode 100644 index 0000000000..8e4bf4ba80 --- /dev/null +++ b/components/engine/api/client/lib/image_list.go @@ -0,0 +1,39 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/parsers/filters" +) + +// ImageList returns a list of images in the docker host. +func (cli *Client) ImageList(options types.ImageListOptions) ([]types.Image, error) { + var images []types.Image + query := url.Values{} + + if options.Filters.Len() > 0 { + filterJSON, err := filters.ToParam(options.Filters) + if err != nil { + return images, err + } + query.Set("filters", filterJSON) + } + if options.MatchName != "" { + // FIXME rename this parameter, to not be confused with the filters flag + query.Set("filter", options.MatchName) + } + if options.All { + query.Set("all", "1") + } + + serverResp, err := cli.get("/images/json", query, nil) + if err != nil { + return images, err + } + defer ensureReaderClosed(serverResp) + + err = json.NewDecoder(serverResp.body).Decode(&images) + return images, err +} diff --git a/components/engine/api/client/lib/image_load.go b/components/engine/api/client/lib/image_load.go new file mode 100644 index 0000000000..954cfd8d7b --- /dev/null +++ b/components/engine/api/client/lib/image_load.go @@ -0,0 +1,17 @@ +package lib + +import ( + "io" + "net/url" +) + +// ImageLoad loads an image in the docker host from the client host. +// It's up to the caller to close the io.ReadCloser returned by +// this function. +func (cli *Client) ImageLoad(input io.Reader) (io.ReadCloser, error) { + resp, err := cli.postRaw("/images/load", url.Values{}, input, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/components/engine/api/client/lib/image_pull.go b/components/engine/api/client/lib/image_pull.go new file mode 100644 index 0000000000..ef5f1ce6e6 --- /dev/null +++ b/components/engine/api/client/lib/image_pull.go @@ -0,0 +1,34 @@ +package lib + +import ( + "io" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImagePull request the docker host to pull an image from a remote registry. +// It executes the privileged function if the operation is unauthorized +// and it tries one more time. +// It's up to the caller to handle the io.ReadCloser and close it properly. +func (cli *Client) ImagePull(options types.ImagePullOptions, privilegeFunc RequestPrivilegeFunc) (io.ReadCloser, error) { + query := url.Values{} + query.Set("fromImage", options.ImageID) + if options.Tag != "" { + query.Set("tag", options.Tag) + } + + resp, err := cli.tryImageCreate(query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized { + newAuthHeader, privilegeErr := privilegeFunc() + if privilegeErr != nil { + return nil, privilegeErr + } + resp, err = cli.tryImageCreate(query, newAuthHeader) + } + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/components/engine/api/client/lib/image_push.go b/components/engine/api/client/lib/image_push.go new file mode 100644 index 0000000000..14c369afac --- /dev/null +++ b/components/engine/api/client/lib/image_push.go @@ -0,0 +1,36 @@ +package lib + +import ( + "io" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImagePush request the docker host to push an image to a remote registry. +// It executes the privileged function if the operation is unauthorized +// and it tries one more time. +// It's up to the caller to handle the io.ReadCloser and close it properly. +func (cli *Client) ImagePush(options types.ImagePushOptions, privilegeFunc RequestPrivilegeFunc) (io.ReadCloser, error) { + query := url.Values{} + query.Set("tag", options.Tag) + + resp, err := cli.tryImagePush(options.ImageID, query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized { + newAuthHeader, privilegeErr := privilegeFunc() + if privilegeErr != nil { + return nil, privilegeErr + } + resp, err = cli.tryImagePush(options.ImageID, query, newAuthHeader) + } + if err != nil { + return nil, err + } + return resp.body, nil +} + +func (cli *Client) tryImagePush(imageID string, query url.Values, registryAuth string) (*serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.post("/images/"+imageID+"/push", query, nil, headers) +} diff --git a/components/engine/api/client/lib/image_remove.go b/components/engine/api/client/lib/image_remove.go new file mode 100644 index 0000000000..fb221da6bd --- /dev/null +++ b/components/engine/api/client/lib/image_remove.go @@ -0,0 +1,30 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageRemove removes an image from the docker host. +func (cli *Client) ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, error) { + query := url.Values{} + + if options.Force { + query.Set("force", "1") + } + if !options.PruneChildren { + query.Set("noprune", "1") + } + + resp, err := cli.delete("/images/"+options.ImageID, query, nil) + if err != nil { + return nil, err + } + defer ensureReaderClosed(resp) + + var dels []types.ImageDelete + err = json.NewDecoder(resp.body).Decode(&dels) + return dels, err +} diff --git a/components/engine/api/client/lib/image_save.go b/components/engine/api/client/lib/image_save.go new file mode 100644 index 0000000000..10fb6130e8 --- /dev/null +++ b/components/engine/api/client/lib/image_save.go @@ -0,0 +1,20 @@ +package lib + +import ( + "io" + "net/url" +) + +// ImageSave retrieves one or more images from the docker host as a io.ReadCloser. +// It's up to the caller to store the images and close the stream. +func (cli *Client) ImageSave(imageIDs []string) (io.ReadCloser, error) { + query := url.Values{ + "names": imageIDs, + } + + resp, err := cli.get("/images/get", query, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/components/engine/api/client/lib/image_search.go b/components/engine/api/client/lib/image_search.go new file mode 100644 index 0000000000..9d4a9ce81a --- /dev/null +++ b/components/engine/api/client/lib/image_search.go @@ -0,0 +1,39 @@ +package lib + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/registry" +) + +// ImageSearch makes the docker host to search by a term in a remote registry. +// The list of results is not sorted in any fashion. +func (cli *Client) ImageSearch(options types.ImageSearchOptions, privilegeFunc RequestPrivilegeFunc) ([]registry.SearchResult, error) { + var results []registry.SearchResult + query := url.Values{} + query.Set("term", options.Term) + + resp, err := cli.tryImageSearch(query, options.RegistryAuth) + if resp.statusCode == http.StatusUnauthorized { + newAuthHeader, privilegeErr := privilegeFunc() + if privilegeErr != nil { + return results, privilegeErr + } + resp, err = cli.tryImageSearch(query, newAuthHeader) + } + if err != nil { + return results, err + } + defer ensureReaderClosed(resp) + + err = json.NewDecoder(resp.body).Decode(&results) + return results, err +} + +func (cli *Client) tryImageSearch(query url.Values, registryAuth string) (*serverResponse, error) { + headers := map[string][]string{"X-Registry-Auth": {registryAuth}} + return cli.get("/images/search", query, headers) +} diff --git a/components/engine/api/client/lib/image_tag.go b/components/engine/api/client/lib/image_tag.go new file mode 100644 index 0000000000..d83e33fe23 --- /dev/null +++ b/components/engine/api/client/lib/image_tag.go @@ -0,0 +1,21 @@ +package lib + +import ( + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageTag tags an image in the docker host +func (cli *Client) ImageTag(options types.ImageTagOptions) error { + query := url.Values{} + query.Set("repo", options.RepositoryName) + query.Set("tag", options.Tag) + if options.Force { + query.Set("force", "1") + } + + resp, err := cli.post("/images/"+options.ImageID+"/tag", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/info.go b/components/engine/api/client/lib/info.go new file mode 100644 index 0000000000..04d854a290 --- /dev/null +++ b/components/engine/api/client/lib/info.go @@ -0,0 +1,25 @@ +package lib + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/docker/docker/api/types" +) + +// Info returns information about the docker server. +func (cli *Client) Info() (types.Info, error) { + var info types.Info + serverResp, err := cli.get("/info", url.Values{}, nil) + if err != nil { + return info, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil { + return info, fmt.Errorf("Error reading remote info: %v", err) + } + + return info, nil +} diff --git a/components/engine/api/client/lib/kill.go b/components/engine/api/client/lib/kill.go new file mode 100644 index 0000000000..4da1a94008 --- /dev/null +++ b/components/engine/api/client/lib/kill.go @@ -0,0 +1,13 @@ +package lib + +import "net/url" + +// ContainerKill terminates the container process but does not remove the container from the docker host. +func (cli *Client) ContainerKill(containerID, signal string) error { + query := url.Values{} + query.Set("signal", signal) + + resp, err := cli.post("/containers/"+containerID+"/kill", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/login.go b/components/engine/api/client/lib/login.go new file mode 100644 index 0000000000..56ee18481c --- /dev/null +++ b/components/engine/api/client/lib/login.go @@ -0,0 +1,28 @@ +package lib + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cliconfig" +) + +// RegistryLogin authenticates the docker server with a given docker registry. +// It returns UnauthorizerError when the authentication fails. +func (cli *Client) RegistryLogin(auth cliconfig.AuthConfig) (types.AuthResponse, error) { + resp, err := cli.post("/auth", url.Values{}, auth, nil) + + if resp != nil && resp.statusCode == http.StatusUnauthorized { + return types.AuthResponse{}, unauthorizedError{err} + } + if err != nil { + return types.AuthResponse{}, err + } + defer ensureReaderClosed(resp) + + var response types.AuthResponse + err = json.NewDecoder(resp.body).Decode(&response) + return response, err +} diff --git a/components/engine/api/client/lib/logs.go b/components/engine/api/client/lib/logs.go new file mode 100644 index 0000000000..241bac33de --- /dev/null +++ b/components/engine/api/client/lib/logs.go @@ -0,0 +1,46 @@ +package lib + +import ( + "io" + "net/url" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/timeutils" +) + +// ContainerLogs returns the logs generated by a container in an io.ReadCloser. +// It's up to the caller to close the stream. +func (cli *Client) ContainerLogs(options types.ContainerLogsOptions) (io.ReadCloser, error) { + query := url.Values{} + if options.ShowStdout { + query.Set("stdout", "1") + } + + if options.ShowStderr { + query.Set("stderr", "1") + } + + if options.Since != "" { + ts, err := timeutils.GetTimestamp(options.Since, time.Now()) + if err != nil { + return nil, err + } + query.Set("since", ts) + } + + if options.Timestamps { + query.Set("timestamps", "1") + } + + if options.Follow { + query.Set("follow", "1") + } + query.Set("tail", options.Tail) + + resp, err := cli.get("/containers/"+options.ContainerID+"/logs", query, nil) + if err != nil { + return nil, err + } + return resp.body, nil +} diff --git a/components/engine/api/client/lib/network.go b/components/engine/api/client/lib/network.go new file mode 100644 index 0000000000..6f99921f48 --- /dev/null +++ b/components/engine/api/client/lib/network.go @@ -0,0 +1,71 @@ +package lib + +import ( + "encoding/json" + "net/http" + + "github.com/docker/docker/api/types" +) + +// NetworkCreate creates a new network in the docker host. +func (cli *Client) NetworkCreate(options types.NetworkCreate) (types.NetworkCreateResponse, error) { + var response types.NetworkCreateResponse + serverResp, err := cli.post("/networks/create", nil, options, nil) + if err != nil { + return response, err + } + + json.NewDecoder(serverResp.body).Decode(&response) + ensureReaderClosed(serverResp) + return response, err +} + +// NetworkRemove removes an existent network from the docker host. +func (cli *Client) NetworkRemove(networkID string) error { + resp, err := cli.delete("/networks/"+networkID, nil, nil) + ensureReaderClosed(resp) + return err +} + +// NetworkConnect connects a container to an existent network in the docker host. +func (cli *Client) NetworkConnect(networkID, containerID string) error { + nc := types.NetworkConnect{containerID} + resp, err := cli.post("/networks/"+networkID+"/connect", nil, nc, nil) + ensureReaderClosed(resp) + return err +} + +// NetworkDisconnect disconnects a container from an existent network in the docker host. +func (cli *Client) NetworkDisconnect(networkID, containerID string) error { + nc := types.NetworkConnect{containerID} + resp, err := cli.post("/networks/"+networkID+"/disconnect", nil, nc, nil) + ensureReaderClosed(resp) + return err +} + +// NetworkList returns the list of networks configured in the docker host. +func (cli *Client) NetworkList() ([]types.NetworkResource, error) { + var networkResources []types.NetworkResource + resp, err := cli.get("/networks", nil, nil) + if err != nil { + return networkResources, err + } + defer ensureReaderClosed(resp) + err = json.NewDecoder(resp.body).Decode(&networkResources) + return networkResources, err +} + +// NetworkInspect returns the information for a specific network configured in the docker host. +func (cli *Client) NetworkInspect(networkID string) (types.NetworkResource, error) { + var networkResource types.NetworkResource + resp, err := cli.get("/networks/"+networkID, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return networkResource, networkNotFoundError{networkID} + } + return networkResource, err + } + defer ensureReaderClosed(resp) + err = json.NewDecoder(resp.body).Decode(&networkResource) + return networkResource, err +} diff --git a/components/engine/api/client/lib/pause.go b/components/engine/api/client/lib/pause.go new file mode 100644 index 0000000000..3247ec3a57 --- /dev/null +++ b/components/engine/api/client/lib/pause.go @@ -0,0 +1,8 @@ +package lib + +// ContainerPause pauses the main process of a given container without terminating it. +func (cli *Client) ContainerPause(containerID string) error { + resp, err := cli.post("/containers/"+containerID+"/pause", nil, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/privileged.go b/components/engine/api/client/lib/privileged.go new file mode 100644 index 0000000000..cb3da51ea8 --- /dev/null +++ b/components/engine/api/client/lib/privileged.go @@ -0,0 +1,9 @@ +package lib + +// RequestPrivilegeFunc is a function interface that +// clients can supply to retry operations after +// getting an authorization error. +// This function returns the registry authentication +// header value in base 64 format, or an error +// if the privilege request fails. +type RequestPrivilegeFunc func() (string, error) diff --git a/components/engine/api/client/lib/request.go b/components/engine/api/client/lib/request.go new file mode 100644 index 0000000000..db3e5065a2 --- /dev/null +++ b/components/engine/api/client/lib/request.go @@ -0,0 +1,165 @@ +package lib + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/utils" +) + +// serverResponse is a wrapper for http API responses. +type serverResponse struct { + body io.ReadCloser + header http.Header + statusCode int +} + +// head sends an http request to the docker API using the method HEAD. +func (cli *Client) head(path string, query url.Values, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest("HEAD", path, query, nil, headers) +} + +// get sends an http request to the docker API using the method GET. +func (cli *Client) get(path string, query url.Values, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest("GET", path, query, nil, headers) +} + +// post sends an http request to the docker API using the method POST. +func (cli *Client) post(path string, query url.Values, body interface{}, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest("POST", path, query, body, headers) +} + +// postRaw sends the raw input to the docker API using the method POST. +func (cli *Client) postRaw(path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { + return cli.sendClientRequest("POST", path, query, body, headers) +} + +// put sends an http request to the docker API using the method PUT. +func (cli *Client) put(path string, query url.Values, body interface{}, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest("PUT", path, query, body, headers) +} + +// putRaw sends the raw input to the docker API using the method PUT. +func (cli *Client) putRaw(path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { + return cli.sendClientRequest("PUT", path, query, body, headers) +} + +// delete sends an http request to the docker API using the method DELETE. +func (cli *Client) delete(path string, query url.Values, headers map[string][]string) (*serverResponse, error) { + return cli.sendRequest("DELETE", path, query, nil, headers) +} + +func (cli *Client) sendRequest(method, path string, query url.Values, body interface{}, headers map[string][]string) (*serverResponse, error) { + params, err := encodeData(body) + if err != nil { + return nil, err + } + + if body != nil { + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + return cli.sendClientRequest(method, path, query, params, headers) +} + +func (cli *Client) sendClientRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { + serverResp := &serverResponse{ + body: nil, + statusCode: -1, + } + + expectedPayload := (method == "POST" || method == "PUT") + if expectedPayload && body == nil { + body = bytes.NewReader([]byte{}) + } + + req, err := cli.newRequest(method, path, query, body, headers) + req.URL.Host = cli.addr + req.URL.Scheme = cli.scheme + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + resp, err := cli.httpClient.Do(req) + if resp != nil { + serverResp.statusCode = resp.StatusCode + } + + if err != nil { + if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { + return serverResp, ErrConnectionFailed + } + + if cli.scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") { + return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) + } + if cli.scheme == "https" && strings.Contains(err.Error(), "remote error: bad certificate") { + return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) + } + + return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err) + } + + if serverResp.statusCode < 200 || serverResp.statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return serverResp, err + } + if len(body) == 0 { + return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL) + } + return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body)) + } + + serverResp.body = resp.Body + serverResp.header = resp.Header + return serverResp, nil +} + +func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) { + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest(method, apiPath, body) + if err != nil { + return nil, 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.customHTTPHeaders { + req.Header.Set(k, v) + } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + + return req, nil +} + +func 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 ensureReaderClosed(response *serverResponse) { + if response != nil && response.body != nil { + response.body.Close() + } +} diff --git a/components/engine/api/client/lib/resize.go b/components/engine/api/client/lib/resize.go new file mode 100644 index 0000000000..dffd81d544 --- /dev/null +++ b/components/engine/api/client/lib/resize.go @@ -0,0 +1,28 @@ +package lib + +import ( + "net/url" + "strconv" + + "github.com/docker/docker/api/types" +) + +// ContainerResize changes the size of the tty for a container. +func (cli *Client) ContainerResize(options types.ResizeOptions) error { + return cli.resize("/containers/"+options.ID, options.Height, options.Width) +} + +// ContainerExecResize changes the size of the tty for an exec process running inside a container. +func (cli *Client) ContainerExecResize(options types.ResizeOptions) error { + return cli.resize("/exec/"+options.ID, options.Height, options.Width) +} + +func (cli *Client) resize(basePath string, height, width int) error { + query := url.Values{} + query.Set("h", strconv.Itoa(height)) + query.Set("w", strconv.Itoa(width)) + + resp, err := cli.post(basePath+"/resize", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/version.go b/components/engine/api/client/lib/version.go new file mode 100644 index 0000000000..c6d5cd4c7d --- /dev/null +++ b/components/engine/api/client/lib/version.go @@ -0,0 +1,38 @@ +package lib + +import ( + "encoding/json" + "runtime" + + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/utils" +) + +// SystemVersion returns information of the docker client and server host. +func (cli *Client) SystemVersion() (types.VersionResponse, error) { + client := &types.Version{ + Version: dockerversion.Version, + APIVersion: api.Version, + GoVersion: runtime.Version(), + GitCommit: dockerversion.GitCommit, + BuildTime: dockerversion.BuildTime, + Os: runtime.GOOS, + Arch: runtime.GOARCH, + Experimental: utils.ExperimentalBuild(), + } + + resp, err := cli.get("/version", nil, nil) + if err != nil { + return types.VersionResponse{Client: client}, err + } + defer ensureReaderClosed(resp) + + var server types.Version + err = json.NewDecoder(resp.body).Decode(&server) + if err != nil { + return types.VersionResponse{Client: client}, err + } + return types.VersionResponse{Client: client, Server: &server}, nil +} diff --git a/components/engine/api/client/lib/volume.go b/components/engine/api/client/lib/volume.go new file mode 100644 index 0000000000..afb75b4983 --- /dev/null +++ b/components/engine/api/client/lib/volume.go @@ -0,0 +1,66 @@ +package lib + +import ( + "encoding/json" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/parsers/filters" +) + +// VolumeList returns the volumes configured in the docker host. +func (cli *Client) VolumeList(filter filters.Args) (types.VolumesListResponse, error) { + var volumes types.VolumesListResponse + query := url.Values{} + + if filter.Len() > 0 { + filterJSON, err := filters.ToParam(filter) + if err != nil { + return volumes, err + } + query.Set("filters", filterJSON) + } + resp, err := cli.get("/volumes", query, nil) + if err != nil { + return volumes, err + } + defer ensureReaderClosed(resp) + + err = json.NewDecoder(resp.body).Decode(&volumes) + return volumes, err +} + +// VolumeInspect returns the information about a specific volume in the docker host. +func (cli *Client) VolumeInspect(volumeID string) (types.Volume, error) { + var volume types.Volume + resp, err := cli.get("/volumes/"+volumeID, nil, nil) + if err != nil { + if resp.statusCode == http.StatusNotFound { + return volume, volumeNotFoundError{volumeID} + } + return volume, err + } + defer ensureReaderClosed(resp) + err = json.NewDecoder(resp.body).Decode(&volume) + return volume, err +} + +// VolumeCreate creates a volume in the docker host. +func (cli *Client) VolumeCreate(options types.VolumeCreateRequest) (types.Volume, error) { + var volume types.Volume + resp, err := cli.post("/volumes/create", nil, options, nil) + if err != nil { + return volume, err + } + defer ensureReaderClosed(resp) + err = json.NewDecoder(resp.body).Decode(&volume) + return volume, err +} + +// VolumeRemove removes a volume from the docker host. +func (cli *Client) VolumeRemove(volumeID string) error { + resp, err := cli.delete("/volumes/"+volumeID, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/components/engine/api/client/lib/wait.go b/components/engine/api/client/lib/wait.go new file mode 100644 index 0000000000..70993d84aa --- /dev/null +++ b/components/engine/api/client/lib/wait.go @@ -0,0 +1,24 @@ +package lib + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" +) + +// ContainerWait pauses execution util a container is exits. +// It returns the API status code as response of its readiness. +func (cli *Client) ContainerWait(containerID string) (int, error) { + resp, err := cli.post("/containers/"+containerID+"/wait", nil, nil, nil) + if err != nil { + return -1, err + } + defer ensureReaderClosed(resp) + + var res types.ContainerWaitResponse + if err := json.NewDecoder(resp.body).Decode(&res); err != nil { + return -1, err + } + + return res.StatusCode, nil +} diff --git a/components/engine/api/client/load.go b/components/engine/api/client/load.go index 378170e17e..10681dc71a 100644 --- a/components/engine/api/client/load.go +++ b/components/engine/api/client/load.go @@ -17,26 +17,24 @@ func (cli *DockerCli) CmdLoad(args ...string) error { cmd := Cli.Subcmd("load", nil, Cli.DockerCommands["load"].Description, 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 - ) + var input io.Reader = cli.in if *infile != "" { - input, err = os.Open(*infile) + file, err := os.Open(*infile) if err != nil { return err } + defer file.Close() + input = file } - sopts := &streamOpts{ - rawTerminal: true, - in: input, - out: cli.out, - } - if _, err := cli.stream("POST", "/images/load", sopts); err != nil { + + responseBody, err := cli.client.ImageLoad(input) + if err != nil { return err } - return nil + defer responseBody.Close() + + _, err = io.Copy(cli.out, responseBody) + return err } diff --git a/components/engine/api/client/login.go b/components/engine/api/client/login.go index e941a14efe..31022919c9 100644 --- a/components/engine/api/client/login.go +++ b/components/engine/api/client/login.go @@ -2,14 +2,13 @@ package client import ( "bufio" - "encoding/json" "fmt" "io" "os" "runtime" "strings" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/lib" Cli "github.com/docker/docker/cli" "github.com/docker/docker/cliconfig" flag "github.com/docker/docker/pkg/mflag" @@ -120,24 +119,15 @@ func (cli *DockerCli) CmdLogin(args ...string) error { authconfig.ServerAddress = serverAddress cli.configFile.AuthConfigs[serverAddress] = authconfig - serverResp, err := cli.call("POST", "/auth", cli.configFile.AuthConfigs[serverAddress], nil) - if serverResp.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 - } + auth := cli.configFile.AuthConfigs[serverAddress] + response, err := cli.client.RegistryLogin(auth) if err != nil { - return err - } - - defer serverResp.body.Close() - - var response types.AuthResponse - if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil { - // Upon error, remove entry - delete(cli.configFile.AuthConfigs, serverAddress) + if lib.IsErrUnauthorized(err) { + 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 } diff --git a/components/engine/api/client/logs.go b/components/engine/api/client/logs.go index 09c3d9398f..77c754f4da 100644 --- a/components/engine/api/client/logs.go +++ b/components/engine/api/client/logs.go @@ -1,15 +1,13 @@ package client import ( - "encoding/json" "fmt" - "net/url" - "time" + "io" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/pkg/timeutils" + "github.com/docker/docker/pkg/stdcopy" ) var validDrivers = map[string]bool{ @@ -32,47 +30,34 @@ func (cli *DockerCli) CmdLogs(args ...string) error { name := cmd.Arg(0) - serverResp, err := cli.call("GET", "/containers/"+name+"/json", nil, nil) + c, err := cli.client.ContainerInspect(name) if err != nil { return err } - var c types.ContainerJSON - if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil { - return err - } - if !validDrivers[c.HostConfig.LogConfig.Type] { return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type) } - v := url.Values{} - v.Set("stdout", "1") - v.Set("stderr", "1") - - if *since != "" { - ts, err := timeutils.GetTimestamp(*since, time.Now()) - if err != nil { - return err - } - v.Set("since", ts) + options := types.ContainerLogsOptions{ + ContainerID: name, + ShowStdout: true, + ShowStderr: true, + Since: *since, + Timestamps: *times, + Follow: *follow, + Tail: *tail, } - - if *times { - v.Set("timestamps", "1") + responseBody, err := cli.client.ContainerLogs(options) + if err != nil { + return err } + defer responseBody.Close() - if *follow { - v.Set("follow", "1") + if c.Config.Tty { + _, err = io.Copy(cli.out, responseBody) + } else { + _, err = stdcopy.StdCopy(cli.out, cli.err, responseBody) } - v.Set("tail", *tail) - - sopts := &streamOpts{ - rawTerminal: c.Config.Tty, - out: cli.out, - err: cli.err, - } - - _, err = cli.stream("GET", "/containers/"+name+"/logs?"+v.Encode(), sopts) return err } diff --git a/components/engine/api/client/network.go b/components/engine/api/client/network.go index 059a0faf56..35abf5d91d 100644 --- a/components/engine/api/client/network.go +++ b/components/engine/api/client/network.go @@ -1,15 +1,10 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "io" "net" - "net/http" "strings" "text/tabwriter" - "text/template" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" @@ -76,12 +71,8 @@ func (cli *DockerCli) CmdNetworkCreate(args ...string) error { Options: flOpts.GetAll(), CheckDuplicate: true, } - obj, _, err := readBody(cli.call("POST", "/networks/create", nc, nil)) - if err != nil { - return err - } - var resp types.NetworkCreateResponse - err = json.Unmarshal(obj, &resp) + + resp, err := cli.client.NetworkCreate(nc) if err != nil { return err } @@ -95,15 +86,13 @@ func (cli *DockerCli) CmdNetworkCreate(args ...string) error { func (cli *DockerCli) CmdNetworkRm(args ...string) error { cmd := Cli.Subcmd("network rm", []string{"NETWORK [NETWORK...]"}, "Deletes one or more networks", false) cmd.Require(flag.Min, 1) - err := cmd.ParseFlags(args, true) - if err != nil { + if err := cmd.ParseFlags(args, true); err != nil { return err } status := 0 for _, net := range cmd.Args() { - _, _, err = readBody(cli.call("DELETE", "/networks/"+net, nil, nil)) - if err != nil { + if err := cli.client.NetworkRemove(net); err != nil { fmt.Fprintf(cli.err, "%s\n", err) status = 1 continue @@ -121,14 +110,11 @@ func (cli *DockerCli) CmdNetworkRm(args ...string) error { func (cli *DockerCli) CmdNetworkConnect(args ...string) error { cmd := Cli.Subcmd("network connect", []string{"NETWORK CONTAINER"}, "Connects a container to a network", false) cmd.Require(flag.Exact, 2) - err := cmd.ParseFlags(args, true) - if err != nil { + if err := cmd.ParseFlags(args, true); err != nil { return err } - nc := types.NetworkConnect{Container: cmd.Arg(1)} - _, _, err = readBody(cli.call("POST", "/networks/"+cmd.Arg(0)+"/connect", nc, nil)) - return err + return cli.client.NetworkConnect(cmd.Arg(0), cmd.Arg(1)) } // CmdNetworkDisconnect disconnects a container from a network @@ -137,14 +123,11 @@ func (cli *DockerCli) CmdNetworkConnect(args ...string) error { func (cli *DockerCli) CmdNetworkDisconnect(args ...string) error { cmd := Cli.Subcmd("network disconnect", []string{"NETWORK CONTAINER"}, "Disconnects container from a network", false) cmd.Require(flag.Exact, 2) - err := cmd.ParseFlags(args, true) - if err != nil { + if err := cmd.ParseFlags(args, true); err != nil { return err } - nc := types.NetworkConnect{Container: cmd.Arg(1)} - _, _, err = readBody(cli.call("POST", "/networks/"+cmd.Arg(0)+"/disconnect", nc, nil)) - return err + return cli.client.NetworkDisconnect(cmd.Arg(0), cmd.Arg(1)) } // CmdNetworkLs lists all the netorks managed by docker daemon @@ -156,18 +139,11 @@ func (cli *DockerCli) CmdNetworkLs(args ...string) error { noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Do not truncate the output") cmd.Require(flag.Exact, 0) - err := cmd.ParseFlags(args, true) - - if err != nil { - return err - } - obj, _, err := readBody(cli.call("GET", "/networks", nil, nil)) - if err != nil { + if err := cmd.ParseFlags(args, true); err != nil { return err } - var networkResources []types.NetworkResource - err = json.Unmarshal(obj, &networkResources) + networkResources, err := cli.client.NetworkList() if err != nil { return err } @@ -212,73 +188,12 @@ func (cli *DockerCli) CmdNetworkInspect(args ...string) error { return err } - var tmpl *template.Template - if *tmplStr != "" { - var err error - tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr) - if err != nil { - return err - } + inspectSearcher := func(name string) (interface{}, []byte, error) { + i, err := cli.client.NetworkInspect(name) + return i, nil, err } - status := 0 - var networks []types.NetworkResource - buf := new(bytes.Buffer) - for _, name := range cmd.Args() { - obj, statusCode, err := readBody(cli.call("GET", "/networks/"+name, nil, nil)) - if err != nil { - if statusCode == http.StatusNotFound { - fmt.Fprintf(cli.err, "Error: No such network: %s\n", name) - } else { - fmt.Fprintf(cli.err, "%s\n", err) - } - status = 1 - continue - } - var networkResource types.NetworkResource - if err := json.NewDecoder(bytes.NewReader(obj)).Decode(&networkResource); err != nil { - return err - } - - if tmpl == nil { - networks = append(networks, networkResource) - continue - } - - if err := tmpl.Execute(buf, &networkResource); err != nil { - if err := tmpl.Execute(buf, &networkResource); err != nil { - fmt.Fprintf(cli.err, "%s\n", err) - return Cli.StatusError{StatusCode: 1} - } - } - buf.WriteString("\n") - } - - if tmpl != nil { - if _, err := io.Copy(cli.out, buf); err != nil { - return err - } - return nil - } - - if len(networks) == 0 { - io.WriteString(cli.out, "[]") - } - - b, err := json.MarshalIndent(networks, "", " ") - if err != nil { - return err - } - - if _, err := io.Copy(cli.out, bytes.NewReader(b)); err != nil { - return err - } - io.WriteString(cli.out, "\n") - - if status != 0 { - return Cli.StatusError{StatusCode: status} - } - return nil + return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher) } // Consolidates the ipam configuration as a group from different related configurations diff --git a/components/engine/api/client/pause.go b/components/engine/api/client/pause.go index e144a0bd55..76de3e3d59 100644 --- a/components/engine/api/client/pause.go +++ b/components/engine/api/client/pause.go @@ -18,7 +18,7 @@ func (cli *DockerCli) CmdPause(args ...string) error { var errNames []string for _, name := range cmd.Args() { - if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/pause", name), nil, nil)); err != nil { + if err := cli.client.ContainerPause(name); err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { diff --git a/components/engine/api/client/port.go b/components/engine/api/client/port.go index a981bc5e63..ac673267ea 100644 --- a/components/engine/api/client/port.go +++ b/components/engine/api/client/port.go @@ -1,7 +1,6 @@ package client import ( - "encoding/json" "fmt" "strings" @@ -20,23 +19,11 @@ func (cli *DockerCli) CmdPort(args ...string) error { cmd.ParseFlags(args, true) - serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil) + c, err := cli.client.ContainerInspect(cmd.Arg(0)) if err != nil { return err } - defer serverResp.body.Close() - - var c struct { - NetworkSettings struct { - Ports nat.PortMap - } - } - - if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil { - return err - } - if cmd.NArg() == 2 { var ( port = cmd.Arg(1) diff --git a/components/engine/api/client/ps.go b/components/engine/api/client/ps.go index 5bcf4bb2ee..a39bc5c0a9 100644 --- a/components/engine/api/client/ps.go +++ b/components/engine/api/client/ps.go @@ -1,10 +1,6 @@ package client import ( - "encoding/json" - "net/url" - "strconv" - "github.com/docker/docker/api/client/ps" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" @@ -21,7 +17,6 @@ func (cli *DockerCli) CmdPs(args ...string) error { err error psFilterArgs = filters.NewArgs() - v = url.Values{} cmd = Cli.Subcmd("ps", nil, Cli.DockerCommands["ps"].Description, true) quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") @@ -44,26 +39,6 @@ func (cli *DockerCli) CmdPs(args ...string) error { *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() { @@ -72,27 +47,20 @@ func (cli *DockerCli) CmdPs(args ...string) error { } } - if psFilterArgs.Len() > 0 { - filterJSON, err := filters.ToParam(psFilterArgs) - if err != nil { - return err - } - - v.Set("filters", filterJSON) + options := types.ContainerListOptions{ + All: *all, + Limit: *last, + Since: *since, + Before: *before, + Size: *size, + Filter: psFilterArgs, } - serverResp, err := cli.call("GET", "/containers/json?"+v.Encode(), nil, nil) + containers, err := cli.client.ContainerList(options) if err != nil { return err } - defer serverResp.body.Close() - - containers := []types.Container{} - if err := json.NewDecoder(serverResp.body).Decode(&containers); err != nil { - return err - } - f := *format if len(f) == 0 { if len(cli.PsFormat()) > 0 && !*quiet { diff --git a/components/engine/api/client/pull.go b/components/engine/api/client/pull.go index e585a12081..88879308a3 100644 --- a/components/engine/api/client/pull.go +++ b/components/engine/api/client/pull.go @@ -3,10 +3,13 @@ package client import ( "errors" "fmt" - "net/url" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/jsonmessage" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" tagpkg "github.com/docker/docker/tag" @@ -62,15 +65,34 @@ func (cli *DockerCli) CmdPull(args ...string) error { return err } + authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") + if isTrusted() && !ref.HasDigest() { // Check if tag is digest - authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index) - return cli.trustedPull(repoInfo, ref, authConfig) + return cli.trustedPull(repoInfo, ref, authConfig, requestPrivilege) } - v := url.Values{} - v.Set("fromImage", distributionRef.String()) + return cli.imagePullPrivileged(authConfig, distributionRef.String(), "", requestPrivilege) +} - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") - return err +func (cli *DockerCli) imagePullPrivileged(authConfig cliconfig.AuthConfig, imageID, tag string, requestPrivilege lib.RequestPrivilegeFunc) error { + + encodedAuth, err := authConfig.EncodeToBase64() + if err != nil { + return err + } + options := types.ImagePullOptions{ + ImageID: imageID, + Tag: tag, + RegistryAuth: encodedAuth, + } + + responseBody, err := cli.client.ImagePull(options, requestPrivilege) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) } diff --git a/components/engine/api/client/push.go b/components/engine/api/client/push.go index 760c97ef8b..4230eea838 100644 --- a/components/engine/api/client/push.go +++ b/components/engine/api/client/push.go @@ -3,10 +3,14 @@ package client import ( "errors" "fmt" - "net/url" + "io" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/jsonmessage" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" ) @@ -53,13 +57,30 @@ func (cli *DockerCli) CmdPush(args ...string) error { return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to / (ex: %s/%s)", username, repoInfo.LocalName) } + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "push") if isTrusted() { - return cli.trustedPush(repoInfo, tag, authConfig) + return cli.trustedPush(repoInfo, tag, authConfig, requestPrivilege) } - v := url.Values{} - v.Set("tag", tag) + return cli.imagePushPrivileged(authConfig, ref.Name(), tag, cli.out, requestPrivilege) +} - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+ref.Name()+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push") - return err +func (cli *DockerCli) imagePushPrivileged(authConfig cliconfig.AuthConfig, imageID, tag string, outputStream io.Writer, requestPrivilege lib.RequestPrivilegeFunc) error { + encodedAuth, err := authConfig.EncodeToBase64() + if err != nil { + return err + } + options := types.ImagePushOptions{ + ImageID: imageID, + Tag: tag, + RegistryAuth: encodedAuth, + } + + responseBody, err := cli.client.ImagePush(options, requestPrivilege) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, outputStream, cli.outFd, cli.isTerminalOut) } diff --git a/components/engine/api/client/rename.go b/components/engine/api/client/rename.go index 124ee1fc4c..a67d5d02db 100644 --- a/components/engine/api/client/rename.go +++ b/components/engine/api/client/rename.go @@ -24,7 +24,7 @@ func (cli *DockerCli) CmdRename(args ...string) error { return fmt.Errorf("Error: Neither old nor new names may be empty") } - if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/rename?name=%s", oldName, newName), nil, nil)); err != nil { + if err := cli.client.ContainerRename(oldName, newName); err != nil { fmt.Fprintf(cli.err, "%s\n", err) return fmt.Errorf("Error: failed to rename container named %s", oldName) } diff --git a/components/engine/api/client/restart.go b/components/engine/api/client/restart.go index 92c612926e..2bad7633e9 100644 --- a/components/engine/api/client/restart.go +++ b/components/engine/api/client/restart.go @@ -2,8 +2,6 @@ package client import ( "fmt" - "net/url" - "strconv" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" @@ -19,13 +17,9 @@ func (cli *DockerCli) CmdRestart(args ...string) error { 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 { + if err := cli.client.ContainerRestart(name, *nSeconds); err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { diff --git a/components/engine/api/client/rm.go b/components/engine/api/client/rm.go index cf88c765d3..b26cb53d02 100644 --- a/components/engine/api/client/rm.go +++ b/components/engine/api/client/rm.go @@ -2,9 +2,9 @@ package client import ( "fmt" - "net/url" "strings" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -21,18 +21,6 @@ func (cli *DockerCli) CmdRm(args ...string) error { 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 == "" { @@ -40,8 +28,14 @@ func (cli *DockerCli) CmdRm(args ...string) error { } name = strings.Trim(name, "/") - _, _, err := readBody(cli.call("DELETE", "/containers/"+name+"?"+val.Encode(), nil, nil)) - if err != nil { + options := types.ContainerRemoveOptions{ + ContainerID: name, + RemoveVolumes: *v, + RemoveLinks: *link, + Force: *force, + } + + if err := cli.client.ContainerRemove(options); err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { diff --git a/components/engine/api/client/rmi.go b/components/engine/api/client/rmi.go index b4e3700684..9569370ea2 100644 --- a/components/engine/api/client/rmi.go +++ b/components/engine/api/client/rmi.go @@ -1,7 +1,6 @@ package client import ( - "encoding/json" "fmt" "net/url" @@ -31,20 +30,17 @@ func (cli *DockerCli) CmdRmi(args ...string) error { var errNames []string for _, name := range cmd.Args() { - serverResp, err := cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil, nil) + options := types.ImageRemoveOptions{ + ImageID: name, + Force: *force, + PruneChildren: !*noprune, + } + + dels, err := cli.client.ImageRemove(options) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { - defer serverResp.body.Close() - - dels := []types.ImageDelete{} - if err := json.NewDecoder(serverResp.body).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) diff --git a/components/engine/api/client/run.go b/components/engine/api/client/run.go index 1ae5d67d7e..d93574b29b 100644 --- a/components/engine/api/client/run.go +++ b/components/engine/api/client/run.go @@ -3,12 +3,12 @@ package client import ( "fmt" "io" - "net/url" "os" "runtime" "strings" "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" derr "github.com/docker/docker/errors" "github.com/docker/docker/opts" @@ -168,70 +168,57 @@ func (cli *DockerCli) CmdRun(args ...string) error { 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() + + options := types.ContainerAttachOptions{ + ContainerID: createResponse.ID, + Stream: true, + Stdin: config.AttachStdin, + Stdout: config.AttachStdout, + Stderr: config.AttachStderr, } - case err := <-errCh: + + resp, err := cli.client.ContainerAttach(options) if err != nil { - logrus.Debugf("Error hijack: %s", err) return err } + errCh = promise.Go(func() error { + return cli.holdHijackedConnection(config.Tty, in, out, stderr, resp) + }) } defer func() { if *flAutoRemove { - if _, _, err = readBody(cli.call("DELETE", "/containers/"+createResponse.ID+"?v=1", nil, nil)); err != nil { + options := types.ContainerRemoveOptions{ + ContainerID: createResponse.ID, + RemoveVolumes: true, + } + if err := cli.client.ContainerRemove(options); 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 { + if err := cli.client.ContainerStart(createResponse.ID); err != nil { cmd.ReportError(err.Error(), false) return runStartContainerErr(err) } @@ -262,7 +249,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { 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 { + if status, err = cli.client.ContainerWait(createResponse.ID); err != nil { return runStartContainerErr(err) } if _, status, err = getExitCode(cli, createResponse.ID); err != nil { @@ -272,7 +259,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { // 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 { + if status, err = cli.client.ContainerWait(createResponse.ID); err != nil { return err } } else { diff --git a/components/engine/api/client/save.go b/components/engine/api/client/save.go index f14f9a450c..e7e0166d6f 100644 --- a/components/engine/api/client/save.go +++ b/components/engine/api/client/save.go @@ -2,7 +2,7 @@ package client import ( "errors" - "net/url" + "io" "os" Cli "github.com/docker/docker/cli" @@ -35,18 +35,12 @@ func (cli *DockerCli) CmdSave(args ...string) error { } } - sopts := &streamOpts{ - rawTerminal: true, - out: output, - } - - v := url.Values{} - for _, arg := range cmd.Args() { - v.Add("names", arg) - } - if _, err := cli.stream("GET", "/images/get?"+v.Encode(), sopts); err != nil { + responseBody, err := cli.client.ImageSave(cmd.Args()) + if err != nil { return err } + defer responseBody.Close() - return nil + _, err = io.Copy(output, responseBody) + return err } diff --git a/components/engine/api/client/search.go b/components/engine/api/client/search.go index 1a47064477..fea66d55b7 100644 --- a/components/engine/api/client/search.go +++ b/components/engine/api/client/search.go @@ -1,26 +1,19 @@ package client import ( - "encoding/json" "fmt" "net/url" "sort" "strings" "text/tabwriter" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" "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 @@ -42,19 +35,26 @@ func (cli *DockerCli) CmdSearch(args ...string) error { return err } - rdr, _, err := cli.clientRequestAttemptLogin("GET", "/images/search?"+v.Encode(), nil, nil, indexInfo, "search") + authConfig := registry.ResolveAuthConfig(cli.configFile, indexInfo) + requestPrivilege := cli.registryAuthenticationPrivilegedFunc(indexInfo, "search") + + encodedAuth, err := authConfig.EncodeToBase64() if err != nil { return err } - defer rdr.Close() + options := types.ImageSearchOptions{ + Term: name, + RegistryAuth: encodedAuth, + } - results := ByStars{} - if err := json.NewDecoder(rdr).Decode(&results); err != nil { + unorderedResults, err := cli.client.ImageSearch(options, requestPrivilege) + if err != nil { return err } - sort.Sort(sort.Reverse(results)) + results := searchResultsByStars(unorderedResults) + sort.Sort(results) w := tabwriter.NewWriter(cli.out, 10, 1, 3, ' ', 0) fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") @@ -81,3 +81,10 @@ func (cli *DockerCli) CmdSearch(args ...string) error { w.Flush() return nil } + +// SearchResultsByStars sorts search results in descending order by number of stars. +type searchResultsByStars []registry.SearchResult + +func (r searchResultsByStars) Len() int { return len(r) } +func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount } diff --git a/components/engine/api/client/start.go b/components/engine/api/client/start.go index bff9c30ec8..787f87d118 100644 --- a/components/engine/api/client/start.go +++ b/components/engine/api/client/start.go @@ -1,11 +1,10 @@ package client import ( - "encoding/json" "fmt" "io" - "net/url" "os" + "strings" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" @@ -34,7 +33,8 @@ func (cli *DockerCli) forwardAllSignals(cid string) chan os.Signal { fmt.Fprintf(cli.err, "Unsupported signal: %v. Discarding.\n", s) continue } - if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/kill?signal=%s", cid, sig), nil, nil)); err != nil { + + if err := cli.client.ContainerKill(cid, sig); err != nil { logrus.Debugf("Error sending signal: %s", err) } } @@ -53,119 +53,91 @@ func (cli *DockerCli) CmdStart(args ...string) error { cmd.ParseFlags(args, true) - var ( - cErr chan error - tty bool - ) - if *attach || *openStdin { + // We're going to attach to a container. + // 1. Ensure we only have one container. if cmd.NArg() > 1 { return fmt.Errorf("You cannot start and attach multiple containers at once.") } - serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil) + // 2. Attach to the container. + containerID := cmd.Arg(0) + c, err := cli.client.ContainerInspect(containerID) if err != nil { return err } - defer serverResp.body.Close() - - var c types.ContainerJSON - if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil { - return err - } - - tty = c.Config.Tty - - if !tty { - sigc := cli.forwardAllSignals(cmd.Arg(0)) + if !c.Config.Tty { + sigc := cli.forwardAllSignals(containerID) defer signal.StopCatch(sigc) } + options := types.ContainerAttachOptions{ + ContainerID: containerID, + Stream: true, + Stdin: *openStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + } + var in io.ReadCloser - - v := url.Values{} - v.Set("stream", "1") - - if *openStdin && c.Config.OpenStdin { - v.Set("stdin", "1") + if options.Stdin { in = cli.in } - v.Set("stdout", "1") - v.Set("stderr", "1") + resp, err := cli.client.ContainerAttach(options) + if err != nil { + return err + } + defer resp.Close() - 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) + cErr := promise.Go(func() error { + return cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp) }) - // 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 - } + // 3. Start the container. + if err := cli.client.ContainerStart(containerID); 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 { + // 4. Wait for attachement to break. + if c.Config.Tty && cli.isTerminalOut { + if err := cli.monitorTtySize(containerID, 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)) + _, status, err := getExitCode(cli, containerID) if err != nil { return err } if status != 0 { return Cli.StatusError{StatusCode: status} } + } else { + // We're not going to attach to anything. + // Start as many containers as we want. + return cli.startContainersWithoutAttachments(cmd.Args()) + } + + return nil +} + +func (cli *DockerCli) startContainersWithoutAttachments(containerIDs []string) error { + var failedContainers []string + for _, containerID := range containerIDs { + if err := cli.client.ContainerStart(containerID); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + failedContainers = append(failedContainers, containerID) + } else { + fmt.Fprintf(cli.out, "%s\n", containerID) + } + } + + if len(failedContainers) > 0 { + return fmt.Errorf("Error: failed to start containers: %v", strings.Join(failedContainers, ", ")) } return nil } diff --git a/components/engine/api/client/stats.go b/components/engine/api/client/stats.go index 659764bfbc..6ffee7f27c 100644 --- a/components/engine/api/client/stats.go +++ b/components/engine/api/client/stats.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "net/url" "sort" "strings" "sync" @@ -37,26 +36,19 @@ type stats struct { } func (s *containerStats) Collect(cli *DockerCli, streamStats bool) { - v := url.Values{} - if streamStats { - v.Set("stream", "1") - } else { - v.Set("stream", "0") - } - serverResp, err := cli.call("GET", "/containers/"+s.Name+"/stats?"+v.Encode(), nil, nil) + responseBody, err := cli.client.ContainerStats(s.Name, streamStats) if err != nil { s.mu.Lock() s.err = err s.mu.Unlock() return } - - defer serverResp.body.Close() + defer responseBody.Close() var ( previousCPU uint64 previousSystem uint64 - dec = json.NewDecoder(serverResp.body) + dec = json.NewDecoder(responseBody) u = make(chan error, 1) ) go func() { @@ -156,18 +148,13 @@ func (cli *DockerCli) CmdStats(args ...string) error { showAll := len(names) == 0 if showAll { - v := url.Values{} - if *all { - v.Set("all", "1") + options := types.ContainerListOptions{ + All: *all, } - body, _, err := readBody(cli.call("GET", "/containers/json?"+v.Encode(), nil, nil)) + cs, err := cli.client.ContainerList(options) if err != nil { return err } - var cs []types.Container - if err := json.Unmarshal(body, &cs); err != nil { - return err - } for _, c := range cs { names = append(names, c.ID[:12]) } @@ -202,14 +189,15 @@ func (cli *DockerCli) CmdStats(args ...string) error { err error } getNewContainers := func(c chan<- watch) { - res, err := cli.call("GET", "/events", nil, nil) + options := types.EventsOptions{} + resBody, err := cli.client.Events(options) if err != nil { c <- watch{err: err} return } - defer res.body.Close() + defer resBody.Close() - dec := json.NewDecoder(res.body) + dec := json.NewDecoder(resBody) for { var j *jsonmessage.JSONMessage if err := dec.Decode(&j); err != nil { diff --git a/components/engine/api/client/stop.go b/components/engine/api/client/stop.go index 91f5e65b0e..d5da5c64a6 100644 --- a/components/engine/api/client/stop.go +++ b/components/engine/api/client/stop.go @@ -2,8 +2,6 @@ package client import ( "fmt" - "net/url" - "strconv" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" @@ -21,13 +19,9 @@ func (cli *DockerCli) CmdStop(args ...string) error { 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 { + if err := cli.client.ContainerStop(name, *nSeconds); err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { diff --git a/components/engine/api/client/tag.go b/components/engine/api/client/tag.go index a4a27cb4cd..385a155c58 100644 --- a/components/engine/api/client/tag.go +++ b/components/engine/api/client/tag.go @@ -2,9 +2,9 @@ package client import ( "errors" - "net/url" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" @@ -20,7 +20,6 @@ func (cli *DockerCli) CmdTag(args ...string) error { cmd.ParseFlags(args, true) - v := url.Values{} ref, err := reference.ParseNamed(cmd.Arg(1)) if err != nil { return err @@ -41,15 +40,13 @@ func (cli *DockerCli) CmdTag(args ...string) error { if err := registry.ValidateRepositoryName(ref); err != nil { return err } - v.Set("repo", ref.Name()) - v.Set("tag", tag) - if *force { - v.Set("force", "1") + options := types.ImageTagOptions{ + ImageID: cmd.Arg(0), + RepositoryName: ref.Name(), + Tag: tag, + Force: *force, } - if _, _, err := readBody(cli.call("POST", "/images/"+cmd.Arg(0)+"/tag?"+v.Encode(), nil, nil)); err != nil { - return err - } - return nil + return cli.client.ImageTag(options) } diff --git a/components/engine/api/client/top.go b/components/engine/api/client/top.go index 8327820aae..90e2d7d369 100644 --- a/components/engine/api/client/top.go +++ b/components/engine/api/client/top.go @@ -1,13 +1,10 @@ package client import ( - "encoding/json" "fmt" - "net/url" "strings" "text/tabwriter" - "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -21,23 +18,16 @@ func (cli *DockerCli) CmdTop(args ...string) error { cmd.ParseFlags(args, true) - val := url.Values{} + var arguments []string if cmd.NArg() > 1 { - val.Set("ps_args", strings.Join(cmd.Args()[1:], " ")) + arguments = cmd.Args()[1:] } - serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/top?"+val.Encode(), nil, nil) + procList, err := cli.client.ContainerTop(cmd.Arg(0), arguments) if err != nil { return err } - defer serverResp.body.Close() - - procList := types.ContainerProcessList{} - if err := json.NewDecoder(serverResp.body).Decode(&procList); err != nil { - return err - } - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) diff --git a/components/engine/api/client/trust.go b/components/engine/api/client/trust.go index da6c3e5768..cb7c8ef10b 100644 --- a/components/engine/api/client/trust.go +++ b/components/engine/api/client/trust.go @@ -22,6 +22,8 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/ansiescape" "github.com/docker/docker/pkg/ioutils" @@ -250,16 +252,15 @@ func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Can func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.NamedTagged) error { fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) - tv := url.Values{} - tv.Set("repo", trustedRef.Name()) - tv.Set("tag", ref.Tag()) - tv.Set("force", "1") - if _, _, err := readBody(cli.call("POST", "/images/"+trustedRef.String()+"/tag?"+tv.Encode(), nil, nil)); err != nil { - return err + options := types.ImageTagOptions{ + ImageID: trustedRef.String(), + RepositoryName: trustedRef.Name(), + Tag: ref.Tag(), + Force: true, } - return nil + return cli.client.ImageTag(options) } func notaryError(err error) error { @@ -278,11 +279,8 @@ func notaryError(err error) error { return err } -func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig) error { - var ( - v = url.Values{} - refs = []target{} - ) +func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig, requestPrivilege lib.RequestPrivilegeFunc) error { + var refs []target notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig) if err != nil { @@ -317,17 +315,14 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr refs = append(refs, r) } - v.Set("fromImage", repoInfo.LocalName.Name()) for i, r := range refs { displayTag := r.reference.String() if displayTag != "" { displayTag = ":" + displayTag } fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.LocalName, displayTag, r.digest) - v.Set("tag", r.digest.String()) - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") - if err != nil { + if err := cli.imagePullPrivileged(authConfig, repoInfo.LocalName.Name(), r.digest.String(), requestPrivilege); err != nil { return err } @@ -385,20 +380,18 @@ func targetStream(in io.Writer) (io.WriteCloser, <-chan []target) { return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan } -func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig cliconfig.AuthConfig) error { +func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig cliconfig.AuthConfig, requestPrivilege lib.RequestPrivilegeFunc) error { streamOut, targetChan := targetStream(cli.out) - v := url.Values{} - v.Set("tag", tag) + reqError := cli.imagePushPrivileged(authConfig, repoInfo.LocalName.Name(), tag, streamOut, requestPrivilege) - _, _, err := cli.clientRequestAttemptLogin("POST", "/images/"+repoInfo.LocalName.Name()+"/push?"+v.Encode(), nil, streamOut, repoInfo.Index, "push") // Close stream channel to finish target parsing if err := streamOut.Close(); err != nil { return err } // Check error from request - if err != nil { - return err + if reqError != nil { + return reqError } // Get target results diff --git a/components/engine/api/client/unpause.go b/components/engine/api/client/unpause.go index 21e25857cf..13c80f2448 100644 --- a/components/engine/api/client/unpause.go +++ b/components/engine/api/client/unpause.go @@ -18,7 +18,7 @@ func (cli *DockerCli) CmdUnpause(args ...string) error { var errNames []string for _, name := range cmd.Args() { - if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/unpause", name), nil, nil)); err != nil { + if err := cli.client.ContainerUnpause(name); err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) } else { diff --git a/components/engine/api/client/utils.go b/components/engine/api/client/utils.go index 7a8a785844..e03ec5ffaa 100644 --- a/components/engine/api/client/utils.go +++ b/components/engine/api/client/utils.go @@ -1,242 +1,33 @@ 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/client/lib" "github.com/docker/docker/api/types" - "github.com/docker/docker/cliconfig" - "github.com/docker/docker/dockerversion" - "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" - "github.com/docker/docker/utils" ) -var ( - errConnectionFailed = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?") -) - -type serverResponse struct { - body io.ReadCloser - header http.Header - statusCode int -} - -// 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) (*serverResponse, error) { - - serverResp := &serverResponse{ - body: nil, - statusCode: -1, - } - - expectedPayload := (method == "POST" || method == "PUT") - if expectedPayload && in == nil { - in = bytes.NewReader([]byte{}) - } - req, err := http.NewRequest(method, fmt.Sprintf("%s/v%s%s", cli.basePath, api.Version, path), in) - if err != nil { - return serverResp, 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) - if resp != nil { - serverResp.statusCode = resp.StatusCode - } - - if err != nil { - if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { - return serverResp, errConnectionFailed - } - - if cli.tlsConfig == nil && strings.Contains(err.Error(), "malformed HTTP response") { - return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) - } - if cli.tlsConfig != nil && strings.Contains(err.Error(), "remote error: bad certificate") { - return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) - } - - return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err) - } - - if serverResp.statusCode < 200 || serverResp.statusCode >= 400 { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return serverResp, err - } - if len(body) == 0 { - return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL) - } - return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body)) - } - - serverResp.body = resp.Body - serverResp.header = resp.Header - return serverResp, nil -} - -// cmdAttempt builds the corresponding registry Auth Header from the given -// authConfig. It returns the servers body, status, error response -func (cli *DockerCli) cmdAttempt(authConfig cliconfig.AuthConfig, method, path string, in io.Reader, out io.Writer) (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 - serverResp, 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(serverResp.body, serverResp.header.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(), "401 Unauthorized") || - strings.Contains(err.Error(), "status code 401") { - serverResp.statusCode = http.StatusUnauthorized - } - } - return serverResp.body, serverResp.statusCode, err -} - -func (cli *DockerCli) clientRequestAttemptLogin(method, path string, in io.Reader, out io.Writer, index *registry.IndexInfo, cmdName string) (io.ReadCloser, int, error) { - - // Resolve the Auth config relevant for this server +func (cli *DockerCli) encodeRegistryAuth(index *registry.IndexInfo) (string, error) { authConfig := registry.ResolveAuthConfig(cli.configFile, index) - body, statusCode, err := cli.cmdAttempt(authConfig, method, path, in, out) - if statusCode == http.StatusUnauthorized { + return authConfig.EncodeToBase64() +} + +func (cli *DockerCli) registryAuthenticationPrivilegedFunc(index *registry.IndexInfo, cmdName string) lib.RequestPrivilegeFunc { + return func() (string, error) { fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) - if err = cli.CmdLogin(index.GetAuthConfigKey()); err != nil { - return nil, -1, err + if err := cli.CmdLogin(index.GetAuthConfigKey()); err != nil { + return "", err } - authConfig = registry.ResolveAuthConfig(cli.configFile, index) - return cli.cmdAttempt(authConfig, method, path, in, out) + return cli.encodeRegistryAuth(index) } - return body, statusCode, err -} - -func (cli *DockerCli) callWrapper(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { - sr, err := cli.call(method, path, data, headers) - return sr.body, sr.header, sr.statusCode, err -} - -func (cli *DockerCli) call(method, path string, data interface{}, headers map[string][]string) (*serverResponse, error) { - params, err := cli.encodeData(data) - if err != nil { - sr := &serverResponse{ - body: nil, - header: nil, - statusCode: -1, - } - return sr, nil - } - - if data != nil { - if headers == nil { - headers = make(map[string][]string) - } - headers["Content-Type"] = []string{"application/json"} - } - - serverResp, err := cli.clientRequest(method, path, params, headers) - return serverResp, 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) (*serverResponse, error) { - serverResp, err := cli.clientRequest(method, path, opts.in, opts.headers) - if err != nil { - return serverResp, err - } - return serverResp, cli.streamBody(serverResp.body, serverResp.header.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) { @@ -244,86 +35,53 @@ func (cli *DockerCli) resizeTty(id string, isExec bool) { 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?" + options := types.ResizeOptions{ + ID: id, + Height: height, + Width: width, } - if _, _, err := readBody(cli.call("POST", path+v.Encode(), nil, nil)); err != nil { + var err error + if !isExec { + err = cli.client.ContainerExecResize(options) + } else { + err = cli.client.ContainerResize(options) + } + + if err != nil { logrus.Debugf("Error resize: %s", err) } } -func waitForExit(cli *DockerCli, containerID string) (int, error) { - serverResp, err := cli.call("POST", "/containers/"+containerID+"/wait", nil, nil) - if err != nil { - return -1, err - } - - defer serverResp.body.Close() - - var res types.ContainerWaitResponse - if err := json.NewDecoder(serverResp.body).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) { - serverResp, err := cli.call("GET", "/containers/"+containerID+"/json", nil, nil) + c, err := cli.client.ContainerInspect(containerID) if err != nil { // If we can't connect, then the daemon probably died. - if err != errConnectionFailed { + if err != lib.ErrConnectionFailed { return false, -1, err } return false, -1, nil } - defer serverResp.body.Close() - - var c types.ContainerJSON - if err := json.NewDecoder(serverResp.body).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) { - serverResp, err := cli.call("GET", "/exec/"+execID+"/json", nil, nil) + resp, err := cli.client.ContainerExecInspect(execID) if err != nil { // If we can't connect, then the daemon probably died. - if err != errConnectionFailed { + if err != lib.ErrConnectionFailed { return false, -1, err } return false, -1, nil } - defer serverResp.body.Close() - - //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(serverResp.body).Decode(&c); err != nil { - return false, -1, err - } - - return c.Running, c.ExitCode, nil + return resp.Running, resp.ExitCode, nil } func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { @@ -368,17 +126,3 @@ func (cli *DockerCli) getTtySize() (int, int) { } return int(ws.Height), int(ws.Width) } - -func readBody(serverResp *serverResponse, err error) ([]byte, int, error) { - if serverResp.body != nil { - defer serverResp.body.Close() - } - if err != nil { - return nil, serverResp.statusCode, err - } - body, err := ioutil.ReadAll(serverResp.body) - if err != nil { - return nil, -1, err - } - return body, serverResp.statusCode, nil -} diff --git a/components/engine/api/client/version.go b/components/engine/api/client/version.go index 188d56684e..2f28feca3d 100644 --- a/components/engine/api/client/version.go +++ b/components/engine/api/client/version.go @@ -1,17 +1,11 @@ package client import ( - "encoding/json" - "runtime" "text/template" "time" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" - "github.com/docker/docker/dockerversion" flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/utils" ) var versionTemplate = `Client: @@ -32,12 +26,6 @@ Server: OS/Arch: {{.Server.Os}}/{{.Server.Arch}}{{if .Server.Experimental}} Experimental: {{.Server.Experimental}}{{end}}{{end}}` -type versionData struct { - Client types.Version - ServerOK bool - Server types.Version -} - // 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. @@ -49,59 +37,36 @@ func (cli *DockerCli) CmdVersion(args ...string) (err error) { cmd.Require(flag.Exact, 0) cmd.ParseFlags(args, true) - if *tmplStr == "" { - *tmplStr = versionTemplate + + templateFormat := versionTemplate + if *tmplStr != "" { + templateFormat = *tmplStr } var tmpl *template.Template - if tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr); err != nil { + if tmpl, err = template.New("").Funcs(funcMap).Parse(templateFormat); err != nil { return Cli.StatusError{StatusCode: 64, Status: "Template parsing error: " + err.Error()} } - vd := versionData{ - Client: types.Version{ - Version: dockerversion.Version, - APIVersion: api.Version, - GoVersion: runtime.Version(), - GitCommit: dockerversion.GitCommit, - BuildTime: dockerversion.BuildTime, - Os: runtime.GOOS, - Arch: runtime.GOARCH, - Experimental: utils.ExperimentalBuild(), - }, + vd, err := cli.client.SystemVersion() + + // first we need to make BuildTime more human friendly + t, errTime := time.Parse(time.RFC3339Nano, vd.Client.BuildTime) + if errTime == nil { + vd.Client.BuildTime = t.Format(time.ANSIC) } - defer func() { - // first we need to make BuildTime more human friendly - t, errTime := time.Parse(time.RFC3339Nano, vd.Client.BuildTime) - if errTime == nil { - vd.Client.BuildTime = t.Format(time.ANSIC) - } + if vd.ServerOK() { t, errTime = time.Parse(time.RFC3339Nano, vd.Server.BuildTime) if errTime == nil { vd.Server.BuildTime = t.Format(time.ANSIC) } - - if err2 := tmpl.Execute(cli.out, vd); err2 != nil && err == nil { - err = err2 - } - cli.out.Write([]byte{'\n'}) - }() - - serverResp, err := cli.call("GET", "/version", nil, nil) - if err != nil { - return err } - defer serverResp.body.Close() - - if err = json.NewDecoder(serverResp.body).Decode(&vd.Server); err != nil { - return Cli.StatusError{StatusCode: 1, - Status: "Error reading remote version: " + err.Error()} + if err2 := tmpl.Execute(cli.out, vd); err2 != nil && err == nil { + err = err2 } - - vd.ServerOK = true - - return + cli.out.Write([]byte{'\n'}) + return err } diff --git a/components/engine/api/client/volume.go b/components/engine/api/client/volume.go index 6943db17d3..1eb790f5ba 100644 --- a/components/engine/api/client/volume.go +++ b/components/engine/api/client/volume.go @@ -1,14 +1,8 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "io" - "net/http" - "net/url" "text/tabwriter" - "text/template" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" @@ -64,25 +58,11 @@ func (cli *DockerCli) CmdVolumeLs(args ...string) error { } } - v := url.Values{} - if volFilterArgs.Len() > 0 { - filterJSON, err := filters.ToParam(volFilterArgs) - if err != nil { - return err - } - v.Set("filters", filterJSON) - } - - resp, err := cli.call("GET", "/volumes?"+v.Encode(), nil, nil) + volumes, err := cli.client.VolumeList(volFilterArgs) if err != nil { return err } - var volumes types.VolumesListResponse - if err := json.NewDecoder(resp.body).Decode(&volumes); err != nil { - return err - } - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) if !*quiet { fmt.Fprintf(w, "DRIVER \tVOLUME NAME") @@ -114,68 +94,12 @@ func (cli *DockerCli) CmdVolumeInspect(args ...string) error { return nil } - var tmpl *template.Template - if *tmplStr != "" { - var err error - tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr) - if err != nil { - return err - } + inspectSearcher := func(name string) (interface{}, []byte, error) { + i, err := cli.client.VolumeInspect(name) + return i, nil, err } - var status = 0 - var volumes []*types.Volume - - for _, name := range cmd.Args() { - resp, err := cli.call("GET", "/volumes/"+name, nil, nil) - if err != nil { - if resp.statusCode != http.StatusNotFound { - return err - } - status = 1 - fmt.Fprintf(cli.err, "Error: No such volume: %s\n", name) - continue - } - - var volume types.Volume - if err := json.NewDecoder(resp.body).Decode(&volume); err != nil { - fmt.Fprintf(cli.err, "Unable to read inspect data: %v\n", err) - status = 1 - break - } - - if tmpl == nil { - volumes = append(volumes, &volume) - continue - } - - buf := bytes.NewBufferString("") - if err := tmpl.Execute(buf, &volume); err != nil { - fmt.Fprintf(cli.err, "Template parsing error: %v\n", err) - status = 1 - break - } - - cli.out.Write(buf.Bytes()) - cli.out.Write([]byte{'\n'}) - } - - if tmpl == nil { - b, err := json.MarshalIndent(volumes, "", " ") - if err != nil { - return err - } - _, err = io.Copy(cli.out, bytes.NewReader(b)) - if err != nil { - return err - } - io.WriteString(cli.out, "\n") - } - - if status != 0 { - return Cli.StatusError{StatusCode: status} - } - return nil + return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher) } // CmdVolumeCreate creates a new container from a given image. @@ -192,24 +116,17 @@ func (cli *DockerCli) CmdVolumeCreate(args ...string) error { cmd.Require(flag.Exact, 0) cmd.ParseFlags(args, true) - volReq := &types.VolumeCreateRequest{ + volReq := types.VolumeCreateRequest{ Driver: *flDriver, DriverOpts: flDriverOpts.GetAll(), + Name: *flName, } - if *flName != "" { - volReq.Name = *flName - } - - resp, err := cli.call("POST", "/volumes/create", volReq, nil) + vol, err := cli.client.VolumeCreate(volReq) if err != nil { return err } - var vol types.Volume - if err := json.NewDecoder(resp.body).Decode(&vol); err != nil { - return err - } fmt.Fprintf(cli.out, "%s\n", vol.Name) return nil } @@ -224,8 +141,7 @@ func (cli *DockerCli) CmdVolumeRm(args ...string) error { var status = 0 for _, name := range cmd.Args() { - _, err := cli.call("DELETE", "/volumes/"+name, nil, nil) - if err != nil { + if err := cli.client.VolumeRemove(name); err != nil { fmt.Fprintf(cli.err, "%s\n", err) status = 1 continue diff --git a/components/engine/api/client/wait.go b/components/engine/api/client/wait.go index 3b03f7069e..bd64b83ca2 100644 --- a/components/engine/api/client/wait.go +++ b/components/engine/api/client/wait.go @@ -20,7 +20,7 @@ func (cli *DockerCli) CmdWait(args ...string) error { var errNames []string for _, name := range cmd.Args() { - status, err := waitForExit(cli, name) + status, err := cli.client.ContainerWait(name) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) errNames = append(errNames, name) diff --git a/components/engine/api/types/client.go b/components/engine/api/types/client.go new file mode 100644 index 0000000000..6841a9a6ca --- /dev/null +++ b/components/engine/api/types/client.go @@ -0,0 +1,234 @@ +package types + +import ( + "bufio" + "io" + "net" + + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/ulimit" + "github.com/docker/docker/runconfig" +) + +// ContainerAttachOptions holds parameters to attach to a container. +type ContainerAttachOptions struct { + ContainerID string + Stream bool + Stdin bool + Stdout bool + Stderr bool +} + +// ContainerCommitOptions holds parameters to commit changes into a container. +type ContainerCommitOptions struct { + ContainerID string + RepositoryName string + Tag string + Comment string + Author string + Changes []string + Pause bool + Config *runconfig.Config +} + +// ContainerExecInspect holds information returned by exec inspect. +type ContainerExecInspect struct { + ExecID string + ContainerID string + Running bool + ExitCode int +} + +// ContainerListOptions holds parameters to list containers with. +type ContainerListOptions struct { + Quiet bool + Size bool + All bool + Latest bool + Since string + Before string + Limit int + Filter filters.Args +} + +// ContainerLogsOptions holds parameters to filter logs with. +type ContainerLogsOptions struct { + ContainerID string + ShowStdout bool + ShowStderr bool + Since string + Timestamps bool + Follow bool + Tail string +} + +// ContainerRemoveOptions holds parameters to remove containers. +type ContainerRemoveOptions struct { + ContainerID string + RemoveVolumes bool + RemoveLinks bool + Force bool +} + +// CopyToContainerOptions holds information +// about files to copy into a container +type CopyToContainerOptions struct { + ContainerID string + Path string + Content io.Reader + AllowOverwriteDirWithFile bool +} + +// EventsOptions hold parameters to filter events with. +type EventsOptions struct { + Since string + Until string + Filters filters.Args +} + +// HijackedResponse holds connection information for a hijacked request. +type HijackedResponse struct { + Conn net.Conn + Reader *bufio.Reader +} + +// Close closes the hijacked connection and reader. +func (h *HijackedResponse) Close() { + h.Conn.Close() +} + +// CloseWriter is an interface that implement structs +// that close input streams to prevent from writing. +type CloseWriter interface { + CloseWrite() error +} + +// CloseWrite closes a readWriter for writing. +func (h *HijackedResponse) CloseWrite() error { + if conn, ok := h.Conn.(CloseWriter); ok { + return conn.CloseWrite() + } + return nil +} + +// ImageBuildOptions holds the information +// necessary to build images. +type ImageBuildOptions struct { + Tags []string + SuppressOutput bool + RemoteContext string + NoCache bool + Remove bool + ForceRemove bool + PullParent bool + Isolation string + CPUSetCPUs string + CPUSetMems string + CPUShares int64 + CPUQuota int64 + CPUPeriod int64 + Memory int64 + MemorySwap int64 + CgroupParent string + ShmSize string + Dockerfile string + Ulimits []*ulimit.Ulimit + BuildArgs []string + AuthConfigs map[string]cliconfig.AuthConfig + Context io.Reader +} + +// ImageBuildResponse holds information +// returned by a server after building +// an image. +type ImageBuildResponse struct { + Body io.ReadCloser + OSType string +} + +// ImageCreateOptions holds information to create images. +type ImageCreateOptions struct { + // Parent is the image to create this image from + Parent string + // Tag is the name to tag this image + Tag string + // RegistryAuth is the base64 encoded credentials for this server + RegistryAuth string +} + +// ImageImportOptions holds information to import images from the client host. +type ImageImportOptions struct { + // Source is the data to send to the server to create this image from + Source io.Reader + // Source is the name of the source to import this image from + SourceName string + // RepositoryName is the name of the repository to import this image + RepositoryName string + // Message is the message to tag the image with + Message string + // Tag is the name to tag this image + Tag string + // Changes are the raw changes to apply to the image + Changes []string +} + +// ImageListOptions holds parameters to filter the list of images with. +type ImageListOptions struct { + MatchName string + All bool + Filters filters.Args +} + +// ImagePullOptions holds information to pull images. +type ImagePullOptions struct { + ImageID string + Tag string + // RegistryAuth is the base64 encoded credentials for this server + RegistryAuth string +} + +//ImagePushOptions holds information to push images. +type ImagePushOptions ImagePullOptions + +// ImageRemoveOptions holds parameters to remove images. +type ImageRemoveOptions struct { + ImageID string + Force bool + PruneChildren bool +} + +// ImageSearchOptions holds parameters to search images with. +type ImageSearchOptions struct { + Term string + RegistryAuth string +} + +// ImageTagOptions holds parameters to tag an image +type ImageTagOptions struct { + ImageID string + RepositoryName string + Tag string + Force bool +} + +// ResizeOptions holds parameters to resize a tty. +// It can be used to resize container ttys and +// exec process ttys too. +type ResizeOptions struct { + ID string + Height int + Width int +} + +// VersionResponse holds version information for the client and the server +type VersionResponse struct { + Client *Version + Server *Version +} + +// ServerOK return true when the client could connect to the docker server +// and parse the information received. It returns false otherwise. +func (v VersionResponse) ServerOK() bool { + return v.Server != nil +} diff --git a/components/engine/cliconfig/config.go b/components/engine/cliconfig/config.go index 87b92acdf8..0bcf87d87d 100644 --- a/components/engine/cliconfig/config.go +++ b/components/engine/cliconfig/config.go @@ -54,6 +54,15 @@ type AuthConfig struct { RegistryToken string `json:"registrytoken,omitempty"` } +// EncodeToBase64 serializes the auth configuration as JSON base64 payload +func (a AuthConfig) EncodeToBase64() (string, error) { + buf, err := json.Marshal(a) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(buf), nil +} + // ConfigFile ~/.docker/config.json file info type ConfigFile struct { AuthConfigs map[string]AuthConfig `json:"auths"` @@ -192,7 +201,14 @@ func Load(configDir string) (*ConfigFile, error) { } defer file.Close() err = configFile.LegacyLoadFromReader(file) - return &configFile, err + if err != nil { + return &configFile, err + } + + if configFile.HTTPHeaders == nil { + configFile.HTTPHeaders = map[string]string{} + } + return &configFile, nil } // SaveToWriter encodes and writes out all the authorization information to diff --git a/components/engine/integration-cli/docker_cli_inspect_test.go b/components/engine/integration-cli/docker_cli_inspect_test.go index 9a30ace533..355cff2b00 100644 --- a/components/engine/integration-cli/docker_cli_inspect_test.go +++ b/components/engine/integration-cli/docker_cli_inspect_test.go @@ -356,3 +356,14 @@ func (s *DockerSuite) TestInspectByPrefix(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(id, checker.Equals, id3) } + +func (s *DockerSuite) TestInspectStopWhenNotFound(c *check.C) { + dockerCmd(c, "run", "--name=busybox", "-d", "busybox", "top") + dockerCmd(c, "run", "--name=not-shown", "-d", "busybox", "top") + out, _, err := dockerCmdWithError("inspect", "--type=container", "--format='{{.Name}}'", "busybox", "missing", "not-shown") + + c.Assert(err, checker.Not(check.IsNil)) + c.Assert(out, checker.Contains, "busybox") + c.Assert(out, checker.Not(checker.Contains), "not-shown") + c.Assert(out, checker.Contains, "Error: No such container: missing") +} diff --git a/components/engine/integration-cli/docker_cli_logs_test.go b/components/engine/integration-cli/docker_cli_logs_test.go index b572475c73..94ca1d3de8 100644 --- a/components/engine/integration-cli/docker_cli_logs_test.go +++ b/components/engine/integration-cli/docker_cli_logs_test.go @@ -349,6 +349,6 @@ func (s *DockerSuite) TestLogsFollowGoroutinesNoOutput(c *check.C) { func (s *DockerSuite) TestLogsCLIContainerNotFound(c *check.C) { name := "testlogsnocontainer" out, _, _ := dockerCmdWithError("logs", name) - message := fmt.Sprintf(".*No such container: %s.*\n", name) - c.Assert(out, checker.Matches, message) + message := fmt.Sprintf("Error: No such container: %s\n", name) + c.Assert(out, checker.Equals, message) } diff --git a/components/engine/integration-cli/docker_cli_network_unix_test.go b/components/engine/integration-cli/docker_cli_network_unix_test.go index 7914838f00..8fc0b7eef2 100644 --- a/components/engine/integration-cli/docker_cli_network_unix_test.go +++ b/components/engine/integration-cli/docker_cli_network_unix_test.go @@ -317,7 +317,7 @@ func (s *DockerSuite) TestDockerInspectMultipleNetwork(c *check.C) { c.Assert(exitCode, checker.Equals, 1) c.Assert(out, checker.Contains, "Error: No such network: nonexistent") networkResources = []types.NetworkResource{} - inspectOut := strings.SplitN(out, "\n", 2)[1] + inspectOut := strings.SplitN(out, "\nError: No such network: nonexistent\n", 2)[0] err = json.Unmarshal([]byte(inspectOut), &networkResources) c.Assert(networkResources, checker.HasLen, 1) diff --git a/components/engine/integration-cli/docker_cli_volume_test.go b/components/engine/integration-cli/docker_cli_volume_test.go index d404fe1538..6d49abe579 100644 --- a/components/engine/integration-cli/docker_cli_volume_test.go +++ b/components/engine/integration-cli/docker_cli_volume_test.go @@ -53,15 +53,17 @@ func (s *DockerSuite) TestVolumeCliInspect(c *check.C) { func (s *DockerSuite) TestVolumeCliInspectMulti(c *check.C) { dockerCmd(c, "volume", "create", "--name", "test1") dockerCmd(c, "volume", "create", "--name", "test2") + dockerCmd(c, "volume", "create", "--name", "not-shown") - out, _, err := dockerCmdWithError("volume", "inspect", "--format='{{ .Name }}'", "test1", "test2", "doesntexist") + out, _, err := dockerCmdWithError("volume", "inspect", "--format='{{ .Name }}'", "test1", "test2", "doesntexist", "not-shown") c.Assert(err, checker.NotNil) outArr := strings.Split(strings.TrimSpace(out), "\n") c.Assert(len(outArr), check.Equals, 3, check.Commentf("\n%s", out)) - c.Assert(strings.Contains(out, "test1\n"), check.Equals, true) - c.Assert(strings.Contains(out, "test2\n"), check.Equals, true) - c.Assert(strings.Contains(out, "Error: No such volume: doesntexist\n"), check.Equals, true) + c.Assert(out, checker.Contains, "test1") + c.Assert(out, checker.Contains, "test2") + c.Assert(out, checker.Contains, "Error: No such volume: doesntexist") + c.Assert(out, checker.Not(checker.Contains), "not-shown") } func (s *DockerSuite) TestVolumeCliLs(c *check.C) {