- rename `api/client/ps` to `api/client/formatter`
- add a a image formatter
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
| ... | ... |
@@ -68,11 +68,17 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
|
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 | 70 |
// PsFormat returns the format string specified in the configuration. |
| 71 |
-// String contains columns and format specification, for example {{ID}\t{{Name}}.
|
|
| 71 |
+// String contains columns and format specification, for example {{ID}}\t{{Name}}.
|
|
| 72 | 72 |
func (cli *DockerCli) PsFormat() string {
|
| 73 | 73 |
return cli.configFile.PsFormat |
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 |
+// ImagesFormat returns the format string specified in the configuration. |
|
| 77 |
+// String contains columns and format specification, for example {{ID}}\t{{Name}}.
|
|
| 78 |
+func (cli *DockerCli) ImagesFormat() string {
|
|
| 79 |
+ return cli.configFile.ImagesFormat |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 76 | 82 |
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. |
| 77 | 83 |
// The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config |
| 78 | 84 |
// is set the client scheme will be set to https. |
| 79 | 85 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,222 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strconv" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api" |
|
| 9 |
+ "github.com/docker/docker/api/types" |
|
| 10 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 11 |
+ "github.com/docker/docker/pkg/stringutils" |
|
| 12 |
+ "github.com/docker/go-units" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+const ( |
|
| 16 |
+ tableKey = "table" |
|
| 17 |
+ |
|
| 18 |
+ containerIDHeader = "CONTAINER ID" |
|
| 19 |
+ imageHeader = "IMAGE" |
|
| 20 |
+ namesHeader = "NAMES" |
|
| 21 |
+ commandHeader = "COMMAND" |
|
| 22 |
+ createdSinceHeader = "CREATED" |
|
| 23 |
+ createdAtHeader = "CREATED AT" |
|
| 24 |
+ runningForHeader = "CREATED" |
|
| 25 |
+ statusHeader = "STATUS" |
|
| 26 |
+ portsHeader = "PORTS" |
|
| 27 |
+ sizeHeader = "SIZE" |
|
| 28 |
+ labelsHeader = "LABELS" |
|
| 29 |
+ imageIDHeader = "IMAGE ID" |
|
| 30 |
+ repositoryHeader = "REPOSITORY" |
|
| 31 |
+ tagHeader = "TAG" |
|
| 32 |
+ digestHeader = "DIGEST" |
|
| 33 |
+) |
|
| 34 |
+ |
|
| 35 |
+type containerContext struct {
|
|
| 36 |
+ baseSubContext |
|
| 37 |
+ trunc bool |
|
| 38 |
+ c types.Container |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+func (c *containerContext) ID() string {
|
|
| 42 |
+ c.addHeader(containerIDHeader) |
|
| 43 |
+ if c.trunc {
|
|
| 44 |
+ return stringid.TruncateID(c.c.ID) |
|
| 45 |
+ } |
|
| 46 |
+ return c.c.ID |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+func (c *containerContext) Names() string {
|
|
| 50 |
+ c.addHeader(namesHeader) |
|
| 51 |
+ names := stripNamePrefix(c.c.Names) |
|
| 52 |
+ if c.trunc {
|
|
| 53 |
+ for _, name := range names {
|
|
| 54 |
+ if len(strings.Split(name, "/")) == 1 {
|
|
| 55 |
+ names = []string{name}
|
|
| 56 |
+ break |
|
| 57 |
+ } |
|
| 58 |
+ } |
|
| 59 |
+ } |
|
| 60 |
+ return strings.Join(names, ",") |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+func (c *containerContext) Image() string {
|
|
| 64 |
+ c.addHeader(imageHeader) |
|
| 65 |
+ if c.c.Image == "" {
|
|
| 66 |
+ return "<no image>" |
|
| 67 |
+ } |
|
| 68 |
+ if c.trunc {
|
|
| 69 |
+ if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
|
| 70 |
+ return trunc |
|
| 71 |
+ } |
|
| 72 |
+ } |
|
| 73 |
+ return c.c.Image |
|
| 74 |
+} |
|
| 75 |
+ |
|
| 76 |
+func (c *containerContext) Command() string {
|
|
| 77 |
+ c.addHeader(commandHeader) |
|
| 78 |
+ command := c.c.Command |
|
| 79 |
+ if c.trunc {
|
|
| 80 |
+ command = stringutils.Truncate(command, 20) |
|
| 81 |
+ } |
|
| 82 |
+ return strconv.Quote(command) |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+func (c *containerContext) CreatedAt() string {
|
|
| 86 |
+ c.addHeader(createdAtHeader) |
|
| 87 |
+ return time.Unix(int64(c.c.Created), 0).String() |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+func (c *containerContext) RunningFor() string {
|
|
| 91 |
+ c.addHeader(runningForHeader) |
|
| 92 |
+ createdAt := time.Unix(int64(c.c.Created), 0) |
|
| 93 |
+ return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+func (c *containerContext) Ports() string {
|
|
| 97 |
+ c.addHeader(portsHeader) |
|
| 98 |
+ return api.DisplayablePorts(c.c.Ports) |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+func (c *containerContext) Status() string {
|
|
| 102 |
+ c.addHeader(statusHeader) |
|
| 103 |
+ return c.c.Status |
|
| 104 |
+} |
|
| 105 |
+ |
|
| 106 |
+func (c *containerContext) Size() string {
|
|
| 107 |
+ c.addHeader(sizeHeader) |
|
| 108 |
+ srw := units.HumanSize(float64(c.c.SizeRw)) |
|
| 109 |
+ sv := units.HumanSize(float64(c.c.SizeRootFs)) |
|
| 110 |
+ |
|
| 111 |
+ sf := srw |
|
| 112 |
+ if c.c.SizeRootFs > 0 {
|
|
| 113 |
+ sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
|
| 114 |
+ } |
|
| 115 |
+ return sf |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+func (c *containerContext) Labels() string {
|
|
| 119 |
+ c.addHeader(labelsHeader) |
|
| 120 |
+ if c.c.Labels == nil {
|
|
| 121 |
+ return "" |
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ var joinLabels []string |
|
| 125 |
+ for k, v := range c.c.Labels {
|
|
| 126 |
+ joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
|
| 127 |
+ } |
|
| 128 |
+ return strings.Join(joinLabels, ",") |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+func (c *containerContext) Label(name string) string {
|
|
| 132 |
+ n := strings.Split(name, ".") |
|
| 133 |
+ r := strings.NewReplacer("-", " ", "_", " ")
|
|
| 134 |
+ h := r.Replace(n[len(n)-1]) |
|
| 135 |
+ |
|
| 136 |
+ c.addHeader(h) |
|
| 137 |
+ |
|
| 138 |
+ if c.c.Labels == nil {
|
|
| 139 |
+ return "" |
|
| 140 |
+ } |
|
| 141 |
+ return c.c.Labels[name] |
|
| 142 |
+} |
|
| 143 |
+ |
|
| 144 |
+type imageContext struct {
|
|
| 145 |
+ baseSubContext |
|
| 146 |
+ trunc bool |
|
| 147 |
+ i types.Image |
|
| 148 |
+ repo string |
|
| 149 |
+ tag string |
|
| 150 |
+ digest string |
|
| 151 |
+} |
|
| 152 |
+ |
|
| 153 |
+func (c *imageContext) ID() string {
|
|
| 154 |
+ c.addHeader(imageIDHeader) |
|
| 155 |
+ if c.trunc {
|
|
| 156 |
+ return stringid.TruncateID(c.i.ID) |
|
| 157 |
+ } |
|
| 158 |
+ return c.i.ID |
|
| 159 |
+} |
|
| 160 |
+ |
|
| 161 |
+func (c *imageContext) Repository() string {
|
|
| 162 |
+ c.addHeader(repositoryHeader) |
|
| 163 |
+ return c.repo |
|
| 164 |
+} |
|
| 165 |
+ |
|
| 166 |
+func (c *imageContext) Tag() string {
|
|
| 167 |
+ c.addHeader(tagHeader) |
|
| 168 |
+ return c.tag |
|
| 169 |
+} |
|
| 170 |
+ |
|
| 171 |
+func (c *imageContext) Digest() string {
|
|
| 172 |
+ c.addHeader(digestHeader) |
|
| 173 |
+ return c.digest |
|
| 174 |
+} |
|
| 175 |
+ |
|
| 176 |
+func (c *imageContext) CreatedSince() string {
|
|
| 177 |
+ c.addHeader(createdSinceHeader) |
|
| 178 |
+ createdAt := time.Unix(int64(c.i.Created), 0) |
|
| 179 |
+ return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 182 |
+func (c *imageContext) CreatedAt() string {
|
|
| 183 |
+ c.addHeader(createdAtHeader) |
|
| 184 |
+ return time.Unix(int64(c.i.Created), 0).String() |
|
| 185 |
+} |
|
| 186 |
+ |
|
| 187 |
+func (c *imageContext) Size() string {
|
|
| 188 |
+ c.addHeader(sizeHeader) |
|
| 189 |
+ return units.HumanSize(float64(c.i.Size)) |
|
| 190 |
+} |
|
| 191 |
+ |
|
| 192 |
+type subContext interface {
|
|
| 193 |
+ fullHeader() string |
|
| 194 |
+ addHeader(header string) |
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+type baseSubContext struct {
|
|
| 198 |
+ header []string |
|
| 199 |
+} |
|
| 200 |
+ |
|
| 201 |
+func (c *baseSubContext) fullHeader() string {
|
|
| 202 |
+ if c.header == nil {
|
|
| 203 |
+ return "" |
|
| 204 |
+ } |
|
| 205 |
+ return strings.Join(c.header, "\t") |
|
| 206 |
+} |
|
| 207 |
+ |
|
| 208 |
+func (c *baseSubContext) addHeader(header string) {
|
|
| 209 |
+ if c.header == nil {
|
|
| 210 |
+ c.header = []string{}
|
|
| 211 |
+ } |
|
| 212 |
+ c.header = append(c.header, strings.ToUpper(header)) |
|
| 213 |
+} |
|
| 214 |
+ |
|
| 215 |
+func stripNamePrefix(ss []string) []string {
|
|
| 216 |
+ for i, s := range ss {
|
|
| 217 |
+ ss[i] = s[1:] |
|
| 218 |
+ } |
|
| 219 |
+ |
|
| 220 |
+ return ss |
|
| 221 |
+} |
| 0 | 222 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,192 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "reflect" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/types" |
|
| 9 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func TestContainerPsContext(t *testing.T) {
|
|
| 13 |
+ containerID := stringid.GenerateRandomID() |
|
| 14 |
+ unix := time.Now().Unix() |
|
| 15 |
+ |
|
| 16 |
+ var ctx containerContext |
|
| 17 |
+ cases := []struct {
|
|
| 18 |
+ container types.Container |
|
| 19 |
+ trunc bool |
|
| 20 |
+ expValue string |
|
| 21 |
+ expHeader string |
|
| 22 |
+ call func() string |
|
| 23 |
+ }{
|
|
| 24 |
+ {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
|
|
| 25 |
+ {types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
|
|
| 26 |
+ {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
|
|
| 27 |
+ {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
|
|
| 28 |
+ {types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
|
|
| 29 |
+ {types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
|
|
| 30 |
+ {types.Container{
|
|
| 31 |
+ Image: "a5a665ff33eced1e0803148700880edab4", |
|
| 32 |
+ ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", |
|
| 33 |
+ }, |
|
| 34 |
+ true, |
|
| 35 |
+ "a5a665ff33ec", |
|
| 36 |
+ imageHeader, |
|
| 37 |
+ ctx.Image, |
|
| 38 |
+ }, |
|
| 39 |
+ {types.Container{
|
|
| 40 |
+ Image: "a5a665ff33eced1e0803148700880edab4", |
|
| 41 |
+ ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", |
|
| 42 |
+ }, |
|
| 43 |
+ false, |
|
| 44 |
+ "a5a665ff33eced1e0803148700880edab4", |
|
| 45 |
+ imageHeader, |
|
| 46 |
+ ctx.Image, |
|
| 47 |
+ }, |
|
| 48 |
+ {types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
|
|
| 49 |
+ {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
|
|
| 50 |
+ {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
|
| 51 |
+ {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
|
|
| 52 |
+ {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
|
|
| 53 |
+ {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
|
|
| 54 |
+ {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
|
|
| 55 |
+ {types.Container{}, true, "", labelsHeader, ctx.Labels},
|
|
| 56 |
+ {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
|
|
| 57 |
+ {types.Container{Created: unix}, true, "Less than a second", runningForHeader, ctx.RunningFor},
|
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ for _, c := range cases {
|
|
| 61 |
+ ctx = containerContext{c: c.container, trunc: c.trunc}
|
|
| 62 |
+ v := c.call() |
|
| 63 |
+ if strings.Contains(v, ",") {
|
|
| 64 |
+ compareMultipleValues(t, v, c.expValue) |
|
| 65 |
+ } else if v != c.expValue {
|
|
| 66 |
+ t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ h := ctx.fullHeader() |
|
| 70 |
+ if h != c.expHeader {
|
|
| 71 |
+ t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 72 |
+ } |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
|
| 76 |
+ ctx = containerContext{c: c1, trunc: true}
|
|
| 77 |
+ |
|
| 78 |
+ sid := ctx.Label("com.docker.swarm.swarm-id")
|
|
| 79 |
+ node := ctx.Label("com.docker.swarm.node_name")
|
|
| 80 |
+ if sid != "33" {
|
|
| 81 |
+ t.Fatalf("Expected 33, was %s\n", sid)
|
|
| 82 |
+ } |
|
| 83 |
+ |
|
| 84 |
+ if node != "ubuntu" {
|
|
| 85 |
+ t.Fatalf("Expected ubuntu, was %s\n", node)
|
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ h := ctx.fullHeader() |
|
| 89 |
+ if h != "SWARM ID\tNODE NAME" {
|
|
| 90 |
+ t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
|
| 91 |
+ |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ c2 := types.Container{}
|
|
| 95 |
+ ctx = containerContext{c: c2, trunc: true}
|
|
| 96 |
+ |
|
| 97 |
+ label := ctx.Label("anything.really")
|
|
| 98 |
+ if label != "" {
|
|
| 99 |
+ t.Fatalf("Expected an empty string, was %s", label)
|
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ ctx = containerContext{c: c2, trunc: true}
|
|
| 103 |
+ fullHeader := ctx.fullHeader() |
|
| 104 |
+ if fullHeader != "" {
|
|
| 105 |
+ t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+func TestImagesContext(t *testing.T) {
|
|
| 111 |
+ imageID := stringid.GenerateRandomID() |
|
| 112 |
+ unix := time.Now().Unix() |
|
| 113 |
+ |
|
| 114 |
+ var ctx imageContext |
|
| 115 |
+ cases := []struct {
|
|
| 116 |
+ imageCtx imageContext |
|
| 117 |
+ expValue string |
|
| 118 |
+ expHeader string |
|
| 119 |
+ call func() string |
|
| 120 |
+ }{
|
|
| 121 |
+ {imageContext{
|
|
| 122 |
+ i: types.Image{ID: imageID},
|
|
| 123 |
+ trunc: true, |
|
| 124 |
+ }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, |
|
| 125 |
+ {imageContext{
|
|
| 126 |
+ i: types.Image{ID: imageID},
|
|
| 127 |
+ trunc: false, |
|
| 128 |
+ }, imageID, imageIDHeader, ctx.ID}, |
|
| 129 |
+ {imageContext{
|
|
| 130 |
+ i: types.Image{Size: 10},
|
|
| 131 |
+ trunc: true, |
|
| 132 |
+ }, "10 B", sizeHeader, ctx.Size}, |
|
| 133 |
+ {imageContext{
|
|
| 134 |
+ i: types.Image{Created: unix},
|
|
| 135 |
+ trunc: true, |
|
| 136 |
+ }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, |
|
| 137 |
+ // FIXME |
|
| 138 |
+ // {imageContext{
|
|
| 139 |
+ // i: types.Image{Created: unix},
|
|
| 140 |
+ // trunc: true, |
|
| 141 |
+ // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, |
|
| 142 |
+ {imageContext{
|
|
| 143 |
+ i: types.Image{},
|
|
| 144 |
+ repo: "busybox", |
|
| 145 |
+ }, "busybox", repositoryHeader, ctx.Repository}, |
|
| 146 |
+ {imageContext{
|
|
| 147 |
+ i: types.Image{},
|
|
| 148 |
+ tag: "latest", |
|
| 149 |
+ }, "latest", tagHeader, ctx.Tag}, |
|
| 150 |
+ {imageContext{
|
|
| 151 |
+ i: types.Image{},
|
|
| 152 |
+ digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", |
|
| 153 |
+ }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, |
|
| 154 |
+ } |
|
| 155 |
+ |
|
| 156 |
+ for _, c := range cases {
|
|
| 157 |
+ ctx = c.imageCtx |
|
| 158 |
+ v := c.call() |
|
| 159 |
+ if strings.Contains(v, ",") {
|
|
| 160 |
+ compareMultipleValues(t, v, c.expValue) |
|
| 161 |
+ } else if v != c.expValue {
|
|
| 162 |
+ t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 163 |
+ } |
|
| 164 |
+ |
|
| 165 |
+ h := ctx.fullHeader() |
|
| 166 |
+ if h != c.expHeader {
|
|
| 167 |
+ t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 168 |
+ } |
|
| 169 |
+ } |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+func compareMultipleValues(t *testing.T, value, expected string) {
|
|
| 173 |
+ // comma-separated values means probably a map input, which won't |
|
| 174 |
+ // be guaranteed to have the same order as our expected value |
|
| 175 |
+ // We'll create maps and use reflect.DeepEquals to check instead: |
|
| 176 |
+ entriesMap := make(map[string]string) |
|
| 177 |
+ expMap := make(map[string]string) |
|
| 178 |
+ entries := strings.Split(value, ",") |
|
| 179 |
+ expectedEntries := strings.Split(expected, ",") |
|
| 180 |
+ for _, entry := range entries {
|
|
| 181 |
+ keyval := strings.Split(entry, "=") |
|
| 182 |
+ entriesMap[keyval[0]] = keyval[1] |
|
| 183 |
+ } |
|
| 184 |
+ for _, expected := range expectedEntries {
|
|
| 185 |
+ keyval := strings.Split(expected, "=") |
|
| 186 |
+ expMap[keyval[0]] = keyval[1] |
|
| 187 |
+ } |
|
| 188 |
+ if !reflect.DeepEqual(expMap, entriesMap) {
|
|
| 189 |
+ t.Fatalf("Expected entries: %v, got: %v", expected, value)
|
|
| 190 |
+ } |
|
| 191 |
+} |
| 0 | 192 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,254 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "io" |
|
| 6 |
+ "strings" |
|
| 7 |
+ "text/tabwriter" |
|
| 8 |
+ "text/template" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/types" |
|
| 11 |
+ "github.com/docker/docker/reference" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+const ( |
|
| 15 |
+ tableFormatKey = "table" |
|
| 16 |
+ rawFormatKey = "raw" |
|
| 17 |
+ |
|
| 18 |
+ defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
| 19 |
+ defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
| 20 |
+ defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
| 21 |
+ defaultQuietFormat = "{{.ID}}"
|
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+// Context contains information required by the formatter to print the output as desired. |
|
| 25 |
+type Context struct {
|
|
| 26 |
+ // Output is the output stream to which the formatted string is written. |
|
| 27 |
+ Output io.Writer |
|
| 28 |
+ // Format is used to choose raw, table or custom format for the output. |
|
| 29 |
+ Format string |
|
| 30 |
+ // Quiet when set to true will simply print minimal information. |
|
| 31 |
+ Quiet bool |
|
| 32 |
+ // Trunc when set to true will truncate the output of certain fields such as Container ID. |
|
| 33 |
+ Trunc bool |
|
| 34 |
+ |
|
| 35 |
+ // internal element |
|
| 36 |
+ table bool |
|
| 37 |
+ finalFormat string |
|
| 38 |
+ header string |
|
| 39 |
+ buffer *bytes.Buffer |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func (c *Context) preformat() {
|
|
| 43 |
+ c.finalFormat = c.Format |
|
| 44 |
+ |
|
| 45 |
+ if strings.HasPrefix(c.Format, tableKey) {
|
|
| 46 |
+ c.table = true |
|
| 47 |
+ c.finalFormat = c.finalFormat[len(tableKey):] |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ c.finalFormat = strings.Trim(c.finalFormat, " ") |
|
| 51 |
+ r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") |
|
| 52 |
+ c.finalFormat = r.Replace(c.finalFormat) |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+func (c *Context) parseFormat() (*template.Template, error) {
|
|
| 56 |
+ tmpl, err := template.New("").Parse(c.finalFormat)
|
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
| 59 |
+ c.buffer.WriteTo(c.Output) |
|
| 60 |
+ } |
|
| 61 |
+ return tmpl, err |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
|
| 65 |
+ if c.table {
|
|
| 66 |
+ if len(c.header) == 0 {
|
|
| 67 |
+ // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template |
|
| 68 |
+ tmpl.Execute(bytes.NewBufferString(""), subContext)
|
|
| 69 |
+ c.header = subContext.fullHeader() |
|
| 70 |
+ } |
|
| 71 |
+ |
|
| 72 |
+ t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) |
|
| 73 |
+ t.Write([]byte(c.header)) |
|
| 74 |
+ t.Write([]byte("\n"))
|
|
| 75 |
+ c.buffer.WriteTo(t) |
|
| 76 |
+ t.Flush() |
|
| 77 |
+ } else {
|
|
| 78 |
+ c.buffer.WriteTo(c.Output) |
|
| 79 |
+ } |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
|
|
| 83 |
+ if err := tmpl.Execute(c.buffer, subContext); err != nil {
|
|
| 84 |
+ c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
| 85 |
+ c.buffer.WriteTo(c.Output) |
|
| 86 |
+ return err |
|
| 87 |
+ } |
|
| 88 |
+ if c.table && len(c.header) == 0 {
|
|
| 89 |
+ c.header = subContext.fullHeader() |
|
| 90 |
+ } |
|
| 91 |
+ c.buffer.WriteString("\n")
|
|
| 92 |
+ return nil |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. |
|
| 96 |
+type ContainerContext struct {
|
|
| 97 |
+ Context |
|
| 98 |
+ // Size when set to true will display the size of the output. |
|
| 99 |
+ Size bool |
|
| 100 |
+ // Containers |
|
| 101 |
+ Containers []types.Container |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+// ImageContext contains image specific information required by the formater, encapsulate a Context struct. |
|
| 105 |
+type ImageContext struct {
|
|
| 106 |
+ Context |
|
| 107 |
+ Digest bool |
|
| 108 |
+ // Images |
|
| 109 |
+ Images []types.Image |
|
| 110 |
+} |
|
| 111 |
+ |
|
| 112 |
+func (ctx ContainerContext) Write() {
|
|
| 113 |
+ switch ctx.Format {
|
|
| 114 |
+ case tableFormatKey: |
|
| 115 |
+ ctx.Format = defaultContainerTableFormat |
|
| 116 |
+ if ctx.Quiet {
|
|
| 117 |
+ ctx.Format = defaultQuietFormat |
|
| 118 |
+ } |
|
| 119 |
+ case rawFormatKey: |
|
| 120 |
+ if ctx.Quiet {
|
|
| 121 |
+ ctx.Format = `container_id: {{.ID}}`
|
|
| 122 |
+ } else {
|
|
| 123 |
+ ctx.Format = `container_id: {{.ID}}
|
|
| 124 |
+image: {{.Image}}
|
|
| 125 |
+command: {{.Command}}
|
|
| 126 |
+created_at: {{.CreatedAt}}
|
|
| 127 |
+status: {{.Status}}
|
|
| 128 |
+names: {{.Names}}
|
|
| 129 |
+labels: {{.Labels}}
|
|
| 130 |
+ports: {{.Ports}}
|
|
| 131 |
+` |
|
| 132 |
+ if ctx.Size {
|
|
| 133 |
+ ctx.Format += `size: {{.Size}}
|
|
| 134 |
+` |
|
| 135 |
+ } |
|
| 136 |
+ } |
|
| 137 |
+ } |
|
| 138 |
+ |
|
| 139 |
+ ctx.buffer = bytes.NewBufferString("")
|
|
| 140 |
+ ctx.preformat() |
|
| 141 |
+ if ctx.table && ctx.Size {
|
|
| 142 |
+ ctx.finalFormat += "\t{{.Size}}"
|
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ tmpl, err := ctx.parseFormat() |
|
| 146 |
+ if err != nil {
|
|
| 147 |
+ return |
|
| 148 |
+ } |
|
| 149 |
+ |
|
| 150 |
+ for _, container := range ctx.Containers {
|
|
| 151 |
+ containerCtx := &containerContext{
|
|
| 152 |
+ trunc: ctx.Trunc, |
|
| 153 |
+ c: container, |
|
| 154 |
+ } |
|
| 155 |
+ err = ctx.contextFormat(tmpl, containerCtx) |
|
| 156 |
+ if err != nil {
|
|
| 157 |
+ return |
|
| 158 |
+ } |
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ ctx.postformat(tmpl, &containerContext{})
|
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+func (ctx ImageContext) Write() {
|
|
| 165 |
+ switch ctx.Format {
|
|
| 166 |
+ case tableFormatKey: |
|
| 167 |
+ ctx.Format = defaultImageTableFormat |
|
| 168 |
+ if ctx.Digest {
|
|
| 169 |
+ ctx.Format = defaultImageTableFormatWithDigest |
|
| 170 |
+ } |
|
| 171 |
+ if ctx.Quiet {
|
|
| 172 |
+ ctx.Format = defaultQuietFormat |
|
| 173 |
+ } |
|
| 174 |
+ case rawFormatKey: |
|
| 175 |
+ if ctx.Quiet {
|
|
| 176 |
+ ctx.Format = `image_id: {{.ID}}`
|
|
| 177 |
+ } else {
|
|
| 178 |
+ if ctx.Digest {
|
|
| 179 |
+ ctx.Format = `repository: {{ .Repository }}
|
|
| 180 |
+tag: {{.Tag}}
|
|
| 181 |
+digest: {{.Digest}}
|
|
| 182 |
+image_id: {{.ID}}
|
|
| 183 |
+created_at: {{.CreatedAt}}
|
|
| 184 |
+virtual_size: {{.Size}}
|
|
| 185 |
+` |
|
| 186 |
+ } else {
|
|
| 187 |
+ ctx.Format = `repository: {{ .Repository }}
|
|
| 188 |
+tag: {{.Tag}}
|
|
| 189 |
+image_id: {{.ID}}
|
|
| 190 |
+created_at: {{.CreatedAt}}
|
|
| 191 |
+virtual_size: {{.Size}}
|
|
| 192 |
+` |
|
| 193 |
+ } |
|
| 194 |
+ } |
|
| 195 |
+ } |
|
| 196 |
+ |
|
| 197 |
+ ctx.buffer = bytes.NewBufferString("")
|
|
| 198 |
+ ctx.preformat() |
|
| 199 |
+ if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
|
| 200 |
+ ctx.finalFormat += "\t{{.Digest}}"
|
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ tmpl, err := ctx.parseFormat() |
|
| 204 |
+ if err != nil {
|
|
| 205 |
+ return |
|
| 206 |
+ } |
|
| 207 |
+ |
|
| 208 |
+ for _, image := range ctx.Images {
|
|
| 209 |
+ |
|
| 210 |
+ repoTags := image.RepoTags |
|
| 211 |
+ repoDigests := image.RepoDigests |
|
| 212 |
+ |
|
| 213 |
+ if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
|
|
| 214 |
+ // dangling image - clear out either repoTags or repoDigests so we only show it once below |
|
| 215 |
+ repoDigests = []string{}
|
|
| 216 |
+ } |
|
| 217 |
+ // combine the tags and digests lists |
|
| 218 |
+ tagsAndDigests := append(repoTags, repoDigests...) |
|
| 219 |
+ for _, repoAndRef := range tagsAndDigests {
|
|
| 220 |
+ repo := "<none>" |
|
| 221 |
+ tag := "<none>" |
|
| 222 |
+ digest := "<none>" |
|
| 223 |
+ |
|
| 224 |
+ if !strings.HasPrefix(repoAndRef, "<none>") {
|
|
| 225 |
+ ref, err := reference.ParseNamed(repoAndRef) |
|
| 226 |
+ if err != nil {
|
|
| 227 |
+ continue |
|
| 228 |
+ } |
|
| 229 |
+ repo = ref.Name() |
|
| 230 |
+ |
|
| 231 |
+ switch x := ref.(type) {
|
|
| 232 |
+ case reference.Canonical: |
|
| 233 |
+ digest = x.Digest().String() |
|
| 234 |
+ case reference.NamedTagged: |
|
| 235 |
+ tag = x.Tag() |
|
| 236 |
+ } |
|
| 237 |
+ } |
|
| 238 |
+ imageCtx := &imageContext{
|
|
| 239 |
+ trunc: ctx.Trunc, |
|
| 240 |
+ i: image, |
|
| 241 |
+ repo: repo, |
|
| 242 |
+ tag: tag, |
|
| 243 |
+ digest: digest, |
|
| 244 |
+ } |
|
| 245 |
+ err = ctx.contextFormat(tmpl, imageCtx) |
|
| 246 |
+ if err != nil {
|
|
| 247 |
+ return |
|
| 248 |
+ } |
|
| 249 |
+ } |
|
| 250 |
+ } |
|
| 251 |
+ |
|
| 252 |
+ ctx.postformat(tmpl, &imageContext{})
|
|
| 253 |
+} |
| 0 | 254 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,535 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "testing" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/types" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+func TestContainerContextWrite(t *testing.T) {
|
|
| 12 |
+ unixTime := time.Now().AddDate(0, 0, -1).Unix() |
|
| 13 |
+ expectedTime := time.Unix(unixTime, 0).String() |
|
| 14 |
+ |
|
| 15 |
+ contexts := []struct {
|
|
| 16 |
+ context ContainerContext |
|
| 17 |
+ expected string |
|
| 18 |
+ }{
|
|
| 19 |
+ // Errors |
|
| 20 |
+ {
|
|
| 21 |
+ ContainerContext{
|
|
| 22 |
+ Context: Context{
|
|
| 23 |
+ Format: "{{InvalidFunction}}",
|
|
| 24 |
+ }, |
|
| 25 |
+ }, |
|
| 26 |
+ `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 27 |
+`, |
|
| 28 |
+ }, |
|
| 29 |
+ {
|
|
| 30 |
+ ContainerContext{
|
|
| 31 |
+ Context: Context{
|
|
| 32 |
+ Format: "{{nil}}",
|
|
| 33 |
+ }, |
|
| 34 |
+ }, |
|
| 35 |
+ `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 36 |
+`, |
|
| 37 |
+ }, |
|
| 38 |
+ // Table Format |
|
| 39 |
+ {
|
|
| 40 |
+ ContainerContext{
|
|
| 41 |
+ Context: Context{
|
|
| 42 |
+ Format: "table", |
|
| 43 |
+ }, |
|
| 44 |
+ }, |
|
| 45 |
+ `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
|
| 46 |
+containerID1 ubuntu "" 24 hours ago foobar_baz |
|
| 47 |
+containerID2 ubuntu "" 24 hours ago foobar_bar |
|
| 48 |
+`, |
|
| 49 |
+ }, |
|
| 50 |
+ {
|
|
| 51 |
+ ContainerContext{
|
|
| 52 |
+ Context: Context{
|
|
| 53 |
+ Format: "table {{.Image}}",
|
|
| 54 |
+ }, |
|
| 55 |
+ }, |
|
| 56 |
+ "IMAGE\nubuntu\nubuntu\n", |
|
| 57 |
+ }, |
|
| 58 |
+ {
|
|
| 59 |
+ ContainerContext{
|
|
| 60 |
+ Context: Context{
|
|
| 61 |
+ Format: "table {{.Image}}",
|
|
| 62 |
+ }, |
|
| 63 |
+ Size: true, |
|
| 64 |
+ }, |
|
| 65 |
+ "IMAGE SIZE\nubuntu 0 B\nubuntu 0 B\n", |
|
| 66 |
+ }, |
|
| 67 |
+ {
|
|
| 68 |
+ ContainerContext{
|
|
| 69 |
+ Context: Context{
|
|
| 70 |
+ Format: "table {{.Image}}",
|
|
| 71 |
+ Quiet: true, |
|
| 72 |
+ }, |
|
| 73 |
+ }, |
|
| 74 |
+ "IMAGE\nubuntu\nubuntu\n", |
|
| 75 |
+ }, |
|
| 76 |
+ {
|
|
| 77 |
+ ContainerContext{
|
|
| 78 |
+ Context: Context{
|
|
| 79 |
+ Format: "table", |
|
| 80 |
+ Quiet: true, |
|
| 81 |
+ }, |
|
| 82 |
+ }, |
|
| 83 |
+ "containerID1\ncontainerID2\n", |
|
| 84 |
+ }, |
|
| 85 |
+ // Raw Format |
|
| 86 |
+ {
|
|
| 87 |
+ ContainerContext{
|
|
| 88 |
+ Context: Context{
|
|
| 89 |
+ Format: "raw", |
|
| 90 |
+ }, |
|
| 91 |
+ }, |
|
| 92 |
+ fmt.Sprintf(`container_id: containerID1 |
|
| 93 |
+image: ubuntu |
|
| 94 |
+command: "" |
|
| 95 |
+created_at: %s |
|
| 96 |
+status: |
|
| 97 |
+names: foobar_baz |
|
| 98 |
+labels: |
|
| 99 |
+ports: |
|
| 100 |
+ |
|
| 101 |
+container_id: containerID2 |
|
| 102 |
+image: ubuntu |
|
| 103 |
+command: "" |
|
| 104 |
+created_at: %s |
|
| 105 |
+status: |
|
| 106 |
+names: foobar_bar |
|
| 107 |
+labels: |
|
| 108 |
+ports: |
|
| 109 |
+ |
|
| 110 |
+`, expectedTime, expectedTime), |
|
| 111 |
+ }, |
|
| 112 |
+ {
|
|
| 113 |
+ ContainerContext{
|
|
| 114 |
+ Context: Context{
|
|
| 115 |
+ Format: "raw", |
|
| 116 |
+ }, |
|
| 117 |
+ Size: true, |
|
| 118 |
+ }, |
|
| 119 |
+ fmt.Sprintf(`container_id: containerID1 |
|
| 120 |
+image: ubuntu |
|
| 121 |
+command: "" |
|
| 122 |
+created_at: %s |
|
| 123 |
+status: |
|
| 124 |
+names: foobar_baz |
|
| 125 |
+labels: |
|
| 126 |
+ports: |
|
| 127 |
+size: 0 B |
|
| 128 |
+ |
|
| 129 |
+container_id: containerID2 |
|
| 130 |
+image: ubuntu |
|
| 131 |
+command: "" |
|
| 132 |
+created_at: %s |
|
| 133 |
+status: |
|
| 134 |
+names: foobar_bar |
|
| 135 |
+labels: |
|
| 136 |
+ports: |
|
| 137 |
+size: 0 B |
|
| 138 |
+ |
|
| 139 |
+`, expectedTime, expectedTime), |
|
| 140 |
+ }, |
|
| 141 |
+ {
|
|
| 142 |
+ ContainerContext{
|
|
| 143 |
+ Context: Context{
|
|
| 144 |
+ Format: "raw", |
|
| 145 |
+ Quiet: true, |
|
| 146 |
+ }, |
|
| 147 |
+ }, |
|
| 148 |
+ "container_id: containerID1\ncontainer_id: containerID2\n", |
|
| 149 |
+ }, |
|
| 150 |
+ // Custom Format |
|
| 151 |
+ {
|
|
| 152 |
+ ContainerContext{
|
|
| 153 |
+ Context: Context{
|
|
| 154 |
+ Format: "{{.Image}}",
|
|
| 155 |
+ }, |
|
| 156 |
+ }, |
|
| 157 |
+ "ubuntu\nubuntu\n", |
|
| 158 |
+ }, |
|
| 159 |
+ {
|
|
| 160 |
+ ContainerContext{
|
|
| 161 |
+ Context: Context{
|
|
| 162 |
+ Format: "{{.Image}}",
|
|
| 163 |
+ }, |
|
| 164 |
+ Size: true, |
|
| 165 |
+ }, |
|
| 166 |
+ "ubuntu\nubuntu\n", |
|
| 167 |
+ }, |
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 170 |
+ for _, context := range contexts {
|
|
| 171 |
+ containers := []types.Container{
|
|
| 172 |
+ {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
|
| 173 |
+ {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
|
| 174 |
+ } |
|
| 175 |
+ out := bytes.NewBufferString("")
|
|
| 176 |
+ context.context.Output = out |
|
| 177 |
+ context.context.Containers = containers |
|
| 178 |
+ context.context.Write() |
|
| 179 |
+ actual := out.String() |
|
| 180 |
+ if actual != context.expected {
|
|
| 181 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 182 |
+ } |
|
| 183 |
+ // Clean buffer |
|
| 184 |
+ out.Reset() |
|
| 185 |
+ } |
|
| 186 |
+} |
|
| 187 |
+ |
|
| 188 |
+func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
|
| 189 |
+ out := bytes.NewBufferString("")
|
|
| 190 |
+ containers := []types.Container{}
|
|
| 191 |
+ |
|
| 192 |
+ contexts := []struct {
|
|
| 193 |
+ context ContainerContext |
|
| 194 |
+ expected string |
|
| 195 |
+ }{
|
|
| 196 |
+ {
|
|
| 197 |
+ ContainerContext{
|
|
| 198 |
+ Context: Context{
|
|
| 199 |
+ Format: "{{.Image}}",
|
|
| 200 |
+ Output: out, |
|
| 201 |
+ }, |
|
| 202 |
+ }, |
|
| 203 |
+ "", |
|
| 204 |
+ }, |
|
| 205 |
+ {
|
|
| 206 |
+ ContainerContext{
|
|
| 207 |
+ Context: Context{
|
|
| 208 |
+ Format: "table {{.Image}}",
|
|
| 209 |
+ Output: out, |
|
| 210 |
+ }, |
|
| 211 |
+ }, |
|
| 212 |
+ "IMAGE\n", |
|
| 213 |
+ }, |
|
| 214 |
+ {
|
|
| 215 |
+ ContainerContext{
|
|
| 216 |
+ Context: Context{
|
|
| 217 |
+ Format: "{{.Image}}",
|
|
| 218 |
+ Output: out, |
|
| 219 |
+ }, |
|
| 220 |
+ Size: true, |
|
| 221 |
+ }, |
|
| 222 |
+ "", |
|
| 223 |
+ }, |
|
| 224 |
+ {
|
|
| 225 |
+ ContainerContext{
|
|
| 226 |
+ Context: Context{
|
|
| 227 |
+ Format: "table {{.Image}}",
|
|
| 228 |
+ Output: out, |
|
| 229 |
+ }, |
|
| 230 |
+ Size: true, |
|
| 231 |
+ }, |
|
| 232 |
+ "IMAGE SIZE\n", |
|
| 233 |
+ }, |
|
| 234 |
+ } |
|
| 235 |
+ |
|
| 236 |
+ for _, context := range contexts {
|
|
| 237 |
+ context.context.Containers = containers |
|
| 238 |
+ context.context.Write() |
|
| 239 |
+ actual := out.String() |
|
| 240 |
+ if actual != context.expected {
|
|
| 241 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 242 |
+ } |
|
| 243 |
+ // Clean buffer |
|
| 244 |
+ out.Reset() |
|
| 245 |
+ } |
|
| 246 |
+} |
|
| 247 |
+ |
|
| 248 |
+func TestImageContextWrite(t *testing.T) {
|
|
| 249 |
+ unixTime := time.Now().AddDate(0, 0, -1).Unix() |
|
| 250 |
+ expectedTime := time.Unix(unixTime, 0).String() |
|
| 251 |
+ |
|
| 252 |
+ contexts := []struct {
|
|
| 253 |
+ context ImageContext |
|
| 254 |
+ expected string |
|
| 255 |
+ }{
|
|
| 256 |
+ // Errors |
|
| 257 |
+ {
|
|
| 258 |
+ ImageContext{
|
|
| 259 |
+ Context: Context{
|
|
| 260 |
+ Format: "{{InvalidFunction}}",
|
|
| 261 |
+ }, |
|
| 262 |
+ }, |
|
| 263 |
+ `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 264 |
+`, |
|
| 265 |
+ }, |
|
| 266 |
+ {
|
|
| 267 |
+ ImageContext{
|
|
| 268 |
+ Context: Context{
|
|
| 269 |
+ Format: "{{nil}}",
|
|
| 270 |
+ }, |
|
| 271 |
+ }, |
|
| 272 |
+ `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 273 |
+`, |
|
| 274 |
+ }, |
|
| 275 |
+ // Table Format |
|
| 276 |
+ {
|
|
| 277 |
+ ImageContext{
|
|
| 278 |
+ Context: Context{
|
|
| 279 |
+ Format: "table", |
|
| 280 |
+ }, |
|
| 281 |
+ }, |
|
| 282 |
+ `REPOSITORY TAG IMAGE ID CREATED SIZE |
|
| 283 |
+image tag1 imageID1 24 hours ago 0 B |
|
| 284 |
+image <none> imageID1 24 hours ago 0 B |
|
| 285 |
+image tag2 imageID2 24 hours ago 0 B |
|
| 286 |
+<none> <none> imageID3 24 hours ago 0 B |
|
| 287 |
+`, |
|
| 288 |
+ }, |
|
| 289 |
+ {
|
|
| 290 |
+ ImageContext{
|
|
| 291 |
+ Context: Context{
|
|
| 292 |
+ Format: "table {{.Repository}}",
|
|
| 293 |
+ }, |
|
| 294 |
+ }, |
|
| 295 |
+ "REPOSITORY\nimage\nimage\nimage\n<none>\n", |
|
| 296 |
+ }, |
|
| 297 |
+ {
|
|
| 298 |
+ ImageContext{
|
|
| 299 |
+ Context: Context{
|
|
| 300 |
+ Format: "table {{.Repository}}",
|
|
| 301 |
+ }, |
|
| 302 |
+ Digest: true, |
|
| 303 |
+ }, |
|
| 304 |
+ `REPOSITORY DIGEST |
|
| 305 |
+image <none> |
|
| 306 |
+image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf |
|
| 307 |
+image <none> |
|
| 308 |
+<none> <none> |
|
| 309 |
+`, |
|
| 310 |
+ }, |
|
| 311 |
+ {
|
|
| 312 |
+ ImageContext{
|
|
| 313 |
+ Context: Context{
|
|
| 314 |
+ Format: "table {{.Repository}}",
|
|
| 315 |
+ Quiet: true, |
|
| 316 |
+ }, |
|
| 317 |
+ }, |
|
| 318 |
+ "REPOSITORY\nimage\nimage\nimage\n<none>\n", |
|
| 319 |
+ }, |
|
| 320 |
+ {
|
|
| 321 |
+ ImageContext{
|
|
| 322 |
+ Context: Context{
|
|
| 323 |
+ Format: "table", |
|
| 324 |
+ Quiet: true, |
|
| 325 |
+ }, |
|
| 326 |
+ }, |
|
| 327 |
+ "imageID1\nimageID1\nimageID2\nimageID3\n", |
|
| 328 |
+ }, |
|
| 329 |
+ {
|
|
| 330 |
+ ImageContext{
|
|
| 331 |
+ Context: Context{
|
|
| 332 |
+ Format: "table", |
|
| 333 |
+ Quiet: false, |
|
| 334 |
+ }, |
|
| 335 |
+ Digest: true, |
|
| 336 |
+ }, |
|
| 337 |
+ `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE |
|
| 338 |
+image tag1 <none> imageID1 24 hours ago 0 B |
|
| 339 |
+image <none> sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B |
|
| 340 |
+image tag2 <none> imageID2 24 hours ago 0 B |
|
| 341 |
+<none> <none> <none> imageID3 24 hours ago 0 B |
|
| 342 |
+`, |
|
| 343 |
+ }, |
|
| 344 |
+ {
|
|
| 345 |
+ ImageContext{
|
|
| 346 |
+ Context: Context{
|
|
| 347 |
+ Format: "table", |
|
| 348 |
+ Quiet: true, |
|
| 349 |
+ }, |
|
| 350 |
+ Digest: true, |
|
| 351 |
+ }, |
|
| 352 |
+ "imageID1\nimageID1\nimageID2\nimageID3\n", |
|
| 353 |
+ }, |
|
| 354 |
+ // Raw Format |
|
| 355 |
+ {
|
|
| 356 |
+ ImageContext{
|
|
| 357 |
+ Context: Context{
|
|
| 358 |
+ Format: "raw", |
|
| 359 |
+ }, |
|
| 360 |
+ }, |
|
| 361 |
+ fmt.Sprintf(`repository: image |
|
| 362 |
+tag: tag1 |
|
| 363 |
+image_id: imageID1 |
|
| 364 |
+created_at: %s |
|
| 365 |
+virtual_size: 0 B |
|
| 366 |
+ |
|
| 367 |
+repository: image |
|
| 368 |
+tag: <none> |
|
| 369 |
+image_id: imageID1 |
|
| 370 |
+created_at: %s |
|
| 371 |
+virtual_size: 0 B |
|
| 372 |
+ |
|
| 373 |
+repository: image |
|
| 374 |
+tag: tag2 |
|
| 375 |
+image_id: imageID2 |
|
| 376 |
+created_at: %s |
|
| 377 |
+virtual_size: 0 B |
|
| 378 |
+ |
|
| 379 |
+repository: <none> |
|
| 380 |
+tag: <none> |
|
| 381 |
+image_id: imageID3 |
|
| 382 |
+created_at: %s |
|
| 383 |
+virtual_size: 0 B |
|
| 384 |
+ |
|
| 385 |
+`, expectedTime, expectedTime, expectedTime, expectedTime), |
|
| 386 |
+ }, |
|
| 387 |
+ {
|
|
| 388 |
+ ImageContext{
|
|
| 389 |
+ Context: Context{
|
|
| 390 |
+ Format: "raw", |
|
| 391 |
+ }, |
|
| 392 |
+ Digest: true, |
|
| 393 |
+ }, |
|
| 394 |
+ fmt.Sprintf(`repository: image |
|
| 395 |
+tag: tag1 |
|
| 396 |
+digest: <none> |
|
| 397 |
+image_id: imageID1 |
|
| 398 |
+created_at: %s |
|
| 399 |
+virtual_size: 0 B |
|
| 400 |
+ |
|
| 401 |
+repository: image |
|
| 402 |
+tag: <none> |
|
| 403 |
+digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf |
|
| 404 |
+image_id: imageID1 |
|
| 405 |
+created_at: %s |
|
| 406 |
+virtual_size: 0 B |
|
| 407 |
+ |
|
| 408 |
+repository: image |
|
| 409 |
+tag: tag2 |
|
| 410 |
+digest: <none> |
|
| 411 |
+image_id: imageID2 |
|
| 412 |
+created_at: %s |
|
| 413 |
+virtual_size: 0 B |
|
| 414 |
+ |
|
| 415 |
+repository: <none> |
|
| 416 |
+tag: <none> |
|
| 417 |
+digest: <none> |
|
| 418 |
+image_id: imageID3 |
|
| 419 |
+created_at: %s |
|
| 420 |
+virtual_size: 0 B |
|
| 421 |
+ |
|
| 422 |
+`, expectedTime, expectedTime, expectedTime, expectedTime), |
|
| 423 |
+ }, |
|
| 424 |
+ {
|
|
| 425 |
+ ImageContext{
|
|
| 426 |
+ Context: Context{
|
|
| 427 |
+ Format: "raw", |
|
| 428 |
+ Quiet: true, |
|
| 429 |
+ }, |
|
| 430 |
+ }, |
|
| 431 |
+ `image_id: imageID1 |
|
| 432 |
+image_id: imageID1 |
|
| 433 |
+image_id: imageID2 |
|
| 434 |
+image_id: imageID3 |
|
| 435 |
+`, |
|
| 436 |
+ }, |
|
| 437 |
+ // Custom Format |
|
| 438 |
+ {
|
|
| 439 |
+ ImageContext{
|
|
| 440 |
+ Context: Context{
|
|
| 441 |
+ Format: "{{.Repository}}",
|
|
| 442 |
+ }, |
|
| 443 |
+ }, |
|
| 444 |
+ "image\nimage\nimage\n<none>\n", |
|
| 445 |
+ }, |
|
| 446 |
+ {
|
|
| 447 |
+ ImageContext{
|
|
| 448 |
+ Context: Context{
|
|
| 449 |
+ Format: "{{.Repository}}",
|
|
| 450 |
+ }, |
|
| 451 |
+ Digest: true, |
|
| 452 |
+ }, |
|
| 453 |
+ "image\nimage\nimage\n<none>\n", |
|
| 454 |
+ }, |
|
| 455 |
+ } |
|
| 456 |
+ |
|
| 457 |
+ for _, context := range contexts {
|
|
| 458 |
+ images := []types.Image{
|
|
| 459 |
+ {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
|
| 460 |
+ {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
|
| 461 |
+ {ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
|
| 462 |
+ } |
|
| 463 |
+ out := bytes.NewBufferString("")
|
|
| 464 |
+ context.context.Output = out |
|
| 465 |
+ context.context.Images = images |
|
| 466 |
+ context.context.Write() |
|
| 467 |
+ actual := out.String() |
|
| 468 |
+ if actual != context.expected {
|
|
| 469 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 470 |
+ } |
|
| 471 |
+ // Clean buffer |
|
| 472 |
+ out.Reset() |
|
| 473 |
+ } |
|
| 474 |
+} |
|
| 475 |
+ |
|
| 476 |
+func TestImageContextWriteWithNoImage(t *testing.T) {
|
|
| 477 |
+ out := bytes.NewBufferString("")
|
|
| 478 |
+ images := []types.Image{}
|
|
| 479 |
+ |
|
| 480 |
+ contexts := []struct {
|
|
| 481 |
+ context ImageContext |
|
| 482 |
+ expected string |
|
| 483 |
+ }{
|
|
| 484 |
+ {
|
|
| 485 |
+ ImageContext{
|
|
| 486 |
+ Context: Context{
|
|
| 487 |
+ Format: "{{.Repository}}",
|
|
| 488 |
+ Output: out, |
|
| 489 |
+ }, |
|
| 490 |
+ }, |
|
| 491 |
+ "", |
|
| 492 |
+ }, |
|
| 493 |
+ {
|
|
| 494 |
+ ImageContext{
|
|
| 495 |
+ Context: Context{
|
|
| 496 |
+ Format: "table {{.Repository}}",
|
|
| 497 |
+ Output: out, |
|
| 498 |
+ }, |
|
| 499 |
+ }, |
|
| 500 |
+ "REPOSITORY\n", |
|
| 501 |
+ }, |
|
| 502 |
+ {
|
|
| 503 |
+ ImageContext{
|
|
| 504 |
+ Context: Context{
|
|
| 505 |
+ Format: "{{.Repository}}",
|
|
| 506 |
+ Output: out, |
|
| 507 |
+ }, |
|
| 508 |
+ Digest: true, |
|
| 509 |
+ }, |
|
| 510 |
+ "", |
|
| 511 |
+ }, |
|
| 512 |
+ {
|
|
| 513 |
+ ImageContext{
|
|
| 514 |
+ Context: Context{
|
|
| 515 |
+ Format: "table {{.Repository}}",
|
|
| 516 |
+ Output: out, |
|
| 517 |
+ }, |
|
| 518 |
+ Digest: true, |
|
| 519 |
+ }, |
|
| 520 |
+ "REPOSITORY DIGEST\n", |
|
| 521 |
+ }, |
|
| 522 |
+ } |
|
| 523 |
+ |
|
| 524 |
+ for _, context := range contexts {
|
|
| 525 |
+ context.context.Images = images |
|
| 526 |
+ context.context.Write() |
|
| 527 |
+ actual := out.String() |
|
| 528 |
+ if actual != context.expected {
|
|
| 529 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 530 |
+ } |
|
| 531 |
+ // Clean buffer |
|
| 532 |
+ out.Reset() |
|
| 533 |
+ } |
|
| 534 |
+} |
| ... | ... |
@@ -1,19 +1,12 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "fmt" |
|
| 5 |
- "strings" |
|
| 6 |
- "text/tabwriter" |
|
| 7 |
- "time" |
|
| 8 |
- |
|
| 4 |
+ "github.com/docker/docker/api/client/formatter" |
|
| 9 | 5 |
"github.com/docker/docker/api/types" |
| 10 | 6 |
"github.com/docker/docker/api/types/filters" |
| 11 | 7 |
Cli "github.com/docker/docker/cli" |
| 12 | 8 |
"github.com/docker/docker/opts" |
| 13 | 9 |
flag "github.com/docker/docker/pkg/mflag" |
| 14 |
- "github.com/docker/docker/pkg/stringid" |
|
| 15 |
- "github.com/docker/docker/reference" |
|
| 16 |
- "github.com/docker/go-units" |
|
| 17 | 10 |
) |
| 18 | 11 |
|
| 19 | 12 |
// CmdImages lists the images in a specified repository, or all top-level images if no repository is specified. |
| ... | ... |
@@ -25,6 +18,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
|
| 25 | 25 |
all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)")
|
| 26 | 26 |
noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output")
|
| 27 | 27 |
showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests")
|
| 28 |
+ format := cmd.String([]string{"-format"}, "", "Pretty-print images using a Go template")
|
|
| 28 | 29 |
|
| 29 | 30 |
flFilter := opts.NewListOpts(nil) |
| 30 | 31 |
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
| ... | ... |
@@ -59,66 +53,27 @@ func (cli *DockerCli) CmdImages(args ...string) error {
|
| 59 | 59 |
return err |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 |
- w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) |
|
| 63 |
- if !*quiet {
|
|
| 64 |
- if *showDigests {
|
|
| 65 |
- fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tSIZE") |
|
| 62 |
+ f := *format |
|
| 63 |
+ if len(f) == 0 {
|
|
| 64 |
+ if len(cli.ImagesFormat()) > 0 && !*quiet {
|
|
| 65 |
+ f = cli.ImagesFormat() |
|
| 66 | 66 |
} else {
|
| 67 |
- fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE") |
|
| 67 |
+ f = "table" |
|
| 68 | 68 |
} |
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 |
- for _, image := range images {
|
|
| 72 |
- ID := image.ID |
|
| 73 |
- if !*noTrunc {
|
|
| 74 |
- ID = stringid.TruncateID(ID) |
|
| 75 |
- } |
|
| 76 |
- |
|
| 77 |
- repoTags := image.RepoTags |
|
| 78 |
- repoDigests := image.RepoDigests |
|
| 79 |
- |
|
| 80 |
- if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
|
|
| 81 |
- // dangling image - clear out either repoTags or repoDigsts so we only show it once below |
|
| 82 |
- repoDigests = []string{}
|
|
| 83 |
- } |
|
| 84 |
- |
|
| 85 |
- // combine the tags and digests lists |
|
| 86 |
- tagsAndDigests := append(repoTags, repoDigests...) |
|
| 87 |
- for _, repoAndRef := range tagsAndDigests {
|
|
| 88 |
- // default repo, tag, and digest to none - if there's a value, it'll be set below |
|
| 89 |
- repo := "<none>" |
|
| 90 |
- tag := "<none>" |
|
| 91 |
- digest := "<none>" |
|
| 92 |
- |
|
| 93 |
- if !strings.HasPrefix(repoAndRef, "<none>") {
|
|
| 94 |
- ref, err := reference.ParseNamed(repoAndRef) |
|
| 95 |
- if err != nil {
|
|
| 96 |
- return err |
|
| 97 |
- } |
|
| 98 |
- repo = ref.Name() |
|
| 99 |
- |
|
| 100 |
- switch x := ref.(type) {
|
|
| 101 |
- case reference.Canonical: |
|
| 102 |
- digest = x.Digest().String() |
|
| 103 |
- case reference.NamedTagged: |
|
| 104 |
- tag = x.Tag() |
|
| 105 |
- } |
|
| 106 |
- } |
|
| 107 |
- |
|
| 108 |
- if !*quiet {
|
|
| 109 |
- if *showDigests {
|
|
| 110 |
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size))) |
|
| 111 |
- } else {
|
|
| 112 |
- fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size))) |
|
| 113 |
- } |
|
| 114 |
- } else {
|
|
| 115 |
- fmt.Fprintln(w, ID) |
|
| 116 |
- } |
|
| 117 |
- } |
|
| 71 |
+ imagesCtx := formatter.ImageContext{
|
|
| 72 |
+ Context: formatter.Context{
|
|
| 73 |
+ Output: cli.out, |
|
| 74 |
+ Format: f, |
|
| 75 |
+ Quiet: *quiet, |
|
| 76 |
+ Trunc: !*noTrunc, |
|
| 77 |
+ }, |
|
| 78 |
+ Digest: *showDigests, |
|
| 79 |
+ Images: images, |
|
| 118 | 80 |
} |
| 119 | 81 |
|
| 120 |
- if !*quiet {
|
|
| 121 |
- w.Flush() |
|
| 122 |
- } |
|
| 82 |
+ imagesCtx.Write() |
|
| 83 |
+ |
|
| 123 | 84 |
return nil |
| 124 | 85 |
} |
| ... | ... |
@@ -1,7 +1,7 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "github.com/docker/docker/api/client/ps" |
|
| 4 |
+ "github.com/docker/docker/api/client/formatter" |
|
| 5 | 5 |
"github.com/docker/docker/api/types" |
| 6 | 6 |
"github.com/docker/docker/api/types/filters" |
| 7 | 7 |
Cli "github.com/docker/docker/cli" |
| ... | ... |
@@ -70,15 +70,18 @@ func (cli *DockerCli) CmdPs(args ...string) error {
|
| 70 | 70 |
} |
| 71 | 71 |
} |
| 72 | 72 |
|
| 73 |
- psCtx := ps.Context{
|
|
| 74 |
- Output: cli.out, |
|
| 75 |
- Format: f, |
|
| 76 |
- Quiet: *quiet, |
|
| 77 |
- Size: *size, |
|
| 78 |
- Trunc: !*noTrunc, |
|
| 73 |
+ psCtx := formatter.ContainerContext{
|
|
| 74 |
+ Context: formatter.Context{
|
|
| 75 |
+ Output: cli.out, |
|
| 76 |
+ Format: f, |
|
| 77 |
+ Quiet: *quiet, |
|
| 78 |
+ Trunc: !*noTrunc, |
|
| 79 |
+ }, |
|
| 80 |
+ Size: *size, |
|
| 81 |
+ Containers: containers, |
|
| 79 | 82 |
} |
| 80 | 83 |
|
| 81 |
- ps.Format(psCtx, containers) |
|
| 84 |
+ psCtx.Write() |
|
| 82 | 85 |
|
| 83 | 86 |
return nil |
| 84 | 87 |
} |
| 85 | 88 |
deleted file mode 100644 |
| ... | ... |
@@ -1,160 +0,0 @@ |
| 1 |
-package ps |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "fmt" |
|
| 5 |
- "strconv" |
|
| 6 |
- "strings" |
|
| 7 |
- "time" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/docker/api" |
|
| 10 |
- "github.com/docker/docker/api/types" |
|
| 11 |
- "github.com/docker/docker/pkg/stringid" |
|
| 12 |
- "github.com/docker/docker/pkg/stringutils" |
|
| 13 |
- "github.com/docker/go-units" |
|
| 14 |
-) |
|
| 15 |
- |
|
| 16 |
-const ( |
|
| 17 |
- tableKey = "table" |
|
| 18 |
- |
|
| 19 |
- idHeader = "CONTAINER ID" |
|
| 20 |
- imageHeader = "IMAGE" |
|
| 21 |
- namesHeader = "NAMES" |
|
| 22 |
- commandHeader = "COMMAND" |
|
| 23 |
- createdAtHeader = "CREATED AT" |
|
| 24 |
- runningForHeader = "CREATED" |
|
| 25 |
- statusHeader = "STATUS" |
|
| 26 |
- portsHeader = "PORTS" |
|
| 27 |
- sizeHeader = "SIZE" |
|
| 28 |
- labelsHeader = "LABELS" |
|
| 29 |
-) |
|
| 30 |
- |
|
| 31 |
-type containerContext struct {
|
|
| 32 |
- trunc bool |
|
| 33 |
- header []string |
|
| 34 |
- c types.Container |
|
| 35 |
-} |
|
| 36 |
- |
|
| 37 |
-func (c *containerContext) ID() string {
|
|
| 38 |
- c.addHeader(idHeader) |
|
| 39 |
- if c.trunc {
|
|
| 40 |
- return stringid.TruncateID(c.c.ID) |
|
| 41 |
- } |
|
| 42 |
- return c.c.ID |
|
| 43 |
-} |
|
| 44 |
- |
|
| 45 |
-func (c *containerContext) Names() string {
|
|
| 46 |
- c.addHeader(namesHeader) |
|
| 47 |
- names := stripNamePrefix(c.c.Names) |
|
| 48 |
- if c.trunc {
|
|
| 49 |
- for _, name := range names {
|
|
| 50 |
- if len(strings.Split(name, "/")) == 1 {
|
|
| 51 |
- names = []string{name}
|
|
| 52 |
- break |
|
| 53 |
- } |
|
| 54 |
- } |
|
| 55 |
- } |
|
| 56 |
- return strings.Join(names, ",") |
|
| 57 |
-} |
|
| 58 |
- |
|
| 59 |
-func (c *containerContext) Image() string {
|
|
| 60 |
- c.addHeader(imageHeader) |
|
| 61 |
- if c.c.Image == "" {
|
|
| 62 |
- return "<no image>" |
|
| 63 |
- } |
|
| 64 |
- if c.trunc {
|
|
| 65 |
- if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
|
| 66 |
- return trunc |
|
| 67 |
- } |
|
| 68 |
- } |
|
| 69 |
- return c.c.Image |
|
| 70 |
-} |
|
| 71 |
- |
|
| 72 |
-func (c *containerContext) Command() string {
|
|
| 73 |
- c.addHeader(commandHeader) |
|
| 74 |
- command := c.c.Command |
|
| 75 |
- if c.trunc {
|
|
| 76 |
- command = stringutils.Truncate(command, 20) |
|
| 77 |
- } |
|
| 78 |
- return strconv.Quote(command) |
|
| 79 |
-} |
|
| 80 |
- |
|
| 81 |
-func (c *containerContext) CreatedAt() string {
|
|
| 82 |
- c.addHeader(createdAtHeader) |
|
| 83 |
- return time.Unix(int64(c.c.Created), 0).String() |
|
| 84 |
-} |
|
| 85 |
- |
|
| 86 |
-func (c *containerContext) RunningFor() string {
|
|
| 87 |
- c.addHeader(runningForHeader) |
|
| 88 |
- createdAt := time.Unix(int64(c.c.Created), 0) |
|
| 89 |
- return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 90 |
-} |
|
| 91 |
- |
|
| 92 |
-func (c *containerContext) Ports() string {
|
|
| 93 |
- c.addHeader(portsHeader) |
|
| 94 |
- return api.DisplayablePorts(c.c.Ports) |
|
| 95 |
-} |
|
| 96 |
- |
|
| 97 |
-func (c *containerContext) Status() string {
|
|
| 98 |
- c.addHeader(statusHeader) |
|
| 99 |
- return c.c.Status |
|
| 100 |
-} |
|
| 101 |
- |
|
| 102 |
-func (c *containerContext) Size() string {
|
|
| 103 |
- c.addHeader(sizeHeader) |
|
| 104 |
- srw := units.HumanSize(float64(c.c.SizeRw)) |
|
| 105 |
- sv := units.HumanSize(float64(c.c.SizeRootFs)) |
|
| 106 |
- |
|
| 107 |
- sf := srw |
|
| 108 |
- if c.c.SizeRootFs > 0 {
|
|
| 109 |
- sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
|
| 110 |
- } |
|
| 111 |
- return sf |
|
| 112 |
-} |
|
| 113 |
- |
|
| 114 |
-func (c *containerContext) Labels() string {
|
|
| 115 |
- c.addHeader(labelsHeader) |
|
| 116 |
- if c.c.Labels == nil {
|
|
| 117 |
- return "" |
|
| 118 |
- } |
|
| 119 |
- |
|
| 120 |
- var joinLabels []string |
|
| 121 |
- for k, v := range c.c.Labels {
|
|
| 122 |
- joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
|
| 123 |
- } |
|
| 124 |
- return strings.Join(joinLabels, ",") |
|
| 125 |
-} |
|
| 126 |
- |
|
| 127 |
-func (c *containerContext) Label(name string) string {
|
|
| 128 |
- n := strings.Split(name, ".") |
|
| 129 |
- r := strings.NewReplacer("-", " ", "_", " ")
|
|
| 130 |
- h := r.Replace(n[len(n)-1]) |
|
| 131 |
- |
|
| 132 |
- c.addHeader(h) |
|
| 133 |
- |
|
| 134 |
- if c.c.Labels == nil {
|
|
| 135 |
- return "" |
|
| 136 |
- } |
|
| 137 |
- return c.c.Labels[name] |
|
| 138 |
-} |
|
| 139 |
- |
|
| 140 |
-func (c *containerContext) fullHeader() string {
|
|
| 141 |
- if c.header == nil {
|
|
| 142 |
- return "" |
|
| 143 |
- } |
|
| 144 |
- return strings.Join(c.header, "\t") |
|
| 145 |
-} |
|
| 146 |
- |
|
| 147 |
-func (c *containerContext) addHeader(header string) {
|
|
| 148 |
- if c.header == nil {
|
|
| 149 |
- c.header = []string{}
|
|
| 150 |
- } |
|
| 151 |
- c.header = append(c.header, strings.ToUpper(header)) |
|
| 152 |
-} |
|
| 153 |
- |
|
| 154 |
-func stripNamePrefix(ss []string) []string {
|
|
| 155 |
- for i, s := range ss {
|
|
| 156 |
- ss[i] = s[1:] |
|
| 157 |
- } |
|
| 158 |
- |
|
| 159 |
- return ss |
|
| 160 |
-} |
| 161 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,126 +0,0 @@ |
| 1 |
-package ps |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "reflect" |
|
| 5 |
- "strings" |
|
| 6 |
- "testing" |
|
| 7 |
- "time" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/docker/api/types" |
|
| 10 |
- "github.com/docker/docker/pkg/stringid" |
|
| 11 |
-) |
|
| 12 |
- |
|
| 13 |
-func TestContainerPsContext(t *testing.T) {
|
|
| 14 |
- containerID := stringid.GenerateRandomID() |
|
| 15 |
- unix := time.Now().Unix() |
|
| 16 |
- |
|
| 17 |
- var ctx containerContext |
|
| 18 |
- cases := []struct {
|
|
| 19 |
- container types.Container |
|
| 20 |
- trunc bool |
|
| 21 |
- expValue string |
|
| 22 |
- expHeader string |
|
| 23 |
- call func() string |
|
| 24 |
- }{
|
|
| 25 |
- {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), idHeader, ctx.ID},
|
|
| 26 |
- {types.Container{ID: containerID}, false, containerID, idHeader, ctx.ID},
|
|
| 27 |
- {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
|
|
| 28 |
- {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
|
|
| 29 |
- {types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
|
|
| 30 |
- {types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
|
|
| 31 |
- {types.Container{
|
|
| 32 |
- Image: "a5a665ff33eced1e0803148700880edab4", |
|
| 33 |
- ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", |
|
| 34 |
- }, |
|
| 35 |
- true, |
|
| 36 |
- "a5a665ff33ec", |
|
| 37 |
- imageHeader, |
|
| 38 |
- ctx.Image, |
|
| 39 |
- }, |
|
| 40 |
- {types.Container{
|
|
| 41 |
- Image: "a5a665ff33eced1e0803148700880edab4", |
|
| 42 |
- ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", |
|
| 43 |
- }, |
|
| 44 |
- false, |
|
| 45 |
- "a5a665ff33eced1e0803148700880edab4", |
|
| 46 |
- imageHeader, |
|
| 47 |
- ctx.Image, |
|
| 48 |
- }, |
|
| 49 |
- {types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
|
|
| 50 |
- {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
|
|
| 51 |
- {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
|
| 52 |
- {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
|
|
| 53 |
- {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
|
|
| 54 |
- {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
|
|
| 55 |
- {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
|
|
| 56 |
- {types.Container{}, true, "", labelsHeader, ctx.Labels},
|
|
| 57 |
- {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
|
|
| 58 |
- {types.Container{Created: unix}, true, "Less than a second", runningForHeader, ctx.RunningFor},
|
|
| 59 |
- } |
|
| 60 |
- |
|
| 61 |
- for _, c := range cases {
|
|
| 62 |
- ctx = containerContext{c: c.container, trunc: c.trunc}
|
|
| 63 |
- v := c.call() |
|
| 64 |
- if strings.Contains(v, ",") {
|
|
| 65 |
- // comma-separated values means probably a map input, which won't |
|
| 66 |
- // be guaranteed to have the same order as our expected value |
|
| 67 |
- // We'll create maps and use reflect.DeepEquals to check instead: |
|
| 68 |
- entriesMap := make(map[string]string) |
|
| 69 |
- expMap := make(map[string]string) |
|
| 70 |
- entries := strings.Split(v, ",") |
|
| 71 |
- expectedEntries := strings.Split(c.expValue, ",") |
|
| 72 |
- for _, entry := range entries {
|
|
| 73 |
- keyval := strings.Split(entry, "=") |
|
| 74 |
- entriesMap[keyval[0]] = keyval[1] |
|
| 75 |
- } |
|
| 76 |
- for _, expected := range expectedEntries {
|
|
| 77 |
- keyval := strings.Split(expected, "=") |
|
| 78 |
- expMap[keyval[0]] = keyval[1] |
|
| 79 |
- } |
|
| 80 |
- if !reflect.DeepEqual(expMap, entriesMap) {
|
|
| 81 |
- t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
|
|
| 82 |
- } |
|
| 83 |
- } else if v != c.expValue {
|
|
| 84 |
- t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 85 |
- } |
|
| 86 |
- |
|
| 87 |
- h := ctx.fullHeader() |
|
| 88 |
- if h != c.expHeader {
|
|
| 89 |
- t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 90 |
- } |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
|
| 94 |
- ctx = containerContext{c: c1, trunc: true}
|
|
| 95 |
- |
|
| 96 |
- sid := ctx.Label("com.docker.swarm.swarm-id")
|
|
| 97 |
- node := ctx.Label("com.docker.swarm.node_name")
|
|
| 98 |
- if sid != "33" {
|
|
| 99 |
- t.Fatalf("Expected 33, was %s\n", sid)
|
|
| 100 |
- } |
|
| 101 |
- |
|
| 102 |
- if node != "ubuntu" {
|
|
| 103 |
- t.Fatalf("Expected ubuntu, was %s\n", node)
|
|
| 104 |
- } |
|
| 105 |
- |
|
| 106 |
- h := ctx.fullHeader() |
|
| 107 |
- if h != "SWARM ID\tNODE NAME" {
|
|
| 108 |
- t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
|
| 109 |
- |
|
| 110 |
- } |
|
| 111 |
- |
|
| 112 |
- c2 := types.Container{}
|
|
| 113 |
- ctx = containerContext{c: c2, trunc: true}
|
|
| 114 |
- |
|
| 115 |
- label := ctx.Label("anything.really")
|
|
| 116 |
- if label != "" {
|
|
| 117 |
- t.Fatalf("Expected an empty string, was %s", label)
|
|
| 118 |
- } |
|
| 119 |
- |
|
| 120 |
- ctx = containerContext{c: c2, trunc: true}
|
|
| 121 |
- fullHeader := ctx.fullHeader() |
|
| 122 |
- if fullHeader != "" {
|
|
| 123 |
- t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
|
| 124 |
- } |
|
| 125 |
- |
|
| 126 |
-} |
| 127 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,140 +0,0 @@ |
| 1 |
-package ps |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "fmt" |
|
| 6 |
- "io" |
|
| 7 |
- "strings" |
|
| 8 |
- "text/tabwriter" |
|
| 9 |
- "text/template" |
|
| 10 |
- |
|
| 11 |
- "github.com/docker/docker/api/types" |
|
| 12 |
-) |
|
| 13 |
- |
|
| 14 |
-const ( |
|
| 15 |
- tableFormatKey = "table" |
|
| 16 |
- rawFormatKey = "raw" |
|
| 17 |
- |
|
| 18 |
- defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
| 19 |
- defaultQuietFormat = "{{.ID}}"
|
|
| 20 |
-) |
|
| 21 |
- |
|
| 22 |
-// Context contains information required by the formatter to print the output as desired. |
|
| 23 |
-type Context struct {
|
|
| 24 |
- // Output is the output stream to which the formatted string is written. |
|
| 25 |
- Output io.Writer |
|
| 26 |
- // Format is used to choose raw, table or custom format for the output. |
|
| 27 |
- Format string |
|
| 28 |
- // Size when set to true will display the size of the output. |
|
| 29 |
- Size bool |
|
| 30 |
- // Quiet when set to true will simply print minimal information. |
|
| 31 |
- Quiet bool |
|
| 32 |
- // Trunc when set to true will truncate the output of certain fields such as Container ID. |
|
| 33 |
- Trunc bool |
|
| 34 |
-} |
|
| 35 |
- |
|
| 36 |
-// Format helps to format the output using the parameters set in the Context. |
|
| 37 |
-// Currently Format allow to display in raw, table or custom format the output. |
|
| 38 |
-func Format(ctx Context, containers []types.Container) {
|
|
| 39 |
- switch ctx.Format {
|
|
| 40 |
- case tableFormatKey: |
|
| 41 |
- tableFormat(ctx, containers) |
|
| 42 |
- case rawFormatKey: |
|
| 43 |
- rawFormat(ctx, containers) |
|
| 44 |
- default: |
|
| 45 |
- customFormat(ctx, containers) |
|
| 46 |
- } |
|
| 47 |
-} |
|
| 48 |
- |
|
| 49 |
-func rawFormat(ctx Context, containers []types.Container) {
|
|
| 50 |
- if ctx.Quiet {
|
|
| 51 |
- ctx.Format = `container_id: {{.ID}}`
|
|
| 52 |
- } else {
|
|
| 53 |
- ctx.Format = `container_id: {{.ID}}
|
|
| 54 |
-image: {{.Image}}
|
|
| 55 |
-command: {{.Command}}
|
|
| 56 |
-created_at: {{.CreatedAt}}
|
|
| 57 |
-status: {{.Status}}
|
|
| 58 |
-names: {{.Names}}
|
|
| 59 |
-labels: {{.Labels}}
|
|
| 60 |
-ports: {{.Ports}}
|
|
| 61 |
-` |
|
| 62 |
- if ctx.Size {
|
|
| 63 |
- ctx.Format += `size: {{.Size}}
|
|
| 64 |
-` |
|
| 65 |
- } |
|
| 66 |
- } |
|
| 67 |
- |
|
| 68 |
- customFormat(ctx, containers) |
|
| 69 |
-} |
|
| 70 |
- |
|
| 71 |
-func tableFormat(ctx Context, containers []types.Container) {
|
|
| 72 |
- ctx.Format = defaultTableFormat |
|
| 73 |
- if ctx.Quiet {
|
|
| 74 |
- ctx.Format = defaultQuietFormat |
|
| 75 |
- } |
|
| 76 |
- |
|
| 77 |
- customFormat(ctx, containers) |
|
| 78 |
-} |
|
| 79 |
- |
|
| 80 |
-func customFormat(ctx Context, containers []types.Container) {
|
|
| 81 |
- var ( |
|
| 82 |
- table bool |
|
| 83 |
- header string |
|
| 84 |
- format = ctx.Format |
|
| 85 |
- buffer = bytes.NewBufferString("")
|
|
| 86 |
- ) |
|
| 87 |
- |
|
| 88 |
- if strings.HasPrefix(ctx.Format, tableKey) {
|
|
| 89 |
- table = true |
|
| 90 |
- format = format[len(tableKey):] |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- format = strings.Trim(format, " ") |
|
| 94 |
- r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") |
|
| 95 |
- format = r.Replace(format) |
|
| 96 |
- |
|
| 97 |
- if table && ctx.Size {
|
|
| 98 |
- format += "\t{{.Size}}"
|
|
| 99 |
- } |
|
| 100 |
- |
|
| 101 |
- tmpl, err := template.New("").Parse(format)
|
|
| 102 |
- if err != nil {
|
|
| 103 |
- buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
| 104 |
- buffer.WriteTo(ctx.Output) |
|
| 105 |
- return |
|
| 106 |
- } |
|
| 107 |
- |
|
| 108 |
- for _, container := range containers {
|
|
| 109 |
- containerCtx := &containerContext{
|
|
| 110 |
- trunc: ctx.Trunc, |
|
| 111 |
- c: container, |
|
| 112 |
- } |
|
| 113 |
- if err := tmpl.Execute(buffer, containerCtx); err != nil {
|
|
| 114 |
- buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
| 115 |
- buffer.WriteTo(ctx.Output) |
|
| 116 |
- return |
|
| 117 |
- } |
|
| 118 |
- if table && len(header) == 0 {
|
|
| 119 |
- header = containerCtx.fullHeader() |
|
| 120 |
- } |
|
| 121 |
- buffer.WriteString("\n")
|
|
| 122 |
- } |
|
| 123 |
- |
|
| 124 |
- if table {
|
|
| 125 |
- if len(header) == 0 {
|
|
| 126 |
- // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template |
|
| 127 |
- containerCtx := &containerContext{}
|
|
| 128 |
- tmpl.Execute(bytes.NewBufferString(""), containerCtx)
|
|
| 129 |
- header = containerCtx.fullHeader() |
|
| 130 |
- } |
|
| 131 |
- |
|
| 132 |
- t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0) |
|
| 133 |
- t.Write([]byte(header)) |
|
| 134 |
- t.Write([]byte("\n"))
|
|
| 135 |
- buffer.WriteTo(t) |
|
| 136 |
- t.Flush() |
|
| 137 |
- } else {
|
|
| 138 |
- buffer.WriteTo(ctx.Output) |
|
| 139 |
- } |
|
| 140 |
-} |
| 141 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,213 +0,0 @@ |
| 1 |
-package ps |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "fmt" |
|
| 6 |
- "testing" |
|
| 7 |
- "time" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/docker/api/types" |
|
| 10 |
-) |
|
| 11 |
- |
|
| 12 |
-func TestFormat(t *testing.T) {
|
|
| 13 |
- unixTime := time.Now().Add(-50 * time.Hour).Unix() |
|
| 14 |
- expectedTime := time.Unix(unixTime, 0).String() |
|
| 15 |
- |
|
| 16 |
- contexts := []struct {
|
|
| 17 |
- context Context |
|
| 18 |
- expected string |
|
| 19 |
- }{
|
|
| 20 |
- // Errors |
|
| 21 |
- {
|
|
| 22 |
- Context{
|
|
| 23 |
- Format: "{{InvalidFunction}}",
|
|
| 24 |
- }, |
|
| 25 |
- `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 26 |
-`, |
|
| 27 |
- }, |
|
| 28 |
- {
|
|
| 29 |
- Context{
|
|
| 30 |
- Format: "{{nil}}",
|
|
| 31 |
- }, |
|
| 32 |
- `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 33 |
-`, |
|
| 34 |
- }, |
|
| 35 |
- // Table Format |
|
| 36 |
- {
|
|
| 37 |
- Context{
|
|
| 38 |
- Format: "table", |
|
| 39 |
- }, |
|
| 40 |
- `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
|
| 41 |
-containerID1 ubuntu "" 2 days ago foobar_baz |
|
| 42 |
-containerID2 ubuntu "" 2 days ago foobar_bar |
|
| 43 |
-`, |
|
| 44 |
- }, |
|
| 45 |
- {
|
|
| 46 |
- Context{
|
|
| 47 |
- Format: "table {{.Image}}",
|
|
| 48 |
- }, |
|
| 49 |
- "IMAGE\nubuntu\nubuntu\n", |
|
| 50 |
- }, |
|
| 51 |
- {
|
|
| 52 |
- Context{
|
|
| 53 |
- Format: "table {{.Image}}",
|
|
| 54 |
- Size: true, |
|
| 55 |
- }, |
|
| 56 |
- "IMAGE SIZE\nubuntu 0 B\nubuntu 0 B\n", |
|
| 57 |
- }, |
|
| 58 |
- {
|
|
| 59 |
- Context{
|
|
| 60 |
- Format: "table {{.Image}}",
|
|
| 61 |
- Quiet: true, |
|
| 62 |
- }, |
|
| 63 |
- "IMAGE\nubuntu\nubuntu\n", |
|
| 64 |
- }, |
|
| 65 |
- {
|
|
| 66 |
- Context{
|
|
| 67 |
- Format: "table", |
|
| 68 |
- Quiet: true, |
|
| 69 |
- }, |
|
| 70 |
- "containerID1\ncontainerID2\n", |
|
| 71 |
- }, |
|
| 72 |
- // Raw Format |
|
| 73 |
- {
|
|
| 74 |
- Context{
|
|
| 75 |
- Format: "raw", |
|
| 76 |
- }, |
|
| 77 |
- fmt.Sprintf(`container_id: containerID1 |
|
| 78 |
-image: ubuntu |
|
| 79 |
-command: "" |
|
| 80 |
-created_at: %s |
|
| 81 |
-status: |
|
| 82 |
-names: foobar_baz |
|
| 83 |
-labels: |
|
| 84 |
-ports: |
|
| 85 |
- |
|
| 86 |
-container_id: containerID2 |
|
| 87 |
-image: ubuntu |
|
| 88 |
-command: "" |
|
| 89 |
-created_at: %s |
|
| 90 |
-status: |
|
| 91 |
-names: foobar_bar |
|
| 92 |
-labels: |
|
| 93 |
-ports: |
|
| 94 |
- |
|
| 95 |
-`, expectedTime, expectedTime), |
|
| 96 |
- }, |
|
| 97 |
- {
|
|
| 98 |
- Context{
|
|
| 99 |
- Format: "raw", |
|
| 100 |
- Size: true, |
|
| 101 |
- }, |
|
| 102 |
- fmt.Sprintf(`container_id: containerID1 |
|
| 103 |
-image: ubuntu |
|
| 104 |
-command: "" |
|
| 105 |
-created_at: %s |
|
| 106 |
-status: |
|
| 107 |
-names: foobar_baz |
|
| 108 |
-labels: |
|
| 109 |
-ports: |
|
| 110 |
-size: 0 B |
|
| 111 |
- |
|
| 112 |
-container_id: containerID2 |
|
| 113 |
-image: ubuntu |
|
| 114 |
-command: "" |
|
| 115 |
-created_at: %s |
|
| 116 |
-status: |
|
| 117 |
-names: foobar_bar |
|
| 118 |
-labels: |
|
| 119 |
-ports: |
|
| 120 |
-size: 0 B |
|
| 121 |
- |
|
| 122 |
-`, expectedTime, expectedTime), |
|
| 123 |
- }, |
|
| 124 |
- {
|
|
| 125 |
- Context{
|
|
| 126 |
- Format: "raw", |
|
| 127 |
- Quiet: true, |
|
| 128 |
- }, |
|
| 129 |
- "container_id: containerID1\ncontainer_id: containerID2\n", |
|
| 130 |
- }, |
|
| 131 |
- // Custom Format |
|
| 132 |
- {
|
|
| 133 |
- Context{
|
|
| 134 |
- Format: "{{.Image}}",
|
|
| 135 |
- }, |
|
| 136 |
- "ubuntu\nubuntu\n", |
|
| 137 |
- }, |
|
| 138 |
- {
|
|
| 139 |
- Context{
|
|
| 140 |
- Format: "{{.Image}}",
|
|
| 141 |
- Size: true, |
|
| 142 |
- }, |
|
| 143 |
- "ubuntu\nubuntu\n", |
|
| 144 |
- }, |
|
| 145 |
- } |
|
| 146 |
- |
|
| 147 |
- for _, context := range contexts {
|
|
| 148 |
- containers := []types.Container{
|
|
| 149 |
- {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
|
| 150 |
- {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
|
| 151 |
- } |
|
| 152 |
- out := bytes.NewBufferString("")
|
|
| 153 |
- context.context.Output = out |
|
| 154 |
- Format(context.context, containers) |
|
| 155 |
- actual := out.String() |
|
| 156 |
- if actual != context.expected {
|
|
| 157 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 158 |
- } |
|
| 159 |
- // Clean buffer |
|
| 160 |
- out.Reset() |
|
| 161 |
- } |
|
| 162 |
-} |
|
| 163 |
- |
|
| 164 |
-func TestCustomFormatNoContainers(t *testing.T) {
|
|
| 165 |
- out := bytes.NewBufferString("")
|
|
| 166 |
- containers := []types.Container{}
|
|
| 167 |
- |
|
| 168 |
- contexts := []struct {
|
|
| 169 |
- context Context |
|
| 170 |
- expected string |
|
| 171 |
- }{
|
|
| 172 |
- {
|
|
| 173 |
- Context{
|
|
| 174 |
- Format: "{{.Image}}",
|
|
| 175 |
- Output: out, |
|
| 176 |
- }, |
|
| 177 |
- "", |
|
| 178 |
- }, |
|
| 179 |
- {
|
|
| 180 |
- Context{
|
|
| 181 |
- Format: "table {{.Image}}",
|
|
| 182 |
- Output: out, |
|
| 183 |
- }, |
|
| 184 |
- "IMAGE\n", |
|
| 185 |
- }, |
|
| 186 |
- {
|
|
| 187 |
- Context{
|
|
| 188 |
- Format: "{{.Image}}",
|
|
| 189 |
- Output: out, |
|
| 190 |
- Size: true, |
|
| 191 |
- }, |
|
| 192 |
- "", |
|
| 193 |
- }, |
|
| 194 |
- {
|
|
| 195 |
- Context{
|
|
| 196 |
- Format: "table {{.Image}}",
|
|
| 197 |
- Output: out, |
|
| 198 |
- Size: true, |
|
| 199 |
- }, |
|
| 200 |
- "IMAGE SIZE\n", |
|
| 201 |
- }, |
|
| 202 |
- } |
|
| 203 |
- |
|
| 204 |
- for _, context := range contexts {
|
|
| 205 |
- customFormat(context.context, containers) |
|
| 206 |
- actual := out.String() |
|
| 207 |
- if actual != context.expected {
|
|
| 208 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 209 |
- } |
|
| 210 |
- // Clean buffer |
|
| 211 |
- out.Reset() |
|
| 212 |
- } |
|
| 213 |
-} |
| ... | ... |
@@ -47,10 +47,11 @@ func SetConfigDir(dir string) {
|
| 47 | 47 |
|
| 48 | 48 |
// ConfigFile ~/.docker/config.json file info |
| 49 | 49 |
type ConfigFile struct {
|
| 50 |
- AuthConfigs map[string]types.AuthConfig `json:"auths"` |
|
| 51 |
- HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` |
|
| 52 |
- PsFormat string `json:"psFormat,omitempty"` |
|
| 53 |
- filename string // Note: not serialized - for internal use only |
|
| 50 |
+ AuthConfigs map[string]types.AuthConfig `json:"auths"` |
|
| 51 |
+ HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` |
|
| 52 |
+ PsFormat string `json:"psFormat,omitempty"` |
|
| 53 |
+ ImagesFormat string `json:"imagesFormat,omitempty"` |
|
| 54 |
+ filename string // Note: not serialized - for internal use only |
|
| 54 | 55 |
} |
| 55 | 56 |
|
| 56 | 57 |
// NewConfigFile initializes an empty configuration file for the given filename 'fn' |
| ... | ... |
@@ -927,6 +927,9 @@ _docker_images() {
|
| 927 | 927 |
fi |
| 928 | 928 |
return |
| 929 | 929 |
;; |
| 930 |
+ --format) |
|
| 931 |
+ return |
|
| 932 |
+ ;; |
|
| 930 | 933 |
esac |
| 931 | 934 |
|
| 932 | 935 |
case "${words[$cword-2]}$prev=" in
|
| ... | ... |
@@ -941,7 +944,7 @@ _docker_images() {
|
| 941 | 941 |
|
| 942 | 942 |
case "$cur" in |
| 943 | 943 |
-*) |
| 944 |
- COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --help --no-trunc --quiet -q" -- "$cur" ) ) |
|
| 944 |
+ COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --format --help --no-trunc --quiet -q" -- "$cur" ) ) |
|
| 945 | 945 |
;; |
| 946 | 946 |
=) |
| 947 | 947 |
return |
| ... | ... |
@@ -692,8 +692,9 @@ __docker_subcommand() {
|
| 692 | 692 |
_arguments $(__docker_arguments) \ |
| 693 | 693 |
$opts_help \ |
| 694 | 694 |
"($help -a --all)"{-a,--all}"[Show all images]" \
|
| 695 |
- "($help)--digest[Show digests]" \ |
|
| 695 |
+ "($help)--digests[Show digests]" \ |
|
| 696 | 696 |
"($help)*"{-f=,--filter=}"[Filter values]:filter: " \
|
| 697 |
+ "($help)--format[Pretty-print containers using a Go template]:format: " \ |
|
| 697 | 698 |
"($help)--no-trunc[Do not truncate output]" \ |
| 698 | 699 |
"($help -q --quiet)"{-q,--quiet}"[Only show numeric IDs]" \
|
| 699 | 700 |
"($help -): :__docker_repositories" && ret=0 |
| ... | ... |
@@ -103,6 +103,12 @@ Docker's client uses this property. If this property is not set, the client |
| 103 | 103 |
falls back to the default table format. For a list of supported formatting |
| 104 | 104 |
directives, see the [**Formatting** section in the `docker ps` documentation](ps.md) |
| 105 | 105 |
|
| 106 |
+The property `imagesFormat` specifies the default format for `docker images` output. |
|
| 107 |
+When the `--format` flag is not provided with the `docker images` command, |
|
| 108 |
+Docker's client uses this property. If this property is not set, the client |
|
| 109 |
+falls back to the default table format. For a list of supported formatting |
|
| 110 |
+directives, see the [**Formatting** section in the `docker images` documentation](images.md) |
|
| 111 |
+ |
|
| 106 | 112 |
Following is a sample `config.json` file: |
| 107 | 113 |
|
| 108 | 114 |
{
|
| ... | ... |
@@ -110,6 +116,7 @@ Following is a sample `config.json` file: |
| 110 | 110 |
"MyHeader": "MyValue" |
| 111 | 111 |
}, |
| 112 | 112 |
"psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
|
| 113 |
+ "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}"
|
|
| 113 | 114 |
} |
| 114 | 115 |
|
| 115 | 116 |
### Notary |
| ... | ... |
@@ -177,3 +177,53 @@ In this example, with the `0.1` value, it returns an empty set because no matche |
| 177 | 177 |
|
| 178 | 178 |
$ docker images --filter "label=com.example.version=0.1" |
| 179 | 179 |
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE |
| 180 |
+ |
|
| 181 |
+## Formatting |
|
| 182 |
+ |
|
| 183 |
+The formatting option (`--format`) will pretty print container output |
|
| 184 |
+using a Go template. |
|
| 185 |
+ |
|
| 186 |
+Valid placeholders for the Go template are listed below: |
|
| 187 |
+ |
|
| 188 |
+Placeholder | Description |
|
| 189 |
+---- | ---- |
|
| 190 |
+`.ID` | Image ID |
|
| 191 |
+`.Repository` | Image repository |
|
| 192 |
+`.Tag` | Image tag |
|
| 193 |
+`.Digest` | Image digest |
|
| 194 |
+`.CreatedSince` | Elapsed time since the image was created. |
|
| 195 |
+`.CreatedAt` | Time when the image was created. |
|
| 196 |
+`.Size` | Image disk size. |
|
| 197 |
+ |
|
| 198 |
+When using the `--format` option, the `image` command will either |
|
| 199 |
+output the data exactly as the template declares or, when using the |
|
| 200 |
+`table` directive, will include column headers as well. |
|
| 201 |
+ |
|
| 202 |
+The following example uses a template without headers and outputs the |
|
| 203 |
+`ID` and `Repository` entries separated by a colon for all images: |
|
| 204 |
+ |
|
| 205 |
+ $ docker images --format "{{.ID}}: {{.Repository}}"
|
|
| 206 |
+ 77af4d6b9913: <none> |
|
| 207 |
+ b6fa739cedf5: committ |
|
| 208 |
+ 78a85c484f71: <none> |
|
| 209 |
+ 30557a29d5ab: docker |
|
| 210 |
+ 5ed6274db6ce: <none> |
|
| 211 |
+ 746b819f315e: postgres |
|
| 212 |
+ 746b819f315e: postgres |
|
| 213 |
+ 746b819f315e: postgres |
|
| 214 |
+ 746b819f315e: postgres |
|
| 215 |
+ |
|
| 216 |
+To list all images with their repository and tag in a table format you |
|
| 217 |
+can use: |
|
| 218 |
+ |
|
| 219 |
+ $ docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
|
|
| 220 |
+ IMAGE ID REPOSITORY TAG |
|
| 221 |
+ 77af4d6b9913 <none> <none> |
|
| 222 |
+ b6fa739cedf5 committ latest |
|
| 223 |
+ 78a85c484f71 <none> <none> |
|
| 224 |
+ 30557a29d5ab docker latest |
|
| 225 |
+ 5ed6274db6ce <none> <none> |
|
| 226 |
+ 746b819f315e postgres 9 |
|
| 227 |
+ 746b819f315e postgres 9.3 |
|
| 228 |
+ 746b819f315e postgres 9.3.5 |
|
| 229 |
+ 746b819f315e postgres latest |
| ... | ... |
@@ -2,6 +2,9 @@ package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "io/ioutil" |
|
| 6 |
+ "os" |
|
| 7 |
+ "path/filepath" |
|
| 5 | 8 |
"reflect" |
| 6 | 9 |
"sort" |
| 7 | 10 |
"strings" |
| ... | ... |
@@ -48,17 +51,17 @@ func (s *DockerSuite) TestImagesOrderedByCreationDate(c *check.C) {
|
| 48 | 48 |
testRequires(c, DaemonIsLinux) |
| 49 | 49 |
id1, err := buildImage("order:test_a",
|
| 50 | 50 |
`FROM scratch |
| 51 |
- MAINTAINER dockerio1`, true) |
|
| 51 |
+ MAINTAINER dockerio1`, true) |
|
| 52 | 52 |
c.Assert(err, checker.IsNil) |
| 53 | 53 |
time.Sleep(1 * time.Second) |
| 54 | 54 |
id2, err := buildImage("order:test_c",
|
| 55 | 55 |
`FROM scratch |
| 56 |
- MAINTAINER dockerio2`, true) |
|
| 56 |
+ MAINTAINER dockerio2`, true) |
|
| 57 | 57 |
c.Assert(err, checker.IsNil) |
| 58 | 58 |
time.Sleep(1 * time.Second) |
| 59 | 59 |
id3, err := buildImage("order:test_b",
|
| 60 | 60 |
`FROM scratch |
| 61 |
- MAINTAINER dockerio3`, true) |
|
| 61 |
+ MAINTAINER dockerio3`, true) |
|
| 62 | 62 |
c.Assert(err, checker.IsNil) |
| 63 | 63 |
|
| 64 | 64 |
out, _ := dockerCmd(c, "images", "-q", "--no-trunc") |
| ... | ... |
@@ -81,17 +84,17 @@ func (s *DockerSuite) TestImagesFilterLabelMatch(c *check.C) {
|
| 81 | 81 |
imageName3 := "images_filter_test3" |
| 82 | 82 |
image1ID, err := buildImage(imageName1, |
| 83 | 83 |
`FROM scratch |
| 84 |
- LABEL match me`, true) |
|
| 84 |
+ LABEL match me`, true) |
|
| 85 | 85 |
c.Assert(err, check.IsNil) |
| 86 | 86 |
|
| 87 | 87 |
image2ID, err := buildImage(imageName2, |
| 88 | 88 |
`FROM scratch |
| 89 |
- LABEL match="me too"`, true) |
|
| 89 |
+ LABEL match="me too"`, true) |
|
| 90 | 90 |
c.Assert(err, check.IsNil) |
| 91 | 91 |
|
| 92 | 92 |
image3ID, err := buildImage(imageName3, |
| 93 | 93 |
`FROM scratch |
| 94 |
- LABEL nomatch me`, true) |
|
| 94 |
+ LABEL nomatch me`, true) |
|
| 95 | 95 |
c.Assert(err, check.IsNil) |
| 96 | 96 |
|
| 97 | 97 |
out, _ := dockerCmd(c, "images", "--no-trunc", "-q", "-f", "label=match") |
| ... | ... |
@@ -123,9 +126,9 @@ func (s *DockerSuite) TestImagesFilterSpaceTrimCase(c *check.C) {
|
| 123 | 123 |
imageName := "images_filter_test" |
| 124 | 124 |
buildImage(imageName, |
| 125 | 125 |
`FROM scratch |
| 126 |
- RUN touch /test/foo |
|
| 127 |
- RUN touch /test/bar |
|
| 128 |
- RUN touch /test/baz`, true) |
|
| 126 |
+ RUN touch /test/foo |
|
| 127 |
+ RUN touch /test/bar |
|
| 128 |
+ RUN touch /test/baz`, true) |
|
| 129 | 129 |
|
| 130 | 130 |
filters := []string{
|
| 131 | 131 |
"dangling=true", |
| ... | ... |
@@ -233,3 +236,46 @@ func (s *DockerSuite) TestImagesFilterNameWithPort(c *check.C) {
|
| 233 | 233 |
out, _ = dockerCmd(c, "images", tag+":no-such-tag") |
| 234 | 234 |
c.Assert(out, checker.Not(checker.Contains), tag) |
| 235 | 235 |
} |
| 236 |
+ |
|
| 237 |
+func (s *DockerSuite) TestImagesFormat(c *check.C) {
|
|
| 238 |
+ // testRequires(c, DaemonIsLinux) |
|
| 239 |
+ tag := "myimage" |
|
| 240 |
+ dockerCmd(c, "tag", "busybox", tag+":v1") |
|
| 241 |
+ dockerCmd(c, "tag", "busybox", tag+":v2") |
|
| 242 |
+ |
|
| 243 |
+ out, _ := dockerCmd(c, "images", "--format", "{{.Repository}}", tag)
|
|
| 244 |
+ lines := strings.Split(strings.TrimSpace(string(out)), "\n") |
|
| 245 |
+ |
|
| 246 |
+ expected := []string{"myimage", "myimage"}
|
|
| 247 |
+ var names []string |
|
| 248 |
+ for _, l := range lines {
|
|
| 249 |
+ names = append(names, l) |
|
| 250 |
+ } |
|
| 251 |
+ c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
|
|
| 252 |
+} |
|
| 253 |
+ |
|
| 254 |
+// ImagesDefaultFormatAndQuiet |
|
| 255 |
+func (s *DockerSuite) TestImagesFormatDefaultFormat(c *check.C) {
|
|
| 256 |
+ testRequires(c, DaemonIsLinux) |
|
| 257 |
+ |
|
| 258 |
+ // create container 1 |
|
| 259 |
+ out, _ := dockerCmd(c, "run", "-d", "busybox", "true") |
|
| 260 |
+ containerID1 := strings.TrimSpace(out) |
|
| 261 |
+ |
|
| 262 |
+ // tag as foobox |
|
| 263 |
+ out, _ = dockerCmd(c, "commit", containerID1, "myimage") |
|
| 264 |
+ imageID := stringid.TruncateID(strings.TrimSpace(out)) |
|
| 265 |
+ |
|
| 266 |
+ config := `{
|
|
| 267 |
+ "imagesFormat": "{{ .ID }} default"
|
|
| 268 |
+}` |
|
| 269 |
+ d, err := ioutil.TempDir("", "integration-cli-")
|
|
| 270 |
+ c.Assert(err, checker.IsNil) |
|
| 271 |
+ defer os.RemoveAll(d) |
|
| 272 |
+ |
|
| 273 |
+ err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644) |
|
| 274 |
+ c.Assert(err, checker.IsNil) |
|
| 275 |
+ |
|
| 276 |
+ out, _ = dockerCmd(c, "--config", d, "images", "-q", "myimage") |
|
| 277 |
+ c.Assert(out, checker.Equals, imageID+"\n", check.Commentf("Expected to print only the image id, got %v\n", out))
|
|
| 278 |
+} |
| ... | ... |
@@ -568,7 +568,7 @@ func (s *DockerSuite) TestPsFormatHeaders(c *check.C) {
|
| 568 | 568 |
func (s *DockerSuite) TestPsDefaultFormatAndQuiet(c *check.C) {
|
| 569 | 569 |
testRequires(c, DaemonIsLinux) |
| 570 | 570 |
config := `{
|
| 571 |
- "psFormat": "{{ .ID }} default"
|
|
| 571 |
+ "psFormat": "default {{ .ID }}"
|
|
| 572 | 572 |
}` |
| 573 | 573 |
d, err := ioutil.TempDir("", "integration-cli-")
|
| 574 | 574 |
c.Assert(err, checker.IsNil) |
| ... | ... |
@@ -40,6 +40,17 @@ versions. |
| 40 | 40 |
**-f**, **--filter**=[] |
| 41 | 41 |
Filters the output. The dangling=true filter finds unused images. While label=com.foo=amd64 filters for images with a com.foo value of amd64. The label=com.foo filter finds images with the label com.foo of any value. |
| 42 | 42 |
|
| 43 |
+**--format**="*TEMPLATE*" |
|
| 44 |
+ Pretty-print containers using a Go template. |
|
| 45 |
+ Valid placeholders: |
|
| 46 |
+ .ID - Image ID |
|
| 47 |
+ .Repository - Image repository |
|
| 48 |
+ .Tag - Image tag |
|
| 49 |
+ .Digest - Image digest |
|
| 50 |
+ .CreatedSince - Elapsed time since the image was created. |
|
| 51 |
+ .CreatedAt - Time when the image was created.. |
|
| 52 |
+ .Size - Image disk size. |
|
| 53 |
+ |
|
| 43 | 54 |
**--help** |
| 44 | 55 |
Print usage statement |
| 45 | 56 |
|