This fix addresses an issue where including the `{{.Size}}` format
field in conjunction with `docker ps --format`, without the `--size`
flag, would not correctly display the size of containers.
This is done by doing a check on the custom format string, and setting
the size flag on the options struct if the field is found. This struct
gets passed to the engine API which then generates the correct query.
An integration test is included which runs `docker ps --format "table
{{.Size}}"` without `--size`, and checks that the returned output is
not `0 B`.
Fixes #21991
As suggested by @cpuguy83, a parser is implemented to process the format
string as a template, and then traverses the template tree to determine
if `.Size` was called.
This was then reworked by making use of template execution with a
pre-processor struct that will set the `--size` option if the template
calls for the field.
The pre-processor now also sets a boolean in the context passed to the
writer. There is an integration test for this that calls `docker ps
--size --format "{{.Size}}"` and then checks that `size: {{.Size}}` is
not appended, as it would with previous behavior.
Finally, a change was made to the formatter to not automatically
add a `{{.Size}}` if a custom format is provided.
Signed-off-by: Paulo Ribeiro <paigr.io@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Upstream-commit: 55cdb6dcd0f709301573ddb9f3348f9288572b91
Component: engine
555 lines
11 KiB
Go
555 lines
11 KiB
Go
package formatter
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/engine-api/types"
|
|
)
|
|
|
|
func TestContainerContextWrite(t *testing.T) {
|
|
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
|
expectedTime := time.Unix(unixTime, 0).String()
|
|
|
|
contexts := []struct {
|
|
context ContainerContext
|
|
expected string
|
|
}{
|
|
// Errors
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "{{InvalidFunction}}",
|
|
},
|
|
},
|
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
|
`,
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "{{nil}}",
|
|
},
|
|
},
|
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
|
`,
|
|
},
|
|
// Table Format
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table",
|
|
},
|
|
},
|
|
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
|
containerID1 ubuntu "" 24 hours ago foobar_baz
|
|
containerID2 ubuntu "" 24 hours ago foobar_bar
|
|
`,
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}",
|
|
},
|
|
},
|
|
"IMAGE\nubuntu\nubuntu\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}",
|
|
},
|
|
Size: true,
|
|
},
|
|
"IMAGE\nubuntu\nubuntu\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}",
|
|
Quiet: true,
|
|
},
|
|
},
|
|
"IMAGE\nubuntu\nubuntu\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table",
|
|
Quiet: true,
|
|
},
|
|
},
|
|
"containerID1\ncontainerID2\n",
|
|
},
|
|
// Raw Format
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "raw",
|
|
},
|
|
},
|
|
fmt.Sprintf(`container_id: containerID1
|
|
image: ubuntu
|
|
command: ""
|
|
created_at: %s
|
|
status:
|
|
names: foobar_baz
|
|
labels:
|
|
ports:
|
|
|
|
container_id: containerID2
|
|
image: ubuntu
|
|
command: ""
|
|
created_at: %s
|
|
status:
|
|
names: foobar_bar
|
|
labels:
|
|
ports:
|
|
|
|
`, expectedTime, expectedTime),
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "raw",
|
|
},
|
|
Size: true,
|
|
},
|
|
fmt.Sprintf(`container_id: containerID1
|
|
image: ubuntu
|
|
command: ""
|
|
created_at: %s
|
|
status:
|
|
names: foobar_baz
|
|
labels:
|
|
ports:
|
|
size: 0 B
|
|
|
|
container_id: containerID2
|
|
image: ubuntu
|
|
command: ""
|
|
created_at: %s
|
|
status:
|
|
names: foobar_bar
|
|
labels:
|
|
ports:
|
|
size: 0 B
|
|
|
|
`, expectedTime, expectedTime),
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "raw",
|
|
Quiet: true,
|
|
},
|
|
},
|
|
"container_id: containerID1\ncontainer_id: containerID2\n",
|
|
},
|
|
// Custom Format
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "{{.Image}}",
|
|
},
|
|
},
|
|
"ubuntu\nubuntu\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "{{.Image}}",
|
|
},
|
|
Size: true,
|
|
},
|
|
"ubuntu\nubuntu\n",
|
|
},
|
|
}
|
|
|
|
for _, context := range contexts {
|
|
containers := []types.Container{
|
|
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
|
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
|
}
|
|
out := bytes.NewBufferString("")
|
|
context.context.Output = out
|
|
context.context.Containers = containers
|
|
context.context.Write()
|
|
actual := out.String()
|
|
if actual != context.expected {
|
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
}
|
|
// Clean buffer
|
|
out.Reset()
|
|
}
|
|
}
|
|
|
|
func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
|
out := bytes.NewBufferString("")
|
|
containers := []types.Container{}
|
|
|
|
contexts := []struct {
|
|
context ContainerContext
|
|
expected string
|
|
}{
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "{{.Image}}",
|
|
Output: out,
|
|
},
|
|
},
|
|
"",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}",
|
|
Output: out,
|
|
},
|
|
},
|
|
"IMAGE\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "{{.Image}}",
|
|
Output: out,
|
|
},
|
|
Size: true,
|
|
},
|
|
"",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}",
|
|
Output: out,
|
|
},
|
|
Size: true,
|
|
},
|
|
"IMAGE\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}\t{{.Size}}",
|
|
Output: out,
|
|
},
|
|
},
|
|
"IMAGE SIZE\n",
|
|
},
|
|
{
|
|
ContainerContext{
|
|
Context: Context{
|
|
Format: "table {{.Image}}\t{{.Size}}",
|
|
Output: out,
|
|
},
|
|
Size: true,
|
|
},
|
|
"IMAGE SIZE\n",
|
|
},
|
|
}
|
|
|
|
for _, context := range contexts {
|
|
context.context.Containers = containers
|
|
context.context.Write()
|
|
actual := out.String()
|
|
if actual != context.expected {
|
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
}
|
|
// Clean buffer
|
|
out.Reset()
|
|
}
|
|
}
|
|
|
|
func TestImageContextWrite(t *testing.T) {
|
|
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
|
expectedTime := time.Unix(unixTime, 0).String()
|
|
|
|
contexts := []struct {
|
|
context ImageContext
|
|
expected string
|
|
}{
|
|
// Errors
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "{{InvalidFunction}}",
|
|
},
|
|
},
|
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
|
`,
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "{{nil}}",
|
|
},
|
|
},
|
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
|
`,
|
|
},
|
|
// Table Format
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table",
|
|
},
|
|
},
|
|
`REPOSITORY TAG IMAGE ID CREATED SIZE
|
|
image tag1 imageID1 24 hours ago 0 B
|
|
image <none> imageID1 24 hours ago 0 B
|
|
image tag2 imageID2 24 hours ago 0 B
|
|
<none> <none> imageID3 24 hours ago 0 B
|
|
`,
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table {{.Repository}}",
|
|
},
|
|
},
|
|
"REPOSITORY\nimage\nimage\nimage\n<none>\n",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table {{.Repository}}",
|
|
},
|
|
Digest: true,
|
|
},
|
|
`REPOSITORY DIGEST
|
|
image <none>
|
|
image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
|
image <none>
|
|
<none> <none>
|
|
`,
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table {{.Repository}}",
|
|
Quiet: true,
|
|
},
|
|
},
|
|
"REPOSITORY\nimage\nimage\nimage\n<none>\n",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table",
|
|
Quiet: true,
|
|
},
|
|
},
|
|
"imageID1\nimageID1\nimageID2\nimageID3\n",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table",
|
|
Quiet: false,
|
|
},
|
|
Digest: true,
|
|
},
|
|
`REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
|
|
image tag1 <none> imageID1 24 hours ago 0 B
|
|
image <none> sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B
|
|
image tag2 <none> imageID2 24 hours ago 0 B
|
|
<none> <none> <none> imageID3 24 hours ago 0 B
|
|
`,
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table",
|
|
Quiet: true,
|
|
},
|
|
Digest: true,
|
|
},
|
|
"imageID1\nimageID1\nimageID2\nimageID3\n",
|
|
},
|
|
// Raw Format
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "raw",
|
|
},
|
|
},
|
|
fmt.Sprintf(`repository: image
|
|
tag: tag1
|
|
image_id: imageID1
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
repository: image
|
|
tag: <none>
|
|
image_id: imageID1
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
repository: image
|
|
tag: tag2
|
|
image_id: imageID2
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
repository: <none>
|
|
tag: <none>
|
|
image_id: imageID3
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
`, expectedTime, expectedTime, expectedTime, expectedTime),
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "raw",
|
|
},
|
|
Digest: true,
|
|
},
|
|
fmt.Sprintf(`repository: image
|
|
tag: tag1
|
|
digest: <none>
|
|
image_id: imageID1
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
repository: image
|
|
tag: <none>
|
|
digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
|
image_id: imageID1
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
repository: image
|
|
tag: tag2
|
|
digest: <none>
|
|
image_id: imageID2
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
repository: <none>
|
|
tag: <none>
|
|
digest: <none>
|
|
image_id: imageID3
|
|
created_at: %s
|
|
virtual_size: 0 B
|
|
|
|
`, expectedTime, expectedTime, expectedTime, expectedTime),
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "raw",
|
|
Quiet: true,
|
|
},
|
|
},
|
|
`image_id: imageID1
|
|
image_id: imageID1
|
|
image_id: imageID2
|
|
image_id: imageID3
|
|
`,
|
|
},
|
|
// Custom Format
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "{{.Repository}}",
|
|
},
|
|
},
|
|
"image\nimage\nimage\n<none>\n",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "{{.Repository}}",
|
|
},
|
|
Digest: true,
|
|
},
|
|
"image\nimage\nimage\n<none>\n",
|
|
},
|
|
}
|
|
|
|
for _, context := range contexts {
|
|
images := []types.Image{
|
|
{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
|
{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
|
{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
|
}
|
|
out := bytes.NewBufferString("")
|
|
context.context.Output = out
|
|
context.context.Images = images
|
|
context.context.Write()
|
|
actual := out.String()
|
|
if actual != context.expected {
|
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
}
|
|
// Clean buffer
|
|
out.Reset()
|
|
}
|
|
}
|
|
|
|
func TestImageContextWriteWithNoImage(t *testing.T) {
|
|
out := bytes.NewBufferString("")
|
|
images := []types.Image{}
|
|
|
|
contexts := []struct {
|
|
context ImageContext
|
|
expected string
|
|
}{
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "{{.Repository}}",
|
|
Output: out,
|
|
},
|
|
},
|
|
"",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table {{.Repository}}",
|
|
Output: out,
|
|
},
|
|
},
|
|
"REPOSITORY\n",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "{{.Repository}}",
|
|
Output: out,
|
|
},
|
|
Digest: true,
|
|
},
|
|
"",
|
|
},
|
|
{
|
|
ImageContext{
|
|
Context: Context{
|
|
Format: "table {{.Repository}}",
|
|
Output: out,
|
|
},
|
|
Digest: true,
|
|
},
|
|
"REPOSITORY DIGEST\n",
|
|
},
|
|
}
|
|
|
|
for _, context := range contexts {
|
|
context.context.Images = images
|
|
context.context.Write()
|
|
actual := out.String()
|
|
if actual != context.expected {
|
|
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
}
|
|
// Clean buffer
|
|
out.Reset()
|
|
}
|
|
}
|