This fix is an attempt to address https://github.com/docker/docker/pull/28213#issuecomment-273840405 Currently when specify table format with table `--format "table {{.ID}}..."`, the delimiter in the header section of the table is always `"\t"`. That is actually different from the content of the table as the delimiter could be anything (or even contatenated with `.`, for example): ``` $ docker service ps web --format 'table {{.Name}}.{{.ID}}' --no-trunc NAME ID web.1.inyhxhvjcijl0hdbu8lgrwwh7 \_ web.1.p9m4kx2srjqmfms4igam0uqlb ``` This fix is an attampt to address the skewness of the table when delimiter is not `"\t"`. The basic idea is that, when header consists of `table` key, the header section will be redendered the same way as content section. A map mapping each placeholder name to the HEADER entry name is used for the context of the header. Unit tests have been updated and added to cover the changes. This fix is related to #28313. Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Upstream-commit: ea61dac9e6d04879445f9c34729055ac1bb15050 Component: engine
426 lines
12 KiB
Go
426 lines
12 KiB
Go
package formatter
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/distribution/reference"
|
|
mounttypes "github.com/docker/docker/api/types/mount"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/cli/command/inspect"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
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 }}
|
|
Service Mode:
|
|
{{- if .IsModeGlobal }} Global
|
|
{{- else if .IsModeReplicated }} Replicated
|
|
{{- if .ModeReplicatedReplicas }}
|
|
Replicas: {{ .ModeReplicatedReplicas }}
|
|
{{- end }}{{ end }}
|
|
{{- if .HasUpdateStatus }}
|
|
UpdateStatus:
|
|
State: {{ .UpdateStatusState }}
|
|
{{- if .HasUpdateStatusStarted }}
|
|
Started: {{ .UpdateStatusStarted }}
|
|
{{- end }}
|
|
{{- 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 }}
|
|
{{- if .HasUpdateMonitor}}
|
|
Monitoring Period: {{ .UpdateMonitor }}
|
|
{{- end }}
|
|
Max failure ratio: {{ .UpdateMaxFailureRatio }}
|
|
{{- 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:
|
|
{{- if gt .ResourceReservationNanoCPUs 0.0 }}
|
|
CPU: {{ .ResourceReservationNanoCPUs }}
|
|
{{- end }}
|
|
{{- if .ResourceReservationMemory }}
|
|
Memory: {{ .ResourceReservationMemory }}
|
|
{{- end }}{{ end }}
|
|
{{- if .HasResourceLimits }}
|
|
Limits:
|
|
{{- if gt .ResourceLimitsNanoCPUs 0.0 }}
|
|
CPU: {{ .ResourceLimitsNanoCPUs }}
|
|
{{- end }}
|
|
{{- if .ResourceLimitMemory }}
|
|
Memory: {{ .ResourceLimitMemory }}
|
|
{{- end }}{{ end }}{{ end }}
|
|
{{- if .Networks }}
|
|
Networks:
|
|
{{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }}
|
|
Endpoint Mode: {{ .EndpointMode }}
|
|
{{- if .Ports }}
|
|
Ports:
|
|
{{- range $port := .Ports }}
|
|
PublishedPort {{ $port.PublishedPort }}
|
|
Protocol = {{ $port.Protocol }}
|
|
TargetPort = {{ $port.TargetPort }}
|
|
PublishMode = {{ $port.PublishMode }}
|
|
{{- 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) MarshalJSON() ([]byte, error) {
|
|
return marshalJSON(ctx)
|
|
}
|
|
|
|
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) IsModeReplicated() bool {
|
|
return ctx.Service.Spec.Mode.Replicated != nil
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 {
|
|
return ctx.Service.Spec.Mode.Replicated.Replicas
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) HasUpdateStatus() bool {
|
|
return ctx.Service.UpdateStatus != nil && ctx.Service.UpdateStatus.State != ""
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState {
|
|
return ctx.Service.UpdateStatus.State
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) HasUpdateStatusStarted() bool {
|
|
return ctx.Service.UpdateStatus.StartedAt != nil
|
|
}
|
|
|
|
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 && ctx.Service.UpdateStatus.CompletedAt != nil
|
|
}
|
|
|
|
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) HasUpdateMonitor() bool {
|
|
return ctx.Service.Spec.UpdateConfig.Monitor.Nanoseconds() > 0
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) UpdateMonitor() time.Duration {
|
|
return ctx.Service.Spec.UpdateConfig.Monitor
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) UpdateMaxFailureRatio() float32 {
|
|
return ctx.Service.Spec.UpdateConfig.MaxFailureRatio
|
|
}
|
|
|
|
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 {
|
|
if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Reservations == nil {
|
|
return false
|
|
}
|
|
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 {
|
|
if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Limits == nil {
|
|
return false
|
|
}
|
|
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) EndpointMode() string {
|
|
if ctx.Service.Spec.EndpointSpec == nil {
|
|
return ""
|
|
}
|
|
|
|
return string(ctx.Service.Spec.EndpointSpec.Mode)
|
|
}
|
|
|
|
func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
|
|
return ctx.Service.Endpoint.Ports
|
|
}
|
|
|
|
const (
|
|
defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}"
|
|
|
|
serviceIDHeader = "ID"
|
|
modeHeader = "MODE"
|
|
replicasHeader = "REPLICAS"
|
|
)
|
|
|
|
// NewServiceListFormat returns a Format for rendering using a service Context
|
|
func NewServiceListFormat(source string, quiet bool) Format {
|
|
switch source {
|
|
case TableFormatKey:
|
|
if quiet {
|
|
return defaultQuietFormat
|
|
}
|
|
return defaultServiceTableFormat
|
|
case RawFormatKey:
|
|
if quiet {
|
|
return `id: {{.ID}}`
|
|
}
|
|
return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\n`
|
|
}
|
|
return Format(source)
|
|
}
|
|
|
|
// ServiceListInfo stores the information about mode and replicas to be used by template
|
|
type ServiceListInfo struct {
|
|
Mode string
|
|
Replicas string
|
|
}
|
|
|
|
// ServiceListWrite writes the context
|
|
func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error {
|
|
render := func(format func(subContext subContext) error) error {
|
|
for _, service := range services {
|
|
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
|
|
if err := format(serviceCtx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
serviceCtx := serviceContext{}
|
|
serviceCtx.header = map[string]string{
|
|
"ID": serviceIDHeader,
|
|
"Name": nameHeader,
|
|
"Mode": modeHeader,
|
|
"Replicas": replicasHeader,
|
|
"Image": imageHeader,
|
|
}
|
|
return ctx.Write(&serviceCtx, render)
|
|
}
|
|
|
|
type serviceContext struct {
|
|
HeaderContext
|
|
service swarm.Service
|
|
mode string
|
|
replicas string
|
|
}
|
|
|
|
func (c *serviceContext) MarshalJSON() ([]byte, error) {
|
|
return marshalJSON(c)
|
|
}
|
|
|
|
func (c *serviceContext) ID() string {
|
|
return stringid.TruncateID(c.service.ID)
|
|
}
|
|
|
|
func (c *serviceContext) Name() string {
|
|
return c.service.Spec.Name
|
|
}
|
|
|
|
func (c *serviceContext) Mode() string {
|
|
return c.mode
|
|
}
|
|
|
|
func (c *serviceContext) Replicas() string {
|
|
return c.replicas
|
|
}
|
|
|
|
func (c *serviceContext) Image() string {
|
|
image := c.service.Spec.TaskTemplate.ContainerSpec.Image
|
|
if ref, err := reference.ParseNormalizedNamed(image); err == nil {
|
|
// update image string for display, (strips any digest)
|
|
if nt, ok := ref.(reference.NamedTagged); ok {
|
|
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
|
|
image = reference.FamiliarString(namedTagged)
|
|
}
|
|
}
|
|
}
|
|
|
|
return image
|
|
}
|