mirror of https://github.com/docker/cli.git
Fix docker ps --format with templating functions
Before this patch, using a template that used templating functions (such as `lower` or `json`) caused the command to fail in the pre-processor step (in `buildContainerListOptions`): docker ps --format='{{upper .Names}}' template: :1:8: executing "" at <.Names>: invalid value; expected string This problem was due to the pre-processing using a different "context" type than was used in the actual template, and custom functions to not be defined when instantiating the Go template. With this patch, using functions in templates works correctly: docker ps --format='{{upper .Names}}' MUSING_NEUMANN ELOQUENT_MEITNER Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
ba63a92655
commit
69f216f6e4
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,27 +59,6 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return &cmd
|
return &cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// listOptionsProcessor is used to set any container list options which may only
|
|
||||||
// be embedded in the format template.
|
|
||||||
// This is passed directly into tmpl.Execute in order to allow the preprocessor
|
|
||||||
// to set any list options that were not provided by flags (e.g. `.Size`).
|
|
||||||
// It is using a `map[string]bool` so that unknown fields passed into the
|
|
||||||
// template format do not cause errors. These errors will get picked up when
|
|
||||||
// running through the actual template processor.
|
|
||||||
type listOptionsProcessor map[string]bool
|
|
||||||
|
|
||||||
// Size sets the size of the map when called by a template execution.
|
|
||||||
func (o listOptionsProcessor) Size() bool {
|
|
||||||
o["size"] = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label is needed here as it allows the correct pre-processing
|
|
||||||
// because Label() is a method with arguments
|
|
||||||
func (o listOptionsProcessor) Label(name string) string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) {
|
func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) {
|
||||||
options := &types.ContainerListOptions{
|
options := &types.ContainerListOptions{
|
||||||
All: opts.all,
|
All: opts.all,
|
||||||
|
@ -91,20 +71,32 @@ func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, er
|
||||||
options.Limit = 1
|
options.Limit = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := templates.Parse(opts.format)
|
options.Size = opts.size
|
||||||
|
if !options.Size && len(opts.format) > 0 {
|
||||||
|
// The --size option isn't set, but .Size may be used in the template.
|
||||||
|
// Parse and execute the given template to detect if the .Size field is
|
||||||
|
// used. If it is, then automatically enable the --size option. See #24696
|
||||||
|
//
|
||||||
|
// Only requesting container size information when needed is an optimization,
|
||||||
|
// because calculating the size is a costly operation.
|
||||||
|
tmpl, err := templates.NewParse("", opts.format)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "failed to parse template")
|
||||||
}
|
}
|
||||||
|
|
||||||
optionsProcessor := listOptionsProcessor{}
|
optionsProcessor := formatter.NewContainerContext()
|
||||||
// This shouldn't error out but swallowing the error makes it harder
|
|
||||||
// to track down if preProcessor issues come up. Ref #24696
|
// This shouldn't error out but swallowing the error makes it harder
|
||||||
if err := tmpl.Execute(ioutil.Discard, optionsProcessor); err != nil {
|
// to track down if preProcessor issues come up.
|
||||||
return nil, err
|
if err := tmpl.Execute(ioutil.Discard, optionsProcessor); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to execute template")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := optionsProcessor.FieldsUsed["Size"]; ok {
|
||||||
|
options.Size = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// At the moment all we need is to capture .Size for preprocessor
|
|
||||||
options.Size = opts.size || optionsProcessor["size"]
|
|
||||||
|
|
||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||||
last: 5,
|
last: 5,
|
||||||
filter: filters,
|
filter: filters,
|
||||||
// With .Size, size should be true
|
// With .Size, size should be true
|
||||||
format: "{{.Size}} {{.CreatedAt}} {{.Networks}}",
|
format: "{{.Size}} {{.CreatedAt}} {{upper .Networks}}",
|
||||||
},
|
},
|
||||||
expectedAll: true,
|
expectedAll: true,
|
||||||
expectedSize: true,
|
expectedSize: true,
|
||||||
|
|
|
@ -62,24 +62,32 @@ ports: {{- pad .Ports 1 0}}
|
||||||
func ContainerWrite(ctx Context, containers []types.Container) error {
|
func ContainerWrite(ctx Context, containers []types.Container) error {
|
||||||
render := func(format func(subContext SubContext) error) error {
|
render := func(format func(subContext SubContext) error) error {
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
err := format(&containerContext{trunc: ctx.Trunc, c: container})
|
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return ctx.Write(newContainerContext(), render)
|
return ctx.Write(NewContainerContext(), render)
|
||||||
}
|
}
|
||||||
|
|
||||||
type containerContext struct {
|
// ContainerContext is a struct used for rendering a list of containers in a Go template.
|
||||||
|
type ContainerContext struct {
|
||||||
HeaderContext
|
HeaderContext
|
||||||
trunc bool
|
trunc bool
|
||||||
c types.Container
|
c types.Container
|
||||||
|
|
||||||
|
// FieldsUsed is used in the pre-processing step to detect which fields are
|
||||||
|
// used in the template. It's currently only used to detect use of the .Size
|
||||||
|
// field which (if used) automatically sets the '--size' option when making
|
||||||
|
// the API call.
|
||||||
|
FieldsUsed map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContainerContext() *containerContext {
|
// NewContainerContext creates a new context for rendering containers
|
||||||
containerCtx := containerContext{}
|
func NewContainerContext() *ContainerContext {
|
||||||
|
containerCtx := ContainerContext{}
|
||||||
containerCtx.Header = SubHeaderContext{
|
containerCtx.Header = SubHeaderContext{
|
||||||
"ID": ContainerIDHeader,
|
"ID": ContainerIDHeader,
|
||||||
"Names": namesHeader,
|
"Names": namesHeader,
|
||||||
|
@ -99,18 +107,24 @@ func newContainerContext() *containerContext {
|
||||||
return &containerCtx
|
return &containerCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) MarshalJSON() ([]byte, error) {
|
// MarshalJSON makes ContainerContext implement json.Marshaler
|
||||||
|
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
|
||||||
return MarshalJSON(c)
|
return MarshalJSON(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) ID() string {
|
// ID returns the container's ID as a string. Depending on the `--no-trunc`
|
||||||
|
// option being set, the full or truncated ID is returned.
|
||||||
|
func (c *ContainerContext) ID() string {
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
return stringid.TruncateID(c.c.ID)
|
return stringid.TruncateID(c.c.ID)
|
||||||
}
|
}
|
||||||
return c.c.ID
|
return c.c.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Names() string {
|
// Names returns a comma-separated string of the container's names, with their
|
||||||
|
// slash (/) prefix stripped. Additional names for the container (related to the
|
||||||
|
// legacy `--link` feature) are omitted.
|
||||||
|
func (c *ContainerContext) Names() string {
|
||||||
names := stripNamePrefix(c.c.Names)
|
names := stripNamePrefix(c.c.Names)
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
@ -123,7 +137,9 @@ func (c *containerContext) Names() string {
|
||||||
return strings.Join(names, ",")
|
return strings.Join(names, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Image() string {
|
// Image returns the container's image reference. If the trunc option is set,
|
||||||
|
// the image's registry digest can be included.
|
||||||
|
func (c *ContainerContext) Image() string {
|
||||||
if c.c.Image == "" {
|
if c.c.Image == "" {
|
||||||
return "<no image>"
|
return "<no image>"
|
||||||
}
|
}
|
||||||
|
@ -150,7 +166,9 @@ func (c *containerContext) Image() string {
|
||||||
return c.c.Image
|
return c.c.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Command() string {
|
// Command returns's the container's command. If the trunc option is set, the
|
||||||
|
// returned command is truncated (ellipsized).
|
||||||
|
func (c *ContainerContext) Command() string {
|
||||||
command := c.c.Command
|
command := c.c.Command
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
command = Ellipsis(command, 20)
|
command = Ellipsis(command, 20)
|
||||||
|
@ -158,28 +176,46 @@ func (c *containerContext) Command() string {
|
||||||
return strconv.Quote(command)
|
return strconv.Quote(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) CreatedAt() string {
|
// CreatedAt returns the "Created" date/time of the container as a unix timestamp.
|
||||||
|
func (c *ContainerContext) CreatedAt() string {
|
||||||
return time.Unix(c.c.Created, 0).String()
|
return time.Unix(c.c.Created, 0).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) RunningFor() string {
|
// RunningFor returns a human-readable representation of the duration for which
|
||||||
|
// the container has been running.
|
||||||
|
//
|
||||||
|
// Note that this duration is calculated on the client, and as such is influenced
|
||||||
|
// by clock skew between the client and the daemon.
|
||||||
|
func (c *ContainerContext) RunningFor() string {
|
||||||
createdAt := time.Unix(c.c.Created, 0)
|
createdAt := time.Unix(c.c.Created, 0)
|
||||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Ports() string {
|
// Ports returns a comma-separated string representing open ports of the container
|
||||||
|
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
|
||||||
|
// it's used by command 'docker ps'
|
||||||
|
// Both published and exposed ports are included.
|
||||||
|
func (c *ContainerContext) Ports() string {
|
||||||
return DisplayablePorts(c.c.Ports)
|
return DisplayablePorts(c.c.Ports)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) State() string {
|
// State returns the container's current state (e.g. "running" or "paused")
|
||||||
|
func (c *ContainerContext) State() string {
|
||||||
return c.c.State
|
return c.c.State
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Status() string {
|
// Status returns the container's status in a human readable form (for example,
|
||||||
|
// "Up 24 hours" or "Exited (0) 8 days ago")
|
||||||
|
func (c *ContainerContext) Status() string {
|
||||||
return c.c.Status
|
return c.c.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Size() string {
|
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
|
||||||
|
func (c *ContainerContext) Size() string {
|
||||||
|
if c.FieldsUsed == nil {
|
||||||
|
c.FieldsUsed = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
c.FieldsUsed["Size"] = struct{}{}
|
||||||
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
|
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
|
||||||
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
|
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
|
||||||
|
|
||||||
|
@ -190,7 +226,8 @@ func (c *containerContext) Size() string {
|
||||||
return sf
|
return sf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Labels() string {
|
// Labels returns a comma-separated string of labels present on the container.
|
||||||
|
func (c *ContainerContext) Labels() string {
|
||||||
if c.c.Labels == nil {
|
if c.c.Labels == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -202,14 +239,18 @@ func (c *containerContext) Labels() string {
|
||||||
return strings.Join(joinLabels, ",")
|
return strings.Join(joinLabels, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Label(name string) string {
|
// Label returns the value of the label with the given name or an empty string
|
||||||
|
// if the given label does not exist.
|
||||||
|
func (c *ContainerContext) Label(name string) string {
|
||||||
if c.c.Labels == nil {
|
if c.c.Labels == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return c.c.Labels[name]
|
return c.c.Labels[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Mounts() string {
|
// Mounts returns a comma-separated string of mount names present on the container.
|
||||||
|
// If the trunc option is set, names can be truncated (ellipsized).
|
||||||
|
func (c *ContainerContext) Mounts() string {
|
||||||
var name string
|
var name string
|
||||||
var mounts []string
|
var mounts []string
|
||||||
for _, m := range c.c.Mounts {
|
for _, m := range c.c.Mounts {
|
||||||
|
@ -226,7 +267,8 @@ func (c *containerContext) Mounts() string {
|
||||||
return strings.Join(mounts, ",")
|
return strings.Join(mounts, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) LocalVolumes() string {
|
// LocalVolumes returns the number of volumes using the "local" volume driver.
|
||||||
|
func (c *ContainerContext) LocalVolumes() string {
|
||||||
count := 0
|
count := 0
|
||||||
for _, m := range c.c.Mounts {
|
for _, m := range c.c.Mounts {
|
||||||
if m.Driver == "local" {
|
if m.Driver == "local" {
|
||||||
|
@ -237,7 +279,9 @@ func (c *containerContext) LocalVolumes() string {
|
||||||
return fmt.Sprintf("%d", count)
|
return fmt.Sprintf("%d", count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerContext) Networks() string {
|
// Networks returns a comma-separated string of networks that the container is
|
||||||
|
// attached to.
|
||||||
|
func (c *ContainerContext) Networks() string {
|
||||||
if c.c.NetworkSettings == nil {
|
if c.c.NetworkSettings == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||||
containerID := stringid.GenerateRandomID()
|
containerID := stringid.GenerateRandomID()
|
||||||
unix := time.Now().Add(-65 * time.Second).Unix()
|
unix := time.Now().Add(-65 * time.Second).Unix()
|
||||||
|
|
||||||
var ctx containerContext
|
var ctx ContainerContext
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
container types.Container
|
container types.Container
|
||||||
trunc bool
|
trunc bool
|
||||||
|
@ -87,7 +87,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
ctx = containerContext{c: c.container, trunc: c.trunc}
|
ctx = ContainerContext{c: c.container, trunc: c.trunc}
|
||||||
v := c.call()
|
v := c.call()
|
||||||
if strings.Contains(v, ",") {
|
if strings.Contains(v, ",") {
|
||||||
compareMultipleValues(t, v, c.expValue)
|
compareMultipleValues(t, v, c.expValue)
|
||||||
|
@ -97,7 +97,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
||||||
ctx = containerContext{c: c1, trunc: true}
|
ctx = ContainerContext{c: c1, trunc: true}
|
||||||
|
|
||||||
sid := ctx.Label("com.docker.swarm.swarm-id")
|
sid := ctx.Label("com.docker.swarm.swarm-id")
|
||||||
node := ctx.Label("com.docker.swarm.node_name")
|
node := ctx.Label("com.docker.swarm.node_name")
|
||||||
|
@ -110,7 +110,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c2 := types.Container{}
|
c2 := types.Container{}
|
||||||
ctx = containerContext{c: c2, trunc: true}
|
ctx = ContainerContext{c: c2, trunc: true}
|
||||||
|
|
||||||
label := ctx.Label("anything.really")
|
label := ctx.Label("anything.really")
|
||||||
if label != "" {
|
if label != "" {
|
||||||
|
|
|
@ -136,7 +136,7 @@ func (ctx *DiskUsageContext) Write() (err error) {
|
||||||
|
|
||||||
type diskUsageContext struct {
|
type diskUsageContext struct {
|
||||||
Images []*imageContext
|
Images []*imageContext
|
||||||
Containers []*containerContext
|
Containers []*ContainerContext
|
||||||
Volumes []*volumeContext
|
Volumes []*volumeContext
|
||||||
BuildCache []*buildCacheContext
|
BuildCache []*buildCacheContext
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,7 @@ type diskUsageContext struct {
|
||||||
func (ctx *DiskUsageContext) verboseWrite() error {
|
func (ctx *DiskUsageContext) verboseWrite() error {
|
||||||
duc := &diskUsageContext{
|
duc := &diskUsageContext{
|
||||||
Images: make([]*imageContext, 0, len(ctx.Images)),
|
Images: make([]*imageContext, 0, len(ctx.Images)),
|
||||||
Containers: make([]*containerContext, 0, len(ctx.Containers)),
|
Containers: make([]*ContainerContext, 0, len(ctx.Containers)),
|
||||||
Volumes: make([]*volumeContext, 0, len(ctx.Volumes)),
|
Volumes: make([]*volumeContext, 0, len(ctx.Volumes)),
|
||||||
BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
|
BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||||
for _, c := range ctx.Containers {
|
for _, c := range ctx.Containers {
|
||||||
// Don't display the virtual size
|
// Don't display the virtual size
|
||||||
c.SizeRootFs = 0
|
c.SizeRootFs = 0
|
||||||
duc.Containers = append(duc.Containers, &containerContext{trunc: trunc, c: *c})
|
duc.Containers = append(duc.Containers, &ContainerContext{trunc: trunc, c: *c})
|
||||||
}
|
}
|
||||||
|
|
||||||
// And volumes
|
// And volumes
|
||||||
|
@ -227,7 +227,7 @@ func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.postFormat(tmpl, newContainerContext())
|
ctx.postFormat(tmpl, NewContainerContext())
|
||||||
|
|
||||||
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
|
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue