From 589df17a1a1dc649a4c3095cea6dd05e0c2a3bb5 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Wed, 2 Dec 2015 23:53:06 -0500 Subject: [PATCH 01/53] Extract API client struct as standalone client. Signed-off-by: David Calavera --- api/client/cli.go | 122 +++++++++++++++--------------- api/client/lib/client.go | 98 ++++++++++++++++++++++++ api/client/lib/request.go | 155 ++++++++++++++++++++++++++++++++++++++ cliconfig/config.go | 9 ++- 4 files changed, 320 insertions(+), 64 deletions(-) create mode 100644 api/client/lib/client.go create mode 100644 api/client/lib/request.go diff --git a/api/client/cli.go b/api/client/cli.go index 834c47a4d3..7848fd9228 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -6,14 +6,14 @@ import ( "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 +24,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 +34,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,6 +42,22 @@ type DockerCli struct { isTerminalIn bool // isTerminalOut indicates whether the client's STDOUT is a TTY isTerminalOut bool + // client is the http client that performs all API operations + client *lib.Client + + // DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE + // TODO: Remove + // 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 + // 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 // transport holds the client transport instance. transport *http.Transport } @@ -98,50 +102,35 @@ 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 + client, err := lib.NewClient(host, clientFlags.Common.TLSOptions, customHeaders) + if err != nil { + return err } + cli.client = client - 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" - } + // FIXME: Deprecated, only to keep the old code running. + cli.transport = client.HTTPClient.Transport.(*http.Transport) + cli.basePath = client.BasePath + cli.addr = client.Addr + cli.scheme = client.Scheme if cli.in != nil { cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) @@ -150,20 +139,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/api/client/lib/client.go b/api/client/lib/client.go new file mode 100644 index 0000000000..dbb088a9ca --- /dev/null +++ b/api/client/lib/client.go @@ -0,0 +1,98 @@ +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 + // 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{ + Addr: addr, + BasePath: basePath, + Scheme: scheme, + 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/api/client/lib/request.go b/api/client/lib/request.go new file mode 100644 index 0000000000..328b087a50 --- /dev/null +++ b/api/client/lib/request.go @@ -0,0 +1,155 @@ +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) +} + +// 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, 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{}) + } + + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest(method, apiPath, 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.customHTTPHeaders { + req.Header.Set(k, v) + } + + 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.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 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/cliconfig/config.go b/cliconfig/config.go index 87b92acdf8..b28d6e5bcf 100644 --- a/cliconfig/config.go +++ b/cliconfig/config.go @@ -192,7 +192,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 From 8c9ad7b818c0a7b1e39f8df1fabba243a0961c2d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Wed, 2 Dec 2015 23:55:07 -0500 Subject: [PATCH 02/53] Implement docker commit with standalone client lib. Signed-off-by: David Calavera --- api/client/commit.go | 45 ++++++---------------- api/client/lib/container_commit.go | 62 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 api/client/lib/container_commit.go diff --git a/api/client/commit.go b/api/client/commit.go index 36fcb3c77e..fb9cfe2820 100644 --- a/api/client/commit.go +++ b/api/client/commit.go @@ -1,18 +1,15 @@ package client import ( - "encoding/json" "errors" "fmt" - "net/url" "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/lib" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" - "github.com/docker/docker/runconfig" ) // CmdCommit creates a new image from a container's changes. @@ -59,42 +56,22 @@ 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) + options := lib.ContainerCommitOptions{ + ContainerID: name, + RepositoryName: repositoryName, + Tag: tag, + Comment: *flComment, + Author: *flAuthor, + Changes: flChanges.GetAll(), + Pause: *flPause, + JSONConfig: *flConfig, } - if *flPause != true { - v.Set("pause", "0") - } - - var ( - config *runconfig.Config - response types.ContainerCommitResponse - ) - - if *flConfig != "" { - config = &runconfig.Config{} - if err := json.Unmarshal([]byte(*flConfig), config); err != nil { - return err - } - } - serverResp, err := cli.call("POST", "/commit?"+v.Encode(), config, nil) + response, err := cli.client.ContainerCommit(options) if err != nil { return err } - defer serverResp.body.Close() - - if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil { - return err - } - fmt.Fprintln(cli.out, response.ID) return nil } diff --git a/api/client/lib/container_commit.go b/api/client/lib/container_commit.go new file mode 100644 index 0000000000..9dc3440282 --- /dev/null +++ b/api/client/lib/container_commit.go @@ -0,0 +1,62 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/runconfig" +) + +// ContainerCommitOptions hods parameters to commit changes into a container. +type ContainerCommitOptions struct { + ContainerID string + RepositoryName string + Tag string + Comment string + Author string + Changes []string + Pause bool + JSONConfig string +} + +// 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 ( + config *runconfig.Config + response types.ContainerCommitResponse + ) + + if options.JSONConfig != "" { + config = &runconfig.Config{} + if err := json.Unmarshal([]byte(options.JSONConfig), config); err != nil { + return response, err + } + } + + resp, err := cli.POST("/commit", query, config, nil) + if err != nil { + return response, err + } + + defer resp.body.Close() + + if err := json.NewDecoder(resp.body).Decode(&response); err != nil { + return response, err + } + + return response, nil +} From 1b2b91ba43dc6fa1b4b758fc5a8090ce6cc597ff Mon Sep 17 00:00:00 2001 From: David Calavera Date: Wed, 2 Dec 2015 23:56:03 -0500 Subject: [PATCH 03/53] Implement docker cp with standalone client lib. Signed-off-by: David Calavera --- api/client/cp.go | 90 ++++++----------------------------- api/client/lib/copy.go | 103 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 77 deletions(-) create mode 100644 api/client/lib/copy.go diff --git a/api/client/cp.go b/api/client/cp.go index bd97441ca5..16e2cf471e 100644 --- a/api/client/cp.go +++ b/api/client/cp.go @@ -1,16 +1,13 @@ package client import ( - "encoding/base64" - "encoding/json" "fmt" "io" - "net/http" - "net/url" "os" "path/filepath" "strings" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/pkg/archive" @@ -129,38 +126,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.StatContainerPath(containerName, path) } func resolveLocalPath(localPath string) (absPath string, err error) { @@ -200,39 +166,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 +187,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 +286,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 := lib.CopyToContainerOptions{ + ContainerID: dstContainer, + Path: resolvedDstPath, + Content: content, + AllowOverwriteDirWithFile: false, } - return nil + return cli.client.CopyToContainer(options) } diff --git a/api/client/lib/copy.go b/api/client/lib/copy.go new file mode 100644 index 0000000000..ed982220b6 --- /dev/null +++ b/api/client/lib/copy.go @@ -0,0 +1,103 @@ +package lib + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types" +) + +// CopyToContainerOptions holds information +// about files to copy into a container +type CopyToContainerOptions struct { + ContainerID string + Path string + Content io.Reader + AllowOverwriteDirWithFile bool +} + +// StatContainerPath returns Stat information about a path inside the container filesystem. +func (cli *Client) StatContainerPath(containerID, path string) (types.ContainerPathStat, error) { + query := make(url.Values, 1) + 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 CopyToContainerOptions) error { + var 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.PUT(path, query, options.Content, nil) + if err != nil { + return err + } + + 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 +} From 1698fe01f58e090e736c611bc8c21e67544251e9 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 11:14:07 -0500 Subject: [PATCH 04/53] Implement docker image create with standalone client lib. Signed-off-by: David Calavera --- api/client/create.go | 26 ++++++++++++-------------- api/client/lib/image_create.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 api/client/lib/image_create.go diff --git a/api/client/create.go b/api/client/create.go index 9443799e55..172a87bf16 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -10,8 +10,10 @@ import ( "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 +24,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 +40,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 { @@ -56,18 +53,19 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { return err } - registryAuthHeader := []string{ - base64.URLEncoding.EncodeToString(buf), + options := lib.CreateImageOptions{ + Parent: ref.Name(), + Tag: tag, + RegistryAuth: base64.URLEncoding.EncodeToString(buf), } - sopts := &streamOpts{ - rawTerminal: true, - out: out, - headers: map[string][]string{"X-Registry-Auth": registryAuthHeader}, - } - if _, err := cli.stream("POST", "/images/create?"+v.Encode(), sopts); err != nil { + + responseBody, err := cli.client.CreateImage(options) + if err != nil { return err } - return nil + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut) } type cidFile struct { diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go new file mode 100644 index 0000000000..65c210bbbb --- /dev/null +++ b/api/client/lib/image_create.go @@ -0,0 +1,31 @@ +package lib + +import ( + "io" + "net/url" +) + +// CreateImageOptions holds information to create images +type CreateImageOptions 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 +} + +// CreateImage creates a new image based in the parent options. +// It returns the JSON content in the response body. +func (cli *Client) CreateImage(options CreateImageOptions) (io.ReadCloser, error) { + var query url.Values + query.Set("fromImage", options.Parent) + query.Set("tag", options.Tag) + + headers := map[string][]string{"X-Registry-Auth": {options.RegistryAuth}} + resp, err := cli.POST("/images/create", query, nil, headers) + if err != nil { + return nil, err + } + return resp.body, nil +} From 136e8fef64d3dd0e7601ffdad8864b8e1af7c7e5 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 14:14:36 -0500 Subject: [PATCH 05/53] Implement docker container create with standalone client lib. Signed-off-by: David Calavera --- api/client/create.go | 45 ++++++++++++------------------ api/client/lib/container_create.go | 43 ++++++++++++++++++++++++++++ api/client/lib/errors.go | 31 ++++++++++++++++++++ 3 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 api/client/lib/container_create.go create mode 100644 api/client/lib/errors.go diff --git a/api/client/create.go b/api/client/create.go index 172a87bf16..25189fa14a 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -5,9 +5,7 @@ import ( "encoding/json" "fmt" "io" - "net/url" "os" - "strings" "github.com/docker/distribution/reference" "github.com/docker/docker/api/client/lib" @@ -88,11 +86,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 @@ -133,34 +126,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/api/client/lib/container_create.go b/api/client/lib/container_create.go new file mode 100644 index 0000000000..906307f095 --- /dev/null +++ b/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 ( + query url.Values + response types.ContainerCreateResponse + ) + 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) { + } + + if err != nil { + return response, err + } + + if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil { + return response, err + } + + return response, nil +} diff --git a/api/client/lib/errors.go b/api/client/lib/errors.go new file mode 100644 index 0000000000..21a41f03f4 --- /dev/null +++ b/api/client/lib/errors.go @@ -0,0 +1,31 @@ +package lib + +import "fmt" + +// 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("Image not found: %s", i.imageID) +} + +// ImageNotFound returns the ID of the image not found on the docker host. +func (i imageNotFoundError) ImageIDNotFound() string { + return i.imageID +} + +// ImageNotFound is an interface that describes errors caused +// when an image is not found in the docker host. +type ImageNotFound interface { + ImageIDNotFound() string +} + +// IsImageNotFound returns true when the error is caused +// when an image is not found in the docker host. +func IsErrImageNotFound(err error) bool { + _, ok := err.(ImageNotFound) + return ok +} From e562ac42f445f01625f2e1fc220d1aae3a28b8e1 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 14:18:35 -0500 Subject: [PATCH 06/53] Implement docker diff with standalone client lib. Signed-off-by: David Calavera --- api/client/diff.go | 11 +---------- api/client/lib/diff.go | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 api/client/lib/diff.go diff --git a/api/client/diff.go b/api/client/diff.go index 24350a04a0..f180e00172 100644 --- a/api/client/diff.go +++ b/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/api/client/lib/diff.go b/api/client/lib/diff.go new file mode 100644 index 0000000000..f0d027afc1 --- /dev/null +++ b/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 serverResp.body.Close() + + if err := json.NewDecoder(serverResp.body).Decode(&changes); err != nil { + return changes, err + } + + return changes, nil +} From 11c4cc9dded3aaed8be2fdcebc976b63ff80ef2c Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 14:34:35 -0500 Subject: [PATCH 07/53] Implement docker events with standalone client lib. Signed-off-by: David Calavera --- api/client/events.go | 49 +++++++++++-------------------------- api/client/lib/events.go | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 35 deletions(-) create mode 100644 api/client/lib/events.go diff --git a/api/client/events.go b/api/client/events.go index fa84eac518..568be41fad 100644 --- a/api/client/events.go +++ b/api/client/events.go @@ -1,14 +1,12 @@ package client import ( - "net/url" - "time" - + "github.com/docker/docker/api/client/lib" 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 := lib.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/api/client/lib/events.go b/api/client/lib/events.go new file mode 100644 index 0000000000..1d50cb5f3e --- /dev/null +++ b/api/client/lib/events.go @@ -0,0 +1,52 @@ +package lib + +import ( + "io" + "net/url" + "time" + + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/timeutils" +) + +// EventsOptions hold parameters to filter events with. +type EventsOptions struct { + Since string + Until string + Filters filters.Args +} + +// 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 EventsOptions) (io.ReadCloser, error) { + var 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 +} From e0549b8cebee4dbe8614688767c9b666afdd32d9 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 14:49:41 -0500 Subject: [PATCH 08/53] Implement docker export with standalone client lib. Signed-off-by: David Calavera --- api/client/export.go | 13 ++++++------- api/client/lib/export.go | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 api/client/lib/export.go diff --git a/api/client/export.go b/api/client/export.go index 2763b7b045..ec94f6728e 100644 --- a/api/client/export.go +++ b/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/api/client/lib/export.go b/api/client/lib/export.go new file mode 100644 index 0000000000..6cb4d4a18d --- /dev/null +++ b/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 +} From 45eca43f5bead5d9d22e65e0609410b266c32e18 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 14:54:31 -0500 Subject: [PATCH 09/53] Implement docker history with the standalone client lib. Signed-off-by: David Calavera --- api/client/history.go | 11 +---------- api/client/lib/history.go | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 api/client/lib/history.go diff --git a/api/client/history.go b/api/client/history.go index f8a71c870d..fe092df08a 100644 --- a/api/client/history.go +++ b/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/api/client/lib/history.go b/api/client/lib/history.go new file mode 100644 index 0000000000..29064c65fb --- /dev/null +++ b/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 serverResp.body.Close() + + if err := json.NewDecoder(serverResp.body).Decode(&history); err != nil { + return history, err + } + return history, nil +} From 381262fbeab88f4195ec4d0ccd036e3233398204 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 15:20:36 -0500 Subject: [PATCH 10/53] Implement docker images with the standalone client lib. Signed-off-by: David Calavera --- api/client/images.go | 36 ++++++++------------------- api/client/lib/image_list.go | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 api/client/lib/image_list.go diff --git a/api/client/images.go b/api/client/images.go index 26e7c9706e..02c59fdf42 100644 --- a/api/client/images.go +++ b/api/client/images.go @@ -1,15 +1,13 @@ package client import ( - "encoding/json" "fmt" - "net/url" "strings" "text/tabwriter" "time" "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/lib" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" @@ -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 := lib.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/api/client/lib/image_list.go b/api/client/lib/image_list.go new file mode 100644 index 0000000000..b86c3e944a --- /dev/null +++ b/api/client/lib/image_list.go @@ -0,0 +1,48 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/parsers/filters" +) + +// ImageListOptions holds parameters to filter the list of images with. +type ImageListOptions struct { + MatchName string + All bool + Filters filters.Args +} + +// ImageList returns a list of images in the docker host. +func (cli *Client) ImageList(options 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 serverResp.body.Close() + + err = json.NewDecoder(serverResp.body).Decode(&images) + return images, err +} From 6bf757500b404311cf24c6ce656d317f49b7cc37 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 17:11:37 -0500 Subject: [PATCH 11/53] Implement docker import with the standalone client lib. Signed-off-by: David Calavera --- api/client/import.go | 40 ++++++++++++++++++--------------- api/client/lib/image_create.go | 2 +- api/client/lib/image_import.go | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 api/client/lib/image_import.go diff --git a/api/client/import.go b/api/client/import.go index 2838debd98..1a5887a020 100644 --- a/api/client/import.go +++ b/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/client/lib" 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 := lib.ImportImageOptions{ + 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.ImportImage(options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) } diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go index 65c210bbbb..d56bfa647c 100644 --- a/api/client/lib/image_create.go +++ b/api/client/lib/image_create.go @@ -5,7 +5,7 @@ import ( "net/url" ) -// CreateImageOptions holds information to create images +// CreateImageOptions holds information to create images. type CreateImageOptions struct { // Parent is the image to create this image from Parent string diff --git a/api/client/lib/image_import.go b/api/client/lib/image_import.go new file mode 100644 index 0000000000..ebc7130e0e --- /dev/null +++ b/api/client/lib/image_import.go @@ -0,0 +1,41 @@ +package lib + +import ( + "io" + "net/url" +) + +// ImportImageOptions holds information to import images from the client host. +type ImportImageOptions 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 +} + +// ImportImage creates a new image based in the source options. +// It returns the JSON content in the response body. +func (cli *Client) ImportImage(options ImportImageOptions) (io.ReadCloser, error) { + var 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 +} From 900ad2897f58bffd2992c5fd8b1cd50cfb61198f Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 17:20:46 -0500 Subject: [PATCH 12/53] Implement docker info with standalone client lib. Signed-off-by: David Calavera --- api/client/info.go | 74 ++++++++++++++++++------------------------ api/client/lib/info.go | 25 ++++++++++++++ 2 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 api/client/lib/info.go diff --git a/api/client/info.go b/api/client/info.go index cf81418019..1abc20db2e 100644 --- a/api/client/info.go +++ b/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/api/client/lib/info.go b/api/client/lib/info.go new file mode 100644 index 0000000000..4b4bd048a0 --- /dev/null +++ b/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 serverResp.body.Close() + + if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil { + return info, fmt.Errorf("Error reading remote info: %v", err) + } + + return info, nil +} From 535c4c9a59b1e58c897677d6948a595cb3d28639 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 18:27:01 -0500 Subject: [PATCH 13/53] Implement docker build with standalone client lib. Signed-off-by: David Calavera --- api/client/build.go | 136 +++++++++--------------------- api/client/lib/image_build.go | 150 ++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 97 deletions(-) create mode 100644 api/client/lib/image_build.go diff --git a/api/client/build.go b/api/client/build.go index 8de9c6837d..6268464c72 100644 --- a/api/client/build.go +++ b/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/client/lib" 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 := lib.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/api/client/lib/image_build.go b/api/client/lib/image_build.go new file mode 100644 index 0000000000..0b89b05ff6 --- /dev/null +++ b/api/client/lib/image_build.go @@ -0,0 +1,150 @@ +package lib + +import ( + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/ulimit" + "github.com/docker/docker/pkg/units" + "github.com/docker/docker/runconfig" +) + +// 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 +} + +// 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 ImageBuildOptions) (ImageBuildResponse, error) { + query, err := imageBuildOptionsToQuery(options) + if err != nil { + return ImageBuildResponse{}, err + } + + headers := http.Header(make(map[string][]string)) + buf, err := json.Marshal(options.AuthConfigs) + if err != nil { + return 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 ImageBuildResponse{}, err + } + + var osType string + if h, err := httputils.ParseServerHeader(serverResp.header.Get("Server")); err == nil { + osType = h.OS + } + + return ImageBuildResponse{ + Body: serverResp.body, + OSType: osType, + }, nil +} + +func imageBuildOptionsToQuery(options 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 +} From c57e62d00e209288e4f2734d32a3184b4abf4248 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 18:40:56 -0500 Subject: [PATCH 14/53] Implement docker kill with standalone client lib. Signed-off-by: David Calavera --- api/client/kill.go | 2 +- api/client/lib/kill.go | 12 ++++++++++++ api/client/start.go | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 api/client/lib/kill.go diff --git a/api/client/kill.go b/api/client/kill.go index c4c355c74b..e71304644f 100644 --- a/api/client/kill.go +++ b/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/api/client/lib/kill.go b/api/client/lib/kill.go new file mode 100644 index 0000000000..40ce9d383f --- /dev/null +++ b/api/client/lib/kill.go @@ -0,0 +1,12 @@ +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 { + var query url.Values + query.Set("signal", signal) + + _, err := cli.POST("/containers/"+containerID+"/kill", query, nil, nil) + return err +} diff --git a/api/client/start.go b/api/client/start.go index bff9c30ec8..b429639ede 100644 --- a/api/client/start.go +++ b/api/client/start.go @@ -34,7 +34,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) } } From 9073a52ea839ef224931e1105bfa9c715ee48e2c Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 18:49:55 -0500 Subject: [PATCH 15/53] Implement docker load with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/image_load.go | 17 +++++++++++++++++ api/client/load.go | 24 +++++++++++------------- 2 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 api/client/lib/image_load.go diff --git a/api/client/lib/image_load.go b/api/client/lib/image_load.go new file mode 100644 index 0000000000..9f0e04cd54 --- /dev/null +++ b/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/api/client/load.go b/api/client/load.go index 378170e17e..10681dc71a 100644 --- a/api/client/load.go +++ b/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 } From b36531db6020134c8b561db71a67c4346051ed4a Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 19:12:35 -0500 Subject: [PATCH 16/53] Implement docker login with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/errors.go | 32 +++++++++++++++++++------------- api/client/lib/login.go | 28 ++++++++++++++++++++++++++++ api/client/login.go | 28 +++++++++------------------- 3 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 api/client/lib/login.go diff --git a/api/client/lib/errors.go b/api/client/lib/errors.go index 21a41f03f4..3ee56e6333 100644 --- a/api/client/lib/errors.go +++ b/api/client/lib/errors.go @@ -12,20 +12,26 @@ func (i imageNotFoundError) Error() string { return fmt.Sprintf("Image not found: %s", i.imageID) } -// ImageNotFound returns the ID of the image not found on the docker host. -func (i imageNotFoundError) ImageIDNotFound() string { - return i.imageID -} - -// ImageNotFound is an interface that describes errors caused -// when an image is not found in the docker host. -type ImageNotFound interface { - ImageIDNotFound() string -} - -// IsImageNotFound returns true when the error is caused +// IsImageNotFound returns true if the error is caused // when an image is not found in the docker host. func IsErrImageNotFound(err error) bool { - _, ok := err.(ImageNotFound) + _, ok := err.(imageNotFoundError) + 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() +} + +// IsUnauthorized 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/api/client/lib/login.go b/api/client/lib/login.go new file mode 100644 index 0000000000..ca194150ed --- /dev/null +++ b/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 resp.body.Close() + + var response types.AuthResponse + err = json.NewDecoder(resp.body).Decode(&response) + return response, err +} diff --git a/api/client/login.go b/api/client/login.go index e941a14efe..31022919c9 100644 --- a/api/client/login.go +++ b/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 } From 087674264674eaf946d185691ea92eee16f16a4f Mon Sep 17 00:00:00 2001 From: David Calavera Date: Thu, 3 Dec 2015 19:34:16 -0500 Subject: [PATCH 17/53] Implement docker logs with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_inspect.go | 20 +++++++++ api/client/lib/logs.go | 56 +++++++++++++++++++++++++ api/client/logs.go | 55 +++++++++--------------- integration-cli/docker_cli_logs_test.go | 4 +- 4 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 api/client/lib/container_inspect.go create mode 100644 api/client/lib/logs.go diff --git a/api/client/lib/container_inspect.go b/api/client/lib/container_inspect.go new file mode 100644 index 0000000000..73863aafee --- /dev/null +++ b/api/client/lib/container_inspect.go @@ -0,0 +1,20 @@ +package lib + +import ( + "encoding/json" + + "github.com/docker/docker/api/types" +) + +// ContainerInspect returns the all the container information. +func (cli *Client) ContainerInspect(containerID string) (types.ContainerJSON, error) { + serverResp, err := cli.GET("/containers/"+containerID+"/json", nil, nil) + if err != nil { + return types.ContainerJSON{}, err + } + defer serverResp.body.Close() + + var response types.ContainerJSON + json.NewDecoder(serverResp.body).Decode(&response) + return response, err +} diff --git a/api/client/lib/logs.go b/api/client/lib/logs.go new file mode 100644 index 0000000000..a464de7c0b --- /dev/null +++ b/api/client/lib/logs.go @@ -0,0 +1,56 @@ +package lib + +import ( + "io" + "net/url" + "time" + + "github.com/docker/docker/pkg/timeutils" +) + +// ContainerLogsOptions holds parameters to filter logs with. +type ContainerLogsOptions struct { + ContainerID string + ShowStdout bool + ShowStderr bool + Since string + Timestamps bool + Follow bool + Tail string +} + +// 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 ContainerLogsOptions) (io.ReadCloser, error) { + var 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/api/client/logs.go b/api/client/logs.go index 09c3d9398f..52465683a0 100644 --- a/api/client/logs.go +++ b/api/client/logs.go @@ -1,15 +1,13 @@ package client import ( - "encoding/json" "fmt" - "net/url" - "time" + "io" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/lib" 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 := lib.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/integration-cli/docker_cli_logs_test.go b/integration-cli/docker_cli_logs_test.go index b572475c73..94ca1d3de8 100644 --- a/integration-cli/docker_cli_logs_test.go +++ b/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) } From 356768bc01adc6d74e723397fbc3ac7efcb0af64 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 11:33:00 -0500 Subject: [PATCH 18/53] Implement docker network with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/network.go | 67 +++++++++++++++++++++++++++++++++++++++ api/client/lib/request.go | 4 +++ api/client/network.go | 62 +++++++++--------------------------- 3 files changed, 86 insertions(+), 47 deletions(-) create mode 100644 api/client/lib/network.go diff --git a/api/client/lib/network.go b/api/client/lib/network.go new file mode 100644 index 0000000000..bbf7ce5093 --- /dev/null +++ b/api/client/lib/network.go @@ -0,0 +1,67 @@ +package lib + +import ( + "encoding/json" + + "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 { + return networkResource, err + } + defer ensureReaderClosed(resp) + err = json.NewDecoder(resp.body).Decode(&networkResource) + return networkResource, err +} diff --git a/api/client/lib/request.go b/api/client/lib/request.go index 328b087a50..088a42628e 100644 --- a/api/client/lib/request.go +++ b/api/client/lib/request.go @@ -148,7 +148,11 @@ func encodeData(data interface{}) (*bytes.Buffer, error) { return params, nil } +<<<<<<< HEAD func ensureReaderClosed(response *ServerResponse) { +======= +func ensureReaderClosed(response *serverResponse) { +>>>>>>> 9c13063... Implement docker network with standalone client lib. if response != nil && response.body != nil { response.body.Close() } diff --git a/api/client/network.go b/api/client/network.go index 059a0faf56..c3442a77a6 100644 --- a/api/client/network.go +++ b/api/client/network.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net" - "net/http" "strings" "text/tabwriter" "text/template" @@ -76,12 +75,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 +90,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 +114,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 +127,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 +143,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 } @@ -225,31 +205,19 @@ func (cli *DockerCli) CmdNetworkInspect(args ...string) error { var networks []types.NetworkResource buf := new(bytes.Buffer) for _, name := range cmd.Args() { - obj, statusCode, err := readBody(cli.call("GET", "/networks/"+name, nil, nil)) + networkResource, err := cli.client.NetworkInspect(name) 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 + fmt.Fprintf(cli.err, "%s\n", err) + return Cli.StatusError{StatusCode: 1} } - 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} - } + fmt.Fprintf(cli.err, "%s\n", err) + return Cli.StatusError{StatusCode: 1} } buf.WriteString("\n") } From 55333e8f9018585f28f13231a3073e2746d7c969 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 11:38:36 -0500 Subject: [PATCH 19/53] Implement docker pause with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/pause.go | 8 ++++++++ api/client/pause.go | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 api/client/lib/pause.go diff --git a/api/client/lib/pause.go b/api/client/lib/pause.go new file mode 100644 index 0000000000..3247ec3a57 --- /dev/null +++ b/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/api/client/pause.go b/api/client/pause.go index e144a0bd55..76de3e3d59 100644 --- a/api/client/pause.go +++ b/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 { From eeee2eae8671ce05d863aadf289a0695ac62629b Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 11:42:33 -0500 Subject: [PATCH 20/53] Implement docker port with standalone client lib. Signed-off-by: David Calavera --- api/client/port.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/api/client/port.go b/api/client/port.go index a981bc5e63..ac673267ea 100644 --- a/api/client/port.go +++ b/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) From d05aa418b0466553a24d42896f99176cfa29765f Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 11:59:44 -0500 Subject: [PATCH 21/53] Implement docker ps with standanlone client lib. Signed-off-by: David Calavera --- api/client/lib/container_list.go | 66 ++++++++++++++++++++++++++++++++ api/client/ps.go | 50 +++++------------------- 2 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 api/client/lib/container_list.go diff --git a/api/client/lib/container_list.go b/api/client/lib/container_list.go new file mode 100644 index 0000000000..51451f6618 --- /dev/null +++ b/api/client/lib/container_list.go @@ -0,0 +1,66 @@ +package lib + +import ( + "encoding/json" + "net/url" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/parsers/filters" +) + +// 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 +} + +// ContainerList returns the list of containers in the docker host. +func (cli *Client) ContainerList(options ContainerListOptions) ([]types.Container, error) { + var 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/api/client/ps.go b/api/client/ps.go index 5bcf4bb2ee..7810cfeaaf 100644 --- a/api/client/ps.go +++ b/api/client/ps.go @@ -1,12 +1,8 @@ package client import ( - "encoding/json" - "net/url" - "strconv" - + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/client/ps" - "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" @@ -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 := lib.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 { From ac8fb77c7401d2d8cfd75bb8399b0f91484210ad Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 12:39:27 -0500 Subject: [PATCH 22/53] Implement container rename with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_rename.go | 12 ++++++++++++ api/client/rename.go | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 api/client/lib/container_rename.go diff --git a/api/client/lib/container_rename.go b/api/client/lib/container_rename.go new file mode 100644 index 0000000000..5cda44bc6a --- /dev/null +++ b/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 { + var query url.Values + query.Set("name", newContainerName) + resp, err := cli.POST("/containers/"+containerID+"/rename", query, nil, nil) + ensureReaderClosed(resp) + return err +} diff --git a/api/client/rename.go b/api/client/rename.go index 124ee1fc4c..a67d5d02db 100644 --- a/api/client/rename.go +++ b/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) } From b7de53634c3262997593d140c9b68683135b55e1 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 12:44:12 -0500 Subject: [PATCH 23/53] Implement docker restart with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_restart.go | 17 +++++++++++++++++ api/client/restart.go | 8 +------- 2 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 api/client/lib/container_restart.go diff --git a/api/client/lib/container_restart.go b/api/client/lib/container_restart.go new file mode 100644 index 0000000000..e9ff530438 --- /dev/null +++ b/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 { + var 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/api/client/restart.go b/api/client/restart.go index 92c612926e..2bad7633e9 100644 --- a/api/client/restart.go +++ b/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 { From fb6533e6cf1793b653ab0ea9f12b6b6876076d97 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 12:53:57 -0500 Subject: [PATCH 24/53] Implement docker remove with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_remove.go | 30 ++++++++++++++++++++++++++++++ api/client/rm.go | 24 +++++++++--------------- 2 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 api/client/lib/container_remove.go diff --git a/api/client/lib/container_remove.go b/api/client/lib/container_remove.go new file mode 100644 index 0000000000..3c2b0a24b4 --- /dev/null +++ b/api/client/lib/container_remove.go @@ -0,0 +1,30 @@ +package lib + +import "net/url" + +// ContainerRemoveOptions holds parameters to remove containers. +type ContainerRemoveOptions struct { + ContainerID string + RemoveVolumes bool + RemoveLinks bool + Force bool +} + +// ContainerRemove kills and removes a container from the docker host. +func (cli *Client) ContainerRemove(options ContainerRemoveOptions) error { + var 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/api/client/rm.go b/api/client/rm.go index cf88c765d3..cf92e889b7 100644 --- a/api/client/rm.go +++ b/api/client/rm.go @@ -2,9 +2,9 @@ package client import ( "fmt" - "net/url" "strings" + "github.com/docker/docker/api/client/lib" 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 := lib.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 { From 37d6fee8cfddd8e1afabbdaa232fa64a36494579 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:13:43 -0500 Subject: [PATCH 25/53] Implement docker rmi with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/image_remove.go | 37 ++++++++++++++++++++++++++++++++++ api/client/rmi.go | 20 ++++++++---------- 2 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 api/client/lib/image_remove.go diff --git a/api/client/lib/image_remove.go b/api/client/lib/image_remove.go new file mode 100644 index 0000000000..00d1bada83 --- /dev/null +++ b/api/client/lib/image_remove.go @@ -0,0 +1,37 @@ +package lib + +import ( + "encoding/json" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageRemoveOptions holds parameters to remove images. +type ImageRemoveOptions struct { + ImageID string + Force bool + PruneChildren bool +} + +// ImageRemove removes an image from the docker host. +func (cli *Client) ImageRemove(options ImageRemoveOptions) ([]types.ImageDelete, error) { + var 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/api/client/rmi.go b/api/client/rmi.go index b4e3700684..a618031831 100644 --- a/api/client/rmi.go +++ b/api/client/rmi.go @@ -1,11 +1,10 @@ package client import ( - "encoding/json" "fmt" "net/url" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/lib" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -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 := lib.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) From 373f55eecd39625f9f2751d8f940e83a056b6261 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:14:20 -0500 Subject: [PATCH 26/53] Implement docker save with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/image_save.go | 20 ++++++++++++++++++++ api/client/save.go | 18 ++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 api/client/lib/image_save.go diff --git a/api/client/lib/image_save.go b/api/client/lib/image_save.go new file mode 100644 index 0000000000..07d96eb8d3 --- /dev/null +++ b/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/api/client/save.go b/api/client/save.go index f14f9a450c..e7e0166d6f 100644 --- a/api/client/save.go +++ b/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 } From 9ec1cf92f5fda4b5bae02408b469cf8f65c96f9a Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:38:31 -0500 Subject: [PATCH 27/53] Implement docker stop with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_stop.go | 16 ++++++++++++++++ api/client/stop.go | 8 +------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 api/client/lib/container_stop.go diff --git a/api/client/lib/container_stop.go b/api/client/lib/container_stop.go new file mode 100644 index 0000000000..ea9a3d8ab0 --- /dev/null +++ b/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 { + var 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/api/client/stop.go b/api/client/stop.go index 91f5e65b0e..d5da5c64a6 100644 --- a/api/client/stop.go +++ b/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 { From 21ffdf0e0e402cfc00f2bb1476fb2a8510418ec0 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:45:51 -0500 Subject: [PATCH 28/53] Implement docker tag with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/image_tag.go | 25 +++++++++++++++++++++++++ api/client/tag.go | 17 +++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 api/client/lib/image_tag.go diff --git a/api/client/lib/image_tag.go b/api/client/lib/image_tag.go new file mode 100644 index 0000000000..6ef5269cb0 --- /dev/null +++ b/api/client/lib/image_tag.go @@ -0,0 +1,25 @@ +package lib + +import "net/url" + +// ImageTagOptions hold parameters to tag an image +type ImageTagOptions struct { + ImageID string + RepositoryName string + Tag string + Force bool +} + +// 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/api/client/tag.go b/api/client/tag.go index a4a27cb4cd..27bf80cff8 100644 --- a/api/client/tag.go +++ b/api/client/tag.go @@ -2,9 +2,9 @@ package client import ( "errors" - "net/url" "github.com/docker/distribution/reference" + "github.com/docker/docker/api/client/lib" 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 := lib.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) } From 7573c153c5708091177aca058a3cc84a1ab58377 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:52:40 -0500 Subject: [PATCH 29/53] Implement docker top with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_top.go | 29 +++++++++++++++++++++++++++++ api/client/top.go | 16 +++------------- 2 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 api/client/lib/container_top.go diff --git a/api/client/lib/container_top.go b/api/client/lib/container_top.go new file mode 100644 index 0000000000..c4ac649c1a --- /dev/null +++ b/api/client/lib/container_top.go @@ -0,0 +1,29 @@ +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 ( + query url.Values + response types.ContainerProcessList + ) + 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/api/client/top.go b/api/client/top.go index 8327820aae..90e2d7d369 100644 --- a/api/client/top.go +++ b/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")) From d78ce02f881785ac5f2014673b6ee587f74d9279 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:56:04 -0500 Subject: [PATCH 30/53] Implement trusted tagging with standalone client lib. Signed-off-by: David Calavera --- api/client/trust.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/client/trust.go b/api/client/trust.go index da6c3e5768..f41cf02c7c 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -22,6 +22,7 @@ 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/cliconfig" "github.com/docker/docker/pkg/ansiescape" "github.com/docker/docker/pkg/ioutils" @@ -250,16 +251,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 := lib.ImageTagOptions{ + ImageID: trustedRef.String(), + RepositoryName: trustedRef.Name(), + Tag: ref.Tag(), + Force: true, } - return nil + return cli.client.ImageTag(options) } func notaryError(err error) error { From b8be62e28eee322b791251f29595648f795efcb3 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 13:58:16 -0500 Subject: [PATCH 31/53] Implement docker unpause with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/container_unpause.go | 8 ++++++++ api/client/unpause.go | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 api/client/lib/container_unpause.go diff --git a/api/client/lib/container_unpause.go b/api/client/lib/container_unpause.go new file mode 100644 index 0000000000..3596bf8104 --- /dev/null +++ b/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/api/client/unpause.go b/api/client/unpause.go index 21e25857cf..13c80f2448 100644 --- a/api/client/unpause.go +++ b/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 { From 1fe912151b9239b891b2da704c6ddcc6081fad2d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 14:31:24 -0500 Subject: [PATCH 32/53] Implement docker version with standalone client lib. Use Go template definitions for default format. Signed-off-by: David Calavera --- api/client/lib/version.go | 50 +++++++++++++++++++++++++++++ api/client/version.go | 67 ++++++++++----------------------------- 2 files changed, 66 insertions(+), 51 deletions(-) create mode 100644 api/client/lib/version.go diff --git a/api/client/lib/version.go b/api/client/lib/version.go new file mode 100644 index 0000000000..0164a05007 --- /dev/null +++ b/api/client/lib/version.go @@ -0,0 +1,50 @@ +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" +) + +// VersionResponse holds version information for the client and the server +type VersionResponse struct { + Client *types.Version + Server *types.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 +} + +// SystemVersion returns information of the docker client and server host. +func (cli *Client) SystemVersion() (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 VersionResponse{Client: client}, err + } + defer ensureReaderClosed(resp) + + var server types.Version + err = json.NewDecoder(resp.body).Decode(&server) + if err != nil { + return VersionResponse{Client: client}, err + } + return types.VersionResponse{Client: client, Server: &server}, nil +} diff --git a/api/client/version.go b/api/client/version.go index 188d56684e..2f28feca3d 100644 --- a/api/client/version.go +++ b/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 } From 51efb1480a58b2317e1ad1833964ccad4456e6be Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 14:38:15 -0500 Subject: [PATCH 33/53] Implement docker wait with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/wait.go | 24 ++++++++++++++++++++++++ api/client/run.go | 2 +- api/client/utils.go | 16 ---------------- api/client/wait.go | 2 +- 4 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 api/client/lib/wait.go diff --git a/api/client/lib/wait.go b/api/client/lib/wait.go new file mode 100644 index 0000000000..70993d84aa --- /dev/null +++ b/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/api/client/run.go b/api/client/run.go index 1ae5d67d7e..8eb463141a 100644 --- a/api/client/run.go +++ b/api/client/run.go @@ -272,7 +272,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/api/client/utils.go b/api/client/utils.go index 7a8a785844..6ccb93d6c3 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -260,22 +260,6 @@ func (cli *DockerCli) resizeTty(id string, isExec bool) { } } -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) { diff --git a/api/client/wait.go b/api/client/wait.go index 3b03f7069e..bd64b83ca2 100644 --- a/api/client/wait.go +++ b/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) From 7df71ca31dad5eb48093879730b4761b2109291e Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 14:49:22 -0500 Subject: [PATCH 34/53] Implement getExitCode with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/errors.go | 7 ++++++- api/client/lib/request.go | 6 +----- api/client/utils.go | 13 +++---------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/api/client/lib/errors.go b/api/client/lib/errors.go index 3ee56e6333..7a1f49bb1b 100644 --- a/api/client/lib/errors.go +++ b/api/client/lib/errors.go @@ -1,6 +1,11 @@ package lib -import "fmt" +import ( + "errors" + "fmt" +) + +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 { diff --git a/api/client/lib/request.go b/api/client/lib/request.go index 088a42628e..685e5eb3d9 100644 --- a/api/client/lib/request.go +++ b/api/client/lib/request.go @@ -109,7 +109,7 @@ func (cli *Client) sendClientRequest(method, path string, query url.Values, in i if err != nil { if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { - return serverResp, errConnectionFailed + return serverResp, ErrConnectionFailed } if cli.Scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") { @@ -148,11 +148,7 @@ func encodeData(data interface{}) (*bytes.Buffer, error) { return params, nil } -<<<<<<< HEAD func ensureReaderClosed(response *ServerResponse) { -======= -func ensureReaderClosed(response *serverResponse) { ->>>>>>> 9c13063... Implement docker network with standalone client lib. if response != nil && response.body != nil { response.body.Close() } diff --git a/api/client/utils.go b/api/client/utils.go index 6ccb93d6c3..0de171db1e 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -19,7 +19,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/jsonmessage" @@ -263,22 +263,15 @@ func (cli *DockerCli) resizeTty(id string, isExec bool) { // 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 } From 8c9c9e137c5c7a84ed4ac4459c52147daf8a2a80 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 14:57:22 -0500 Subject: [PATCH 35/53] Use ensureReaderClosed consistently to close a response body reader. Signed-off-by: David Calavera --- api/client/lib/container_commit.go | 3 +-- api/client/lib/container_create.go | 2 ++ api/client/lib/container_inspect.go | 2 +- api/client/lib/copy.go | 1 + api/client/lib/diff.go | 2 +- api/client/lib/history.go | 2 +- api/client/lib/image_list.go | 2 +- api/client/lib/info.go | 2 +- api/client/lib/kill.go | 3 ++- api/client/lib/login.go | 2 +- 10 files changed, 12 insertions(+), 9 deletions(-) diff --git a/api/client/lib/container_commit.go b/api/client/lib/container_commit.go index 9dc3440282..c097e65902 100644 --- a/api/client/lib/container_commit.go +++ b/api/client/lib/container_commit.go @@ -51,8 +51,7 @@ func (cli *Client) ContainerCommit(options types.ContainerCommitOptions) (types. if err != nil { return response, err } - - defer resp.body.Close() + defer ensureReaderClosed(resp) if err := json.NewDecoder(resp.body).Decode(&response); err != nil { return response, err diff --git a/api/client/lib/container_create.go b/api/client/lib/container_create.go index 906307f095..26a85e3b4b 100644 --- a/api/client/lib/container_create.go +++ b/api/client/lib/container_create.go @@ -29,11 +29,13 @@ func (cli *Client) ContainerCreate(config *runconfig.ContainerConfigWrapper, con } 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 diff --git a/api/client/lib/container_inspect.go b/api/client/lib/container_inspect.go index 73863aafee..038c11ef1c 100644 --- a/api/client/lib/container_inspect.go +++ b/api/client/lib/container_inspect.go @@ -12,7 +12,7 @@ func (cli *Client) ContainerInspect(containerID string) (types.ContainerJSON, er if err != nil { return types.ContainerJSON{}, err } - defer serverResp.body.Close() + defer ensureReaderClosed(serverResp) var response types.ContainerJSON json.NewDecoder(serverResp.body).Decode(&response) diff --git a/api/client/lib/copy.go b/api/client/lib/copy.go index ed982220b6..c1b8593a98 100644 --- a/api/client/lib/copy.go +++ b/api/client/lib/copy.go @@ -51,6 +51,7 @@ func (cli *Client) CopyToContainer(options CopyToContainerOptions) error { if err != nil { return err } + defer ensureReaderClosed(response) if response.statusCode != http.StatusOK { return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) diff --git a/api/client/lib/diff.go b/api/client/lib/diff.go index f0d027afc1..bf3c0ee838 100644 --- a/api/client/lib/diff.go +++ b/api/client/lib/diff.go @@ -15,7 +15,7 @@ func (cli *Client) ContainerDiff(containerID string) ([]types.ContainerChange, e if err != nil { return changes, err } - defer serverResp.body.Close() + defer ensureReaderClosed(serverResp) if err := json.NewDecoder(serverResp.body).Decode(&changes); err != nil { return changes, err diff --git a/api/client/lib/history.go b/api/client/lib/history.go index 29064c65fb..62f96e6202 100644 --- a/api/client/lib/history.go +++ b/api/client/lib/history.go @@ -14,7 +14,7 @@ func (cli *Client) ImageHistory(imageID string) ([]types.ImageHistory, error) { if err != nil { return history, err } - defer serverResp.body.Close() + defer ensureReaderClosed(serverResp) if err := json.NewDecoder(serverResp.body).Decode(&history); err != nil { return history, err diff --git a/api/client/lib/image_list.go b/api/client/lib/image_list.go index b86c3e944a..e34cc0026f 100644 --- a/api/client/lib/image_list.go +++ b/api/client/lib/image_list.go @@ -41,7 +41,7 @@ func (cli *Client) ImageList(options ImageListOptions) ([]types.Image, error) { if err != nil { return images, err } - defer serverResp.body.Close() + defer ensureReaderClosed(serverResp) err = json.NewDecoder(serverResp.body).Decode(&images) return images, err diff --git a/api/client/lib/info.go b/api/client/lib/info.go index 4b4bd048a0..b9a0fabf7b 100644 --- a/api/client/lib/info.go +++ b/api/client/lib/info.go @@ -15,7 +15,7 @@ func (cli *Client) Info() (types.Info, error) { if err != nil { return info, err } - defer serverResp.body.Close() + defer ensureReaderClosed(serverResp) if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil { return info, fmt.Errorf("Error reading remote info: %v", err) diff --git a/api/client/lib/kill.go b/api/client/lib/kill.go index 40ce9d383f..3e80d70ada 100644 --- a/api/client/lib/kill.go +++ b/api/client/lib/kill.go @@ -7,6 +7,7 @@ func (cli *Client) ContainerKill(containerID, signal string) error { var query url.Values query.Set("signal", signal) - _, err := cli.POST("/containers/"+containerID+"/kill", query, nil, nil) + resp, err := cli.POST("/containers/"+containerID+"/kill", query, nil, nil) + ensureReaderClosed(resp) return err } diff --git a/api/client/lib/login.go b/api/client/lib/login.go index ca194150ed..77399ce985 100644 --- a/api/client/lib/login.go +++ b/api/client/lib/login.go @@ -20,7 +20,7 @@ func (cli *Client) RegistryLogin(auth cliconfig.AuthConfig) (types.AuthResponse, if err != nil { return types.AuthResponse{}, err } - defer resp.body.Close() + defer ensureReaderClosed(resp) var response types.AuthResponse err = json.NewDecoder(resp.body).Decode(&response) From 73bca058ae5d1e4ad2b94835fb1593ba3ca3983b Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 15:17:23 -0500 Subject: [PATCH 36/53] Implement docker volume with standalone client lib. Signed-off-by: David Calavera --- api/client/lib/volume.go | 62 ++++++++++++++++++++++++++++++++++++++++ api/client/volume.go | 46 ++++------------------------- 2 files changed, 68 insertions(+), 40 deletions(-) create mode 100644 api/client/lib/volume.go diff --git a/api/client/lib/volume.go b/api/client/lib/volume.go new file mode 100644 index 0000000000..636b9559fb --- /dev/null +++ b/api/client/lib/volume.go @@ -0,0 +1,62 @@ +package lib + +import ( + "encoding/json" + "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 + var 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 { + 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/api/client/volume.go b/api/client/volume.go index 6943db17d3..2fbc201e74 100644 --- a/api/client/volume.go +++ b/api/client/volume.go @@ -5,8 +5,6 @@ import ( "encoding/json" "fmt" "io" - "net/http" - "net/url" "text/tabwriter" "text/template" @@ -64,25 +62,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") @@ -127,18 +111,8 @@ func (cli *DockerCli) CmdVolumeInspect(args ...string) error { var volumes []*types.Volume for _, name := range cmd.Args() { - resp, err := cli.call("GET", "/volumes/"+name, nil, nil) + volume, err := cli.client.VolumeInspect(name) 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 @@ -192,24 +166,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 +191,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 From a413be33929a637d565687aa07805bfefca547e5 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 15:29:58 -0500 Subject: [PATCH 37/53] Fix client lib errors documentation. Signed-off-by: David Calavera --- api/client/lib/errors.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/client/lib/errors.go b/api/client/lib/errors.go index 7a1f49bb1b..dba6d381af 100644 --- a/api/client/lib/errors.go +++ b/api/client/lib/errors.go @@ -5,6 +5,7 @@ import ( "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. @@ -17,7 +18,7 @@ func (i imageNotFoundError) Error() string { return fmt.Sprintf("Image not found: %s", i.imageID) } -// IsImageNotFound returns true if the error is caused +// 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) @@ -34,7 +35,7 @@ func (u unauthorizedError) Error() string { return u.cause.Error() } -// IsUnauthorized returns true if the error is caused +// IsErrUnauthorized returns true if the error is caused // when an the remote registry authentication fails func IsErrUnauthorized(err error) bool { _, ok := err.(unauthorizedError) From 8b15839ee85b291266d07f97d9ad6ca0326d1339 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 17:02:06 -0500 Subject: [PATCH 38/53] Create interface that clients that talk to the api must fulfill. Signed-off-by: David Calavera --- api/client/build.go | 4 +- api/client/cli.go | 2 +- api/client/client.go | 55 ++++++++++ api/client/commit.go | 4 +- api/client/cp.go | 5 +- api/client/create.go | 4 +- api/client/events.go | 4 +- api/client/images.go | 4 +- api/client/import.go | 6 +- api/client/lib/container_commit.go | 12 --- api/client/lib/container_list.go | 14 +-- api/client/lib/container_remove.go | 14 +-- api/client/lib/copy.go | 15 +-- api/client/lib/events.go | 10 +- api/client/lib/image_build.go | 51 ++------- api/client/lib/image_create.go | 16 +-- api/client/lib/image_import.go | 22 +--- api/client/lib/image_list.go | 9 +- api/client/lib/image_remove.go | 9 +- api/client/lib/image_tag.go | 12 +-- api/client/lib/logs.go | 14 +-- api/client/lib/version.go | 18 +--- api/client/logs.go | 4 +- api/client/ps.go | 4 +- api/client/rm.go | 4 +- api/client/rmi.go | 4 +- api/client/tag.go | 4 +- api/client/trust.go | 4 +- api/types/client.go | 163 +++++++++++++++++++++++++++++ 29 files changed, 283 insertions(+), 208 deletions(-) create mode 100644 api/types/client.go diff --git a/api/client/build.go b/api/client/build.go index 6268464c72..8af15b2465 100644 --- a/api/client/build.go +++ b/api/client/build.go @@ -15,7 +15,7 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/docker/api" - "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/archive" @@ -207,7 +207,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { remoteContext = cmd.Arg(0) } - options := lib.ImageBuildOptions{ + options := types.ImageBuildOptions{ Context: body, Memory: memory, MemorySwap: memorySwap, diff --git a/api/client/cli.go b/api/client/cli.go index 7848fd9228..d7cf4f56b3 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -43,7 +43,7 @@ type DockerCli struct { // isTerminalOut indicates whether the client's STDOUT is a TTY isTerminalOut bool // client is the http client that performs all API operations - client *lib.Client + client apiClient // DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE // TODO: Remove diff --git a/api/client/client.go b/api/client/client.go index 4cfce5f684..8f9efa8374 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -3,3 +3,58 @@ // 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/types" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/runconfig" +) + +// apiClient is an interface that clients that talk with a docker server must implement. +type apiClient interface { + ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) + ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error) + ContainerDiff(containerID string) ([]types.ContainerChange, error) + ContainerExport(containerID string) (io.ReadCloser, error) + ContainerInspect(containerID string) (types.ContainerJSON, 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 + ContainerRestart(containerID string, timeout int) error + ContainerStatPath(containerID, path string) (types.ContainerPathStat, 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) + ImageList(options types.ImageListOptions) ([]types.Image, error) + ImageLoad(input io.Reader) (io.ReadCloser, error) + ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, 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/api/client/commit.go b/api/client/commit.go index fb9cfe2820..100a985f5c 100644 --- a/api/client/commit.go +++ b/api/client/commit.go @@ -5,7 +5,7 @@ import ( "fmt" "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/opts" flag "github.com/docker/docker/pkg/mflag" @@ -56,7 +56,7 @@ func (cli *DockerCli) CmdCommit(args ...string) error { } } - options := lib.ContainerCommitOptions{ + options := types.ContainerCommitOptions{ ContainerID: name, RepositoryName: repositoryName, Tag: tag, diff --git a/api/client/cp.go b/api/client/cp.go index 16e2cf471e..ab92689710 100644 --- a/api/client/cp.go +++ b/api/client/cp.go @@ -7,7 +7,6 @@ import ( "path/filepath" "strings" - "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/pkg/archive" @@ -126,7 +125,7 @@ func splitCpArg(arg string) (container, path string) { } func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) { - return cli.client.StatContainerPath(containerName, path) + return cli.client.ContainerStatPath(containerName, path) } func resolveLocalPath(localPath string) (absPath string, err error) { @@ -286,7 +285,7 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpP content = preparedArchive } - options := lib.CopyToContainerOptions{ + options := types.CopyToContainerOptions{ ContainerID: dstContainer, Path: resolvedDstPath, Content: content, diff --git a/api/client/create.go b/api/client/create.go index 25189fa14a..a5430eb3cb 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -51,13 +51,13 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { return err } - options := lib.CreateImageOptions{ + options := types.ImageCreateOptions{ Parent: ref.Name(), Tag: tag, RegistryAuth: base64.URLEncoding.EncodeToString(buf), } - responseBody, err := cli.client.CreateImage(options) + responseBody, err := cli.client.ImageCreate(options) if err != nil { return err } diff --git a/api/client/events.go b/api/client/events.go index 568be41fad..a6702e130d 100644 --- a/api/client/events.go +++ b/api/client/events.go @@ -1,7 +1,7 @@ package client import ( - "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/jsonmessage" @@ -34,7 +34,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error { } } - options := lib.EventsOptions{ + options := types.EventsOptions{ Since: *since, Until: *until, Filters: eventFilterArgs, diff --git a/api/client/images.go b/api/client/images.go index 02c59fdf42..895f1b0d56 100644 --- a/api/client/images.go +++ b/api/client/images.go @@ -7,7 +7,7 @@ import ( "time" "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/opts" flag "github.com/docker/docker/pkg/mflag" @@ -48,7 +48,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { matchName = cmd.Arg(0) } - options := lib.ImageListOptions{ + options := types.ImageListOptions{ MatchName: matchName, All: *all, Filters: imageFilterArgs, diff --git a/api/client/import.go b/api/client/import.go index 1a5887a020..2e9be55f57 100644 --- a/api/client/import.go +++ b/api/client/import.go @@ -6,7 +6,7 @@ import ( "os" "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/opts" "github.com/docker/docker/pkg/jsonmessage" @@ -67,7 +67,7 @@ func (cli *DockerCli) CmdImport(args ...string) error { } - options := lib.ImportImageOptions{ + options := types.ImageImportOptions{ Source: in, SourceName: srcName, RepositoryName: repository, @@ -76,7 +76,7 @@ func (cli *DockerCli) CmdImport(args ...string) error { Changes: changes, } - responseBody, err := cli.client.ImportImage(options) + responseBody, err := cli.client.ImageImport(options) if err != nil { return err } diff --git a/api/client/lib/container_commit.go b/api/client/lib/container_commit.go index c097e65902..308add49f0 100644 --- a/api/client/lib/container_commit.go +++ b/api/client/lib/container_commit.go @@ -8,18 +8,6 @@ import ( "github.com/docker/docker/runconfig" ) -// ContainerCommitOptions hods parameters to commit changes into a container. -type ContainerCommitOptions struct { - ContainerID string - RepositoryName string - Tag string - Comment string - Author string - Changes []string - Pause bool - JSONConfig string -} - // 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{} diff --git a/api/client/lib/container_list.go b/api/client/lib/container_list.go index 51451f6618..fbb91a6e6b 100644 --- a/api/client/lib/container_list.go +++ b/api/client/lib/container_list.go @@ -9,20 +9,8 @@ import ( "github.com/docker/docker/pkg/parsers/filters" ) -// 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 -} - // ContainerList returns the list of containers in the docker host. -func (cli *Client) ContainerList(options ContainerListOptions) ([]types.Container, error) { +func (cli *Client) ContainerList(options types.ContainerListOptions) ([]types.Container, error) { var query url.Values if options.All { diff --git a/api/client/lib/container_remove.go b/api/client/lib/container_remove.go index 3c2b0a24b4..68072a5c96 100644 --- a/api/client/lib/container_remove.go +++ b/api/client/lib/container_remove.go @@ -1,17 +1,13 @@ package lib -import "net/url" +import ( + "net/url" -// ContainerRemoveOptions holds parameters to remove containers. -type ContainerRemoveOptions struct { - ContainerID string - RemoveVolumes bool - RemoveLinks bool - Force bool -} + "github.com/docker/docker/api/types" +) // ContainerRemove kills and removes a container from the docker host. -func (cli *Client) ContainerRemove(options ContainerRemoveOptions) error { +func (cli *Client) ContainerRemove(options types.ContainerRemoveOptions) error { var query url.Values if options.RemoveVolumes { query.Set("v", "1") diff --git a/api/client/lib/copy.go b/api/client/lib/copy.go index c1b8593a98..458896e69f 100644 --- a/api/client/lib/copy.go +++ b/api/client/lib/copy.go @@ -13,17 +13,8 @@ import ( "github.com/docker/docker/api/types" ) -// CopyToContainerOptions holds information -// about files to copy into a container -type CopyToContainerOptions struct { - ContainerID string - Path string - Content io.Reader - AllowOverwriteDirWithFile bool -} - -// StatContainerPath returns Stat information about a path inside the container filesystem. -func (cli *Client) StatContainerPath(containerID, path string) (types.ContainerPathStat, error) { +// ContainerStatPath returns Stat information about a path inside the container filesystem. +func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerPathStat, error) { query := make(url.Values, 1) query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. @@ -37,7 +28,7 @@ func (cli *Client) StatContainerPath(containerID, path string) (types.ContainerP } // CopyToContainer copies content into the container filesystem. -func (cli *Client) CopyToContainer(options CopyToContainerOptions) error { +func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error { var 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. diff --git a/api/client/lib/events.go b/api/client/lib/events.go index 1d50cb5f3e..ff514af509 100644 --- a/api/client/lib/events.go +++ b/api/client/lib/events.go @@ -5,20 +5,14 @@ import ( "net/url" "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/parsers/filters" "github.com/docker/docker/pkg/timeutils" ) -// EventsOptions hold parameters to filter events with. -type EventsOptions struct { - Since string - Until string - Filters filters.Args -} - // 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 EventsOptions) (io.ReadCloser, error) { +func (cli *Client) Events(options types.EventsOptions) (io.ReadCloser, error) { var query url.Values ref := time.Now() diff --git a/api/client/lib/image_build.go b/api/client/lib/image_build.go index 0b89b05ff6..1f3c1f8222 100644 --- a/api/client/lib/image_build.go +++ b/api/client/lib/image_build.go @@ -3,73 +3,36 @@ package lib import ( "encoding/base64" "encoding/json" - "io" "net/http" "net/url" "strconv" - "github.com/docker/docker/cliconfig" + "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/httputils" - "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/pkg/units" "github.com/docker/docker/runconfig" ) -// 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 -} - // 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 ImageBuildOptions) (ImageBuildResponse, error) { +func (cli *Client) ImageBuild(options types.ImageBuildOptions) (types.ImageBuildResponse, error) { query, err := imageBuildOptionsToQuery(options) if err != nil { - return ImageBuildResponse{}, err + return types.ImageBuildResponse{}, err } headers := http.Header(make(map[string][]string)) buf, err := json.Marshal(options.AuthConfigs) if err != nil { - return ImageBuildResponse{}, err + 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 ImageBuildResponse{}, err + return types.ImageBuildResponse{}, err } var osType string @@ -77,13 +40,13 @@ func (cli *Client) ImageBuild(options ImageBuildOptions) (ImageBuildResponse, er osType = h.OS } - return ImageBuildResponse{ + return types.ImageBuildResponse{ Body: serverResp.body, OSType: osType, }, nil } -func imageBuildOptionsToQuery(options ImageBuildOptions) (url.Values, error) { +func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { query := url.Values{ "t": options.Tags, } diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go index d56bfa647c..f7e5bc97fb 100644 --- a/api/client/lib/image_create.go +++ b/api/client/lib/image_create.go @@ -3,21 +3,13 @@ package lib import ( "io" "net/url" + + "github.com/docker/docker/api/types" ) -// CreateImageOptions holds information to create images. -type CreateImageOptions 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 -} - -// CreateImage creates a new image based in the parent options. +// ImageCreate creates a new image based in the parent options. // It returns the JSON content in the response body. -func (cli *Client) CreateImage(options CreateImageOptions) (io.ReadCloser, error) { +func (cli *Client) ImageCreate(options types.ImageCreateOptions) (io.ReadCloser, error) { var query url.Values query.Set("fromImage", options.Parent) query.Set("tag", options.Tag) diff --git a/api/client/lib/image_import.go b/api/client/lib/image_import.go index ebc7130e0e..9495036469 100644 --- a/api/client/lib/image_import.go +++ b/api/client/lib/image_import.go @@ -3,27 +3,13 @@ package lib import ( "io" "net/url" + + "github.com/docker/docker/api/types" ) -// ImportImageOptions holds information to import images from the client host. -type ImportImageOptions 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 -} - -// ImportImage creates a new image based in the source options. +// ImageImport creates a new image based in the source options. // It returns the JSON content in the response body. -func (cli *Client) ImportImage(options ImportImageOptions) (io.ReadCloser, error) { +func (cli *Client) ImageImport(options types.ImageImportOptions) (io.ReadCloser, error) { var query url.Values query.Set("fromSrc", options.SourceName) query.Set("repo", options.RepositoryName) diff --git a/api/client/lib/image_list.go b/api/client/lib/image_list.go index e34cc0026f..5e27112944 100644 --- a/api/client/lib/image_list.go +++ b/api/client/lib/image_list.go @@ -8,15 +8,8 @@ import ( "github.com/docker/docker/pkg/parsers/filters" ) -// ImageListOptions holds parameters to filter the list of images with. -type ImageListOptions struct { - MatchName string - All bool - Filters filters.Args -} - // ImageList returns a list of images in the docker host. -func (cli *Client) ImageList(options ImageListOptions) ([]types.Image, error) { +func (cli *Client) ImageList(options types.ImageListOptions) ([]types.Image, error) { var ( images []types.Image query url.Values diff --git a/api/client/lib/image_remove.go b/api/client/lib/image_remove.go index 00d1bada83..ebf352dbb8 100644 --- a/api/client/lib/image_remove.go +++ b/api/client/lib/image_remove.go @@ -7,15 +7,8 @@ import ( "github.com/docker/docker/api/types" ) -// ImageRemoveOptions holds parameters to remove images. -type ImageRemoveOptions struct { - ImageID string - Force bool - PruneChildren bool -} - // ImageRemove removes an image from the docker host. -func (cli *Client) ImageRemove(options ImageRemoveOptions) ([]types.ImageDelete, error) { +func (cli *Client) ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, error) { var query url.Values if options.Force { diff --git a/api/client/lib/image_tag.go b/api/client/lib/image_tag.go index 6ef5269cb0..23097501ed 100644 --- a/api/client/lib/image_tag.go +++ b/api/client/lib/image_tag.go @@ -1,14 +1,10 @@ package lib -import "net/url" +import ( + "net/url" -// ImageTagOptions hold parameters to tag an image -type ImageTagOptions struct { - ImageID string - RepositoryName string - Tag string - Force bool -} + "github.com/docker/docker/api/types" +) // ImageTag tags an image in the docker host func (cli *Client) ImageTag(options types.ImageTagOptions) error { diff --git a/api/client/lib/logs.go b/api/client/lib/logs.go index a464de7c0b..677d29124d 100644 --- a/api/client/lib/logs.go +++ b/api/client/lib/logs.go @@ -5,23 +5,13 @@ import ( "net/url" "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/timeutils" ) -// ContainerLogsOptions holds parameters to filter logs with. -type ContainerLogsOptions struct { - ContainerID string - ShowStdout bool - ShowStderr bool - Since string - Timestamps bool - Follow bool - Tail string -} - // 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 ContainerLogsOptions) (io.ReadCloser, error) { +func (cli *Client) ContainerLogs(options types.ContainerLogsOptions) (io.ReadCloser, error) { var query url.Values if options.ShowStdout { query.Set("stdout", "1") diff --git a/api/client/lib/version.go b/api/client/lib/version.go index 0164a05007..b460e4b886 100644 --- a/api/client/lib/version.go +++ b/api/client/lib/version.go @@ -10,20 +10,8 @@ import ( "github.com/docker/docker/utils" ) -// VersionResponse holds version information for the client and the server -type VersionResponse struct { - Client *types.Version - Server *types.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 -} - // SystemVersion returns information of the docker client and server host. -func (cli *Client) SystemVersion() (VersionResponse, error) { +func (cli *Client) SystemVersion() (types.VersionResponse, error) { client := &types.Version{ Version: dockerversion.Version, APIVersion: api.Version, @@ -37,14 +25,14 @@ func (cli *Client) SystemVersion() (VersionResponse, error) { resp, err := cli.GET("/version", nil, nil) if err != nil { - return VersionResponse{Client: client}, err + return types.VersionResponse{Client: client}, err } defer ensureReaderClosed(resp) var server types.Version err = json.NewDecoder(resp.body).Decode(&server) if err != nil { - return VersionResponse{Client: client}, err + return types.VersionResponse{Client: client}, err } return types.VersionResponse{Client: client, Server: &server}, nil } diff --git a/api/client/logs.go b/api/client/logs.go index 52465683a0..77c754f4da 100644 --- a/api/client/logs.go +++ b/api/client/logs.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/stdcopy" @@ -39,7 +39,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error { return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type) } - options := lib.ContainerLogsOptions{ + options := types.ContainerLogsOptions{ ContainerID: name, ShowStdout: true, ShowStderr: true, diff --git a/api/client/ps.go b/api/client/ps.go index 7810cfeaaf..a39bc5c0a9 100644 --- a/api/client/ps.go +++ b/api/client/ps.go @@ -1,8 +1,8 @@ package client import ( - "github.com/docker/docker/api/client/lib" "github.com/docker/docker/api/client/ps" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" @@ -47,7 +47,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { } } - options := lib.ContainerListOptions{ + options := types.ContainerListOptions{ All: *all, Limit: *last, Since: *since, diff --git a/api/client/rm.go b/api/client/rm.go index cf92e889b7..b26cb53d02 100644 --- a/api/client/rm.go +++ b/api/client/rm.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -28,7 +28,7 @@ func (cli *DockerCli) CmdRm(args ...string) error { } name = strings.Trim(name, "/") - options := lib.ContainerRemoveOptions{ + options := types.ContainerRemoveOptions{ ContainerID: name, RemoveVolumes: *v, RemoveLinks: *link, diff --git a/api/client/rmi.go b/api/client/rmi.go index a618031831..9569370ea2 100644 --- a/api/client/rmi.go +++ b/api/client/rmi.go @@ -4,7 +4,7 @@ import ( "fmt" "net/url" - "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -30,7 +30,7 @@ func (cli *DockerCli) CmdRmi(args ...string) error { var errNames []string for _, name := range cmd.Args() { - options := lib.ImageRemoveOptions{ + options := types.ImageRemoveOptions{ ImageID: name, Force: *force, PruneChildren: !*noprune, diff --git a/api/client/tag.go b/api/client/tag.go index 27bf80cff8..385a155c58 100644 --- a/api/client/tag.go +++ b/api/client/tag.go @@ -4,7 +4,7 @@ import ( "errors" "github.com/docker/distribution/reference" - "github.com/docker/docker/api/client/lib" + "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" @@ -41,7 +41,7 @@ func (cli *DockerCli) CmdTag(args ...string) error { return err } - options := lib.ImageTagOptions{ + options := types.ImageTagOptions{ ImageID: cmd.Arg(0), RepositoryName: ref.Name(), Tag: tag, diff --git a/api/client/trust.go b/api/client/trust.go index f41cf02c7c..4a550a381a 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -22,7 +22,7 @@ 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" @@ -252,7 +252,7 @@ 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()) - options := lib.ImageTagOptions{ + options := types.ImageTagOptions{ ImageID: trustedRef.String(), RepositoryName: trustedRef.Name(), Tag: ref.Tag(), diff --git a/api/types/client.go b/api/types/client.go new file mode 100644 index 0000000000..b528999f25 --- /dev/null +++ b/api/types/client.go @@ -0,0 +1,163 @@ +package types + +import ( + "io" + + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/pkg/parsers/filters" + "github.com/docker/docker/pkg/ulimit" +) + +// ContainerCommitOptions hods parameters to commit changes into a container. +type ContainerCommitOptions struct { + ContainerID string + RepositoryName string + Tag string + Comment string + Author string + Changes []string + Pause bool + JSONConfig string +} + +// 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 +} + +// 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 +} + +// ImageRemoveOptions holds parameters to remove images. +type ImageRemoveOptions struct { + ImageID string + Force bool + PruneChildren bool +} + +// ImageTagOptions hold parameters to tag an image +type ImageTagOptions struct { + ImageID string + RepositoryName string + Tag string + Force bool +} + +// 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 +} From 0b0431a856fd9087b15665790cb8cda9e4286eff Mon Sep 17 00:00:00 2001 From: David Calavera Date: Fri, 4 Dec 2015 21:06:12 -0500 Subject: [PATCH 39/53] Change references to query values. Signed-off-by: David Calavera --- api/client/lib/container_create.go | 6 ++---- api/client/lib/container_list.go | 2 +- api/client/lib/container_remove.go | 2 +- api/client/lib/container_rename.go | 2 +- api/client/lib/container_restart.go | 2 +- api/client/lib/container_stop.go | 2 +- api/client/lib/container_top.go | 6 ++---- api/client/lib/copy.go | 4 ++-- api/client/lib/events.go | 2 +- api/client/lib/image_create.go | 2 +- api/client/lib/image_import.go | 2 +- api/client/lib/image_list.go | 7 ++----- api/client/lib/image_remove.go | 2 +- api/client/lib/kill.go | 2 +- api/client/lib/logs.go | 2 +- api/client/lib/volume.go | 2 +- 16 files changed, 20 insertions(+), 27 deletions(-) diff --git a/api/client/lib/container_create.go b/api/client/lib/container_create.go index 26a85e3b4b..73626677a5 100644 --- a/api/client/lib/container_create.go +++ b/api/client/lib/container_create.go @@ -12,10 +12,8 @@ import ( // 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 ( - query url.Values - response types.ContainerCreateResponse - ) + var response types.ContainerCreateResponse + query := url.Values{} if containerName != "" { query.Set("name", containerName) } diff --git a/api/client/lib/container_list.go b/api/client/lib/container_list.go index fbb91a6e6b..e1d9a32c1a 100644 --- a/api/client/lib/container_list.go +++ b/api/client/lib/container_list.go @@ -11,7 +11,7 @@ import ( // ContainerList returns the list of containers in the docker host. func (cli *Client) ContainerList(options types.ContainerListOptions) ([]types.Container, error) { - var query url.Values + query := url.Values{} if options.All { query.Set("all", "1") diff --git a/api/client/lib/container_remove.go b/api/client/lib/container_remove.go index 68072a5c96..1fe80d054f 100644 --- a/api/client/lib/container_remove.go +++ b/api/client/lib/container_remove.go @@ -8,7 +8,7 @@ import ( // ContainerRemove kills and removes a container from the docker host. func (cli *Client) ContainerRemove(options types.ContainerRemoveOptions) error { - var query url.Values + query := url.Values{} if options.RemoveVolumes { query.Set("v", "1") } diff --git a/api/client/lib/container_rename.go b/api/client/lib/container_rename.go index 5cda44bc6a..db7e878076 100644 --- a/api/client/lib/container_rename.go +++ b/api/client/lib/container_rename.go @@ -4,7 +4,7 @@ import "net/url" // ContainerRename changes the name of a given container. func (cli *Client) ContainerRename(containerID, newContainerName string) error { - var query url.Values + query := url.Values{} query.Set("name", newContainerName) resp, err := cli.POST("/containers/"+containerID+"/rename", query, nil, nil) ensureReaderClosed(resp) diff --git a/api/client/lib/container_restart.go b/api/client/lib/container_restart.go index e9ff530438..b9eab92d35 100644 --- a/api/client/lib/container_restart.go +++ b/api/client/lib/container_restart.go @@ -9,7 +9,7 @@ import ( // 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 { - var query url.Values + query := url.Values{} query.Set("t", strconv.Itoa(timeout)) resp, err := cli.POST("/containers"+containerID+"/restart", query, nil, nil) ensureReaderClosed(resp) diff --git a/api/client/lib/container_stop.go b/api/client/lib/container_stop.go index ea9a3d8ab0..ced540f13b 100644 --- a/api/client/lib/container_stop.go +++ b/api/client/lib/container_stop.go @@ -8,7 +8,7 @@ import ( // 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 { - var query url.Values + query := url.Values{} query.Set("t", strconv.Itoa(timeout)) resp, err := cli.POST("/containers/"+containerID+"/stop", query, nil, nil) ensureReaderClosed(resp) diff --git a/api/client/lib/container_top.go b/api/client/lib/container_top.go index c4ac649c1a..abee1ed417 100644 --- a/api/client/lib/container_top.go +++ b/api/client/lib/container_top.go @@ -10,10 +10,8 @@ import ( // ContainerTop shows process information from within a container. func (cli *Client) ContainerTop(containerID string, arguments []string) (types.ContainerProcessList, error) { - var ( - query url.Values - response types.ContainerProcessList - ) + var response types.ContainerProcessList + query := url.Values{} if len(arguments) > 0 { query.Set("ps_args", strings.Join(arguments, " ")) } diff --git a/api/client/lib/copy.go b/api/client/lib/copy.go index 458896e69f..f2192762c2 100644 --- a/api/client/lib/copy.go +++ b/api/client/lib/copy.go @@ -15,7 +15,7 @@ import ( // ContainerStatPath returns Stat information about a path inside the container filesystem. func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerPathStat, error) { - query := make(url.Values, 1) + query := url.Values{} query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. urlStr := fmt.Sprintf("/containers/%s/archive", containerID) @@ -29,7 +29,7 @@ func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerP // CopyToContainer copies content into the container filesystem. func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error { - var query url.Values + 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 { diff --git a/api/client/lib/events.go b/api/client/lib/events.go index ff514af509..cbc0756ee5 100644 --- a/api/client/lib/events.go +++ b/api/client/lib/events.go @@ -13,7 +13,7 @@ import ( // 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) { - var query url.Values + query := url.Values{} ref := time.Now() if options.Since != "" { diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go index f7e5bc97fb..ad1a6de49e 100644 --- a/api/client/lib/image_create.go +++ b/api/client/lib/image_create.go @@ -10,7 +10,7 @@ import ( // 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) { - var query url.Values + query := url.Values{} query.Set("fromImage", options.Parent) query.Set("tag", options.Tag) diff --git a/api/client/lib/image_import.go b/api/client/lib/image_import.go index 9495036469..fd1f921b6f 100644 --- a/api/client/lib/image_import.go +++ b/api/client/lib/image_import.go @@ -10,7 +10,7 @@ import ( // 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) { - var query url.Values + query := url.Values{} query.Set("fromSrc", options.SourceName) query.Set("repo", options.RepositoryName) query.Set("tag", options.Tag) diff --git a/api/client/lib/image_list.go b/api/client/lib/image_list.go index 5e27112944..902bbabe04 100644 --- a/api/client/lib/image_list.go +++ b/api/client/lib/image_list.go @@ -10,11 +10,8 @@ import ( // 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 - ) - + var images []types.Image + query := url.Values{} if options.Filters.Len() > 0 { filterJSON, err := filters.ToParam(options.Filters) if err != nil { diff --git a/api/client/lib/image_remove.go b/api/client/lib/image_remove.go index ebf352dbb8..bab02793a7 100644 --- a/api/client/lib/image_remove.go +++ b/api/client/lib/image_remove.go @@ -9,7 +9,7 @@ import ( // ImageRemove removes an image from the docker host. func (cli *Client) ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, error) { - var query url.Values + query := url.Values{} if options.Force { query.Set("force", "1") diff --git a/api/client/lib/kill.go b/api/client/lib/kill.go index 3e80d70ada..5399b7a28d 100644 --- a/api/client/lib/kill.go +++ b/api/client/lib/kill.go @@ -4,7 +4,7 @@ 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 { - var query url.Values + query := url.Values{} query.Set("signal", signal) resp, err := cli.POST("/containers/"+containerID+"/kill", query, nil, nil) diff --git a/api/client/lib/logs.go b/api/client/lib/logs.go index 677d29124d..e961a6d750 100644 --- a/api/client/lib/logs.go +++ b/api/client/lib/logs.go @@ -12,7 +12,7 @@ import ( // 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) { - var query url.Values + query := url.Values{} if options.ShowStdout { query.Set("stdout", "1") } diff --git a/api/client/lib/volume.go b/api/client/lib/volume.go index 636b9559fb..868886f742 100644 --- a/api/client/lib/volume.go +++ b/api/client/lib/volume.go @@ -11,7 +11,7 @@ import ( // VolumeList returns the volumes configured in the docker host. func (cli *Client) VolumeList(filter filters.Args) (types.VolumesListResponse, error) { var volumes types.VolumesListResponse - var query url.Values + query := url.Values{} if filter.Len() > 0 { filterJSON, err := filters.ToParam(filter) From d9a62c5f2b11131d4b8c3d62af60cd7e6ceaa350 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sat, 5 Dec 2015 20:28:36 -0500 Subject: [PATCH 40/53] Lowercase http method functions. Signed-off-by: David Calavera --- api/client/lib/container_commit.go | 2 +- api/client/lib/container_create.go | 2 +- api/client/lib/container_inspect.go | 2 +- api/client/lib/container_list.go | 2 +- api/client/lib/container_remove.go | 2 +- api/client/lib/container_rename.go | 2 +- api/client/lib/container_restart.go | 2 +- api/client/lib/container_stop.go | 2 +- api/client/lib/container_unpause.go | 2 +- api/client/lib/copy.go | 6 ++--- api/client/lib/diff.go | 2 +- api/client/lib/events.go | 2 +- api/client/lib/export.go | 2 +- api/client/lib/history.go | 2 +- api/client/lib/image_build.go | 2 +- api/client/lib/image_create.go | 2 +- api/client/lib/image_import.go | 2 +- api/client/lib/image_list.go | 3 ++- api/client/lib/image_load.go | 2 +- api/client/lib/image_remove.go | 2 +- api/client/lib/image_save.go | 2 +- api/client/lib/image_tag.go | 2 +- api/client/lib/info.go | 2 +- api/client/lib/kill.go | 2 +- api/client/lib/login.go | 2 +- api/client/lib/logs.go | 2 +- api/client/lib/network.go | 12 ++++----- api/client/lib/request.go | 41 ++++++++++++++++------------- api/client/lib/version.go | 2 +- api/client/lib/volume.go | 8 +++--- 30 files changed, 63 insertions(+), 57 deletions(-) diff --git a/api/client/lib/container_commit.go b/api/client/lib/container_commit.go index 308add49f0..b827bc534c 100644 --- a/api/client/lib/container_commit.go +++ b/api/client/lib/container_commit.go @@ -35,7 +35,7 @@ func (cli *Client) ContainerCommit(options types.ContainerCommitOptions) (types. } } - resp, err := cli.POST("/commit", query, config, nil) + resp, err := cli.post("/commit", query, config, nil) if err != nil { return response, err } diff --git a/api/client/lib/container_create.go b/api/client/lib/container_create.go index 73626677a5..a5f24b09fc 100644 --- a/api/client/lib/container_create.go +++ b/api/client/lib/container_create.go @@ -18,7 +18,7 @@ func (cli *Client) ContainerCreate(config *runconfig.ContainerConfigWrapper, con query.Set("name", containerName) } - serverResp, err := cli.POST("/containers/create", query, config, nil) + 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} diff --git a/api/client/lib/container_inspect.go b/api/client/lib/container_inspect.go index 038c11ef1c..c633df0148 100644 --- a/api/client/lib/container_inspect.go +++ b/api/client/lib/container_inspect.go @@ -8,7 +8,7 @@ import ( // ContainerInspect returns the all the container information. func (cli *Client) ContainerInspect(containerID string) (types.ContainerJSON, error) { - serverResp, err := cli.GET("/containers/"+containerID+"/json", nil, nil) + serverResp, err := cli.get("/containers/"+containerID+"/json", nil, nil) if err != nil { return types.ContainerJSON{}, err } diff --git a/api/client/lib/container_list.go b/api/client/lib/container_list.go index e1d9a32c1a..5392894c76 100644 --- a/api/client/lib/container_list.go +++ b/api/client/lib/container_list.go @@ -42,7 +42,7 @@ func (cli *Client) ContainerList(options types.ContainerListOptions) ([]types.Co query.Set("filters", filterJSON) } - resp, err := cli.GET("/containers/json", query, nil) + resp, err := cli.get("/containers/json", query, nil) if err != nil { return nil, err } diff --git a/api/client/lib/container_remove.go b/api/client/lib/container_remove.go index 1fe80d054f..aff5012b36 100644 --- a/api/client/lib/container_remove.go +++ b/api/client/lib/container_remove.go @@ -20,7 +20,7 @@ func (cli *Client) ContainerRemove(options types.ContainerRemoveOptions) error { query.Set("force", "1") } - resp, err := cli.DELETE("/containers/"+options.ContainerID, query, nil) + resp, err := cli.delete("/containers/"+options.ContainerID, query, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/container_rename.go b/api/client/lib/container_rename.go index db7e878076..1aed00ada1 100644 --- a/api/client/lib/container_rename.go +++ b/api/client/lib/container_rename.go @@ -6,7 +6,7 @@ import "net/url" 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) + resp, err := cli.post("/containers/"+containerID+"/rename", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/container_restart.go b/api/client/lib/container_restart.go index b9eab92d35..74b5022b90 100644 --- a/api/client/lib/container_restart.go +++ b/api/client/lib/container_restart.go @@ -11,7 +11,7 @@ import ( 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) + resp, err := cli.post("/containers/"+containerID+"/restart", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/container_stop.go b/api/client/lib/container_stop.go index ced540f13b..3bb73ea75e 100644 --- a/api/client/lib/container_stop.go +++ b/api/client/lib/container_stop.go @@ -10,7 +10,7 @@ import ( 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) + resp, err := cli.post("/containers/"+containerID+"/stop", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/container_unpause.go b/api/client/lib/container_unpause.go index 3596bf8104..146cbd8fc4 100644 --- a/api/client/lib/container_unpause.go +++ b/api/client/lib/container_unpause.go @@ -2,7 +2,7 @@ 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) + resp, err := cli.post("/containers/"+containerID+"/unpause", nil, nil, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/copy.go b/api/client/lib/copy.go index f2192762c2..e26a7f79b0 100644 --- a/api/client/lib/copy.go +++ b/api/client/lib/copy.go @@ -19,7 +19,7 @@ func (cli *Client) ContainerStatPath(containerID, path string) (types.ContainerP 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) + response, err := cli.head(urlStr, query, nil) if err != nil { return types.ContainerPathStat{}, err } @@ -38,7 +38,7 @@ func (cli *Client) CopyToContainer(options types.CopyToContainerOptions) error { path := fmt.Sprintf("/containers/%s/archive", options.ContainerID) - response, err := cli.PUT(path, query, options.Content, nil) + response, err := cli.putRaw(path, query, options.Content, nil) if err != nil { return err } @@ -58,7 +58,7 @@ func (cli *Client) CopyFromContainer(containerID, srcPath string) (io.ReadCloser 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) + response, err := cli.get(apiPath, query, nil) if err != nil { return nil, types.ContainerPathStat{}, err } diff --git a/api/client/lib/diff.go b/api/client/lib/diff.go index bf3c0ee838..15954c5ed7 100644 --- a/api/client/lib/diff.go +++ b/api/client/lib/diff.go @@ -11,7 +11,7 @@ import ( func (cli *Client) ContainerDiff(containerID string) ([]types.ContainerChange, error) { var changes []types.ContainerChange - serverResp, err := cli.GET("/containers/"+containerID+"/changes", url.Values{}, nil) + serverResp, err := cli.get("/containers/"+containerID+"/changes", url.Values{}, nil) if err != nil { return changes, err } diff --git a/api/client/lib/events.go b/api/client/lib/events.go index cbc0756ee5..1d63a845a0 100644 --- a/api/client/lib/events.go +++ b/api/client/lib/events.go @@ -38,7 +38,7 @@ func (cli *Client) Events(options types.EventsOptions) (io.ReadCloser, error) { query.Set("filters", filterJSON) } - serverResponse, err := cli.GET("/events", query, nil) + serverResponse, err := cli.get("/events", query, nil) if err != nil { return nil, err } diff --git a/api/client/lib/export.go b/api/client/lib/export.go index 6cb4d4a18d..07893c5b83 100644 --- a/api/client/lib/export.go +++ b/api/client/lib/export.go @@ -9,7 +9,7 @@ import ( // 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) + serverResp, err := cli.get("/containers/"+containerID+"/export", url.Values{}, nil) if err != nil { return nil, err } diff --git a/api/client/lib/history.go b/api/client/lib/history.go index 62f96e6202..e729f70eff 100644 --- a/api/client/lib/history.go +++ b/api/client/lib/history.go @@ -10,7 +10,7 @@ import ( // 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) + serverResp, err := cli.get("/images/"+imageID+"/history", url.Values{}, nil) if err != nil { return history, err } diff --git a/api/client/lib/image_build.go b/api/client/lib/image_build.go index 1f3c1f8222..46ad161472 100644 --- a/api/client/lib/image_build.go +++ b/api/client/lib/image_build.go @@ -30,7 +30,7 @@ func (cli *Client) ImageBuild(options types.ImageBuildOptions) (types.ImageBuild headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) headers.Set("Content-Type", "application/tar") - serverResp, err := cli.POSTRaw("/build", query, options.Context, headers) + serverResp, err := cli.postRaw("/build", query, options.Context, headers) if err != nil { return types.ImageBuildResponse{}, err } diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go index ad1a6de49e..3d4e70e01c 100644 --- a/api/client/lib/image_create.go +++ b/api/client/lib/image_create.go @@ -15,7 +15,7 @@ func (cli *Client) ImageCreate(options types.ImageCreateOptions) (io.ReadCloser, query.Set("tag", options.Tag) headers := map[string][]string{"X-Registry-Auth": {options.RegistryAuth}} - resp, err := cli.POST("/images/create", query, nil, headers) + resp, err := cli.post("/images/create", query, nil, headers) if err != nil { return nil, err } diff --git a/api/client/lib/image_import.go b/api/client/lib/image_import.go index fd1f921b6f..873010f3e5 100644 --- a/api/client/lib/image_import.go +++ b/api/client/lib/image_import.go @@ -19,7 +19,7 @@ func (cli *Client) ImageImport(options types.ImageImportOptions) (io.ReadCloser, query.Add("changes", change) } - resp, err := cli.POSTRaw("/images/create", query, options.Source, nil) + resp, err := cli.postRaw("/images/create", query, options.Source, nil) if err != nil { return nil, err } diff --git a/api/client/lib/image_list.go b/api/client/lib/image_list.go index 902bbabe04..8e4bf4ba80 100644 --- a/api/client/lib/image_list.go +++ b/api/client/lib/image_list.go @@ -12,6 +12,7 @@ import ( 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 { @@ -27,7 +28,7 @@ func (cli *Client) ImageList(options types.ImageListOptions) ([]types.Image, err query.Set("all", "1") } - serverResp, err := cli.GET("/images/json?", query, nil) + serverResp, err := cli.get("/images/json", query, nil) if err != nil { return images, err } diff --git a/api/client/lib/image_load.go b/api/client/lib/image_load.go index 9f0e04cd54..954cfd8d7b 100644 --- a/api/client/lib/image_load.go +++ b/api/client/lib/image_load.go @@ -9,7 +9,7 @@ import ( // 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) + resp, err := cli.postRaw("/images/load", url.Values{}, input, nil) if err != nil { return nil, err } diff --git a/api/client/lib/image_remove.go b/api/client/lib/image_remove.go index bab02793a7..fb221da6bd 100644 --- a/api/client/lib/image_remove.go +++ b/api/client/lib/image_remove.go @@ -18,7 +18,7 @@ func (cli *Client) ImageRemove(options types.ImageRemoveOptions) ([]types.ImageD query.Set("noprune", "1") } - resp, err := cli.DELETE("/images/"+options.ImageID, query, nil) + resp, err := cli.delete("/images/"+options.ImageID, query, nil) if err != nil { return nil, err } diff --git a/api/client/lib/image_save.go b/api/client/lib/image_save.go index 07d96eb8d3..10fb6130e8 100644 --- a/api/client/lib/image_save.go +++ b/api/client/lib/image_save.go @@ -12,7 +12,7 @@ func (cli *Client) ImageSave(imageIDs []string) (io.ReadCloser, error) { "names": imageIDs, } - resp, err := cli.GET("/images/get", query, nil) + resp, err := cli.get("/images/get", query, nil) if err != nil { return nil, err } diff --git a/api/client/lib/image_tag.go b/api/client/lib/image_tag.go index 23097501ed..d83e33fe23 100644 --- a/api/client/lib/image_tag.go +++ b/api/client/lib/image_tag.go @@ -15,7 +15,7 @@ func (cli *Client) ImageTag(options types.ImageTagOptions) error { query.Set("force", "1") } - resp, err := cli.POST("/images/"+options.ImageID+"/tag", query, nil, nil) + resp, err := cli.post("/images/"+options.ImageID+"/tag", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/info.go b/api/client/lib/info.go index b9a0fabf7b..04d854a290 100644 --- a/api/client/lib/info.go +++ b/api/client/lib/info.go @@ -11,7 +11,7 @@ import ( // 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) + serverResp, err := cli.get("/info", url.Values{}, nil) if err != nil { return info, err } diff --git a/api/client/lib/kill.go b/api/client/lib/kill.go index 5399b7a28d..4da1a94008 100644 --- a/api/client/lib/kill.go +++ b/api/client/lib/kill.go @@ -7,7 +7,7 @@ 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) + resp, err := cli.post("/containers/"+containerID+"/kill", query, nil, nil) ensureReaderClosed(resp) return err } diff --git a/api/client/lib/login.go b/api/client/lib/login.go index 77399ce985..56ee18481c 100644 --- a/api/client/lib/login.go +++ b/api/client/lib/login.go @@ -12,7 +12,7 @@ import ( // 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) + resp, err := cli.post("/auth", url.Values{}, auth, nil) if resp != nil && resp.statusCode == http.StatusUnauthorized { return types.AuthResponse{}, unauthorizedError{err} diff --git a/api/client/lib/logs.go b/api/client/lib/logs.go index e961a6d750..241bac33de 100644 --- a/api/client/lib/logs.go +++ b/api/client/lib/logs.go @@ -38,7 +38,7 @@ func (cli *Client) ContainerLogs(options types.ContainerLogsOptions) (io.ReadClo } query.Set("tail", options.Tail) - resp, err := cli.GET("/containers/"+options.ContainerID+"/logs", query, nil) + resp, err := cli.get("/containers/"+options.ContainerID+"/logs", query, nil) if err != nil { return nil, err } diff --git a/api/client/lib/network.go b/api/client/lib/network.go index bbf7ce5093..2401b1bfbd 100644 --- a/api/client/lib/network.go +++ b/api/client/lib/network.go @@ -9,7 +9,7 @@ import ( // 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) + serverResp, err := cli.post("/networks/create", nil, options, nil) if err != nil { return response, err } @@ -21,7 +21,7 @@ func (cli *Client) NetworkCreate(options types.NetworkCreate) (types.NetworkCrea // NetworkRemove removes an existent network from the docker host. func (cli *Client) NetworkRemove(networkID string) error { - resp, err := cli.DELETE("/networks/"+networkID, nil, nil) + resp, err := cli.delete("/networks/"+networkID, nil, nil) ensureReaderClosed(resp) return err } @@ -29,7 +29,7 @@ func (cli *Client) NetworkRemove(networkID string) error { // 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) + resp, err := cli.post("/networks/"+networkID+"/connect", nil, nc, nil) ensureReaderClosed(resp) return err } @@ -37,7 +37,7 @@ func (cli *Client) NetworkConnect(networkID, containerID string) error { // 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) + resp, err := cli.post("/networks/"+networkID+"/disconnect", nil, nc, nil) ensureReaderClosed(resp) return err } @@ -45,7 +45,7 @@ func (cli *Client) NetworkDisconnect(networkID, containerID string) error { // 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) + resp, err := cli.get("/networks", nil, nil) if err != nil { return networkResources, err } @@ -57,7 +57,7 @@ func (cli *Client) NetworkList() ([]types.NetworkResource, error) { // 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) + resp, err := cli.get("/networks/"+networkID, nil, nil) if err != nil { return networkResource, err } diff --git a/api/client/lib/request.go b/api/client/lib/request.go index 685e5eb3d9..9f98ddadba 100644 --- a/api/client/lib/request.go +++ b/api/client/lib/request.go @@ -13,44 +13,49 @@ import ( "github.com/docker/docker/utils" ) -// ServerResponse is a wrapper for http API responses. -type ServerResponse struct { +// 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) { +// 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) { +// 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) { +// 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) { +// 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) { +// 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) } -// 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) { +// 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) { +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 @@ -66,8 +71,8 @@ func (cli *Client) sendRequest(method, path string, query url.Values, body inter return cli.sendClientRequest(method, path, query, params, headers) } -func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*ServerResponse, error) { - serverResp := &ServerResponse{ +func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*serverResponse, error) { + serverResp := &serverResponse{ body: nil, statusCode: -1, } @@ -148,7 +153,7 @@ func encodeData(data interface{}) (*bytes.Buffer, error) { return params, nil } -func ensureReaderClosed(response *ServerResponse) { +func ensureReaderClosed(response *serverResponse) { if response != nil && response.body != nil { response.body.Close() } diff --git a/api/client/lib/version.go b/api/client/lib/version.go index b460e4b886..c6d5cd4c7d 100644 --- a/api/client/lib/version.go +++ b/api/client/lib/version.go @@ -23,7 +23,7 @@ func (cli *Client) SystemVersion() (types.VersionResponse, error) { Experimental: utils.ExperimentalBuild(), } - resp, err := cli.GET("/version", nil, nil) + resp, err := cli.get("/version", nil, nil) if err != nil { return types.VersionResponse{Client: client}, err } diff --git a/api/client/lib/volume.go b/api/client/lib/volume.go index 868886f742..3e0497155b 100644 --- a/api/client/lib/volume.go +++ b/api/client/lib/volume.go @@ -20,7 +20,7 @@ func (cli *Client) VolumeList(filter filters.Args) (types.VolumesListResponse, e } query.Set("filters", filterJSON) } - resp, err := cli.GET("/volumes", query, nil) + resp, err := cli.get("/volumes", query, nil) if err != nil { return volumes, err } @@ -33,7 +33,7 @@ func (cli *Client) VolumeList(filter filters.Args) (types.VolumesListResponse, e // 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) + resp, err := cli.get("/volumes/"+volumeID, nil, nil) if err != nil { return volume, err } @@ -45,7 +45,7 @@ func (cli *Client) VolumeInspect(volumeID string) (types.Volume, error) { // 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) + resp, err := cli.post("/volumes/create", nil, options, nil) if err != nil { return volume, err } @@ -56,7 +56,7 @@ func (cli *Client) VolumeCreate(options types.VolumeCreateRequest) (types.Volume // VolumeRemove removes a volume from the docker host. func (cli *Client) VolumeRemove(volumeID string) error { - resp, err := cli.DELETE("/volumes"+volumeID, nil, nil) + resp, err := cli.delete("/volumes/"+volumeID, nil, nil) ensureReaderClosed(resp) return err } From 5e80ac9c8438252ebb16f6c4aef450e2861a2e5d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sat, 5 Dec 2015 22:22:00 -0500 Subject: [PATCH 41/53] Implement docker attach with standalone client lib. Signed-off-by: David Calavera --- api/client/attach.go | 40 ++++--- api/client/client.go | 1 + api/client/hijack.go | 69 +++++++++++- api/client/lib/client.go | 4 + api/client/lib/container_attach.go | 30 ++++++ api/client/lib/hijack.go | 165 +++++++++++++++++++++++++++++ api/client/lib/request.go | 47 ++++---- api/types/client.go | 38 ++++++- 8 files changed, 350 insertions(+), 44 deletions(-) create mode 100644 api/client/lib/container_attach.go create mode 100644 api/client/lib/hijack.go diff --git a/api/client/attach.go b/api/client/attach.go index 9ebd02d760..f0979e2bf8 100644 --- a/api/client/attach.go +++ b/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/api/client/client.go b/api/client/client.go index 8f9efa8374..acd5bd35ea 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -15,6 +15,7 @@ import ( // 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) diff --git a/api/client/hijack.go b/api/client/hijack.go index bf152c6a9e..1c495afe9e 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -15,11 +15,79 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api" + "github.com/docker/docker/api/types" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/term" ) +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 + } + defer term.RestoreTerminal(cli.inFd, oldState) + } + + receiveStdout := make(chan error, 1) + if outputStream != nil || errorStream != nil { + go func() { + defer func() { + if inputStream != nil { + if setRawTerminal && cli.isTerminalIn { + term.RestoreTerminal(cli.inFd, oldState) + } + inputStream.Close() + } + }() + + // When TTY is ON, use regular copy + if setRawTerminal && outputStream != nil { + _, err = io.Copy(outputStream, resp.Reader) + } else { + _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) + } + logrus.Debugf("[hijack] End of stdout") + receiveStdout <- err + }() + } + + stdinDone := make(chan struct{}) + go func() { + if inputStream != nil { + io.Copy(resp.Conn, inputStream) + logrus.Debugf("[hijack] End of stdin") + } + + if err := resp.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) + } + close(stdinDone) + }() + + select { + case err := <-receiveStdout: + if err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + case <-stdinDone: + if outputStream != nil || errorStream != nil { + if err := <-receiveStdout; err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + } + } + + return nil +} + type tlsClientCon struct { *tls.Conn rawConn net.Conn @@ -190,7 +258,6 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se } var oldState *term.State - if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" { oldState, err = term.SetRawTerminal(cli.inFd) if err != nil { diff --git a/api/client/lib/client.go b/api/client/lib/client.go index dbb088a9ca..31bd3efc02 100644 --- a/api/client/lib/client.go +++ b/api/client/lib/client.go @@ -24,6 +24,8 @@ type Client struct { 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. @@ -78,9 +80,11 @@ func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsc 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, diff --git a/api/client/lib/container_attach.go b/api/client/lib/container_attach.go new file mode 100644 index 0000000000..4ed821fa1d --- /dev/null +++ b/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/api/client/lib/hijack.go b/api/client/lib/hijack.go new file mode 100644 index 0000000000..4c9475bffa --- /dev/null +++ b/api/client/lib/hijack.go @@ -0,0 +1,165 @@ +package lib + +import ( + "crypto/tls" + "errors" + "fmt" + "io" + "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 io.Reader, headers map[string][]string) (*types.HijackedResponse, error) { + bodyEncoded, err := encodeData(body) + if err != nil { + return nil, err + } + + req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) + if err != nil { + return nil, 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 nil, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") + } + 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 := 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/api/client/lib/request.go b/api/client/lib/request.go index 9f98ddadba..9fcab7e425 100644 --- a/api/client/lib/request.go +++ b/api/client/lib/request.go @@ -71,38 +71,21 @@ func (cli *Client) sendRequest(method, path string, query url.Values, body inter return cli.sendClientRequest(method, path, query, params, headers) } -func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*serverResponse, error) { +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 && in == nil { - in = bytes.NewReader([]byte{}) - } - - apiPath := cli.getAPIPath(path, query) - req, err := http.NewRequest(method, apiPath, 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.customHTTPHeaders { - req.Header.Set(k, v) + 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 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") } @@ -143,6 +126,28 @@ func (cli *Client) sendClientRequest(method, path string, query url.Values, in i 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 { diff --git a/api/types/client.go b/api/types/client.go index b528999f25..dbe299cbdb 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -1,14 +1,25 @@ package types import ( + "bufio" "io" + "net" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/parsers/filters" "github.com/docker/docker/pkg/ulimit" ) -// ContainerCommitOptions hods parameters to commit changes into a container. +// 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 @@ -67,6 +78,31 @@ type EventsOptions struct { 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 { From 962b2d8b9b0201db2132ec17f06513e33ad419d0 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sat, 5 Dec 2015 22:53:52 -0500 Subject: [PATCH 42/53] Implement docker start with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 1 + api/client/lib/container_start.go | 8 ++ api/client/start.go | 128 +++++++++++++----------------- 3 files changed, 62 insertions(+), 75 deletions(-) create mode 100644 api/client/lib/container_start.go diff --git a/api/client/client.go b/api/client/client.go index acd5bd35ea..376c18b25e 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -29,6 +29,7 @@ type apiClient interface { ContainerRename(containerID, newContainerName string) error ContainerRestart(containerID string, timeout int) error ContainerStatPath(containerID, path string) (types.ContainerPathStat, error) + ContainerStart(containerID string) error ContainerStop(containerID string, timeout int) error ContainerTop(containerID string, arguments []string) (types.ContainerProcessList, error) ContainerUnpause(containerID string) error diff --git a/api/client/lib/container_start.go b/api/client/lib/container_start.go new file mode 100644 index 0000000000..2c65b5df30 --- /dev/null +++ b/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/api/client/start.go b/api/client/start.go index b429639ede..3baef202a8 100644 --- a/api/client/start.go +++ b/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" @@ -54,119 +53,98 @@ 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 } } - } - 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) - } + // 3. Start the container. + if err := cli.client.ContainerStart(containerID); err != nil { + return err } - } - 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 } From bcb848c87f496fe2b946530521a0d262a489c019 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sat, 5 Dec 2015 23:18:06 -0500 Subject: [PATCH 43/53] Implement docker run with standalone client lib. Signed-off-by: David Calavera --- api/client/run.go | 55 +++++++++++++++++---------------------------- api/client/start.go | 7 ------ 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/api/client/run.go b/api/client/run.go index 8eb463141a..d93574b29b 100644 --- a/api/client/run.go +++ b/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 { diff --git a/api/client/start.go b/api/client/start.go index 3baef202a8..787f87d118 100644 --- a/api/client/start.go +++ b/api/client/start.go @@ -95,13 +95,6 @@ func (cli *DockerCli) CmdStart(args ...string) error { return cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp) }) - select { - case err := <-cErr: - if err != nil { - return err - } - } - // 3. Start the container. if err := cli.client.ContainerStart(containerID); err != nil { return err From 3f9f23114f7cccd9e9972d457f0f1d2502eaa4af Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 00:33:38 -0500 Subject: [PATCH 44/53] Implement docker exec with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 6 +++- api/client/exec.go | 55 ++++++++---------------------- api/client/hijack.go | 2 +- api/client/lib/container_attach.go | 2 +- api/client/lib/exec.go | 49 ++++++++++++++++++++++++++ api/client/lib/hijack.go | 13 ++++--- api/client/utils.go | 19 ++--------- api/types/client.go | 8 +++++ 8 files changed, 87 insertions(+), 67 deletions(-) create mode 100644 api/client/lib/exec.go diff --git a/api/client/client.go b/api/client/client.go index 376c18b25e..30e3bf9bb6 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -15,10 +15,14 @@ import ( // apiClient is an interface that clients that talk with a docker server must implement. type apiClient interface { - ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) + 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) + ContainerExecStart(execID string, config types.ExecStartCheck) error ContainerExport(containerID string) (io.ReadCloser, error) ContainerInspect(containerID string) (types.ContainerJSON, error) ContainerKill(containerID, signal string) error diff --git a/api/client/exec.go b/api/client/exec.go index a9c836c943..06cdd2e4c8 100644 --- a/api/client/exec.go +++ b/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/api/client/hijack.go b/api/client/hijack.go index 1c495afe9e..696546dd27 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -21,7 +21,7 @@ import ( "github.com/docker/docker/pkg/term" ) -func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp *types.HijackedResponse) error { +func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { var ( err error oldState *term.State diff --git a/api/client/lib/container_attach.go b/api/client/lib/container_attach.go index 4ed821fa1d..439ec2d0d7 100644 --- a/api/client/lib/container_attach.go +++ b/api/client/lib/container_attach.go @@ -10,7 +10,7 @@ import ( // 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) { +func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (types.HijackedResponse, error) { query := url.Values{} if options.Stream { query.Set("stream", "1") diff --git a/api/client/lib/exec.go b/api/client/lib/exec.go new file mode 100644 index 0000000000..4b042c71f2 --- /dev/null +++ b/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/api/client/lib/hijack.go b/api/client/lib/hijack.go index 4c9475bffa..70ada0369b 100644 --- a/api/client/lib/hijack.go +++ b/api/client/lib/hijack.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "errors" "fmt" - "io" "net" "net/http/httputil" "net/url" @@ -30,15 +29,15 @@ func (c *tlsClientCon) CloseWrite() error { } // postHijacked sends a POST request and hijacks the connection. -func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, headers map[string][]string) (*types.HijackedResponse, error) { +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 nil, err + return types.HijackedResponse{}, err } req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) if err != nil { - return nil, err + return types.HijackedResponse{}, err } req.Host = cli.Addr @@ -48,9 +47,9 @@ func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, h conn, err := dial(cli.Proto, cli.Addr, cli.tlsConfig) if err != nil { if strings.Contains(err.Error(), "connection refused") { - return nil, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") + return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") } - return nil, err + return types.HijackedResponse{}, err } // When we set up a TCP connection for hijack, there could be long periods @@ -71,7 +70,7 @@ func (cli *Client) postHijacked(path string, query url.Values, body io.Reader, h rwc, br := clientconn.Hijack() - return &types.HijackedResponse{rwc, br}, nil + return types.HijackedResponse{rwc, br}, nil } func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) { diff --git a/api/client/utils.go b/api/client/utils.go index 0de171db1e..afa0353b09 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -278,29 +278,16 @@ func getExitCode(cli *DockerCli, containerID string) (bool, int, error) { // 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 { diff --git a/api/types/client.go b/api/types/client.go index dbe299cbdb..8ce653d590 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -31,6 +31,14 @@ type ContainerCommitOptions struct { JSONConfig string } +// 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 From e59d54bd99a1a06bf178f8dbda29980136f94a58 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 02:34:23 -0500 Subject: [PATCH 45/53] Implement docker stats with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 1 + api/client/lib/container_stats.go | 22 +++++++++++++++++++++ api/client/stats.go | 32 ++++++++++--------------------- 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 api/client/lib/container_stats.go diff --git a/api/client/client.go b/api/client/client.go index 30e3bf9bb6..d3c502ac1e 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -33,6 +33,7 @@ type apiClient interface { ContainerRename(containerID, newContainerName string) 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) diff --git a/api/client/lib/container_stats.go b/api/client/lib/container_stats.go new file mode 100644 index 0000000000..a18fae7a0b --- /dev/null +++ b/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/api/client/stats.go b/api/client/stats.go index 659764bfbc..6ffee7f27c 100644 --- a/api/client/stats.go +++ b/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 { From e78f02c4dbc3cada909c114fef6b6643969ab912 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 15:17:34 -0500 Subject: [PATCH 46/53] Implement docker pull with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 2 ++ api/client/create.go | 7 ++----- api/client/lib/image_create.go | 9 +++++--- api/client/lib/image_pull.go | 34 ++++++++++++++++++++++++++++++ api/client/lib/privileged.go | 9 ++++++++ api/client/pull.go | 38 +++++++++++++++++++++++++++------- api/client/trust.go | 13 ++++-------- api/client/utils.go | 15 ++++++++++++++ api/types/client.go | 8 +++++++ cliconfig/config.go | 9 ++++++++ 10 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 api/client/lib/image_pull.go create mode 100644 api/client/lib/privileged.go diff --git a/api/client/client.go b/api/client/client.go index d3c502ac1e..c71617ac2a 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -7,6 +7,7 @@ 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" @@ -48,6 +49,7 @@ type apiClient interface { ImageImport(options types.ImageImportOptions) (io.ReadCloser, 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) ImageRemove(options types.ImageRemoveOptions) ([]types.ImageDelete, error) ImageSave(imageIDs []string) (io.ReadCloser, error) ImageTag(options types.ImageTagOptions) error diff --git a/api/client/create.go b/api/client/create.go index a5430eb3cb..857ef90aac 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -1,8 +1,6 @@ package client import ( - "encoding/base64" - "encoding/json" "fmt" "io" "os" @@ -45,8 +43,7 @@ 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 } @@ -54,7 +51,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { options := types.ImageCreateOptions{ Parent: ref.Name(), Tag: tag, - RegistryAuth: base64.URLEncoding.EncodeToString(buf), + RegistryAuth: encodedAuth, } responseBody, err := cli.client.ImageCreate(options) diff --git a/api/client/lib/image_create.go b/api/client/lib/image_create.go index 3d4e70e01c..75d4b75e4a 100644 --- a/api/client/lib/image_create.go +++ b/api/client/lib/image_create.go @@ -13,11 +13,14 @@ func (cli *Client) ImageCreate(options types.ImageCreateOptions) (io.ReadCloser, query := url.Values{} query.Set("fromImage", options.Parent) query.Set("tag", options.Tag) - - headers := map[string][]string{"X-Registry-Auth": {options.RegistryAuth}} - resp, err := cli.post("/images/create", query, nil, headers) + 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/api/client/lib/image_pull.go b/api/client/lib/image_pull.go new file mode 100644 index 0000000000..ef5f1ce6e6 --- /dev/null +++ b/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/api/client/lib/privileged.go b/api/client/lib/privileged.go new file mode 100644 index 0000000000..cb3da51ea8 --- /dev/null +++ b/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/api/client/pull.go b/api/client/pull.go index e585a12081..88879308a3 100644 --- a/api/client/pull.go +++ b/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()) - - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") - return err + return cli.imagePullPrivileged(authConfig, distributionRef.String(), "", requestPrivilege) +} + +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/api/client/trust.go b/api/client/trust.go index 4a550a381a..1bc4f8fe1f 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -22,6 +22,7 @@ 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" @@ -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 } diff --git a/api/client/utils.go b/api/client/utils.go index afa0353b09..095ae8ee63 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -160,6 +160,21 @@ func (cli *DockerCli) cmdAttempt(authConfig cliconfig.AuthConfig, method, path s return serverResp.body, serverResp.statusCode, err } +func (cli *DockerCli) encodeRegistryAuth(index *registry.IndexInfo) (string, error) { + authConfig := registry.ResolveAuthConfig(cli.configFile, index) + 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 "", err + } + return cli.encodeRegistryAuth(index) + } +} + 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 diff --git a/api/types/client.go b/api/types/client.go index 8ce653d590..10f40a8cd8 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -179,6 +179,14 @@ type ImageListOptions struct { 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 +} + // ImageRemoveOptions holds parameters to remove images. type ImageRemoveOptions struct { ImageID string diff --git a/cliconfig/config.go b/cliconfig/config.go index b28d6e5bcf..0bcf87d87d 100644 --- a/cliconfig/config.go +++ b/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"` From 42670e30eef7023d2df9c6c8900041bc9e1546e0 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 15:37:54 -0500 Subject: [PATCH 47/53] Implement docker push with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 1 + api/client/lib/image_push.go | 36 ++++++++++++++++++++++++++++++++++++ api/client/push.go | 35 ++++++++++++++++++++++++++++------- api/client/trust.go | 10 ++++------ api/types/client.go | 3 +++ 5 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 api/client/lib/image_push.go diff --git a/api/client/client.go b/api/client/client.go index c71617ac2a..26aabc00e4 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -50,6 +50,7 @@ type apiClient interface { 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) ImageSave(imageIDs []string) (io.ReadCloser, error) ImageTag(options types.ImageTagOptions) error diff --git a/api/client/lib/image_push.go b/api/client/lib/image_push.go new file mode 100644 index 0000000000..14c369afac --- /dev/null +++ b/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/api/client/push.go b/api/client/push.go index 760c97ef8b..4230eea838 100644 --- a/api/client/push.go +++ b/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) - - _, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+ref.Name()+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push") - return err + return cli.imagePushPrivileged(authConfig, ref.Name(), tag, cli.out, requestPrivilege) +} + +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/api/client/trust.go b/api/client/trust.go index 1bc4f8fe1f..cb7c8ef10b 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -380,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/api/types/client.go b/api/types/client.go index 10f40a8cd8..698dc48543 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -187,6 +187,9 @@ type ImagePullOptions struct { RegistryAuth string } +//ImagePushOptions holds information to push images. +type ImagePushOptions ImagePullOptions + // ImageRemoveOptions holds parameters to remove images. type ImageRemoveOptions struct { ImageID string From cf3efd05d42e92d8f92338be3730894dca85aa26 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 18:34:25 -0500 Subject: [PATCH 48/53] Implement docker inspect with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 2 + api/client/inspect.go | 231 ++++++++++------------------ api/client/inspect/inspector.go | 101 ++++++++++++ api/client/lib/container_inspect.go | 48 +++++- api/client/lib/errors.go | 17 ++ api/client/lib/image_inspect.go | 37 +++++ 6 files changed, 281 insertions(+), 155 deletions(-) create mode 100644 api/client/inspect/inspector.go create mode 100644 api/client/lib/image_inspect.go diff --git a/api/client/client.go b/api/client/client.go index 26aabc00e4..7d7a69ffd2 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -26,6 +26,7 @@ type apiClient interface { 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) @@ -47,6 +48,7 @@ type apiClient interface { 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) diff --git a/api/client/inspect.go b/api/client/inspect.go index fe3666e3b9..d1edf3b6e2 100644 --- a/api/client/inspect.go +++ b/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,10 +30,15 @@ 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 *inspectType != "" && *inspectType != "container" && *inspectType != "image" { + return fmt.Errorf("%q is not a valid value for --type", *inspectType) + } + + var ( + err error + tmpl *template.Template + elementInspector inspect.Inspector + ) if *tmplStr != "" { if tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr); err != nil { @@ -46,160 +47,84 @@ func (cli *DockerCli) CmdInspect(args ...string) error { } } - if *inspectType != "" && *inspectType != "container" && *inspectType != "image" { - return fmt.Errorf("%q is not a valid value for --type", *inspectType) + if tmpl != nil { + elementInspector = inspect.NewTemplateInspector(cli.out, tmpl) + } else { + elementInspector = inspect.NewIndentedInspector(cli.out) } - indented := new(bytes.Buffer) - indented.WriteString("[\n") - status := 0 - isImage := false - - v := url.Values{} - if *size { - v.Set("size", "1") + switch *inspectType { + case "container": + err = cli.inspectContainers(cmd.Args(), *size, elementInspector) + case "images": + err = cli.inspectImages(cmd.Args(), *size, elementInspector) + default: + err = cli.inspectAll(cmd.Args(), *size, elementInspector) } - 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) - } - status = 1 - continue - } - } - } - - 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 - } - } - - 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(",") + if err := elementInspector.Flush(); err != nil { + return err } + return err +} - if indented.Len() > 1 { - // Remove trailing ',' - indented.Truncate(indented.Len() - 1) - } - indented.WriteString("]\n") - - if tmpl == nil { - // Note that we will always write "[]" when "-f" isn't specified, - // to make sure the output would always be array, see - // https://github.com/docker/docker/pull/9500#issuecomment-65846734 - if _, err := io.Copy(cli.out, indented); err != nil { +func (cli *DockerCli) inspectContainers(containerIDs []string, getSize bool, elementInspector inspect.Inspector) error { + for _, containerID := range containerIDs { + if err := cli.inspectContainer(containerID, getSize, elementInspector); err != nil { + if lib.IsErrContainerNotFound(err) { + return fmt.Errorf("Error: No such container: %s\n", containerID) + } return err } } - - if 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) inspectImages(imageIDs []string, getSize bool, elementInspector inspect.Inspector) error { + for _, imageID := range imageIDs { + if err := cli.inspectImage(imageID, getSize, elementInspector); err != nil { + if lib.IsErrImageNotFound(err) { + return fmt.Errorf("Error: No such image: %s\n", imageID) + } + return err + } } - - if rawErr := tmpl.Execute(buf, raw); rawErr != nil { - return buf, false - } - - if strings.Contains(buf.String(), "") { - return buf, false - } - return buf, true + return nil +} + +func (cli *DockerCli) inspectAll(ids []string, getSize bool, elementInspector inspect.Inspector) error { + for _, id := range ids { + if err := cli.inspectContainer(id, getSize, elementInspector); err != nil { + // Search for image with that id if a container doesn't exist. + if lib.IsErrContainerNotFound(err) { + if err := cli.inspectImage(id, getSize, elementInspector); err != nil { + if lib.IsErrImageNotFound(err) { + return fmt.Errorf("Error: No such image or container: %s", id) + } + return err + } + continue + } + return err + } + } + return nil +} + +func (cli *DockerCli) inspectContainer(containerID string, getSize bool, elementInspector inspect.Inspector) error { + c, raw, err := cli.client.ContainerInspectWithRaw(containerID, getSize) + if err != nil { + return err + } + + return elementInspector.Inspect(c, raw) +} + +func (cli *DockerCli) inspectImage(imageID string, getSize bool, elementInspector inspect.Inspector) error { + i, raw, err := cli.client.ImageInspectWithRaw(imageID, getSize) + if err != nil { + return err + } + + return elementInspector.Inspect(i, raw) } diff --git a/api/client/inspect/inspector.go b/api/client/inspect/inspector.go new file mode 100644 index 0000000000..de6945b28e --- /dev/null +++ b/api/client/inspect/inspector.go @@ -0,0 +1,101 @@ +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 { + var raw interface{} + rdr := bytes.NewReader(rawElement) + dec := json.NewDecoder(rdr) + + if rawErr := dec.Decode(&raw); rawErr != nil { + return fmt.Errorf("unable to read inspect data: %v\n", rawErr) + } + + tmplMissingKey := i.tmpl.Option("missingkey=error") + if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { + return fmt.Errorf("Template parsing error: %v\n", err) + } + } + 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 + indented *bytes.Buffer +} + +// NewIndentedInspector generates a new IndentedInspector. +func NewIndentedInspector(outputStream io.Writer) Inspector { + indented := new(bytes.Buffer) + indented.WriteString("[\n") + return &IndentedInspector{ + outputStream: outputStream, + indented: indented, + } +} + +// Inspect writes the raw element with an indented json format. +func (i IndentedInspector) Inspect(_ interface{}, rawElement []byte) error { + if err := json.Indent(i.indented, rawElement, "", " "); err != nil { + return err + } + i.indented.WriteByte(',') + return nil +} + +// Flush write the result of inspecting all elements into the output stream. +func (i IndentedInspector) Flush() error { + if i.indented.Len() > 1 { + // Remove trailing ',' + i.indented.Truncate(i.indented.Len() - 1) + } + i.indented.WriteString("]\n") + + // 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 + _, err := io.Copy(i.outputStream, i.indented) + return err +} diff --git a/api/client/lib/container_inspect.go b/api/client/lib/container_inspect.go index c633df0148..41e413132e 100644 --- a/api/client/lib/container_inspect.go +++ b/api/client/lib/container_inspect.go @@ -1,20 +1,64 @@ package lib import ( + "bytes" "encoding/json" + "io/ioutil" + "net/http" + "net/url" "github.com/docker/docker/api/types" ) -// ContainerInspect returns the all the container information. +// 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 - json.NewDecoder(serverResp.body).Decode(&response) + 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/api/client/lib/errors.go b/api/client/lib/errors.go index dba6d381af..fba4ebbd6a 100644 --- a/api/client/lib/errors.go +++ b/api/client/lib/errors.go @@ -25,6 +25,23 @@ func IsErrImageNotFound(err error) bool { 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("Container not found: %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 +} + // unauthorizedError represents an authorization error in a remote registry. type unauthorizedError struct { cause error diff --git a/api/client/lib/image_inspect.go b/api/client/lib/image_inspect.go new file mode 100644 index 0000000000..f4089cba0b --- /dev/null +++ b/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 +} From d1057e4c4672b584590295f3d9b96a90183bbfcd Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 18:41:57 -0500 Subject: [PATCH 49/53] Implement docker resize with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 2 ++ api/client/lib/resize.go | 28 ++++++++++++++++++++++++++++ api/client/utils.go | 24 +++++++++++++----------- api/types/client.go | 17 ++++++++++++++++- 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 api/client/lib/resize.go diff --git a/api/client/client.go b/api/client/client.go index 7d7a69ffd2..e3be35beb6 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -23,6 +23,7 @@ type apiClient interface { 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) @@ -33,6 +34,7 @@ type apiClient interface { 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) diff --git a/api/client/lib/resize.go b/api/client/lib/resize.go new file mode 100644 index 0000000000..dffd81d544 --- /dev/null +++ b/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/api/client/utils.go b/api/client/utils.go index 095ae8ee63..40b14efac1 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -9,17 +9,16 @@ import ( "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" @@ -259,18 +258,21 @@ 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) } } diff --git a/api/types/client.go b/api/types/client.go index 698dc48543..a89b952319 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -197,7 +197,13 @@ type ImageRemoveOptions struct { PruneChildren bool } -// ImageTagOptions hold parameters to tag an image +// 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 @@ -205,6 +211,15 @@ type ImageTagOptions struct { 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 From e4abf485677cd1d93d584753e19545e6ec8c2335 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 19:29:15 -0500 Subject: [PATCH 50/53] Implement docker search with standalone client lib. Signed-off-by: David Calavera --- api/client/client.go | 2 ++ api/client/lib/image_search.go | 39 ++++++++++++++++++++++++++++++++++ api/client/search.go | 33 ++++++++++++++++------------ 3 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 api/client/lib/image_search.go diff --git a/api/client/client.go b/api/client/client.go index e3be35beb6..52dd4e5cd3 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -11,6 +11,7 @@ import ( "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" ) @@ -56,6 +57,7 @@ type apiClient interface { 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) diff --git a/api/client/lib/image_search.go b/api/client/lib/image_search.go new file mode 100644 index 0000000000..9d4a9ce81a --- /dev/null +++ b/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/api/client/search.go b/api/client/search.go index 1a47064477..fea66d55b7 100644 --- a/api/client/search.go +++ b/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 } From 5a0a6ee9cd55f3e2f57d992a032421d0e126040d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 6 Dec 2015 19:35:56 -0500 Subject: [PATCH 51/53] Remove old http from the docker cli. Everything has been ported to the client library :tada: Signed-off-by: David Calavera --- api/client/cli.go | 24 ---- api/client/hijack.go | 247 -------------------------------------- api/client/lib/client.go | 22 ++-- api/client/lib/hijack.go | 4 +- api/client/lib/request.go | 10 +- api/client/utils.go | 237 ------------------------------------ 6 files changed, 18 insertions(+), 526 deletions(-) diff --git a/api/client/cli.go b/api/client/cli.go index d7cf4f56b3..19c635d776 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -1,11 +1,9 @@ package client import ( - "crypto/tls" "errors" "fmt" "io" - "net/http" "os" "runtime" @@ -44,22 +42,6 @@ type DockerCli struct { isTerminalOut bool // client is the http client that performs all API operations client apiClient - - // DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE - // TODO: Remove - // 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 - // 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 - // transport holds the client transport instance. - transport *http.Transport } // Initialize calls the init function that will setup the configuration for the client @@ -126,12 +108,6 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF } cli.client = client - // FIXME: Deprecated, only to keep the old code running. - cli.transport = client.HTTPClient.Transport.(*http.Transport) - cli.basePath = client.BasePath - cli.addr = client.Addr - cli.scheme = client.Scheme - if cli.in != nil { cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) } diff --git a/api/client/hijack.go b/api/client/hijack.go index 696546dd27..72f5ed43de 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -1,22 +1,11 @@ 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/api/types" - "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/term" ) @@ -87,239 +76,3 @@ func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io return nil } - -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") == "" { - oldState, err = term.SetRawTerminal(cli.inFd) - if err != nil { - return err - } - defer term.RestoreTerminal(cli.inFd, oldState) - } - - receiveStdout := make(chan error, 1) - if stdout != nil || stderr != nil { - go func() { - defer func() { - if in != nil { - if setRawTerminal && cli.isTerminalIn { - term.RestoreTerminal(cli.inFd, oldState) - } - in.Close() - } - }() - - // When TTY is ON, use regular copy - if setRawTerminal && stdout != nil { - _, err = io.Copy(stdout, br) - } else { - _, err = stdcopy.StdCopy(stdout, stderr, br) - } - logrus.Debugf("[hijack] End of stdout") - receiveStdout <- err - }() - } - - stdinDone := make(chan struct{}) - go func() { - if in != nil { - io.Copy(rwc, in) - logrus.Debugf("[hijack] End of stdin") - } - - if conn, ok := rwc.(interface { - CloseWrite() error - }); ok { - if err := conn.CloseWrite(); err != nil { - logrus.Debugf("Couldn't send EOF: %s", err) - } - } - close(stdinDone) - }() - - select { - case err := <-receiveStdout: - if err != nil { - logrus.Debugf("Error receiveStdout: %s", err) - return err - } - case <-stdinDone: - if stdout != nil || stderr != nil { - if err := <-receiveStdout; err != nil { - logrus.Debugf("Error receiveStdout: %s", err) - return err - } - } - } - - return nil -} diff --git a/api/client/lib/client.go b/api/client/lib/client.go index 31bd3efc02..3625a8170f 100644 --- a/api/client/lib/client.go +++ b/api/client/lib/client.go @@ -17,17 +17,17 @@ import ( // against a docker server. type Client struct { // proto holds the client protocol i.e. unix. - Proto string + proto string // addr holds the client address. - Addr string + addr string // basePath holds the path to prepend to the requests - BasePath string + basePath string // scheme holds the scheme of the client i.e. https. - Scheme string + 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 + httpClient *http.Client // version of the server to talk to. version version.Version // custom http headers configured by users @@ -80,12 +80,12 @@ func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsc sockets.ConfigureTCPTransport(transport, proto, addr) return &Client{ - Proto: proto, - Addr: addr, - BasePath: basePath, - Scheme: scheme, + proto: proto, + addr: addr, + basePath: basePath, + scheme: scheme, tlsConfig: tlsConfig, - HTTPClient: &http.Client{Transport: transport}, + httpClient: &http.Client{Transport: transport}, version: version, customHTTPHeaders: httpHeaders, }, nil @@ -94,7 +94,7 @@ func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsc // 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) + apiPath := fmt.Sprintf("%s/v%s%s", cli.basePath, cli.version, p) if len(query) > 0 { apiPath += "?" + query.Encode() } diff --git a/api/client/lib/hijack.go b/api/client/lib/hijack.go index 70ada0369b..4fcb4f62b0 100644 --- a/api/client/lib/hijack.go +++ b/api/client/lib/hijack.go @@ -39,12 +39,12 @@ func (cli *Client) postHijacked(path string, query url.Values, body interface{}, if err != nil { return types.HijackedResponse{}, err } - req.Host = cli.Addr + req.Host = cli.addr req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "tcp") - conn, err := dial(cli.Proto, cli.Addr, cli.tlsConfig) + 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?") diff --git a/api/client/lib/request.go b/api/client/lib/request.go index 9fcab7e425..db3e5065a2 100644 --- a/api/client/lib/request.go +++ b/api/client/lib/request.go @@ -83,14 +83,14 @@ func (cli *Client) sendClientRequest(method, path string, query url.Values, body } req, err := cli.newRequest(method, path, query, body, headers) - req.URL.Host = cli.Addr - req.URL.Scheme = cli.Scheme + 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) + resp, err := cli.httpClient.Do(req) if resp != nil { serverResp.statusCode = resp.StatusCode } @@ -100,10 +100,10 @@ func (cli *Client) sendClientRequest(method, path string, query url.Values, body return serverResp, ErrConnectionFailed } - if cli.Scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") { + 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") { + 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) } diff --git a/api/client/utils.go b/api/client/utils.go index 40b14efac1..e03ec5ffaa 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -1,164 +1,20 @@ package client import ( - "bytes" - "encoding/base64" - "encoding/json" - "errors" "fmt" - "io" - "io/ioutil" - "net/http" "os" gosignal "os/signal" "runtime" - "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) encodeRegistryAuth(index *registry.IndexInfo) (string, error) { authConfig := registry.ResolveAuthConfig(cli.configFile, index) return authConfig.EncodeToBase64() @@ -174,85 +30,6 @@ func (cli *DockerCli) registryAuthenticationPrivilegedFunc(index *registry.Index } } -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 - authConfig := registry.ResolveAuthConfig(cli.configFile, index) - body, statusCode, err := cli.cmdAttempt(authConfig, method, path, in, out) - if statusCode == http.StatusUnauthorized { - fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) - if err = cli.CmdLogin(index.GetAuthConfigKey()); err != nil { - return nil, -1, err - } - authConfig = registry.ResolveAuthConfig(cli.configFile, index) - return cli.cmdAttempt(authConfig, method, path, in, out) - } - 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) { height, width := cli.getTtySize() if height == 0 && width == 0 { @@ -349,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 -} From 57b6796304880331d0d4e9cbefab7b139e718915 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Mon, 7 Dec 2015 22:04:38 -0500 Subject: [PATCH 52/53] Implement all inspect commands with the new inspector interface. It makes the behavior completely consistent across commands. It adds tests to check that execution stops when an element is not found. Signed-off-by: David Calavera --- api/client/inspect.go | 155 +++++++++--------- api/client/inspect/inspector.go | 72 ++++---- api/client/lib/errors.go | 38 ++++- api/client/lib/network.go | 4 + api/client/lib/volume.go | 4 + api/client/network.go | 61 +------ api/client/volume.go | 58 +------ integration-cli/docker_cli_inspect_test.go | 11 ++ .../docker_cli_network_unix_test.go | 2 +- integration-cli/docker_cli_volume_test.go | 10 +- 10 files changed, 190 insertions(+), 225 deletions(-) diff --git a/api/client/inspect.go b/api/client/inspect.go index d1edf3b6e2..375129bdaa 100644 --- a/api/client/inspect.go +++ b/api/client/inspect.go @@ -34,97 +34,100 @@ func (cli *DockerCli) CmdInspect(args ...string) error { return fmt.Errorf("%q is not a valid value for --type", *inspectType) } - var ( - err error - tmpl *template.Template - elementInspector inspect.Inspector - ) - - 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 tmpl != nil { - elementInspector = inspect.NewTemplateInspector(cli.out, tmpl) - } else { - elementInspector = inspect.NewIndentedInspector(cli.out) - } - + var elementSearcher inspectSearcher switch *inspectType { case "container": - err = cli.inspectContainers(cmd.Args(), *size, elementInspector) - case "images": - err = cli.inspectImages(cmd.Args(), *size, elementInspector) + elementSearcher = cli.inspectContainers(*size) + case "image": + elementSearcher = cli.inspectImages(*size) default: - err = cli.inspectAll(cmd.Args(), *size, elementInspector) + elementSearcher = cli.inspectAll(*size) + } + + 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) + } + return nil, nil, err + } + return i, rawImage, err + } + return nil, nil, err + } + return c, rawContainer, err + } +} + +type inspectSearcher func(ref string) (interface{}, []byte, error) + +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()} + } + + var inspectErr error + for _, ref := range references { + element, raw, err := searchByReference(ref) + if err != nil { + inspectErr = err + break + } + + if err := elementInspector.Inspect(element, raw); err != nil { + inspectErr = err + break + } } if err := elementInspector.Flush(); err != nil { - return err + cli.inspectErrorStatus(err) } - return err -} -func (cli *DockerCli) inspectContainers(containerIDs []string, getSize bool, elementInspector inspect.Inspector) error { - for _, containerID := range containerIDs { - if err := cli.inspectContainer(containerID, getSize, elementInspector); err != nil { - if lib.IsErrContainerNotFound(err) { - return fmt.Errorf("Error: No such container: %s\n", containerID) - } - return err - } + if status := cli.inspectErrorStatus(inspectErr); status != 0 { + return Cli.StatusError{StatusCode: status} } return nil } -func (cli *DockerCli) inspectImages(imageIDs []string, getSize bool, elementInspector inspect.Inspector) error { - for _, imageID := range imageIDs { - if err := cli.inspectImage(imageID, getSize, elementInspector); err != nil { - if lib.IsErrImageNotFound(err) { - return fmt.Errorf("Error: No such image: %s\n", imageID) - } - return err - } - } - return nil -} - -func (cli *DockerCli) inspectAll(ids []string, getSize bool, elementInspector inspect.Inspector) error { - for _, id := range ids { - if err := cli.inspectContainer(id, getSize, elementInspector); err != nil { - // Search for image with that id if a container doesn't exist. - if lib.IsErrContainerNotFound(err) { - if err := cli.inspectImage(id, getSize, elementInspector); err != nil { - if lib.IsErrImageNotFound(err) { - return fmt.Errorf("Error: No such image or container: %s", id) - } - return err - } - continue - } - return err - } - } - return nil -} - -func (cli *DockerCli) inspectContainer(containerID string, getSize bool, elementInspector inspect.Inspector) error { - c, raw, err := cli.client.ContainerInspectWithRaw(containerID, getSize) +func (cli *DockerCli) inspectErrorStatus(err error) (status int) { if err != nil { - return err + fmt.Fprintf(cli.err, "%s\n", err) + status = 1 } - - return elementInspector.Inspect(c, raw) + return } -func (cli *DockerCli) inspectImage(imageID string, getSize bool, elementInspector inspect.Inspector) error { - i, raw, err := cli.client.ImageInspectWithRaw(imageID, getSize) - if err != nil { - return err +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) } - - return elementInspector.Inspect(i, raw) + return elementInspector, nil } diff --git a/api/client/inspect/inspector.go b/api/client/inspect/inspector.go index de6945b28e..eee0f5ddb7 100644 --- a/api/client/inspect/inspector.go +++ b/api/client/inspect/inspector.go @@ -33,29 +33,41 @@ func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspe // 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 { +func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error { buffer := new(bytes.Buffer) if err := i.tmpl.Execute(buffer, typedElement); err != nil { - var raw interface{} - rdr := bytes.NewReader(rawElement) - dec := json.NewDecoder(rdr) - - if rawErr := dec.Decode(&raw); rawErr != nil { - return fmt.Errorf("unable to read inspect data: %v\n", rawErr) - } - - tmplMissingKey := i.tmpl.Option("missingkey=error") - if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { - return fmt.Errorf("Template parsing error: %v\n", err) + 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 { +func (i *TemplateInspector) Flush() error { _, err := io.Copy(i.outputStream, i.buffer) return err } @@ -63,39 +75,37 @@ func (i TemplateInspector) Flush() error { // IndentedInspector uses a buffer to stop the indented representation of an element. type IndentedInspector struct { outputStream io.Writer - indented *bytes.Buffer + elements []interface{} } // NewIndentedInspector generates a new IndentedInspector. func NewIndentedInspector(outputStream io.Writer) Inspector { - indented := new(bytes.Buffer) - indented.WriteString("[\n") return &IndentedInspector{ outputStream: outputStream, - indented: indented, } } // Inspect writes the raw element with an indented json format. -func (i IndentedInspector) Inspect(_ interface{}, rawElement []byte) error { - if err := json.Indent(i.indented, rawElement, "", " "); err != nil { - return err - } - i.indented.WriteByte(',') +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 i.indented.Len() > 1 { - // Remove trailing ',' - i.indented.Truncate(i.indented.Len() - 1) +func (i *IndentedInspector) Flush() error { + if len(i.elements) == 0 { + _, err := io.WriteString(i.outputStream, "[]\n") + return err } - i.indented.WriteString("]\n") - // 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 - _, err := io.Copy(i.outputStream, i.indented) + 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/api/client/lib/errors.go b/api/client/lib/errors.go index fba4ebbd6a..95b88075da 100644 --- a/api/client/lib/errors.go +++ b/api/client/lib/errors.go @@ -15,7 +15,7 @@ type imageNotFoundError struct { // Error returns a string representation of an imageNotFoundError func (i imageNotFoundError) Error() string { - return fmt.Sprintf("Image not found: %s", i.imageID) + return fmt.Sprintf("Error: No such image: %s", i.imageID) } // IsErrImageNotFound returns true if the error is caused @@ -32,7 +32,7 @@ type containerNotFoundError struct { // Error returns a string representation of an containerNotFoundError func (e containerNotFoundError) Error() string { - return fmt.Sprintf("Container not found: %s", e.containerID) + return fmt.Sprintf("Error: No such container: %s", e.containerID) } // IsErrContainerNotFound returns true if the error is caused @@ -42,6 +42,40 @@ func IsErrContainerNotFound(err error) bool { 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 diff --git a/api/client/lib/network.go b/api/client/lib/network.go index 2401b1bfbd..6f99921f48 100644 --- a/api/client/lib/network.go +++ b/api/client/lib/network.go @@ -2,6 +2,7 @@ package lib import ( "encoding/json" + "net/http" "github.com/docker/docker/api/types" ) @@ -59,6 +60,9 @@ func (cli *Client) NetworkInspect(networkID string) (types.NetworkResource, erro 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) diff --git a/api/client/lib/volume.go b/api/client/lib/volume.go index 3e0497155b..afb75b4983 100644 --- a/api/client/lib/volume.go +++ b/api/client/lib/volume.go @@ -2,6 +2,7 @@ package lib import ( "encoding/json" + "net/http" "net/url" "github.com/docker/docker/api/types" @@ -35,6 +36,9 @@ 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) diff --git a/api/client/network.go b/api/client/network.go index c3442a77a6..35abf5d91d 100644 --- a/api/client/network.go +++ b/api/client/network.go @@ -1,14 +1,10 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "io" "net" "strings" "text/tabwriter" - "text/template" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" @@ -192,61 +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() { - networkResource, err := cli.client.NetworkInspect(name) - if err != nil { - fmt.Fprintf(cli.err, "%s\n", err) - return Cli.StatusError{StatusCode: 1} - } - if tmpl == nil { - networks = append(networks, networkResource) - continue - } - - 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/api/client/volume.go b/api/client/volume.go index 2fbc201e74..1eb790f5ba 100644 --- a/api/client/volume.go +++ b/api/client/volume.go @@ -1,12 +1,8 @@ package client import ( - "bytes" - "encoding/json" "fmt" - "io" "text/tabwriter" - "text/template" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" @@ -98,58 +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() { - volume, err := cli.client.VolumeInspect(name) - if 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. diff --git a/integration-cli/docker_cli_inspect_test.go b/integration-cli/docker_cli_inspect_test.go index 9a30ace533..355cff2b00 100644 --- a/integration-cli/docker_cli_inspect_test.go +++ b/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/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go index 7914838f00..8fc0b7eef2 100644 --- a/integration-cli/docker_cli_network_unix_test.go +++ b/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/integration-cli/docker_cli_volume_test.go b/integration-cli/docker_cli_volume_test.go index d404fe1538..6d49abe579 100644 --- a/integration-cli/docker_cli_volume_test.go +++ b/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) { From 2ec468e2844365f0d7e2afd744f322cde25c077e Mon Sep 17 00:00:00 2001 From: David Calavera Date: Wed, 9 Dec 2015 12:03:09 -0500 Subject: [PATCH 53/53] Make the commit configuration to be a typed struct rather than accepting a string. Signed-off-by: David Calavera --- api/client/commit.go | 12 +++++++++++- api/client/lib/container_commit.go | 16 ++-------------- api/types/client.go | 3 ++- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/api/client/commit.go b/api/client/commit.go index 100a985f5c..b75520b243 100644 --- a/api/client/commit.go +++ b/api/client/commit.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" ) // CmdCommit creates a new image from a container's changes. @@ -56,6 +58,14 @@ func (cli *DockerCli) CmdCommit(args ...string) error { } } + var config *runconfig.Config + if *flConfig != "" { + config = &runconfig.Config{} + if err := json.Unmarshal([]byte(*flConfig), config); err != nil { + return err + } + } + options := types.ContainerCommitOptions{ ContainerID: name, RepositoryName: repositoryName, @@ -64,7 +74,7 @@ func (cli *DockerCli) CmdCommit(args ...string) error { Author: *flAuthor, Changes: flChanges.GetAll(), Pause: *flPause, - JSONConfig: *flConfig, + Config: config, } response, err := cli.client.ContainerCommit(options) diff --git a/api/client/lib/container_commit.go b/api/client/lib/container_commit.go index b827bc534c..45cf166250 100644 --- a/api/client/lib/container_commit.go +++ b/api/client/lib/container_commit.go @@ -5,7 +5,6 @@ import ( "net/url" "github.com/docker/docker/api/types" - "github.com/docker/docker/runconfig" ) // ContainerCommit applies changes into a container and creates a new tagged image. @@ -23,19 +22,8 @@ func (cli *Client) ContainerCommit(options types.ContainerCommitOptions) (types. query.Set("pause", "0") } - var ( - config *runconfig.Config - response types.ContainerCommitResponse - ) - - if options.JSONConfig != "" { - config = &runconfig.Config{} - if err := json.Unmarshal([]byte(options.JSONConfig), config); err != nil { - return response, err - } - } - - resp, err := cli.post("/commit", query, config, nil) + var response types.ContainerCommitResponse + resp, err := cli.post("/commit", query, options.Config, nil) if err != nil { return response, err } diff --git a/api/types/client.go b/api/types/client.go index a89b952319..6841a9a6ca 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -8,6 +8,7 @@ import ( "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. @@ -28,7 +29,7 @@ type ContainerCommitOptions struct { Author string Changes []string Pause bool - JSONConfig string + Config *runconfig.Config } // ContainerExecInspect holds information returned by exec inspect.