diff --git a/components/engine/api/client/client.go b/components/engine/api/client/client.go index 26aabc00e4..7d7a69ffd2 100644 --- a/components/engine/api/client/client.go +++ b/components/engine/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/components/engine/api/client/inspect.go b/components/engine/api/client/inspect.go index fe3666e3b9..d1edf3b6e2 100644 --- a/components/engine/api/client/inspect.go +++ b/components/engine/api/client/inspect.go @@ -1,16 +1,12 @@ package client import ( - "bytes" "encoding/json" "fmt" - "io" - "net/http" - "net/url" - "strings" "text/template" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/client/inspect" + "github.com/docker/docker/api/client/lib" Cli "github.com/docker/docker/cli" flag "github.com/docker/docker/pkg/mflag" ) @@ -34,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 + } } + return nil +} - if rawErr := tmpl.Execute(buf, raw); rawErr != nil { - return buf, false +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 +} - if strings.Contains(buf.String(), "") { - return buf, false +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 buf, true + + 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/components/engine/api/client/inspect/inspector.go b/components/engine/api/client/inspect/inspector.go new file mode 100644 index 0000000000..de6945b28e --- /dev/null +++ b/components/engine/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/components/engine/api/client/lib/container_inspect.go b/components/engine/api/client/lib/container_inspect.go index c633df0148..41e413132e 100644 --- a/components/engine/api/client/lib/container_inspect.go +++ b/components/engine/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/components/engine/api/client/lib/errors.go b/components/engine/api/client/lib/errors.go index dba6d381af..fba4ebbd6a 100644 --- a/components/engine/api/client/lib/errors.go +++ b/components/engine/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/components/engine/api/client/lib/image_inspect.go b/components/engine/api/client/lib/image_inspect.go new file mode 100644 index 0000000000..f4089cba0b --- /dev/null +++ b/components/engine/api/client/lib/image_inspect.go @@ -0,0 +1,37 @@ +package lib + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/docker/docker/api/types" +) + +// ImageInspectWithRaw returns the image information and it's raw representation. +func (cli *Client) ImageInspectWithRaw(imageID string, getSize bool) (types.ImageInspect, []byte, error) { + query := url.Values{} + if getSize { + query.Set("size", "1") + } + serverResp, err := cli.get("/images/"+imageID+"/json", query, nil) + if err != nil { + if serverResp.statusCode == http.StatusNotFound { + return types.ImageInspect{}, nil, imageNotFoundError{imageID} + } + return types.ImageInspect{}, nil, err + } + defer ensureReaderClosed(serverResp) + + body, err := ioutil.ReadAll(serverResp.body) + if err != nil { + return types.ImageInspect{}, nil, err + } + + var response types.ImageInspect + rdr := bytes.NewReader(body) + err = json.NewDecoder(rdr).Decode(&response) + return response, body, err +}