diff --git a/command/formatter/formatter.go b/command/formatter/formatter.go index 32f9a4d359..e859a1ca26 100644 --- a/command/formatter/formatter.go +++ b/command/formatter/formatter.go @@ -11,11 +11,11 @@ import ( "github.com/docker/docker/utils/templates" ) +// Format keys used to specify certain kinds of output formats const ( - // TableFormatKey is the key used to format as a table - TableFormatKey = "table" - // RawFormatKey is the key used to format as raw JSON - RawFormatKey = "raw" + TableFormatKey = "table" + RawFormatKey = "raw" + PrettyFormatKey = "pretty" defaultQuietFormat = "{{.ID}}" ) diff --git a/command/formatter/service.go b/command/formatter/service.go new file mode 100644 index 0000000000..2ce18aba5c --- /dev/null +++ b/command/formatter/service.go @@ -0,0 +1,285 @@ +package formatter + +import ( + "fmt" + "strings" + "time" + + mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command/inspect" + units "github.com/docker/go-units" +) + +const serviceInspectPrettyTemplate Format = ` +ID: {{.ID}} +Name: {{.Name}} +{{- if .Labels }} +Labels: +{{- range $k, $v := .Labels }} + {{ $k }}{{if $v }}={{ $v }}{{ end }} +{{- end }}{{ end }} +Mode: +{{- if .IsModeGlobal }} Global +{{- else }} Replicated +{{- if .ModeReplicatedReplicas }} + Replicas: {{ .ModeReplicatedReplicas }} +{{- end }}{{ end }} +{{- if .HasUpdateStatus }} +UpdateStatus: + State: {{ .UpdateStatusState }} + Started: {{ .UpdateStatusStarted }} +{{- if .UpdateIsCompleted }} + Completed: {{ .UpdateStatusCompleted }} +{{- end }} + Message: {{ .UpdateStatusMessage }} +{{- end }} +Placement: +{{- if .TaskPlacementConstraints -}} + Contraints: {{ .TaskPlacementConstraints }} +{{- end }} +{{- if .HasUpdateConfig }} +UpdateConfig: + Parallelism: {{ .UpdateParallelism }} +{{- if .HasUpdateDelay -}} + Delay: {{ .UpdateDelay }} +{{- end }} + On failure: {{ .UpdateOnFailure }} +{{- end }} +ContainerSpec: + Image: {{ .ContainerImage }} +{{- if .ContainerArgs }} + Args: {{ range $arg := .ContainerArgs }}{{ $arg }} {{ end }} +{{- end -}} +{{- if .ContainerEnv }} + Env: {{ range $env := .ContainerEnv }}{{ $env }} {{ end }} +{{- end -}} +{{- if .ContainerWorkDir }} + Dir: {{ .ContainerWorkDir }} +{{- end -}} +{{- if .ContainerUser }} + User: {{ .ContainerUser }} +{{- end }} +{{- if .ContainerMounts }} +Mounts: +{{- end }} +{{- range $mount := .ContainerMounts }} + Target = {{ $mount.Target }} + Source = {{ $mount.Source }} + ReadOnly = {{ $mount.ReadOnly }} + Type = {{ $mount.Type }} +{{- end -}} +{{- if .HasResources }} +Resources: +{{- if .HasResourceReservations }} + Reservations: +{{- end }} +{{- if gt .ResourceReservationNanoCPUs 0.0 }} + CPU: {{ .ResourceReservationNanoCPUs }} +{{- end }} +{{- if .ResourceReservationMemory }} + Memory: {{ .ResourceReservationMemory }} +{{- end }} +{{- if .HasResourceLimits }} + Limits: +{{- end }} +{{- if gt .ResourceLimitsNanoCPUs 0.0 }} + CPU: {{ .ResourceLimitsNanoCPUs }} +{{- end }} +{{- if .ResourceLimitMemory }} + Memory: {{ .ResourceLimitMemory }} +{{- end }}{{ end }} +{{- if .Networks }} +Networks: +{{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }} +{{- if .Ports }} +Ports: +{{- range $port := .Ports }} + PublishedPort {{ $port.PublishedPort }} + Protocol = {{ $port.Protocol }} + TargetPort = {{ $port.TargetPort }} +{{- end }} {{ end -}} +` + +// NewServiceFormat returns a Format for rendering using a Context +func NewServiceFormat(source string) Format { + switch source { + case PrettyFormatKey: + return serviceInspectPrettyTemplate + default: + return Format(strings.TrimPrefix(source, RawFormatKey)) + } +} + +// ServiceInspectWrite renders the context for a list of services +func ServiceInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { + if ctx.Format != serviceInspectPrettyTemplate { + return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) + } + render := func(format func(subContext subContext) error) error { + for _, ref := range refs { + serviceI, _, err := getRef(ref) + if err != nil { + return err + } + service, ok := serviceI.(swarm.Service) + if !ok { + return fmt.Errorf("got wrong object to inspect") + } + if err := format(&serviceInspectContext{Service: service}); err != nil { + return err + } + } + return nil + } + return ctx.Write(&serviceInspectContext{}, render) +} + +type serviceInspectContext struct { + swarm.Service + subContext +} + +func (ctx *serviceInspectContext) ID() string { + return ctx.Service.ID +} + +func (ctx *serviceInspectContext) Name() string { + return ctx.Service.Spec.Name +} + +func (ctx *serviceInspectContext) Labels() map[string]string { + return ctx.Service.Spec.Labels +} + +func (ctx *serviceInspectContext) IsModeGlobal() bool { + return ctx.Service.Spec.Mode.Global != nil +} + +func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 { + return ctx.Service.Spec.Mode.Replicated.Replicas +} + +func (ctx *serviceInspectContext) HasUpdateStatus() bool { + return ctx.Service.UpdateStatus.State != "" +} + +func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState { + return ctx.Service.UpdateStatus.State +} + +func (ctx *serviceInspectContext) UpdateStatusStarted() string { + return units.HumanDuration(time.Since(ctx.Service.UpdateStatus.StartedAt)) +} + +func (ctx *serviceInspectContext) UpdateIsCompleted() bool { + return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted +} + +func (ctx *serviceInspectContext) UpdateStatusCompleted() string { + return units.HumanDuration(time.Since(ctx.Service.UpdateStatus.CompletedAt)) +} + +func (ctx *serviceInspectContext) UpdateStatusMessage() string { + return ctx.Service.UpdateStatus.Message +} + +func (ctx *serviceInspectContext) TaskPlacementConstraints() []string { + if ctx.Service.Spec.TaskTemplate.Placement != nil { + return ctx.Service.Spec.TaskTemplate.Placement.Constraints + } + return nil +} + +func (ctx *serviceInspectContext) HasUpdateConfig() bool { + return ctx.Service.Spec.UpdateConfig != nil +} + +func (ctx *serviceInspectContext) UpdateParallelism() uint64 { + return ctx.Service.Spec.UpdateConfig.Parallelism +} + +func (ctx *serviceInspectContext) HasUpdateDelay() bool { + return ctx.Service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 +} + +func (ctx *serviceInspectContext) UpdateDelay() time.Duration { + return ctx.Service.Spec.UpdateConfig.Delay +} + +func (ctx *serviceInspectContext) UpdateOnFailure() string { + return ctx.Service.Spec.UpdateConfig.FailureAction +} + +func (ctx *serviceInspectContext) ContainerImage() string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image +} + +func (ctx *serviceInspectContext) ContainerArgs() []string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Args +} + +func (ctx *serviceInspectContext) ContainerEnv() []string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Env +} + +func (ctx *serviceInspectContext) ContainerWorkDir() string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Dir +} + +func (ctx *serviceInspectContext) ContainerUser() string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.User +} + +func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts +} + +func (ctx *serviceInspectContext) HasResources() bool { + return ctx.Service.Spec.TaskTemplate.Resources != nil +} + +func (ctx *serviceInspectContext) HasResourceReservations() bool { + return ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes > 0 +} + +func (ctx *serviceInspectContext) ResourceReservationNanoCPUs() float64 { + if ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs == 0 { + return float64(0) + } + return float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs) / 1e9 +} + +func (ctx *serviceInspectContext) ResourceReservationMemory() string { + if ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes == 0 { + return "" + } + return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes)) +} + +func (ctx *serviceInspectContext) HasResourceLimits() bool { + return ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes > 0 +} + +func (ctx *serviceInspectContext) ResourceLimitsNanoCPUs() float64 { + return float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs) / 1e9 +} + +func (ctx *serviceInspectContext) ResourceLimitMemory() string { + if ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes == 0 { + return "" + } + return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes)) +} + +func (ctx *serviceInspectContext) Networks() []string { + var out []string + for _, n := range ctx.Service.Spec.Networks { + out = append(out, n.Target) + } + return out +} + +func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { + return ctx.Service.Endpoint.Ports +} diff --git a/command/service/inspect.go b/command/service/inspect.go index 8facb1f28b..054c24383e 100644 --- a/command/service/inspect.go +++ b/command/service/inspect.go @@ -2,19 +2,14 @@ package service import ( "fmt" - "io" "strings" - "time" "golang.org/x/net/context" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" + "github.com/docker/docker/cli/command/formatter" apiclient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -51,6 +46,10 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() + if opts.pretty { + opts.format = "pretty" + } + getRef := func(ref string) (interface{}, []byte, error) { service, _, err := client.ServiceInspectWithRaw(ctx, ref) if err == nil || !apiclient.IsErrServiceNotFound(err) { @@ -59,130 +58,27 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { return nil, nil, fmt.Errorf("Error: no such service: %s", ref) } - if !opts.pretty { - return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef) + f := opts.format + if len(f) == 0 { + f = "raw" + if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 { + f = dockerCli.ConfigFile().ServiceInspectFormat + } } - return printHumanFriendly(dockerCli.Out(), opts.refs, getRef) -} + // check if the user is trying to apply a template to the pretty format, which + // is not supported + if strings.HasPrefix(f, "pretty") && f != "pretty" { + return fmt.Errorf("Cannot supply extra formatting options to the pretty template") + } -func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error { - for idx, ref := range refs { - obj, _, err := getRef(ref) - if err != nil { - return err - } - printService(out, obj.(swarm.Service)) + serviceCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewServiceFormat(f), + } - // TODO: better way to do this? - // print extra space between objects, but not after the last one - if idx+1 != len(refs) { - fmt.Fprintf(out, "\n\n") - } + if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef); err != nil { + return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil } - -// TODO: use a template -func printService(out io.Writer, service swarm.Service) { - fmt.Fprintf(out, "ID:\t\t%s\n", service.ID) - fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name) - if service.Spec.Labels != nil { - fmt.Fprintln(out, "Labels:") - for k, v := range service.Spec.Labels { - fmt.Fprintf(out, " - %s=%s\n", k, v) - } - } - - if service.Spec.Mode.Global != nil { - fmt.Fprintln(out, "Mode:\t\tGlobal") - } else { - fmt.Fprintln(out, "Mode:\t\tReplicated") - if service.Spec.Mode.Replicated.Replicas != nil { - fmt.Fprintf(out, " Replicas:\t%d\n", *service.Spec.Mode.Replicated.Replicas) - } - } - - if service.UpdateStatus.State != "" { - fmt.Fprintln(out, "Update status:") - fmt.Fprintf(out, " State:\t\t%s\n", service.UpdateStatus.State) - fmt.Fprintf(out, " Started:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.StartedAt)))) - if service.UpdateStatus.State == swarm.UpdateStateCompleted { - fmt.Fprintf(out, " Completed:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.CompletedAt)))) - } - fmt.Fprintf(out, " Message:\t%s\n", service.UpdateStatus.Message) - } - - fmt.Fprintln(out, "Placement:") - if service.Spec.TaskTemplate.Placement != nil && len(service.Spec.TaskTemplate.Placement.Constraints) > 0 { - ioutils.FprintfIfNotEmpty(out, " Constraints\t: %s\n", strings.Join(service.Spec.TaskTemplate.Placement.Constraints, ", ")) - } - if service.Spec.UpdateConfig != nil { - fmt.Fprintf(out, "UpdateConfig:\n") - fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism) - if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 { - fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay) - } - fmt.Fprintf(out, " On failure:\t%s\n", service.Spec.UpdateConfig.FailureAction) - } - - fmt.Fprintf(out, "ContainerSpec:\n") - printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec) - - resources := service.Spec.TaskTemplate.Resources - if resources != nil { - fmt.Fprintln(out, "Resources:") - printResources := func(out io.Writer, requirement string, r *swarm.Resources) { - if r == nil || (r.MemoryBytes == 0 && r.NanoCPUs == 0) { - return - } - fmt.Fprintf(out, " %s:\n", requirement) - if r.NanoCPUs != 0 { - fmt.Fprintf(out, " CPU:\t\t%g\n", float64(r.NanoCPUs)/1e9) - } - if r.MemoryBytes != 0 { - fmt.Fprintf(out, " Memory:\t%s\n", units.BytesSize(float64(r.MemoryBytes))) - } - } - printResources(out, "Reservations", resources.Reservations) - printResources(out, "Limits", resources.Limits) - } - if len(service.Spec.Networks) > 0 { - fmt.Fprintf(out, "Networks:") - for _, n := range service.Spec.Networks { - fmt.Fprintf(out, " %s", n.Target) - } - fmt.Fprintln(out, "") - } - - if len(service.Endpoint.Ports) > 0 { - fmt.Fprintln(out, "Ports:") - for _, port := range service.Endpoint.Ports { - ioutils.FprintfIfNotEmpty(out, " Name = %s\n", port.Name) - fmt.Fprintf(out, " Protocol = %s\n", port.Protocol) - fmt.Fprintf(out, " TargetPort = %d\n", port.TargetPort) - fmt.Fprintf(out, " PublishedPort = %d\n", port.PublishedPort) - } - } -} - -func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) { - fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image) - if len(containerSpec.Args) > 0 { - fmt.Fprintf(out, " Args:\t\t%s\n", strings.Join(containerSpec.Args, " ")) - } - if len(containerSpec.Env) > 0 { - fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " ")) - } - ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir) - ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User) - if len(containerSpec.Mounts) > 0 { - fmt.Fprintln(out, " Mounts:") - for _, v := range containerSpec.Mounts { - fmt.Fprintf(out, " Target = %s\n", v.Target) - fmt.Fprintf(out, " Source = %s\n", v.Source) - fmt.Fprintf(out, " ReadOnly = %v\n", v.ReadOnly) - fmt.Fprintf(out, " Type = %v\n", v.Type) - } - } -} diff --git a/command/service/inspect_test.go b/command/service/inspect_test.go index 0e0f2ae74f..8e73a70efa 100644 --- a/command/service/inspect_test.go +++ b/command/service/inspect_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command/formatter" ) func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { @@ -77,7 +78,18 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { }, } - printService(b, s) + ctx := formatter.Context{ + Output: b, + Format: formatter.NewServiceFormat("pretty"), + } + + err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (interface{}, []byte, error) { + return s, nil, nil + }) + if err != nil { + t.Fatal(err) + } + if strings.Contains(b.String(), "UpdateStatus") { t.Fatal("Pretty print failed before parsing UpdateStatus") }