1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Add formatter for service inspect

Allows the user to use `pretty` as the format string.
This enables users to put custom format options into their CLI config
just like is supported for `docker ps` and `docker images`

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2016-07-25 15:24:34 -04:00
parent 45a8f68026
commit 54ba82beab
7 changed files with 344 additions and 140 deletions

View file

@ -11,11 +11,11 @@ import (
"github.com/docker/docker/utils/templates" "github.com/docker/docker/utils/templates"
) )
// Format keys used to specify certain kinds of output formats
const ( const (
// TableFormatKey is the key used to format as a table TableFormatKey = "table"
TableFormatKey = "table" RawFormatKey = "raw"
// RawFormatKey is the key used to format as raw JSON PrettyFormatKey = "pretty"
RawFormatKey = "raw"
defaultQuietFormat = "{{.ID}}" defaultQuietFormat = "{{.ID}}"
) )

View file

@ -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
}

View file

@ -2,19 +2,14 @@ package service
import ( import (
"fmt" "fmt"
"io"
"strings" "strings"
"time"
"golang.org/x/net/context" "golang.org/x/net/context"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli" "github.com/docker/docker/cli"
"github.com/docker/docker/cli/command" "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" apiclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -51,6 +46,10 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()
if opts.pretty {
opts.format = "pretty"
}
getRef := func(ref string) (interface{}, []byte, error) { getRef := func(ref string) (interface{}, []byte, error) {
service, _, err := client.ServiceInspectWithRaw(ctx, ref) service, _, err := client.ServiceInspectWithRaw(ctx, ref)
if err == nil || !apiclient.IsErrServiceNotFound(err) { 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) return nil, nil, fmt.Errorf("Error: no such service: %s", ref)
} }
if !opts.pretty { f := opts.format
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef) 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 { serviceCtx := formatter.Context{
for idx, ref := range refs { Output: dockerCli.Out(),
obj, _, err := getRef(ref) Format: formatter.NewServiceFormat(f),
if err != nil { }
return err
}
printService(out, obj.(swarm.Service))
// TODO: better way to do this? if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef); err != nil {
// print extra space between objects, but not after the last one return cli.StatusError{StatusCode: 1, Status: err.Error()}
if idx+1 != len(refs) {
fmt.Fprintf(out, "\n\n")
}
} }
return nil 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)
}
}
}

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command/formatter"
) )
func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { 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") { if strings.Contains(b.String(), "UpdateStatus") {
t.Fatal("Pretty print failed before parsing UpdateStatus") t.Fatal("Pretty print failed before parsing UpdateStatus")
} }

View file

@ -22,15 +22,16 @@ const (
// ConfigFile ~/.docker/config.json file info // ConfigFile ~/.docker/config.json file info
type ConfigFile struct { type ConfigFile struct {
AuthConfigs map[string]types.AuthConfig `json:"auths"` AuthConfigs map[string]types.AuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
PsFormat string `json:"psFormat,omitempty"` PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"` ImagesFormat string `json:"imagesFormat,omitempty"`
NetworksFormat string `json:"networksFormat,omitempty"` NetworksFormat string `json:"networksFormat,omitempty"`
VolumesFormat string `json:"volumesFormat,omitempty"` VolumesFormat string `json:"volumesFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"` DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"` CredentialsStore string `json:"credsStore,omitempty"`
Filename string `json:"-"` // Note: for internal use only Filename string `json:"-"` // Note: for internal use only
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
} }
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the // LegacyLoadFromReader reads the non-nested configuration data given and sets up the

View file

@ -143,6 +143,13 @@ Docker's client uses this property. If this property is not set, the client
falls back to the default table format. For a list of supported formatting falls back to the default table format. For a list of supported formatting
directives, see the [**Formatting** section in the `docker images` documentation](images.md) directives, see the [**Formatting** section in the `docker images` documentation](images.md)
The property `serviceInspectFormat` specifies the default format for `docker
service inspect` output. When the `--format` flag is not provided with the
`docker service inspect` command, Docker's client uses this property. If this
property is not set, the client falls back to the default json format. For a
list of supported formatting directives, see the
[**Formatting** section in the `docker service inspect` documentation](service_inspect.md)
Following is a sample `config.json` file: Following is a sample `config.json` file:
{ {
@ -151,6 +158,7 @@ Following is a sample `config.json` file:
}, },
"psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}", "psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}",
"imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}", "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
"serviceInspectFormat": "pretty",
"detachKeys": "ctrl-e,e" "detachKeys": "ctrl-e,e"
} }

View file

@ -130,6 +130,8 @@ Ports:
PublishedPort = 4443 PublishedPort = 4443
``` ```
You can also use `--format pretty` for the same effect.
### Finding the number of tasks running as part of a service ### Finding the number of tasks running as part of a service