- Create container.go and image.go for specific context code
- Keep common code in formatter.go and custom.go
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,208 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ "strings" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api" |
|
| 10 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 11 |
+ "github.com/docker/docker/pkg/stringutils" |
|
| 12 |
+ "github.com/docker/engine-api/types" |
|
| 13 |
+ "github.com/docker/go-units" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+const ( |
|
| 17 |
+ defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
| 18 |
+ |
|
| 19 |
+ containerIDHeader = "CONTAINER ID" |
|
| 20 |
+ namesHeader = "NAMES" |
|
| 21 |
+ commandHeader = "COMMAND" |
|
| 22 |
+ runningForHeader = "CREATED" |
|
| 23 |
+ statusHeader = "STATUS" |
|
| 24 |
+ portsHeader = "PORTS" |
|
| 25 |
+ mountsHeader = "MOUNTS" |
|
| 26 |
+) |
|
| 27 |
+ |
|
| 28 |
+// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. |
|
| 29 |
+type ContainerContext struct {
|
|
| 30 |
+ Context |
|
| 31 |
+ // Size when set to true will display the size of the output. |
|
| 32 |
+ Size bool |
|
| 33 |
+ // Containers |
|
| 34 |
+ Containers []types.Container |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func (ctx ContainerContext) Write() {
|
|
| 38 |
+ switch ctx.Format {
|
|
| 39 |
+ case tableFormatKey: |
|
| 40 |
+ if ctx.Quiet {
|
|
| 41 |
+ ctx.Format = defaultQuietFormat |
|
| 42 |
+ } else {
|
|
| 43 |
+ ctx.Format = defaultContainerTableFormat |
|
| 44 |
+ if ctx.Size {
|
|
| 45 |
+ ctx.Format += `\t{{.Size}}`
|
|
| 46 |
+ } |
|
| 47 |
+ } |
|
| 48 |
+ case rawFormatKey: |
|
| 49 |
+ if ctx.Quiet {
|
|
| 50 |
+ ctx.Format = `container_id: {{.ID}}`
|
|
| 51 |
+ } else {
|
|
| 52 |
+ ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
|
| 53 |
+ if ctx.Size {
|
|
| 54 |
+ ctx.Format += `size: {{.Size}}\n`
|
|
| 55 |
+ } |
|
| 56 |
+ } |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ ctx.buffer = bytes.NewBufferString("")
|
|
| 60 |
+ ctx.preformat() |
|
| 61 |
+ |
|
| 62 |
+ tmpl, err := ctx.parseFormat() |
|
| 63 |
+ if err != nil {
|
|
| 64 |
+ return |
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ for _, container := range ctx.Containers {
|
|
| 68 |
+ containerCtx := &containerContext{
|
|
| 69 |
+ trunc: ctx.Trunc, |
|
| 70 |
+ c: container, |
|
| 71 |
+ } |
|
| 72 |
+ err = ctx.contextFormat(tmpl, containerCtx) |
|
| 73 |
+ if err != nil {
|
|
| 74 |
+ return |
|
| 75 |
+ } |
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ ctx.postformat(tmpl, &containerContext{})
|
|
| 79 |
+} |
|
| 80 |
+ |
|
| 81 |
+type containerContext struct {
|
|
| 82 |
+ baseSubContext |
|
| 83 |
+ trunc bool |
|
| 84 |
+ c types.Container |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+func (c *containerContext) ID() string {
|
|
| 88 |
+ c.addHeader(containerIDHeader) |
|
| 89 |
+ if c.trunc {
|
|
| 90 |
+ return stringid.TruncateID(c.c.ID) |
|
| 91 |
+ } |
|
| 92 |
+ return c.c.ID |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+func (c *containerContext) Names() string {
|
|
| 96 |
+ c.addHeader(namesHeader) |
|
| 97 |
+ names := stripNamePrefix(c.c.Names) |
|
| 98 |
+ if c.trunc {
|
|
| 99 |
+ for _, name := range names {
|
|
| 100 |
+ if len(strings.Split(name, "/")) == 1 {
|
|
| 101 |
+ names = []string{name}
|
|
| 102 |
+ break |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 105 |
+ } |
|
| 106 |
+ return strings.Join(names, ",") |
|
| 107 |
+} |
|
| 108 |
+ |
|
| 109 |
+func (c *containerContext) Image() string {
|
|
| 110 |
+ c.addHeader(imageHeader) |
|
| 111 |
+ if c.c.Image == "" {
|
|
| 112 |
+ return "<no image>" |
|
| 113 |
+ } |
|
| 114 |
+ if c.trunc {
|
|
| 115 |
+ if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
|
| 116 |
+ return trunc |
|
| 117 |
+ } |
|
| 118 |
+ } |
|
| 119 |
+ return c.c.Image |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+func (c *containerContext) Command() string {
|
|
| 123 |
+ c.addHeader(commandHeader) |
|
| 124 |
+ command := c.c.Command |
|
| 125 |
+ if c.trunc {
|
|
| 126 |
+ command = stringutils.Truncate(command, 20) |
|
| 127 |
+ } |
|
| 128 |
+ return strconv.Quote(command) |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+func (c *containerContext) CreatedAt() string {
|
|
| 132 |
+ c.addHeader(createdAtHeader) |
|
| 133 |
+ return time.Unix(int64(c.c.Created), 0).String() |
|
| 134 |
+} |
|
| 135 |
+ |
|
| 136 |
+func (c *containerContext) RunningFor() string {
|
|
| 137 |
+ c.addHeader(runningForHeader) |
|
| 138 |
+ createdAt := time.Unix(int64(c.c.Created), 0) |
|
| 139 |
+ return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+func (c *containerContext) Ports() string {
|
|
| 143 |
+ c.addHeader(portsHeader) |
|
| 144 |
+ return api.DisplayablePorts(c.c.Ports) |
|
| 145 |
+} |
|
| 146 |
+ |
|
| 147 |
+func (c *containerContext) Status() string {
|
|
| 148 |
+ c.addHeader(statusHeader) |
|
| 149 |
+ return c.c.Status |
|
| 150 |
+} |
|
| 151 |
+ |
|
| 152 |
+func (c *containerContext) Size() string {
|
|
| 153 |
+ c.addHeader(sizeHeader) |
|
| 154 |
+ srw := units.HumanSize(float64(c.c.SizeRw)) |
|
| 155 |
+ sv := units.HumanSize(float64(c.c.SizeRootFs)) |
|
| 156 |
+ |
|
| 157 |
+ sf := srw |
|
| 158 |
+ if c.c.SizeRootFs > 0 {
|
|
| 159 |
+ sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
|
| 160 |
+ } |
|
| 161 |
+ return sf |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+func (c *containerContext) Labels() string {
|
|
| 165 |
+ c.addHeader(labelsHeader) |
|
| 166 |
+ if c.c.Labels == nil {
|
|
| 167 |
+ return "" |
|
| 168 |
+ } |
|
| 169 |
+ |
|
| 170 |
+ var joinLabels []string |
|
| 171 |
+ for k, v := range c.c.Labels {
|
|
| 172 |
+ joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
|
| 173 |
+ } |
|
| 174 |
+ return strings.Join(joinLabels, ",") |
|
| 175 |
+} |
|
| 176 |
+ |
|
| 177 |
+func (c *containerContext) Label(name string) string {
|
|
| 178 |
+ n := strings.Split(name, ".") |
|
| 179 |
+ r := strings.NewReplacer("-", " ", "_", " ")
|
|
| 180 |
+ h := r.Replace(n[len(n)-1]) |
|
| 181 |
+ |
|
| 182 |
+ c.addHeader(h) |
|
| 183 |
+ |
|
| 184 |
+ if c.c.Labels == nil {
|
|
| 185 |
+ return "" |
|
| 186 |
+ } |
|
| 187 |
+ return c.c.Labels[name] |
|
| 188 |
+} |
|
| 189 |
+ |
|
| 190 |
+func (c *containerContext) Mounts() string {
|
|
| 191 |
+ c.addHeader(mountsHeader) |
|
| 192 |
+ |
|
| 193 |
+ var name string |
|
| 194 |
+ var mounts []string |
|
| 195 |
+ for _, m := range c.c.Mounts {
|
|
| 196 |
+ if m.Name == "" {
|
|
| 197 |
+ name = m.Source |
|
| 198 |
+ } else {
|
|
| 199 |
+ name = m.Name |
|
| 200 |
+ } |
|
| 201 |
+ if c.trunc {
|
|
| 202 |
+ name = stringutils.Truncate(name, 15) |
|
| 203 |
+ } |
|
| 204 |
+ mounts = append(mounts, name) |
|
| 205 |
+ } |
|
| 206 |
+ return strings.Join(mounts, ",") |
|
| 207 |
+} |
| 0 | 208 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,404 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "testing" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 10 |
+ "github.com/docker/engine-api/types" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+func TestContainerPsContext(t *testing.T) {
|
|
| 14 |
+ containerID := stringid.GenerateRandomID() |
|
| 15 |
+ unix := time.Now().Add(-65 * time.Second).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), containerIDHeader, ctx.ID},
|
|
| 26 |
+ {types.Container{ID: containerID}, false, containerID, containerIDHeader, 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, "About a minute", runningForHeader, ctx.RunningFor},
|
|
| 59 |
+ {types.Container{
|
|
| 60 |
+ Mounts: []types.MountPoint{
|
|
| 61 |
+ {
|
|
| 62 |
+ Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", |
|
| 63 |
+ Driver: "local", |
|
| 64 |
+ Source: "/a/path", |
|
| 65 |
+ }, |
|
| 66 |
+ }, |
|
| 67 |
+ }, true, "733908409c91817", mountsHeader, ctx.Mounts}, |
|
| 68 |
+ {types.Container{
|
|
| 69 |
+ Mounts: []types.MountPoint{
|
|
| 70 |
+ {
|
|
| 71 |
+ Driver: "local", |
|
| 72 |
+ Source: "/a/path", |
|
| 73 |
+ }, |
|
| 74 |
+ }, |
|
| 75 |
+ }, false, "/a/path", mountsHeader, ctx.Mounts}, |
|
| 76 |
+ {types.Container{
|
|
| 77 |
+ Mounts: []types.MountPoint{
|
|
| 78 |
+ {
|
|
| 79 |
+ Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", |
|
| 80 |
+ Driver: "local", |
|
| 81 |
+ Source: "/a/path", |
|
| 82 |
+ }, |
|
| 83 |
+ }, |
|
| 84 |
+ }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts}, |
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ for _, c := range cases {
|
|
| 88 |
+ ctx = containerContext{c: c.container, trunc: c.trunc}
|
|
| 89 |
+ v := c.call() |
|
| 90 |
+ if strings.Contains(v, ",") {
|
|
| 91 |
+ compareMultipleValues(t, v, c.expValue) |
|
| 92 |
+ } else if v != c.expValue {
|
|
| 93 |
+ t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 94 |
+ } |
|
| 95 |
+ |
|
| 96 |
+ h := ctx.fullHeader() |
|
| 97 |
+ if h != c.expHeader {
|
|
| 98 |
+ t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 99 |
+ } |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
|
| 103 |
+ ctx = containerContext{c: c1, trunc: true}
|
|
| 104 |
+ |
|
| 105 |
+ sid := ctx.Label("com.docker.swarm.swarm-id")
|
|
| 106 |
+ node := ctx.Label("com.docker.swarm.node_name")
|
|
| 107 |
+ if sid != "33" {
|
|
| 108 |
+ t.Fatalf("Expected 33, was %s\n", sid)
|
|
| 109 |
+ } |
|
| 110 |
+ |
|
| 111 |
+ if node != "ubuntu" {
|
|
| 112 |
+ t.Fatalf("Expected ubuntu, was %s\n", node)
|
|
| 113 |
+ } |
|
| 114 |
+ |
|
| 115 |
+ h := ctx.fullHeader() |
|
| 116 |
+ if h != "SWARM ID\tNODE NAME" {
|
|
| 117 |
+ t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
|
| 118 |
+ |
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ c2 := types.Container{}
|
|
| 122 |
+ ctx = containerContext{c: c2, trunc: true}
|
|
| 123 |
+ |
|
| 124 |
+ label := ctx.Label("anything.really")
|
|
| 125 |
+ if label != "" {
|
|
| 126 |
+ t.Fatalf("Expected an empty string, was %s", label)
|
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ ctx = containerContext{c: c2, trunc: true}
|
|
| 130 |
+ fullHeader := ctx.fullHeader() |
|
| 131 |
+ if fullHeader != "" {
|
|
| 132 |
+ t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+} |
|
| 136 |
+ |
|
| 137 |
+func TestContainerContextWrite(t *testing.T) {
|
|
| 138 |
+ unixTime := time.Now().AddDate(0, 0, -1).Unix() |
|
| 139 |
+ expectedTime := time.Unix(unixTime, 0).String() |
|
| 140 |
+ |
|
| 141 |
+ contexts := []struct {
|
|
| 142 |
+ context ContainerContext |
|
| 143 |
+ expected string |
|
| 144 |
+ }{
|
|
| 145 |
+ // Errors |
|
| 146 |
+ {
|
|
| 147 |
+ ContainerContext{
|
|
| 148 |
+ Context: Context{
|
|
| 149 |
+ Format: "{{InvalidFunction}}",
|
|
| 150 |
+ }, |
|
| 151 |
+ }, |
|
| 152 |
+ `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 153 |
+`, |
|
| 154 |
+ }, |
|
| 155 |
+ {
|
|
| 156 |
+ ContainerContext{
|
|
| 157 |
+ Context: Context{
|
|
| 158 |
+ Format: "{{nil}}",
|
|
| 159 |
+ }, |
|
| 160 |
+ }, |
|
| 161 |
+ `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 162 |
+`, |
|
| 163 |
+ }, |
|
| 164 |
+ // Table Format |
|
| 165 |
+ {
|
|
| 166 |
+ ContainerContext{
|
|
| 167 |
+ Context: Context{
|
|
| 168 |
+ Format: "table", |
|
| 169 |
+ }, |
|
| 170 |
+ Size: true, |
|
| 171 |
+ }, |
|
| 172 |
+ `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE |
|
| 173 |
+containerID1 ubuntu "" 24 hours ago foobar_baz 0 B |
|
| 174 |
+containerID2 ubuntu "" 24 hours ago foobar_bar 0 B |
|
| 175 |
+`, |
|
| 176 |
+ }, |
|
| 177 |
+ {
|
|
| 178 |
+ ContainerContext{
|
|
| 179 |
+ Context: Context{
|
|
| 180 |
+ Format: "table", |
|
| 181 |
+ }, |
|
| 182 |
+ }, |
|
| 183 |
+ `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
|
| 184 |
+containerID1 ubuntu "" 24 hours ago foobar_baz |
|
| 185 |
+containerID2 ubuntu "" 24 hours ago foobar_bar |
|
| 186 |
+`, |
|
| 187 |
+ }, |
|
| 188 |
+ {
|
|
| 189 |
+ ContainerContext{
|
|
| 190 |
+ Context: Context{
|
|
| 191 |
+ Format: "table {{.Image}}",
|
|
| 192 |
+ }, |
|
| 193 |
+ }, |
|
| 194 |
+ "IMAGE\nubuntu\nubuntu\n", |
|
| 195 |
+ }, |
|
| 196 |
+ {
|
|
| 197 |
+ ContainerContext{
|
|
| 198 |
+ Context: Context{
|
|
| 199 |
+ Format: "table {{.Image}}",
|
|
| 200 |
+ }, |
|
| 201 |
+ Size: true, |
|
| 202 |
+ }, |
|
| 203 |
+ "IMAGE\nubuntu\nubuntu\n", |
|
| 204 |
+ }, |
|
| 205 |
+ {
|
|
| 206 |
+ ContainerContext{
|
|
| 207 |
+ Context: Context{
|
|
| 208 |
+ Format: "table {{.Image}}",
|
|
| 209 |
+ Quiet: true, |
|
| 210 |
+ }, |
|
| 211 |
+ }, |
|
| 212 |
+ "IMAGE\nubuntu\nubuntu\n", |
|
| 213 |
+ }, |
|
| 214 |
+ {
|
|
| 215 |
+ ContainerContext{
|
|
| 216 |
+ Context: Context{
|
|
| 217 |
+ Format: "table", |
|
| 218 |
+ Quiet: true, |
|
| 219 |
+ }, |
|
| 220 |
+ }, |
|
| 221 |
+ "containerID1\ncontainerID2\n", |
|
| 222 |
+ }, |
|
| 223 |
+ // Raw Format |
|
| 224 |
+ {
|
|
| 225 |
+ ContainerContext{
|
|
| 226 |
+ Context: Context{
|
|
| 227 |
+ Format: "raw", |
|
| 228 |
+ }, |
|
| 229 |
+ }, |
|
| 230 |
+ fmt.Sprintf(`container_id: containerID1 |
|
| 231 |
+image: ubuntu |
|
| 232 |
+command: "" |
|
| 233 |
+created_at: %s |
|
| 234 |
+status: |
|
| 235 |
+names: foobar_baz |
|
| 236 |
+labels: |
|
| 237 |
+ports: |
|
| 238 |
+ |
|
| 239 |
+container_id: containerID2 |
|
| 240 |
+image: ubuntu |
|
| 241 |
+command: "" |
|
| 242 |
+created_at: %s |
|
| 243 |
+status: |
|
| 244 |
+names: foobar_bar |
|
| 245 |
+labels: |
|
| 246 |
+ports: |
|
| 247 |
+ |
|
| 248 |
+`, expectedTime, expectedTime), |
|
| 249 |
+ }, |
|
| 250 |
+ {
|
|
| 251 |
+ ContainerContext{
|
|
| 252 |
+ Context: Context{
|
|
| 253 |
+ Format: "raw", |
|
| 254 |
+ }, |
|
| 255 |
+ Size: true, |
|
| 256 |
+ }, |
|
| 257 |
+ fmt.Sprintf(`container_id: containerID1 |
|
| 258 |
+image: ubuntu |
|
| 259 |
+command: "" |
|
| 260 |
+created_at: %s |
|
| 261 |
+status: |
|
| 262 |
+names: foobar_baz |
|
| 263 |
+labels: |
|
| 264 |
+ports: |
|
| 265 |
+size: 0 B |
|
| 266 |
+ |
|
| 267 |
+container_id: containerID2 |
|
| 268 |
+image: ubuntu |
|
| 269 |
+command: "" |
|
| 270 |
+created_at: %s |
|
| 271 |
+status: |
|
| 272 |
+names: foobar_bar |
|
| 273 |
+labels: |
|
| 274 |
+ports: |
|
| 275 |
+size: 0 B |
|
| 276 |
+ |
|
| 277 |
+`, expectedTime, expectedTime), |
|
| 278 |
+ }, |
|
| 279 |
+ {
|
|
| 280 |
+ ContainerContext{
|
|
| 281 |
+ Context: Context{
|
|
| 282 |
+ Format: "raw", |
|
| 283 |
+ Quiet: true, |
|
| 284 |
+ }, |
|
| 285 |
+ }, |
|
| 286 |
+ "container_id: containerID1\ncontainer_id: containerID2\n", |
|
| 287 |
+ }, |
|
| 288 |
+ // Custom Format |
|
| 289 |
+ {
|
|
| 290 |
+ ContainerContext{
|
|
| 291 |
+ Context: Context{
|
|
| 292 |
+ Format: "{{.Image}}",
|
|
| 293 |
+ }, |
|
| 294 |
+ }, |
|
| 295 |
+ "ubuntu\nubuntu\n", |
|
| 296 |
+ }, |
|
| 297 |
+ {
|
|
| 298 |
+ ContainerContext{
|
|
| 299 |
+ Context: Context{
|
|
| 300 |
+ Format: "{{.Image}}",
|
|
| 301 |
+ }, |
|
| 302 |
+ Size: true, |
|
| 303 |
+ }, |
|
| 304 |
+ "ubuntu\nubuntu\n", |
|
| 305 |
+ }, |
|
| 306 |
+ } |
|
| 307 |
+ |
|
| 308 |
+ for _, context := range contexts {
|
|
| 309 |
+ containers := []types.Container{
|
|
| 310 |
+ {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
|
| 311 |
+ {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
|
| 312 |
+ } |
|
| 313 |
+ out := bytes.NewBufferString("")
|
|
| 314 |
+ context.context.Output = out |
|
| 315 |
+ context.context.Containers = containers |
|
| 316 |
+ context.context.Write() |
|
| 317 |
+ actual := out.String() |
|
| 318 |
+ if actual != context.expected {
|
|
| 319 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 320 |
+ } |
|
| 321 |
+ // Clean buffer |
|
| 322 |
+ out.Reset() |
|
| 323 |
+ } |
|
| 324 |
+} |
|
| 325 |
+ |
|
| 326 |
+func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
|
| 327 |
+ out := bytes.NewBufferString("")
|
|
| 328 |
+ containers := []types.Container{}
|
|
| 329 |
+ |
|
| 330 |
+ contexts := []struct {
|
|
| 331 |
+ context ContainerContext |
|
| 332 |
+ expected string |
|
| 333 |
+ }{
|
|
| 334 |
+ {
|
|
| 335 |
+ ContainerContext{
|
|
| 336 |
+ Context: Context{
|
|
| 337 |
+ Format: "{{.Image}}",
|
|
| 338 |
+ Output: out, |
|
| 339 |
+ }, |
|
| 340 |
+ }, |
|
| 341 |
+ "", |
|
| 342 |
+ }, |
|
| 343 |
+ {
|
|
| 344 |
+ ContainerContext{
|
|
| 345 |
+ Context: Context{
|
|
| 346 |
+ Format: "table {{.Image}}",
|
|
| 347 |
+ Output: out, |
|
| 348 |
+ }, |
|
| 349 |
+ }, |
|
| 350 |
+ "IMAGE\n", |
|
| 351 |
+ }, |
|
| 352 |
+ {
|
|
| 353 |
+ ContainerContext{
|
|
| 354 |
+ Context: Context{
|
|
| 355 |
+ Format: "{{.Image}}",
|
|
| 356 |
+ Output: out, |
|
| 357 |
+ }, |
|
| 358 |
+ Size: true, |
|
| 359 |
+ }, |
|
| 360 |
+ "", |
|
| 361 |
+ }, |
|
| 362 |
+ {
|
|
| 363 |
+ ContainerContext{
|
|
| 364 |
+ Context: Context{
|
|
| 365 |
+ Format: "table {{.Image}}",
|
|
| 366 |
+ Output: out, |
|
| 367 |
+ }, |
|
| 368 |
+ Size: true, |
|
| 369 |
+ }, |
|
| 370 |
+ "IMAGE\n", |
|
| 371 |
+ }, |
|
| 372 |
+ {
|
|
| 373 |
+ ContainerContext{
|
|
| 374 |
+ Context: Context{
|
|
| 375 |
+ Format: "table {{.Image}}\t{{.Size}}",
|
|
| 376 |
+ Output: out, |
|
| 377 |
+ }, |
|
| 378 |
+ }, |
|
| 379 |
+ "IMAGE SIZE\n", |
|
| 380 |
+ }, |
|
| 381 |
+ {
|
|
| 382 |
+ ContainerContext{
|
|
| 383 |
+ Context: Context{
|
|
| 384 |
+ Format: "table {{.Image}}\t{{.Size}}",
|
|
| 385 |
+ Output: out, |
|
| 386 |
+ }, |
|
| 387 |
+ Size: true, |
|
| 388 |
+ }, |
|
| 389 |
+ "IMAGE SIZE\n", |
|
| 390 |
+ }, |
|
| 391 |
+ } |
|
| 392 |
+ |
|
| 393 |
+ for _, context := range contexts {
|
|
| 394 |
+ context.context.Containers = containers |
|
| 395 |
+ context.context.Write() |
|
| 396 |
+ actual := out.String() |
|
| 397 |
+ if actual != context.expected {
|
|
| 398 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 399 |
+ } |
|
| 400 |
+ // Clean buffer |
|
| 401 |
+ out.Reset() |
|
| 402 |
+ } |
|
| 403 |
+} |
| ... | ... |
@@ -1,215 +1,21 @@ |
| 1 | 1 |
package formatter |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "fmt" |
|
| 5 |
- "strconv" |
|
| 6 | 4 |
"strings" |
| 7 |
- "time" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/docker/api" |
|
| 10 |
- "github.com/docker/docker/pkg/stringid" |
|
| 11 |
- "github.com/docker/docker/pkg/stringutils" |
|
| 12 |
- "github.com/docker/engine-api/types" |
|
| 13 |
- "github.com/docker/go-units" |
|
| 14 | 5 |
) |
| 15 | 6 |
|
| 16 | 7 |
const ( |
| 17 | 8 |
tableKey = "table" |
| 18 | 9 |
|
| 19 |
- containerIDHeader = "CONTAINER ID" |
|
| 20 | 10 |
imageHeader = "IMAGE" |
| 21 |
- namesHeader = "NAMES" |
|
| 22 |
- commandHeader = "COMMAND" |
|
| 23 | 11 |
createdSinceHeader = "CREATED" |
| 24 | 12 |
createdAtHeader = "CREATED AT" |
| 25 |
- runningForHeader = "CREATED" |
|
| 26 |
- statusHeader = "STATUS" |
|
| 27 |
- portsHeader = "PORTS" |
|
| 28 | 13 |
sizeHeader = "SIZE" |
| 29 | 14 |
labelsHeader = "LABELS" |
| 30 |
- imageIDHeader = "IMAGE ID" |
|
| 31 |
- repositoryHeader = "REPOSITORY" |
|
| 32 |
- tagHeader = "TAG" |
|
| 33 |
- digestHeader = "DIGEST" |
|
| 34 |
- mountsHeader = "MOUNTS" |
|
| 15 |
+ nameHeader = "NAME" |
|
| 16 |
+ driverHeader = "DRIVER" |
|
| 35 | 17 |
) |
| 36 | 18 |
|
| 37 |
-type containerContext struct {
|
|
| 38 |
- baseSubContext |
|
| 39 |
- trunc bool |
|
| 40 |
- c types.Container |
|
| 41 |
-} |
|
| 42 |
- |
|
| 43 |
-func (c *containerContext) ID() string {
|
|
| 44 |
- c.addHeader(containerIDHeader) |
|
| 45 |
- if c.trunc {
|
|
| 46 |
- return stringid.TruncateID(c.c.ID) |
|
| 47 |
- } |
|
| 48 |
- return c.c.ID |
|
| 49 |
-} |
|
| 50 |
- |
|
| 51 |
-func (c *containerContext) Names() string {
|
|
| 52 |
- c.addHeader(namesHeader) |
|
| 53 |
- names := stripNamePrefix(c.c.Names) |
|
| 54 |
- if c.trunc {
|
|
| 55 |
- for _, name := range names {
|
|
| 56 |
- if len(strings.Split(name, "/")) == 1 {
|
|
| 57 |
- names = []string{name}
|
|
| 58 |
- break |
|
| 59 |
- } |
|
| 60 |
- } |
|
| 61 |
- } |
|
| 62 |
- return strings.Join(names, ",") |
|
| 63 |
-} |
|
| 64 |
- |
|
| 65 |
-func (c *containerContext) Image() string {
|
|
| 66 |
- c.addHeader(imageHeader) |
|
| 67 |
- if c.c.Image == "" {
|
|
| 68 |
- return "<no image>" |
|
| 69 |
- } |
|
| 70 |
- if c.trunc {
|
|
| 71 |
- if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
|
| 72 |
- return trunc |
|
| 73 |
- } |
|
| 74 |
- } |
|
| 75 |
- return c.c.Image |
|
| 76 |
-} |
|
| 77 |
- |
|
| 78 |
-func (c *containerContext) Command() string {
|
|
| 79 |
- c.addHeader(commandHeader) |
|
| 80 |
- command := c.c.Command |
|
| 81 |
- if c.trunc {
|
|
| 82 |
- command = stringutils.Truncate(command, 20) |
|
| 83 |
- } |
|
| 84 |
- return strconv.Quote(command) |
|
| 85 |
-} |
|
| 86 |
- |
|
| 87 |
-func (c *containerContext) CreatedAt() string {
|
|
| 88 |
- c.addHeader(createdAtHeader) |
|
| 89 |
- return time.Unix(int64(c.c.Created), 0).String() |
|
| 90 |
-} |
|
| 91 |
- |
|
| 92 |
-func (c *containerContext) RunningFor() string {
|
|
| 93 |
- c.addHeader(runningForHeader) |
|
| 94 |
- createdAt := time.Unix(int64(c.c.Created), 0) |
|
| 95 |
- return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 96 |
-} |
|
| 97 |
- |
|
| 98 |
-func (c *containerContext) Ports() string {
|
|
| 99 |
- c.addHeader(portsHeader) |
|
| 100 |
- return api.DisplayablePorts(c.c.Ports) |
|
| 101 |
-} |
|
| 102 |
- |
|
| 103 |
-func (c *containerContext) Status() string {
|
|
| 104 |
- c.addHeader(statusHeader) |
|
| 105 |
- return c.c.Status |
|
| 106 |
-} |
|
| 107 |
- |
|
| 108 |
-func (c *containerContext) Size() string {
|
|
| 109 |
- c.addHeader(sizeHeader) |
|
| 110 |
- srw := units.HumanSize(float64(c.c.SizeRw)) |
|
| 111 |
- sv := units.HumanSize(float64(c.c.SizeRootFs)) |
|
| 112 |
- |
|
| 113 |
- sf := srw |
|
| 114 |
- if c.c.SizeRootFs > 0 {
|
|
| 115 |
- sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
|
| 116 |
- } |
|
| 117 |
- return sf |
|
| 118 |
-} |
|
| 119 |
- |
|
| 120 |
-func (c *containerContext) Labels() string {
|
|
| 121 |
- c.addHeader(labelsHeader) |
|
| 122 |
- if c.c.Labels == nil {
|
|
| 123 |
- return "" |
|
| 124 |
- } |
|
| 125 |
- |
|
| 126 |
- var joinLabels []string |
|
| 127 |
- for k, v := range c.c.Labels {
|
|
| 128 |
- joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
|
| 129 |
- } |
|
| 130 |
- return strings.Join(joinLabels, ",") |
|
| 131 |
-} |
|
| 132 |
- |
|
| 133 |
-func (c *containerContext) Label(name string) string {
|
|
| 134 |
- n := strings.Split(name, ".") |
|
| 135 |
- r := strings.NewReplacer("-", " ", "_", " ")
|
|
| 136 |
- h := r.Replace(n[len(n)-1]) |
|
| 137 |
- |
|
| 138 |
- c.addHeader(h) |
|
| 139 |
- |
|
| 140 |
- if c.c.Labels == nil {
|
|
| 141 |
- return "" |
|
| 142 |
- } |
|
| 143 |
- return c.c.Labels[name] |
|
| 144 |
-} |
|
| 145 |
- |
|
| 146 |
-func (c *containerContext) Mounts() string {
|
|
| 147 |
- c.addHeader(mountsHeader) |
|
| 148 |
- |
|
| 149 |
- var name string |
|
| 150 |
- var mounts []string |
|
| 151 |
- for _, m := range c.c.Mounts {
|
|
| 152 |
- if m.Name == "" {
|
|
| 153 |
- name = m.Source |
|
| 154 |
- } else {
|
|
| 155 |
- name = m.Name |
|
| 156 |
- } |
|
| 157 |
- if c.trunc {
|
|
| 158 |
- name = stringutils.Truncate(name, 15) |
|
| 159 |
- } |
|
| 160 |
- mounts = append(mounts, name) |
|
| 161 |
- } |
|
| 162 |
- return strings.Join(mounts, ",") |
|
| 163 |
-} |
|
| 164 |
- |
|
| 165 |
-type imageContext struct {
|
|
| 166 |
- baseSubContext |
|
| 167 |
- trunc bool |
|
| 168 |
- i types.Image |
|
| 169 |
- repo string |
|
| 170 |
- tag string |
|
| 171 |
- digest string |
|
| 172 |
-} |
|
| 173 |
- |
|
| 174 |
-func (c *imageContext) ID() string {
|
|
| 175 |
- c.addHeader(imageIDHeader) |
|
| 176 |
- if c.trunc {
|
|
| 177 |
- return stringid.TruncateID(c.i.ID) |
|
| 178 |
- } |
|
| 179 |
- return c.i.ID |
|
| 180 |
-} |
|
| 181 |
- |
|
| 182 |
-func (c *imageContext) Repository() string {
|
|
| 183 |
- c.addHeader(repositoryHeader) |
|
| 184 |
- return c.repo |
|
| 185 |
-} |
|
| 186 |
- |
|
| 187 |
-func (c *imageContext) Tag() string {
|
|
| 188 |
- c.addHeader(tagHeader) |
|
| 189 |
- return c.tag |
|
| 190 |
-} |
|
| 191 |
- |
|
| 192 |
-func (c *imageContext) Digest() string {
|
|
| 193 |
- c.addHeader(digestHeader) |
|
| 194 |
- return c.digest |
|
| 195 |
-} |
|
| 196 |
- |
|
| 197 |
-func (c *imageContext) CreatedSince() string {
|
|
| 198 |
- c.addHeader(createdSinceHeader) |
|
| 199 |
- createdAt := time.Unix(int64(c.i.Created), 0) |
|
| 200 |
- return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 201 |
-} |
|
| 202 |
- |
|
| 203 |
-func (c *imageContext) CreatedAt() string {
|
|
| 204 |
- c.addHeader(createdAtHeader) |
|
| 205 |
- return time.Unix(int64(c.i.Created), 0).String() |
|
| 206 |
-} |
|
| 207 |
- |
|
| 208 |
-func (c *imageContext) Size() string {
|
|
| 209 |
- c.addHeader(sizeHeader) |
|
| 210 |
- return units.HumanSize(float64(c.i.Size)) |
|
| 211 |
-} |
|
| 212 |
- |
|
| 213 | 19 |
type subContext interface {
|
| 214 | 20 |
fullHeader() string |
| 215 | 21 |
addHeader(header string) |
| ... | ... |
@@ -4,172 +4,8 @@ import ( |
| 4 | 4 |
"reflect" |
| 5 | 5 |
"strings" |
| 6 | 6 |
"testing" |
| 7 |
- "time" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/docker/pkg/stringid" |
|
| 10 |
- "github.com/docker/engine-api/types" |
|
| 11 | 7 |
) |
| 12 | 8 |
|
| 13 |
-func TestContainerPsContext(t *testing.T) {
|
|
| 14 |
- containerID := stringid.GenerateRandomID() |
|
| 15 |
- unix := time.Now().Add(-65 * time.Second).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), containerIDHeader, ctx.ID},
|
|
| 26 |
- {types.Container{ID: containerID}, false, containerID, containerIDHeader, 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, "About a minute", 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 |
- compareMultipleValues(t, v, c.expValue) |
|
| 66 |
- } else if v != c.expValue {
|
|
| 67 |
- t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 68 |
- } |
|
| 69 |
- |
|
| 70 |
- h := ctx.fullHeader() |
|
| 71 |
- if h != c.expHeader {
|
|
| 72 |
- t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 73 |
- } |
|
| 74 |
- } |
|
| 75 |
- |
|
| 76 |
- c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
|
| 77 |
- ctx = containerContext{c: c1, trunc: true}
|
|
| 78 |
- |
|
| 79 |
- sid := ctx.Label("com.docker.swarm.swarm-id")
|
|
| 80 |
- node := ctx.Label("com.docker.swarm.node_name")
|
|
| 81 |
- if sid != "33" {
|
|
| 82 |
- t.Fatalf("Expected 33, was %s\n", sid)
|
|
| 83 |
- } |
|
| 84 |
- |
|
| 85 |
- if node != "ubuntu" {
|
|
| 86 |
- t.Fatalf("Expected ubuntu, was %s\n", node)
|
|
| 87 |
- } |
|
| 88 |
- |
|
| 89 |
- h := ctx.fullHeader() |
|
| 90 |
- if h != "SWARM ID\tNODE NAME" {
|
|
| 91 |
- t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
|
| 92 |
- |
|
| 93 |
- } |
|
| 94 |
- |
|
| 95 |
- c2 := types.Container{}
|
|
| 96 |
- ctx = containerContext{c: c2, trunc: true}
|
|
| 97 |
- |
|
| 98 |
- label := ctx.Label("anything.really")
|
|
| 99 |
- if label != "" {
|
|
| 100 |
- t.Fatalf("Expected an empty string, was %s", label)
|
|
| 101 |
- } |
|
| 102 |
- |
|
| 103 |
- ctx = containerContext{c: c2, trunc: true}
|
|
| 104 |
- fullHeader := ctx.fullHeader() |
|
| 105 |
- if fullHeader != "" {
|
|
| 106 |
- t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
|
| 107 |
- } |
|
| 108 |
- |
|
| 109 |
-} |
|
| 110 |
- |
|
| 111 |
-func TestImagesContext(t *testing.T) {
|
|
| 112 |
- imageID := stringid.GenerateRandomID() |
|
| 113 |
- unix := time.Now().Unix() |
|
| 114 |
- |
|
| 115 |
- var ctx imageContext |
|
| 116 |
- cases := []struct {
|
|
| 117 |
- imageCtx imageContext |
|
| 118 |
- expValue string |
|
| 119 |
- expHeader string |
|
| 120 |
- call func() string |
|
| 121 |
- }{
|
|
| 122 |
- {imageContext{
|
|
| 123 |
- i: types.Image{ID: imageID},
|
|
| 124 |
- trunc: true, |
|
| 125 |
- }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, |
|
| 126 |
- {imageContext{
|
|
| 127 |
- i: types.Image{ID: imageID},
|
|
| 128 |
- trunc: false, |
|
| 129 |
- }, imageID, imageIDHeader, ctx.ID}, |
|
| 130 |
- {imageContext{
|
|
| 131 |
- i: types.Image{Size: 10},
|
|
| 132 |
- trunc: true, |
|
| 133 |
- }, "10 B", sizeHeader, ctx.Size}, |
|
| 134 |
- {imageContext{
|
|
| 135 |
- i: types.Image{Created: unix},
|
|
| 136 |
- trunc: true, |
|
| 137 |
- }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, |
|
| 138 |
- // FIXME |
|
| 139 |
- // {imageContext{
|
|
| 140 |
- // i: types.Image{Created: unix},
|
|
| 141 |
- // trunc: true, |
|
| 142 |
- // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, |
|
| 143 |
- {imageContext{
|
|
| 144 |
- i: types.Image{},
|
|
| 145 |
- repo: "busybox", |
|
| 146 |
- }, "busybox", repositoryHeader, ctx.Repository}, |
|
| 147 |
- {imageContext{
|
|
| 148 |
- i: types.Image{},
|
|
| 149 |
- tag: "latest", |
|
| 150 |
- }, "latest", tagHeader, ctx.Tag}, |
|
| 151 |
- {imageContext{
|
|
| 152 |
- i: types.Image{},
|
|
| 153 |
- digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", |
|
| 154 |
- }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, |
|
| 155 |
- } |
|
| 156 |
- |
|
| 157 |
- for _, c := range cases {
|
|
| 158 |
- ctx = c.imageCtx |
|
| 159 |
- v := c.call() |
|
| 160 |
- if strings.Contains(v, ",") {
|
|
| 161 |
- compareMultipleValues(t, v, c.expValue) |
|
| 162 |
- } else if v != c.expValue {
|
|
| 163 |
- t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 164 |
- } |
|
| 165 |
- |
|
| 166 |
- h := ctx.fullHeader() |
|
| 167 |
- if h != c.expHeader {
|
|
| 168 |
- t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 169 |
- } |
|
| 170 |
- } |
|
| 171 |
-} |
|
| 172 |
- |
|
| 173 | 9 |
func compareMultipleValues(t *testing.T, value, expected string) {
|
| 174 | 10 |
// comma-separated values means probably a map input, which won't |
| 175 | 11 |
// be guaranteed to have the same order as our expected value |
| ... | ... |
@@ -8,19 +8,14 @@ import ( |
| 8 | 8 |
"text/tabwriter" |
| 9 | 9 |
"text/template" |
| 10 | 10 |
|
| 11 |
- "github.com/docker/docker/reference" |
|
| 12 | 11 |
"github.com/docker/docker/utils/templates" |
| 13 |
- "github.com/docker/engine-api/types" |
|
| 14 | 12 |
) |
| 15 | 13 |
|
| 16 | 14 |
const ( |
| 17 | 15 |
tableFormatKey = "table" |
| 18 | 16 |
rawFormatKey = "raw" |
| 19 | 17 |
|
| 20 |
- defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
| 21 |
- defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
| 22 |
- defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
| 23 |
- defaultQuietFormat = "{{.ID}}"
|
|
| 18 |
+ defaultQuietFormat = "{{.ID}}"
|
|
| 24 | 19 |
) |
| 25 | 20 |
|
| 26 | 21 |
// Context contains information required by the formatter to print the output as desired. |
| ... | ... |
@@ -93,215 +88,3 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) |
| 93 | 93 |
c.buffer.WriteString("\n")
|
| 94 | 94 |
return nil |
| 95 | 95 |
} |
| 96 |
- |
|
| 97 |
-// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. |
|
| 98 |
-type ContainerContext struct {
|
|
| 99 |
- Context |
|
| 100 |
- // Size when set to true will display the size of the output. |
|
| 101 |
- Size bool |
|
| 102 |
- // Containers |
|
| 103 |
- Containers []types.Container |
|
| 104 |
-} |
|
| 105 |
- |
|
| 106 |
-// ImageContext contains image specific information required by the formater, encapsulate a Context struct. |
|
| 107 |
-type ImageContext struct {
|
|
| 108 |
- Context |
|
| 109 |
- Digest bool |
|
| 110 |
- // Images |
|
| 111 |
- Images []types.Image |
|
| 112 |
-} |
|
| 113 |
- |
|
| 114 |
-func (ctx ContainerContext) Write() {
|
|
| 115 |
- switch ctx.Format {
|
|
| 116 |
- case tableFormatKey: |
|
| 117 |
- if ctx.Quiet {
|
|
| 118 |
- ctx.Format = defaultQuietFormat |
|
| 119 |
- } else {
|
|
| 120 |
- ctx.Format = defaultContainerTableFormat |
|
| 121 |
- if ctx.Size {
|
|
| 122 |
- ctx.Format += `\t{{.Size}}`
|
|
| 123 |
- } |
|
| 124 |
- } |
|
| 125 |
- case rawFormatKey: |
|
| 126 |
- if ctx.Quiet {
|
|
| 127 |
- ctx.Format = `container_id: {{.ID}}`
|
|
| 128 |
- } else {
|
|
| 129 |
- ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
|
| 130 |
- if ctx.Size {
|
|
| 131 |
- ctx.Format += `size: {{.Size}}\n`
|
|
| 132 |
- } |
|
| 133 |
- } |
|
| 134 |
- } |
|
| 135 |
- |
|
| 136 |
- ctx.buffer = bytes.NewBufferString("")
|
|
| 137 |
- ctx.preformat() |
|
| 138 |
- |
|
| 139 |
- tmpl, err := ctx.parseFormat() |
|
| 140 |
- if err != nil {
|
|
| 141 |
- return |
|
| 142 |
- } |
|
| 143 |
- |
|
| 144 |
- for _, container := range ctx.Containers {
|
|
| 145 |
- containerCtx := &containerContext{
|
|
| 146 |
- trunc: ctx.Trunc, |
|
| 147 |
- c: container, |
|
| 148 |
- } |
|
| 149 |
- err = ctx.contextFormat(tmpl, containerCtx) |
|
| 150 |
- if err != nil {
|
|
| 151 |
- return |
|
| 152 |
- } |
|
| 153 |
- } |
|
| 154 |
- |
|
| 155 |
- ctx.postformat(tmpl, &containerContext{})
|
|
| 156 |
-} |
|
| 157 |
- |
|
| 158 |
-func isDangling(image types.Image) bool {
|
|
| 159 |
- return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>" |
|
| 160 |
-} |
|
| 161 |
- |
|
| 162 |
-func (ctx ImageContext) Write() {
|
|
| 163 |
- switch ctx.Format {
|
|
| 164 |
- case tableFormatKey: |
|
| 165 |
- ctx.Format = defaultImageTableFormat |
|
| 166 |
- if ctx.Digest {
|
|
| 167 |
- ctx.Format = defaultImageTableFormatWithDigest |
|
| 168 |
- } |
|
| 169 |
- if ctx.Quiet {
|
|
| 170 |
- ctx.Format = defaultQuietFormat |
|
| 171 |
- } |
|
| 172 |
- case rawFormatKey: |
|
| 173 |
- if ctx.Quiet {
|
|
| 174 |
- ctx.Format = `image_id: {{.ID}}`
|
|
| 175 |
- } else {
|
|
| 176 |
- if ctx.Digest {
|
|
| 177 |
- ctx.Format = `repository: {{ .Repository }}
|
|
| 178 |
-tag: {{.Tag}}
|
|
| 179 |
-digest: {{.Digest}}
|
|
| 180 |
-image_id: {{.ID}}
|
|
| 181 |
-created_at: {{.CreatedAt}}
|
|
| 182 |
-virtual_size: {{.Size}}
|
|
| 183 |
-` |
|
| 184 |
- } else {
|
|
| 185 |
- ctx.Format = `repository: {{ .Repository }}
|
|
| 186 |
-tag: {{.Tag}}
|
|
| 187 |
-image_id: {{.ID}}
|
|
| 188 |
-created_at: {{.CreatedAt}}
|
|
| 189 |
-virtual_size: {{.Size}}
|
|
| 190 |
-` |
|
| 191 |
- } |
|
| 192 |
- } |
|
| 193 |
- } |
|
| 194 |
- |
|
| 195 |
- ctx.buffer = bytes.NewBufferString("")
|
|
| 196 |
- ctx.preformat() |
|
| 197 |
- if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
|
| 198 |
- ctx.finalFormat += "\t{{.Digest}}"
|
|
| 199 |
- } |
|
| 200 |
- |
|
| 201 |
- tmpl, err := ctx.parseFormat() |
|
| 202 |
- if err != nil {
|
|
| 203 |
- return |
|
| 204 |
- } |
|
| 205 |
- |
|
| 206 |
- for _, image := range ctx.Images {
|
|
| 207 |
- images := []*imageContext{}
|
|
| 208 |
- if isDangling(image) {
|
|
| 209 |
- images = append(images, &imageContext{
|
|
| 210 |
- trunc: ctx.Trunc, |
|
| 211 |
- i: image, |
|
| 212 |
- repo: "<none>", |
|
| 213 |
- tag: "<none>", |
|
| 214 |
- digest: "<none>", |
|
| 215 |
- }) |
|
| 216 |
- } else {
|
|
| 217 |
- repoTags := map[string][]string{}
|
|
| 218 |
- repoDigests := map[string][]string{}
|
|
| 219 |
- |
|
| 220 |
- for _, refString := range append(image.RepoTags) {
|
|
| 221 |
- ref, err := reference.ParseNamed(refString) |
|
| 222 |
- if err != nil {
|
|
| 223 |
- continue |
|
| 224 |
- } |
|
| 225 |
- if nt, ok := ref.(reference.NamedTagged); ok {
|
|
| 226 |
- repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) |
|
| 227 |
- } |
|
| 228 |
- } |
|
| 229 |
- for _, refString := range append(image.RepoDigests) {
|
|
| 230 |
- ref, err := reference.ParseNamed(refString) |
|
| 231 |
- if err != nil {
|
|
| 232 |
- continue |
|
| 233 |
- } |
|
| 234 |
- if c, ok := ref.(reference.Canonical); ok {
|
|
| 235 |
- repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) |
|
| 236 |
- } |
|
| 237 |
- } |
|
| 238 |
- |
|
| 239 |
- for repo, tags := range repoTags {
|
|
| 240 |
- digests := repoDigests[repo] |
|
| 241 |
- |
|
| 242 |
- // Do not display digests as their own row |
|
| 243 |
- delete(repoDigests, repo) |
|
| 244 |
- |
|
| 245 |
- if !ctx.Digest {
|
|
| 246 |
- // Ignore digest references, just show tag once |
|
| 247 |
- digests = nil |
|
| 248 |
- } |
|
| 249 |
- |
|
| 250 |
- for _, tag := range tags {
|
|
| 251 |
- if len(digests) == 0 {
|
|
| 252 |
- images = append(images, &imageContext{
|
|
| 253 |
- trunc: ctx.Trunc, |
|
| 254 |
- i: image, |
|
| 255 |
- repo: repo, |
|
| 256 |
- tag: tag, |
|
| 257 |
- digest: "<none>", |
|
| 258 |
- }) |
|
| 259 |
- continue |
|
| 260 |
- } |
|
| 261 |
- // Display the digests for each tag |
|
| 262 |
- for _, dgst := range digests {
|
|
| 263 |
- images = append(images, &imageContext{
|
|
| 264 |
- trunc: ctx.Trunc, |
|
| 265 |
- i: image, |
|
| 266 |
- repo: repo, |
|
| 267 |
- tag: tag, |
|
| 268 |
- digest: dgst, |
|
| 269 |
- }) |
|
| 270 |
- } |
|
| 271 |
- |
|
| 272 |
- } |
|
| 273 |
- } |
|
| 274 |
- |
|
| 275 |
- // Show rows for remaining digest only references |
|
| 276 |
- for repo, digests := range repoDigests {
|
|
| 277 |
- // If digests are displayed, show row per digest |
|
| 278 |
- if ctx.Digest {
|
|
| 279 |
- for _, dgst := range digests {
|
|
| 280 |
- images = append(images, &imageContext{
|
|
| 281 |
- trunc: ctx.Trunc, |
|
| 282 |
- i: image, |
|
| 283 |
- repo: repo, |
|
| 284 |
- tag: "<none>", |
|
| 285 |
- digest: dgst, |
|
| 286 |
- }) |
|
| 287 |
- } |
|
| 288 |
- } else {
|
|
| 289 |
- images = append(images, &imageContext{
|
|
| 290 |
- trunc: ctx.Trunc, |
|
| 291 |
- i: image, |
|
| 292 |
- repo: repo, |
|
| 293 |
- tag: "<none>", |
|
| 294 |
- }) |
|
| 295 |
- } |
|
| 296 |
- } |
|
| 297 |
- } |
|
| 298 |
- for _, imageCtx := range images {
|
|
| 299 |
- err = ctx.contextFormat(tmpl, imageCtx) |
|
| 300 |
- if err != nil {
|
|
| 301 |
- return |
|
| 302 |
- } |
|
| 303 |
- } |
|
| 304 |
- } |
|
| 305 |
- |
|
| 306 |
- ctx.postformat(tmpl, &imageContext{})
|
|
| 307 |
-} |
| 308 | 96 |
deleted file mode 100644 |
| ... | ... |
@@ -1,537 +0,0 @@ |
| 1 |
-package formatter |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "fmt" |
|
| 6 |
- "testing" |
|
| 7 |
- "time" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/engine-api/types" |
|
| 10 |
-) |
|
| 11 |
- |
|
| 12 |
-func TestContainerContextWrite(t *testing.T) {
|
|
| 13 |
- unixTime := time.Now().AddDate(0, 0, -1).Unix() |
|
| 14 |
- expectedTime := time.Unix(unixTime, 0).String() |
|
| 15 |
- |
|
| 16 |
- contexts := []struct {
|
|
| 17 |
- context ContainerContext |
|
| 18 |
- expected string |
|
| 19 |
- }{
|
|
| 20 |
- // Errors |
|
| 21 |
- {
|
|
| 22 |
- ContainerContext{
|
|
| 23 |
- Context: Context{
|
|
| 24 |
- Format: "{{InvalidFunction}}",
|
|
| 25 |
- }, |
|
| 26 |
- }, |
|
| 27 |
- `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 28 |
-`, |
|
| 29 |
- }, |
|
| 30 |
- {
|
|
| 31 |
- ContainerContext{
|
|
| 32 |
- Context: Context{
|
|
| 33 |
- Format: "{{nil}}",
|
|
| 34 |
- }, |
|
| 35 |
- }, |
|
| 36 |
- `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 37 |
-`, |
|
| 38 |
- }, |
|
| 39 |
- // Table Format |
|
| 40 |
- {
|
|
| 41 |
- ContainerContext{
|
|
| 42 |
- Context: Context{
|
|
| 43 |
- Format: "table", |
|
| 44 |
- }, |
|
| 45 |
- }, |
|
| 46 |
- `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
|
| 47 |
-containerID1 ubuntu "" 24 hours ago foobar_baz |
|
| 48 |
-containerID2 ubuntu "" 24 hours ago foobar_bar |
|
| 49 |
-`, |
|
| 50 |
- }, |
|
| 51 |
- {
|
|
| 52 |
- ContainerContext{
|
|
| 53 |
- Context: Context{
|
|
| 54 |
- Format: "table {{.Image}}",
|
|
| 55 |
- }, |
|
| 56 |
- }, |
|
| 57 |
- "IMAGE\nubuntu\nubuntu\n", |
|
| 58 |
- }, |
|
| 59 |
- {
|
|
| 60 |
- ContainerContext{
|
|
| 61 |
- Context: Context{
|
|
| 62 |
- Format: "table {{.Image}}",
|
|
| 63 |
- }, |
|
| 64 |
- Size: true, |
|
| 65 |
- }, |
|
| 66 |
- "IMAGE\nubuntu\nubuntu\n", |
|
| 67 |
- }, |
|
| 68 |
- {
|
|
| 69 |
- ContainerContext{
|
|
| 70 |
- Context: Context{
|
|
| 71 |
- Format: "table {{.Image}}",
|
|
| 72 |
- Quiet: true, |
|
| 73 |
- }, |
|
| 74 |
- }, |
|
| 75 |
- "IMAGE\nubuntu\nubuntu\n", |
|
| 76 |
- }, |
|
| 77 |
- {
|
|
| 78 |
- ContainerContext{
|
|
| 79 |
- Context: Context{
|
|
| 80 |
- Format: "table", |
|
| 81 |
- Quiet: true, |
|
| 82 |
- }, |
|
| 83 |
- }, |
|
| 84 |
- "containerID1\ncontainerID2\n", |
|
| 85 |
- }, |
|
| 86 |
- // Raw Format |
|
| 87 |
- {
|
|
| 88 |
- ContainerContext{
|
|
| 89 |
- Context: Context{
|
|
| 90 |
- Format: "raw", |
|
| 91 |
- }, |
|
| 92 |
- }, |
|
| 93 |
- fmt.Sprintf(`container_id: containerID1 |
|
| 94 |
-image: ubuntu |
|
| 95 |
-command: "" |
|
| 96 |
-created_at: %s |
|
| 97 |
-status: |
|
| 98 |
-names: foobar_baz |
|
| 99 |
-labels: |
|
| 100 |
-ports: |
|
| 101 |
- |
|
| 102 |
-container_id: containerID2 |
|
| 103 |
-image: ubuntu |
|
| 104 |
-command: "" |
|
| 105 |
-created_at: %s |
|
| 106 |
-status: |
|
| 107 |
-names: foobar_bar |
|
| 108 |
-labels: |
|
| 109 |
-ports: |
|
| 110 |
- |
|
| 111 |
-`, expectedTime, expectedTime), |
|
| 112 |
- }, |
|
| 113 |
- {
|
|
| 114 |
- ContainerContext{
|
|
| 115 |
- Context: Context{
|
|
| 116 |
- Format: "raw", |
|
| 117 |
- }, |
|
| 118 |
- Size: true, |
|
| 119 |
- }, |
|
| 120 |
- fmt.Sprintf(`container_id: containerID1 |
|
| 121 |
-image: ubuntu |
|
| 122 |
-command: "" |
|
| 123 |
-created_at: %s |
|
| 124 |
-status: |
|
| 125 |
-names: foobar_baz |
|
| 126 |
-labels: |
|
| 127 |
-ports: |
|
| 128 |
-size: 0 B |
|
| 129 |
- |
|
| 130 |
-container_id: containerID2 |
|
| 131 |
-image: ubuntu |
|
| 132 |
-command: "" |
|
| 133 |
-created_at: %s |
|
| 134 |
-status: |
|
| 135 |
-names: foobar_bar |
|
| 136 |
-labels: |
|
| 137 |
-ports: |
|
| 138 |
-size: 0 B |
|
| 139 |
- |
|
| 140 |
-`, expectedTime, expectedTime), |
|
| 141 |
- }, |
|
| 142 |
- {
|
|
| 143 |
- ContainerContext{
|
|
| 144 |
- Context: Context{
|
|
| 145 |
- Format: "raw", |
|
| 146 |
- Quiet: true, |
|
| 147 |
- }, |
|
| 148 |
- }, |
|
| 149 |
- "container_id: containerID1\ncontainer_id: containerID2\n", |
|
| 150 |
- }, |
|
| 151 |
- // Custom Format |
|
| 152 |
- {
|
|
| 153 |
- ContainerContext{
|
|
| 154 |
- Context: Context{
|
|
| 155 |
- Format: "{{.Image}}",
|
|
| 156 |
- }, |
|
| 157 |
- }, |
|
| 158 |
- "ubuntu\nubuntu\n", |
|
| 159 |
- }, |
|
| 160 |
- {
|
|
| 161 |
- ContainerContext{
|
|
| 162 |
- Context: Context{
|
|
| 163 |
- Format: "{{.Image}}",
|
|
| 164 |
- }, |
|
| 165 |
- Size: true, |
|
| 166 |
- }, |
|
| 167 |
- "ubuntu\nubuntu\n", |
|
| 168 |
- }, |
|
| 169 |
- } |
|
| 170 |
- |
|
| 171 |
- for _, context := range contexts {
|
|
| 172 |
- containers := []types.Container{
|
|
| 173 |
- {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
|
| 174 |
- {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
|
| 175 |
- } |
|
| 176 |
- out := bytes.NewBufferString("")
|
|
| 177 |
- context.context.Output = out |
|
| 178 |
- context.context.Containers = containers |
|
| 179 |
- context.context.Write() |
|
| 180 |
- actual := out.String() |
|
| 181 |
- if actual != context.expected {
|
|
| 182 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 183 |
- } |
|
| 184 |
- // Clean buffer |
|
| 185 |
- out.Reset() |
|
| 186 |
- } |
|
| 187 |
-} |
|
| 188 |
- |
|
| 189 |
-func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
|
| 190 |
- out := bytes.NewBufferString("")
|
|
| 191 |
- containers := []types.Container{}
|
|
| 192 |
- |
|
| 193 |
- contexts := []struct {
|
|
| 194 |
- context ContainerContext |
|
| 195 |
- expected string |
|
| 196 |
- }{
|
|
| 197 |
- {
|
|
| 198 |
- ContainerContext{
|
|
| 199 |
- Context: Context{
|
|
| 200 |
- Format: "{{.Image}}",
|
|
| 201 |
- Output: out, |
|
| 202 |
- }, |
|
| 203 |
- }, |
|
| 204 |
- "", |
|
| 205 |
- }, |
|
| 206 |
- {
|
|
| 207 |
- ContainerContext{
|
|
| 208 |
- Context: Context{
|
|
| 209 |
- Format: "table {{.Image}}",
|
|
| 210 |
- Output: out, |
|
| 211 |
- }, |
|
| 212 |
- }, |
|
| 213 |
- "IMAGE\n", |
|
| 214 |
- }, |
|
| 215 |
- {
|
|
| 216 |
- ContainerContext{
|
|
| 217 |
- Context: Context{
|
|
| 218 |
- Format: "{{.Image}}",
|
|
| 219 |
- Output: out, |
|
| 220 |
- }, |
|
| 221 |
- Size: true, |
|
| 222 |
- }, |
|
| 223 |
- "", |
|
| 224 |
- }, |
|
| 225 |
- {
|
|
| 226 |
- ContainerContext{
|
|
| 227 |
- Context: Context{
|
|
| 228 |
- Format: "table {{.Image}}",
|
|
| 229 |
- Output: out, |
|
| 230 |
- }, |
|
| 231 |
- Size: true, |
|
| 232 |
- }, |
|
| 233 |
- "IMAGE\n", |
|
| 234 |
- }, |
|
| 235 |
- {
|
|
| 236 |
- ContainerContext{
|
|
| 237 |
- Context: Context{
|
|
| 238 |
- Format: "table {{.Image}}\t{{.Size}}",
|
|
| 239 |
- Output: out, |
|
| 240 |
- }, |
|
| 241 |
- }, |
|
| 242 |
- "IMAGE SIZE\n", |
|
| 243 |
- }, |
|
| 244 |
- {
|
|
| 245 |
- ContainerContext{
|
|
| 246 |
- Context: Context{
|
|
| 247 |
- Format: "table {{.Image}}\t{{.Size}}",
|
|
| 248 |
- Output: out, |
|
| 249 |
- }, |
|
| 250 |
- Size: true, |
|
| 251 |
- }, |
|
| 252 |
- "IMAGE SIZE\n", |
|
| 253 |
- }, |
|
| 254 |
- } |
|
| 255 |
- |
|
| 256 |
- for _, context := range contexts {
|
|
| 257 |
- context.context.Containers = containers |
|
| 258 |
- context.context.Write() |
|
| 259 |
- actual := out.String() |
|
| 260 |
- if actual != context.expected {
|
|
| 261 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 262 |
- } |
|
| 263 |
- // Clean buffer |
|
| 264 |
- out.Reset() |
|
| 265 |
- } |
|
| 266 |
-} |
|
| 267 |
- |
|
| 268 |
-func TestImageContextWrite(t *testing.T) {
|
|
| 269 |
- unixTime := time.Now().AddDate(0, 0, -1).Unix() |
|
| 270 |
- expectedTime := time.Unix(unixTime, 0).String() |
|
| 271 |
- |
|
| 272 |
- contexts := []struct {
|
|
| 273 |
- context ImageContext |
|
| 274 |
- expected string |
|
| 275 |
- }{
|
|
| 276 |
- // Errors |
|
| 277 |
- {
|
|
| 278 |
- ImageContext{
|
|
| 279 |
- Context: Context{
|
|
| 280 |
- Format: "{{InvalidFunction}}",
|
|
| 281 |
- }, |
|
| 282 |
- }, |
|
| 283 |
- `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 284 |
-`, |
|
| 285 |
- }, |
|
| 286 |
- {
|
|
| 287 |
- ImageContext{
|
|
| 288 |
- Context: Context{
|
|
| 289 |
- Format: "{{nil}}",
|
|
| 290 |
- }, |
|
| 291 |
- }, |
|
| 292 |
- `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 293 |
-`, |
|
| 294 |
- }, |
|
| 295 |
- // Table Format |
|
| 296 |
- {
|
|
| 297 |
- ImageContext{
|
|
| 298 |
- Context: Context{
|
|
| 299 |
- Format: "table", |
|
| 300 |
- }, |
|
| 301 |
- }, |
|
| 302 |
- `REPOSITORY TAG IMAGE ID CREATED SIZE |
|
| 303 |
-image tag1 imageID1 24 hours ago 0 B |
|
| 304 |
-image tag2 imageID2 24 hours ago 0 B |
|
| 305 |
-<none> <none> imageID3 24 hours ago 0 B |
|
| 306 |
-`, |
|
| 307 |
- }, |
|
| 308 |
- {
|
|
| 309 |
- ImageContext{
|
|
| 310 |
- Context: Context{
|
|
| 311 |
- Format: "table {{.Repository}}",
|
|
| 312 |
- }, |
|
| 313 |
- }, |
|
| 314 |
- "REPOSITORY\nimage\nimage\n<none>\n", |
|
| 315 |
- }, |
|
| 316 |
- {
|
|
| 317 |
- ImageContext{
|
|
| 318 |
- Context: Context{
|
|
| 319 |
- Format: "table {{.Repository}}",
|
|
| 320 |
- }, |
|
| 321 |
- Digest: true, |
|
| 322 |
- }, |
|
| 323 |
- `REPOSITORY DIGEST |
|
| 324 |
-image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf |
|
| 325 |
-image <none> |
|
| 326 |
-<none> <none> |
|
| 327 |
-`, |
|
| 328 |
- }, |
|
| 329 |
- {
|
|
| 330 |
- ImageContext{
|
|
| 331 |
- Context: Context{
|
|
| 332 |
- Format: "table {{.Repository}}",
|
|
| 333 |
- Quiet: true, |
|
| 334 |
- }, |
|
| 335 |
- }, |
|
| 336 |
- "REPOSITORY\nimage\nimage\n<none>\n", |
|
| 337 |
- }, |
|
| 338 |
- {
|
|
| 339 |
- ImageContext{
|
|
| 340 |
- Context: Context{
|
|
| 341 |
- Format: "table", |
|
| 342 |
- Quiet: true, |
|
| 343 |
- }, |
|
| 344 |
- }, |
|
| 345 |
- "imageID1\nimageID2\nimageID3\n", |
|
| 346 |
- }, |
|
| 347 |
- {
|
|
| 348 |
- ImageContext{
|
|
| 349 |
- Context: Context{
|
|
| 350 |
- Format: "table", |
|
| 351 |
- Quiet: false, |
|
| 352 |
- }, |
|
| 353 |
- Digest: true, |
|
| 354 |
- }, |
|
| 355 |
- `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE |
|
| 356 |
-image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B |
|
| 357 |
-image tag2 <none> imageID2 24 hours ago 0 B |
|
| 358 |
-<none> <none> <none> imageID3 24 hours ago 0 B |
|
| 359 |
-`, |
|
| 360 |
- }, |
|
| 361 |
- {
|
|
| 362 |
- ImageContext{
|
|
| 363 |
- Context: Context{
|
|
| 364 |
- Format: "table", |
|
| 365 |
- Quiet: true, |
|
| 366 |
- }, |
|
| 367 |
- Digest: true, |
|
| 368 |
- }, |
|
| 369 |
- "imageID1\nimageID2\nimageID3\n", |
|
| 370 |
- }, |
|
| 371 |
- // Raw Format |
|
| 372 |
- {
|
|
| 373 |
- ImageContext{
|
|
| 374 |
- Context: Context{
|
|
| 375 |
- Format: "raw", |
|
| 376 |
- }, |
|
| 377 |
- }, |
|
| 378 |
- fmt.Sprintf(`repository: image |
|
| 379 |
-tag: tag1 |
|
| 380 |
-image_id: imageID1 |
|
| 381 |
-created_at: %s |
|
| 382 |
-virtual_size: 0 B |
|
| 383 |
- |
|
| 384 |
-repository: image |
|
| 385 |
-tag: tag2 |
|
| 386 |
-image_id: imageID2 |
|
| 387 |
-created_at: %s |
|
| 388 |
-virtual_size: 0 B |
|
| 389 |
- |
|
| 390 |
-repository: <none> |
|
| 391 |
-tag: <none> |
|
| 392 |
-image_id: imageID3 |
|
| 393 |
-created_at: %s |
|
| 394 |
-virtual_size: 0 B |
|
| 395 |
- |
|
| 396 |
-`, expectedTime, expectedTime, expectedTime), |
|
| 397 |
- }, |
|
| 398 |
- {
|
|
| 399 |
- ImageContext{
|
|
| 400 |
- Context: Context{
|
|
| 401 |
- Format: "raw", |
|
| 402 |
- }, |
|
| 403 |
- Digest: true, |
|
| 404 |
- }, |
|
| 405 |
- fmt.Sprintf(`repository: image |
|
| 406 |
-tag: tag1 |
|
| 407 |
-digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf |
|
| 408 |
-image_id: imageID1 |
|
| 409 |
-created_at: %s |
|
| 410 |
-virtual_size: 0 B |
|
| 411 |
- |
|
| 412 |
-repository: image |
|
| 413 |
-tag: tag2 |
|
| 414 |
-digest: <none> |
|
| 415 |
-image_id: imageID2 |
|
| 416 |
-created_at: %s |
|
| 417 |
-virtual_size: 0 B |
|
| 418 |
- |
|
| 419 |
-repository: <none> |
|
| 420 |
-tag: <none> |
|
| 421 |
-digest: <none> |
|
| 422 |
-image_id: imageID3 |
|
| 423 |
-created_at: %s |
|
| 424 |
-virtual_size: 0 B |
|
| 425 |
- |
|
| 426 |
-`, expectedTime, expectedTime, expectedTime), |
|
| 427 |
- }, |
|
| 428 |
- {
|
|
| 429 |
- ImageContext{
|
|
| 430 |
- Context: Context{
|
|
| 431 |
- Format: "raw", |
|
| 432 |
- Quiet: true, |
|
| 433 |
- }, |
|
| 434 |
- }, |
|
| 435 |
- `image_id: imageID1 |
|
| 436 |
-image_id: imageID2 |
|
| 437 |
-image_id: imageID3 |
|
| 438 |
-`, |
|
| 439 |
- }, |
|
| 440 |
- // Custom Format |
|
| 441 |
- {
|
|
| 442 |
- ImageContext{
|
|
| 443 |
- Context: Context{
|
|
| 444 |
- Format: "{{.Repository}}",
|
|
| 445 |
- }, |
|
| 446 |
- }, |
|
| 447 |
- "image\nimage\n<none>\n", |
|
| 448 |
- }, |
|
| 449 |
- {
|
|
| 450 |
- ImageContext{
|
|
| 451 |
- Context: Context{
|
|
| 452 |
- Format: "{{.Repository}}",
|
|
| 453 |
- }, |
|
| 454 |
- Digest: true, |
|
| 455 |
- }, |
|
| 456 |
- "image\nimage\n<none>\n", |
|
| 457 |
- }, |
|
| 458 |
- } |
|
| 459 |
- |
|
| 460 |
- for _, context := range contexts {
|
|
| 461 |
- images := []types.Image{
|
|
| 462 |
- {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
|
| 463 |
- {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
|
| 464 |
- {ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
|
| 465 |
- } |
|
| 466 |
- out := bytes.NewBufferString("")
|
|
| 467 |
- context.context.Output = out |
|
| 468 |
- context.context.Images = images |
|
| 469 |
- context.context.Write() |
|
| 470 |
- actual := out.String() |
|
| 471 |
- if actual != context.expected {
|
|
| 472 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 473 |
- } |
|
| 474 |
- // Clean buffer |
|
| 475 |
- out.Reset() |
|
| 476 |
- } |
|
| 477 |
-} |
|
| 478 |
- |
|
| 479 |
-func TestImageContextWriteWithNoImage(t *testing.T) {
|
|
| 480 |
- out := bytes.NewBufferString("")
|
|
| 481 |
- images := []types.Image{}
|
|
| 482 |
- |
|
| 483 |
- contexts := []struct {
|
|
| 484 |
- context ImageContext |
|
| 485 |
- expected string |
|
| 486 |
- }{
|
|
| 487 |
- {
|
|
| 488 |
- ImageContext{
|
|
| 489 |
- Context: Context{
|
|
| 490 |
- Format: "{{.Repository}}",
|
|
| 491 |
- Output: out, |
|
| 492 |
- }, |
|
| 493 |
- }, |
|
| 494 |
- "", |
|
| 495 |
- }, |
|
| 496 |
- {
|
|
| 497 |
- ImageContext{
|
|
| 498 |
- Context: Context{
|
|
| 499 |
- Format: "table {{.Repository}}",
|
|
| 500 |
- Output: out, |
|
| 501 |
- }, |
|
| 502 |
- }, |
|
| 503 |
- "REPOSITORY\n", |
|
| 504 |
- }, |
|
| 505 |
- {
|
|
| 506 |
- ImageContext{
|
|
| 507 |
- Context: Context{
|
|
| 508 |
- Format: "{{.Repository}}",
|
|
| 509 |
- Output: out, |
|
| 510 |
- }, |
|
| 511 |
- Digest: true, |
|
| 512 |
- }, |
|
| 513 |
- "", |
|
| 514 |
- }, |
|
| 515 |
- {
|
|
| 516 |
- ImageContext{
|
|
| 517 |
- Context: Context{
|
|
| 518 |
- Format: "table {{.Repository}}",
|
|
| 519 |
- Output: out, |
|
| 520 |
- }, |
|
| 521 |
- Digest: true, |
|
| 522 |
- }, |
|
| 523 |
- "REPOSITORY DIGEST\n", |
|
| 524 |
- }, |
|
| 525 |
- } |
|
| 526 |
- |
|
| 527 |
- for _, context := range contexts {
|
|
| 528 |
- context.context.Images = images |
|
| 529 |
- context.context.Write() |
|
| 530 |
- actual := out.String() |
|
| 531 |
- if actual != context.expected {
|
|
| 532 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 533 |
- } |
|
| 534 |
- // Clean buffer |
|
| 535 |
- out.Reset() |
|
| 536 |
- } |
|
| 537 |
-} |
| 538 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,229 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "time" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 8 |
+ "github.com/docker/docker/reference" |
|
| 9 |
+ "github.com/docker/engine-api/types" |
|
| 10 |
+ "github.com/docker/go-units" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+const ( |
|
| 14 |
+ defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
| 15 |
+ defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
|
|
| 16 |
+ |
|
| 17 |
+ imageIDHeader = "IMAGE ID" |
|
| 18 |
+ repositoryHeader = "REPOSITORY" |
|
| 19 |
+ tagHeader = "TAG" |
|
| 20 |
+ digestHeader = "DIGEST" |
|
| 21 |
+) |
|
| 22 |
+ |
|
| 23 |
+// ImageContext contains image specific information required by the formater, encapsulate a Context struct. |
|
| 24 |
+type ImageContext struct {
|
|
| 25 |
+ Context |
|
| 26 |
+ Digest bool |
|
| 27 |
+ // Images |
|
| 28 |
+ Images []types.Image |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func isDangling(image types.Image) bool {
|
|
| 32 |
+ return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>" |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func (ctx ImageContext) Write() {
|
|
| 36 |
+ switch ctx.Format {
|
|
| 37 |
+ case tableFormatKey: |
|
| 38 |
+ ctx.Format = defaultImageTableFormat |
|
| 39 |
+ if ctx.Digest {
|
|
| 40 |
+ ctx.Format = defaultImageTableFormatWithDigest |
|
| 41 |
+ } |
|
| 42 |
+ if ctx.Quiet {
|
|
| 43 |
+ ctx.Format = defaultQuietFormat |
|
| 44 |
+ } |
|
| 45 |
+ case rawFormatKey: |
|
| 46 |
+ if ctx.Quiet {
|
|
| 47 |
+ ctx.Format = `image_id: {{.ID}}`
|
|
| 48 |
+ } else {
|
|
| 49 |
+ if ctx.Digest {
|
|
| 50 |
+ ctx.Format = `repository: {{ .Repository }}
|
|
| 51 |
+tag: {{.Tag}}
|
|
| 52 |
+digest: {{.Digest}}
|
|
| 53 |
+image_id: {{.ID}}
|
|
| 54 |
+created_at: {{.CreatedAt}}
|
|
| 55 |
+virtual_size: {{.Size}}
|
|
| 56 |
+` |
|
| 57 |
+ } else {
|
|
| 58 |
+ ctx.Format = `repository: {{ .Repository }}
|
|
| 59 |
+tag: {{.Tag}}
|
|
| 60 |
+image_id: {{.ID}}
|
|
| 61 |
+created_at: {{.CreatedAt}}
|
|
| 62 |
+virtual_size: {{.Size}}
|
|
| 63 |
+` |
|
| 64 |
+ } |
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ ctx.buffer = bytes.NewBufferString("")
|
|
| 69 |
+ ctx.preformat() |
|
| 70 |
+ if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
|
| 71 |
+ ctx.finalFormat += "\t{{.Digest}}"
|
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ tmpl, err := ctx.parseFormat() |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ return |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ for _, image := range ctx.Images {
|
|
| 80 |
+ images := []*imageContext{}
|
|
| 81 |
+ if isDangling(image) {
|
|
| 82 |
+ images = append(images, &imageContext{
|
|
| 83 |
+ trunc: ctx.Trunc, |
|
| 84 |
+ i: image, |
|
| 85 |
+ repo: "<none>", |
|
| 86 |
+ tag: "<none>", |
|
| 87 |
+ digest: "<none>", |
|
| 88 |
+ }) |
|
| 89 |
+ } else {
|
|
| 90 |
+ repoTags := map[string][]string{}
|
|
| 91 |
+ repoDigests := map[string][]string{}
|
|
| 92 |
+ |
|
| 93 |
+ for _, refString := range append(image.RepoTags) {
|
|
| 94 |
+ ref, err := reference.ParseNamed(refString) |
|
| 95 |
+ if err != nil {
|
|
| 96 |
+ continue |
|
| 97 |
+ } |
|
| 98 |
+ if nt, ok := ref.(reference.NamedTagged); ok {
|
|
| 99 |
+ repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) |
|
| 100 |
+ } |
|
| 101 |
+ } |
|
| 102 |
+ for _, refString := range append(image.RepoDigests) {
|
|
| 103 |
+ ref, err := reference.ParseNamed(refString) |
|
| 104 |
+ if err != nil {
|
|
| 105 |
+ continue |
|
| 106 |
+ } |
|
| 107 |
+ if c, ok := ref.(reference.Canonical); ok {
|
|
| 108 |
+ repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) |
|
| 109 |
+ } |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ for repo, tags := range repoTags {
|
|
| 113 |
+ digests := repoDigests[repo] |
|
| 114 |
+ |
|
| 115 |
+ // Do not display digests as their own row |
|
| 116 |
+ delete(repoDigests, repo) |
|
| 117 |
+ |
|
| 118 |
+ if !ctx.Digest {
|
|
| 119 |
+ // Ignore digest references, just show tag once |
|
| 120 |
+ digests = nil |
|
| 121 |
+ } |
|
| 122 |
+ |
|
| 123 |
+ for _, tag := range tags {
|
|
| 124 |
+ if len(digests) == 0 {
|
|
| 125 |
+ images = append(images, &imageContext{
|
|
| 126 |
+ trunc: ctx.Trunc, |
|
| 127 |
+ i: image, |
|
| 128 |
+ repo: repo, |
|
| 129 |
+ tag: tag, |
|
| 130 |
+ digest: "<none>", |
|
| 131 |
+ }) |
|
| 132 |
+ continue |
|
| 133 |
+ } |
|
| 134 |
+ // Display the digests for each tag |
|
| 135 |
+ for _, dgst := range digests {
|
|
| 136 |
+ images = append(images, &imageContext{
|
|
| 137 |
+ trunc: ctx.Trunc, |
|
| 138 |
+ i: image, |
|
| 139 |
+ repo: repo, |
|
| 140 |
+ tag: tag, |
|
| 141 |
+ digest: dgst, |
|
| 142 |
+ }) |
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ } |
|
| 146 |
+ } |
|
| 147 |
+ |
|
| 148 |
+ // Show rows for remaining digest only references |
|
| 149 |
+ for repo, digests := range repoDigests {
|
|
| 150 |
+ // If digests are displayed, show row per digest |
|
| 151 |
+ if ctx.Digest {
|
|
| 152 |
+ for _, dgst := range digests {
|
|
| 153 |
+ images = append(images, &imageContext{
|
|
| 154 |
+ trunc: ctx.Trunc, |
|
| 155 |
+ i: image, |
|
| 156 |
+ repo: repo, |
|
| 157 |
+ tag: "<none>", |
|
| 158 |
+ digest: dgst, |
|
| 159 |
+ }) |
|
| 160 |
+ } |
|
| 161 |
+ } else {
|
|
| 162 |
+ images = append(images, &imageContext{
|
|
| 163 |
+ trunc: ctx.Trunc, |
|
| 164 |
+ i: image, |
|
| 165 |
+ repo: repo, |
|
| 166 |
+ tag: "<none>", |
|
| 167 |
+ }) |
|
| 168 |
+ } |
|
| 169 |
+ } |
|
| 170 |
+ } |
|
| 171 |
+ for _, imageCtx := range images {
|
|
| 172 |
+ err = ctx.contextFormat(tmpl, imageCtx) |
|
| 173 |
+ if err != nil {
|
|
| 174 |
+ return |
|
| 175 |
+ } |
|
| 176 |
+ } |
|
| 177 |
+ } |
|
| 178 |
+ |
|
| 179 |
+ ctx.postformat(tmpl, &imageContext{})
|
|
| 180 |
+} |
|
| 181 |
+ |
|
| 182 |
+type imageContext struct {
|
|
| 183 |
+ baseSubContext |
|
| 184 |
+ trunc bool |
|
| 185 |
+ i types.Image |
|
| 186 |
+ repo string |
|
| 187 |
+ tag string |
|
| 188 |
+ digest string |
|
| 189 |
+} |
|
| 190 |
+ |
|
| 191 |
+func (c *imageContext) ID() string {
|
|
| 192 |
+ c.addHeader(imageIDHeader) |
|
| 193 |
+ if c.trunc {
|
|
| 194 |
+ return stringid.TruncateID(c.i.ID) |
|
| 195 |
+ } |
|
| 196 |
+ return c.i.ID |
|
| 197 |
+} |
|
| 198 |
+ |
|
| 199 |
+func (c *imageContext) Repository() string {
|
|
| 200 |
+ c.addHeader(repositoryHeader) |
|
| 201 |
+ return c.repo |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 204 |
+func (c *imageContext) Tag() string {
|
|
| 205 |
+ c.addHeader(tagHeader) |
|
| 206 |
+ return c.tag |
|
| 207 |
+} |
|
| 208 |
+ |
|
| 209 |
+func (c *imageContext) Digest() string {
|
|
| 210 |
+ c.addHeader(digestHeader) |
|
| 211 |
+ return c.digest |
|
| 212 |
+} |
|
| 213 |
+ |
|
| 214 |
+func (c *imageContext) CreatedSince() string {
|
|
| 215 |
+ c.addHeader(createdSinceHeader) |
|
| 216 |
+ createdAt := time.Unix(int64(c.i.Created), 0) |
|
| 217 |
+ return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
|
| 218 |
+} |
|
| 219 |
+ |
|
| 220 |
+func (c *imageContext) CreatedAt() string {
|
|
| 221 |
+ c.addHeader(createdAtHeader) |
|
| 222 |
+ return time.Unix(int64(c.i.Created), 0).String() |
|
| 223 |
+} |
|
| 224 |
+ |
|
| 225 |
+func (c *imageContext) Size() string {
|
|
| 226 |
+ c.addHeader(sizeHeader) |
|
| 227 |
+ return units.HumanSize(float64(c.i.Size)) |
|
| 228 |
+} |
| 0 | 229 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,345 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "testing" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 10 |
+ "github.com/docker/engine-api/types" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+func TestImageContext(t *testing.T) {
|
|
| 14 |
+ imageID := stringid.GenerateRandomID() |
|
| 15 |
+ unix := time.Now().Unix() |
|
| 16 |
+ |
|
| 17 |
+ var ctx imageContext |
|
| 18 |
+ cases := []struct {
|
|
| 19 |
+ imageCtx imageContext |
|
| 20 |
+ expValue string |
|
| 21 |
+ expHeader string |
|
| 22 |
+ call func() string |
|
| 23 |
+ }{
|
|
| 24 |
+ {imageContext{
|
|
| 25 |
+ i: types.Image{ID: imageID},
|
|
| 26 |
+ trunc: true, |
|
| 27 |
+ }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, |
|
| 28 |
+ {imageContext{
|
|
| 29 |
+ i: types.Image{ID: imageID},
|
|
| 30 |
+ trunc: false, |
|
| 31 |
+ }, imageID, imageIDHeader, ctx.ID}, |
|
| 32 |
+ {imageContext{
|
|
| 33 |
+ i: types.Image{Size: 10},
|
|
| 34 |
+ trunc: true, |
|
| 35 |
+ }, "10 B", sizeHeader, ctx.Size}, |
|
| 36 |
+ {imageContext{
|
|
| 37 |
+ i: types.Image{Created: unix},
|
|
| 38 |
+ trunc: true, |
|
| 39 |
+ }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, |
|
| 40 |
+ // FIXME |
|
| 41 |
+ // {imageContext{
|
|
| 42 |
+ // i: types.Image{Created: unix},
|
|
| 43 |
+ // trunc: true, |
|
| 44 |
+ // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, |
|
| 45 |
+ {imageContext{
|
|
| 46 |
+ i: types.Image{},
|
|
| 47 |
+ repo: "busybox", |
|
| 48 |
+ }, "busybox", repositoryHeader, ctx.Repository}, |
|
| 49 |
+ {imageContext{
|
|
| 50 |
+ i: types.Image{},
|
|
| 51 |
+ tag: "latest", |
|
| 52 |
+ }, "latest", tagHeader, ctx.Tag}, |
|
| 53 |
+ {imageContext{
|
|
| 54 |
+ i: types.Image{},
|
|
| 55 |
+ digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", |
|
| 56 |
+ }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ for _, c := range cases {
|
|
| 60 |
+ ctx = c.imageCtx |
|
| 61 |
+ v := c.call() |
|
| 62 |
+ if strings.Contains(v, ",") {
|
|
| 63 |
+ compareMultipleValues(t, v, c.expValue) |
|
| 64 |
+ } else if v != c.expValue {
|
|
| 65 |
+ t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ h := ctx.fullHeader() |
|
| 69 |
+ if h != c.expHeader {
|
|
| 70 |
+ t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
| 71 |
+ } |
|
| 72 |
+ } |
|
| 73 |
+} |
|
| 74 |
+ |
|
| 75 |
+func TestImageContextWrite(t *testing.T) {
|
|
| 76 |
+ unixTime := time.Now().AddDate(0, 0, -1).Unix() |
|
| 77 |
+ expectedTime := time.Unix(unixTime, 0).String() |
|
| 78 |
+ |
|
| 79 |
+ contexts := []struct {
|
|
| 80 |
+ context ImageContext |
|
| 81 |
+ expected string |
|
| 82 |
+ }{
|
|
| 83 |
+ // Errors |
|
| 84 |
+ {
|
|
| 85 |
+ ImageContext{
|
|
| 86 |
+ Context: Context{
|
|
| 87 |
+ Format: "{{InvalidFunction}}",
|
|
| 88 |
+ }, |
|
| 89 |
+ }, |
|
| 90 |
+ `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 91 |
+`, |
|
| 92 |
+ }, |
|
| 93 |
+ {
|
|
| 94 |
+ ImageContext{
|
|
| 95 |
+ Context: Context{
|
|
| 96 |
+ Format: "{{nil}}",
|
|
| 97 |
+ }, |
|
| 98 |
+ }, |
|
| 99 |
+ `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 100 |
+`, |
|
| 101 |
+ }, |
|
| 102 |
+ // Table Format |
|
| 103 |
+ {
|
|
| 104 |
+ ImageContext{
|
|
| 105 |
+ Context: Context{
|
|
| 106 |
+ Format: "table", |
|
| 107 |
+ }, |
|
| 108 |
+ }, |
|
| 109 |
+ `REPOSITORY TAG IMAGE ID CREATED SIZE |
|
| 110 |
+image tag1 imageID1 24 hours ago 0 B |
|
| 111 |
+image tag2 imageID2 24 hours ago 0 B |
|
| 112 |
+<none> <none> imageID3 24 hours ago 0 B |
|
| 113 |
+`, |
|
| 114 |
+ }, |
|
| 115 |
+ {
|
|
| 116 |
+ ImageContext{
|
|
| 117 |
+ Context: Context{
|
|
| 118 |
+ Format: "table {{.Repository}}",
|
|
| 119 |
+ }, |
|
| 120 |
+ }, |
|
| 121 |
+ "REPOSITORY\nimage\nimage\n<none>\n", |
|
| 122 |
+ }, |
|
| 123 |
+ {
|
|
| 124 |
+ ImageContext{
|
|
| 125 |
+ Context: Context{
|
|
| 126 |
+ Format: "table {{.Repository}}",
|
|
| 127 |
+ }, |
|
| 128 |
+ Digest: true, |
|
| 129 |
+ }, |
|
| 130 |
+ `REPOSITORY DIGEST |
|
| 131 |
+image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf |
|
| 132 |
+image <none> |
|
| 133 |
+<none> <none> |
|
| 134 |
+`, |
|
| 135 |
+ }, |
|
| 136 |
+ {
|
|
| 137 |
+ ImageContext{
|
|
| 138 |
+ Context: Context{
|
|
| 139 |
+ Format: "table {{.Repository}}",
|
|
| 140 |
+ Quiet: true, |
|
| 141 |
+ }, |
|
| 142 |
+ }, |
|
| 143 |
+ "REPOSITORY\nimage\nimage\n<none>\n", |
|
| 144 |
+ }, |
|
| 145 |
+ {
|
|
| 146 |
+ ImageContext{
|
|
| 147 |
+ Context: Context{
|
|
| 148 |
+ Format: "table", |
|
| 149 |
+ Quiet: true, |
|
| 150 |
+ }, |
|
| 151 |
+ }, |
|
| 152 |
+ "imageID1\nimageID2\nimageID3\n", |
|
| 153 |
+ }, |
|
| 154 |
+ {
|
|
| 155 |
+ ImageContext{
|
|
| 156 |
+ Context: Context{
|
|
| 157 |
+ Format: "table", |
|
| 158 |
+ Quiet: false, |
|
| 159 |
+ }, |
|
| 160 |
+ Digest: true, |
|
| 161 |
+ }, |
|
| 162 |
+ `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE |
|
| 163 |
+image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B |
|
| 164 |
+image tag2 <none> imageID2 24 hours ago 0 B |
|
| 165 |
+<none> <none> <none> imageID3 24 hours ago 0 B |
|
| 166 |
+`, |
|
| 167 |
+ }, |
|
| 168 |
+ {
|
|
| 169 |
+ ImageContext{
|
|
| 170 |
+ Context: Context{
|
|
| 171 |
+ Format: "table", |
|
| 172 |
+ Quiet: true, |
|
| 173 |
+ }, |
|
| 174 |
+ Digest: true, |
|
| 175 |
+ }, |
|
| 176 |
+ "imageID1\nimageID2\nimageID3\n", |
|
| 177 |
+ }, |
|
| 178 |
+ // Raw Format |
|
| 179 |
+ {
|
|
| 180 |
+ ImageContext{
|
|
| 181 |
+ Context: Context{
|
|
| 182 |
+ Format: "raw", |
|
| 183 |
+ }, |
|
| 184 |
+ }, |
|
| 185 |
+ fmt.Sprintf(`repository: image |
|
| 186 |
+tag: tag1 |
|
| 187 |
+image_id: imageID1 |
|
| 188 |
+created_at: %s |
|
| 189 |
+virtual_size: 0 B |
|
| 190 |
+ |
|
| 191 |
+repository: image |
|
| 192 |
+tag: tag2 |
|
| 193 |
+image_id: imageID2 |
|
| 194 |
+created_at: %s |
|
| 195 |
+virtual_size: 0 B |
|
| 196 |
+ |
|
| 197 |
+repository: <none> |
|
| 198 |
+tag: <none> |
|
| 199 |
+image_id: imageID3 |
|
| 200 |
+created_at: %s |
|
| 201 |
+virtual_size: 0 B |
|
| 202 |
+ |
|
| 203 |
+`, expectedTime, expectedTime, expectedTime), |
|
| 204 |
+ }, |
|
| 205 |
+ {
|
|
| 206 |
+ ImageContext{
|
|
| 207 |
+ Context: Context{
|
|
| 208 |
+ Format: "raw", |
|
| 209 |
+ }, |
|
| 210 |
+ Digest: true, |
|
| 211 |
+ }, |
|
| 212 |
+ fmt.Sprintf(`repository: image |
|
| 213 |
+tag: tag1 |
|
| 214 |
+digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf |
|
| 215 |
+image_id: imageID1 |
|
| 216 |
+created_at: %s |
|
| 217 |
+virtual_size: 0 B |
|
| 218 |
+ |
|
| 219 |
+repository: image |
|
| 220 |
+tag: tag2 |
|
| 221 |
+digest: <none> |
|
| 222 |
+image_id: imageID2 |
|
| 223 |
+created_at: %s |
|
| 224 |
+virtual_size: 0 B |
|
| 225 |
+ |
|
| 226 |
+repository: <none> |
|
| 227 |
+tag: <none> |
|
| 228 |
+digest: <none> |
|
| 229 |
+image_id: imageID3 |
|
| 230 |
+created_at: %s |
|
| 231 |
+virtual_size: 0 B |
|
| 232 |
+ |
|
| 233 |
+`, expectedTime, expectedTime, expectedTime), |
|
| 234 |
+ }, |
|
| 235 |
+ {
|
|
| 236 |
+ ImageContext{
|
|
| 237 |
+ Context: Context{
|
|
| 238 |
+ Format: "raw", |
|
| 239 |
+ Quiet: true, |
|
| 240 |
+ }, |
|
| 241 |
+ }, |
|
| 242 |
+ `image_id: imageID1 |
|
| 243 |
+image_id: imageID2 |
|
| 244 |
+image_id: imageID3 |
|
| 245 |
+`, |
|
| 246 |
+ }, |
|
| 247 |
+ // Custom Format |
|
| 248 |
+ {
|
|
| 249 |
+ ImageContext{
|
|
| 250 |
+ Context: Context{
|
|
| 251 |
+ Format: "{{.Repository}}",
|
|
| 252 |
+ }, |
|
| 253 |
+ }, |
|
| 254 |
+ "image\nimage\n<none>\n", |
|
| 255 |
+ }, |
|
| 256 |
+ {
|
|
| 257 |
+ ImageContext{
|
|
| 258 |
+ Context: Context{
|
|
| 259 |
+ Format: "{{.Repository}}",
|
|
| 260 |
+ }, |
|
| 261 |
+ Digest: true, |
|
| 262 |
+ }, |
|
| 263 |
+ "image\nimage\n<none>\n", |
|
| 264 |
+ }, |
|
| 265 |
+ } |
|
| 266 |
+ |
|
| 267 |
+ for _, context := range contexts {
|
|
| 268 |
+ images := []types.Image{
|
|
| 269 |
+ {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
|
| 270 |
+ {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
|
| 271 |
+ {ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
|
| 272 |
+ } |
|
| 273 |
+ out := bytes.NewBufferString("")
|
|
| 274 |
+ context.context.Output = out |
|
| 275 |
+ context.context.Images = images |
|
| 276 |
+ context.context.Write() |
|
| 277 |
+ actual := out.String() |
|
| 278 |
+ if actual != context.expected {
|
|
| 279 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 280 |
+ } |
|
| 281 |
+ // Clean buffer |
|
| 282 |
+ out.Reset() |
|
| 283 |
+ } |
|
| 284 |
+} |
|
| 285 |
+ |
|
| 286 |
+func TestImageContextWriteWithNoImage(t *testing.T) {
|
|
| 287 |
+ out := bytes.NewBufferString("")
|
|
| 288 |
+ images := []types.Image{}
|
|
| 289 |
+ |
|
| 290 |
+ contexts := []struct {
|
|
| 291 |
+ context ImageContext |
|
| 292 |
+ expected string |
|
| 293 |
+ }{
|
|
| 294 |
+ {
|
|
| 295 |
+ ImageContext{
|
|
| 296 |
+ Context: Context{
|
|
| 297 |
+ Format: "{{.Repository}}",
|
|
| 298 |
+ Output: out, |
|
| 299 |
+ }, |
|
| 300 |
+ }, |
|
| 301 |
+ "", |
|
| 302 |
+ }, |
|
| 303 |
+ {
|
|
| 304 |
+ ImageContext{
|
|
| 305 |
+ Context: Context{
|
|
| 306 |
+ Format: "table {{.Repository}}",
|
|
| 307 |
+ Output: out, |
|
| 308 |
+ }, |
|
| 309 |
+ }, |
|
| 310 |
+ "REPOSITORY\n", |
|
| 311 |
+ }, |
|
| 312 |
+ {
|
|
| 313 |
+ ImageContext{
|
|
| 314 |
+ Context: Context{
|
|
| 315 |
+ Format: "{{.Repository}}",
|
|
| 316 |
+ Output: out, |
|
| 317 |
+ }, |
|
| 318 |
+ Digest: true, |
|
| 319 |
+ }, |
|
| 320 |
+ "", |
|
| 321 |
+ }, |
|
| 322 |
+ {
|
|
| 323 |
+ ImageContext{
|
|
| 324 |
+ Context: Context{
|
|
| 325 |
+ Format: "table {{.Repository}}",
|
|
| 326 |
+ Output: out, |
|
| 327 |
+ }, |
|
| 328 |
+ Digest: true, |
|
| 329 |
+ }, |
|
| 330 |
+ "REPOSITORY DIGEST\n", |
|
| 331 |
+ }, |
|
| 332 |
+ } |
|
| 333 |
+ |
|
| 334 |
+ for _, context := range contexts {
|
|
| 335 |
+ context.context.Images = images |
|
| 336 |
+ context.context.Write() |
|
| 337 |
+ actual := out.String() |
|
| 338 |
+ if actual != context.expected {
|
|
| 339 |
+ t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 340 |
+ } |
|
| 341 |
+ // Clean buffer |
|
| 342 |
+ out.Reset() |
|
| 343 |
+ } |
|
| 344 |
+} |