Signed-off-by: Daniel Nephin <dnephin@docker.com>
| ... | ... |
@@ -106,27 +106,19 @@ func runPs(dockerCli *command.DockerCli, opts *psOptions) error {
|
| 106 | 106 |
return err |
| 107 | 107 |
} |
| 108 | 108 |
|
| 109 |
- f := opts.format |
|
| 110 |
- if len(f) == 0 {
|
|
| 109 |
+ format := opts.format |
|
| 110 |
+ if len(format) == 0 {
|
|
| 111 | 111 |
if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
|
| 112 |
- f = dockerCli.ConfigFile().PsFormat |
|
| 112 |
+ format = dockerCli.ConfigFile().PsFormat |
|
| 113 | 113 |
} else {
|
| 114 |
- f = "table" |
|
| 114 |
+ format = "table" |
|
| 115 | 115 |
} |
| 116 | 116 |
} |
| 117 | 117 |
|
| 118 |
- psCtx := formatter.ContainerContext{
|
|
| 119 |
- Context: formatter.Context{
|
|
| 120 |
- Output: dockerCli.Out(), |
|
| 121 |
- Format: f, |
|
| 122 |
- Quiet: opts.quiet, |
|
| 123 |
- Trunc: !opts.noTrunc, |
|
| 124 |
- }, |
|
| 125 |
- Size: listOptions.Size, |
|
| 126 |
- Containers: containers, |
|
| 118 |
+ containerCtx := formatter.Context{
|
|
| 119 |
+ Output: dockerCli.Out(), |
|
| 120 |
+ Format: formatter.NewContainerFormat(format, opts.quiet, opts.size), |
|
| 121 |
+ Trunc: !opts.noTrunc, |
|
| 127 | 122 |
} |
| 128 |
- |
|
| 129 |
- psCtx.Write() |
|
| 130 |
- |
|
| 131 |
- return nil |
|
| 123 |
+ return formatter.ContainerWrite(containerCtx, containers) |
|
| 132 | 124 |
} |
| ... | ... |
@@ -1,7 +1,6 @@ |
| 1 | 1 |
package formatter |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bytes" |
|
| 5 | 4 |
"fmt" |
| 6 | 5 |
"strconv" |
| 7 | 6 |
"strings" |
| ... | ... |
@@ -11,7 +10,7 @@ import ( |
| 11 | 11 |
"github.com/docker/docker/api/types" |
| 12 | 12 |
"github.com/docker/docker/pkg/stringid" |
| 13 | 13 |
"github.com/docker/docker/pkg/stringutils" |
| 14 |
- "github.com/docker/go-units" |
|
| 14 |
+ units "github.com/docker/go-units" |
|
| 15 | 15 |
) |
| 16 | 16 |
|
| 17 | 17 |
const ( |
| ... | ... |
@@ -26,67 +25,53 @@ const ( |
| 26 | 26 |
mountsHeader = "MOUNTS" |
| 27 | 27 |
) |
| 28 | 28 |
|
| 29 |
-// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. |
|
| 30 |
-type ContainerContext struct {
|
|
| 31 |
- Context |
|
| 32 |
- // Size when set to true will display the size of the output. |
|
| 33 |
- Size bool |
|
| 34 |
- // Containers |
|
| 35 |
- Containers []types.Container |
|
| 36 |
-} |
|
| 37 |
- |
|
| 38 |
-func (ctx ContainerContext) Write() {
|
|
| 39 |
- switch ctx.Format {
|
|
| 40 |
- case tableFormatKey: |
|
| 41 |
- if ctx.Quiet {
|
|
| 42 |
- ctx.Format = defaultQuietFormat |
|
| 43 |
- } else {
|
|
| 44 |
- ctx.Format = defaultContainerTableFormat |
|
| 45 |
- if ctx.Size {
|
|
| 46 |
- ctx.Format += `\t{{.Size}}`
|
|
| 47 |
- } |
|
| 29 |
+// NewContainerFormat returns a Format for rendering using a Context |
|
| 30 |
+func NewContainerFormat(source string, quiet bool, size bool) Format {
|
|
| 31 |
+ switch source {
|
|
| 32 |
+ case TableFormatKey: |
|
| 33 |
+ if quiet {
|
|
| 34 |
+ return defaultQuietFormat |
|
| 48 | 35 |
} |
| 49 |
- case rawFormatKey: |
|
| 50 |
- if ctx.Quiet {
|
|
| 51 |
- ctx.Format = `container_id: {{.ID}}`
|
|
| 52 |
- } else {
|
|
| 53 |
- ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
|
| 54 |
- if ctx.Size {
|
|
| 55 |
- ctx.Format += `size: {{.Size}}\n`
|
|
| 56 |
- } |
|
| 36 |
+ format := defaultContainerTableFormat |
|
| 37 |
+ if size {
|
|
| 38 |
+ format += `\t{{.Size}}`
|
|
| 57 | 39 |
} |
| 58 |
- } |
|
| 59 |
- |
|
| 60 |
- ctx.buffer = bytes.NewBufferString("")
|
|
| 61 |
- ctx.preformat() |
|
| 62 |
- |
|
| 63 |
- tmpl, err := ctx.parseFormat() |
|
| 64 |
- if err != nil {
|
|
| 65 |
- return |
|
| 66 |
- } |
|
| 67 |
- |
|
| 68 |
- for _, container := range ctx.Containers {
|
|
| 69 |
- containerCtx := &containerContext{
|
|
| 70 |
- trunc: ctx.Trunc, |
|
| 71 |
- c: container, |
|
| 40 |
+ return Format(format) |
|
| 41 |
+ case RawFormatKey: |
|
| 42 |
+ if quiet {
|
|
| 43 |
+ return `container_id: {{.ID}}`
|
|
| 72 | 44 |
} |
| 73 |
- err = ctx.contextFormat(tmpl, containerCtx) |
|
| 74 |
- if err != nil {
|
|
| 75 |
- return |
|
| 45 |
+ format := `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
|
| 46 |
+ if size {
|
|
| 47 |
+ format += `size: {{.Size}}\n`
|
|
| 76 | 48 |
} |
| 49 |
+ return Format(format) |
|
| 77 | 50 |
} |
| 51 |
+ return Format(source) |
|
| 52 |
+} |
|
| 78 | 53 |
|
| 79 |
- ctx.postformat(tmpl, &containerContext{})
|
|
| 54 |
+// ContainerWrite renders the context for a list of containers |
|
| 55 |
+func ContainerWrite(ctx Context, containers []types.Container) error {
|
|
| 56 |
+ render := func(format func(subContext subContext) error) error {
|
|
| 57 |
+ for _, container := range containers {
|
|
| 58 |
+ err := format(&containerContext{trunc: ctx.Trunc, c: container})
|
|
| 59 |
+ if err != nil {
|
|
| 60 |
+ return err |
|
| 61 |
+ } |
|
| 62 |
+ } |
|
| 63 |
+ return nil |
|
| 64 |
+ } |
|
| 65 |
+ return ctx.Write(&containerContext{}, render)
|
|
| 80 | 66 |
} |
| 81 | 67 |
|
| 82 | 68 |
type containerContext struct {
|
| 83 |
- baseSubContext |
|
| 69 |
+ HeaderContext |
|
| 84 | 70 |
trunc bool |
| 85 | 71 |
c types.Container |
| 86 | 72 |
} |
| 87 | 73 |
|
| 88 | 74 |
func (c *containerContext) ID() string {
|
| 89 |
- c.addHeader(containerIDHeader) |
|
| 75 |
+ c.AddHeader(containerIDHeader) |
|
| 90 | 76 |
if c.trunc {
|
| 91 | 77 |
return stringid.TruncateID(c.c.ID) |
| 92 | 78 |
} |
| ... | ... |
@@ -94,7 +79,7 @@ func (c *containerContext) ID() string {
|
| 94 | 94 |
} |
| 95 | 95 |
|
| 96 | 96 |
func (c *containerContext) Names() string {
|
| 97 |
- c.addHeader(namesHeader) |
|
| 97 |
+ c.AddHeader(namesHeader) |
|
| 98 | 98 |
names := stripNamePrefix(c.c.Names) |
| 99 | 99 |
if c.trunc {
|
| 100 | 100 |
for _, name := range names {
|
| ... | ... |
@@ -108,7 +93,7 @@ func (c *containerContext) Names() string {
|
| 108 | 108 |
} |
| 109 | 109 |
|
| 110 | 110 |
func (c *containerContext) Image() string {
|
| 111 |
- c.addHeader(imageHeader) |
|
| 111 |
+ c.AddHeader(imageHeader) |
|
| 112 | 112 |
if c.c.Image == "" {
|
| 113 | 113 |
return "<no image>" |
| 114 | 114 |
} |
| ... | ... |
@@ -121,7 +106,7 @@ func (c *containerContext) Image() string {
|
| 121 | 121 |
} |
| 122 | 122 |
|
| 123 | 123 |
func (c *containerContext) Command() string {
|
| 124 |
- c.addHeader(commandHeader) |
|
| 124 |
+ c.AddHeader(commandHeader) |
|
| 125 | 125 |
command := c.c.Command |
| 126 | 126 |
if c.trunc {
|
| 127 | 127 |
command = stringutils.Ellipsis(command, 20) |
| ... | ... |
@@ -130,28 +115,28 @@ func (c *containerContext) Command() string {
|
| 130 | 130 |
} |
| 131 | 131 |
|
| 132 | 132 |
func (c *containerContext) CreatedAt() string {
|
| 133 |
- c.addHeader(createdAtHeader) |
|
| 133 |
+ c.AddHeader(createdAtHeader) |
|
| 134 | 134 |
return time.Unix(int64(c.c.Created), 0).String() |
| 135 | 135 |
} |
| 136 | 136 |
|
| 137 | 137 |
func (c *containerContext) RunningFor() string {
|
| 138 |
- c.addHeader(runningForHeader) |
|
| 138 |
+ c.AddHeader(runningForHeader) |
|
| 139 | 139 |
createdAt := time.Unix(int64(c.c.Created), 0) |
| 140 | 140 |
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
| 141 | 141 |
} |
| 142 | 142 |
|
| 143 | 143 |
func (c *containerContext) Ports() string {
|
| 144 |
- c.addHeader(portsHeader) |
|
| 144 |
+ c.AddHeader(portsHeader) |
|
| 145 | 145 |
return api.DisplayablePorts(c.c.Ports) |
| 146 | 146 |
} |
| 147 | 147 |
|
| 148 | 148 |
func (c *containerContext) Status() string {
|
| 149 |
- c.addHeader(statusHeader) |
|
| 149 |
+ c.AddHeader(statusHeader) |
|
| 150 | 150 |
return c.c.Status |
| 151 | 151 |
} |
| 152 | 152 |
|
| 153 | 153 |
func (c *containerContext) Size() string {
|
| 154 |
- c.addHeader(sizeHeader) |
|
| 154 |
+ c.AddHeader(sizeHeader) |
|
| 155 | 155 |
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) |
| 156 | 156 |
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) |
| 157 | 157 |
|
| ... | ... |
@@ -163,7 +148,7 @@ func (c *containerContext) Size() string {
|
| 163 | 163 |
} |
| 164 | 164 |
|
| 165 | 165 |
func (c *containerContext) Labels() string {
|
| 166 |
- c.addHeader(labelsHeader) |
|
| 166 |
+ c.AddHeader(labelsHeader) |
|
| 167 | 167 |
if c.c.Labels == nil {
|
| 168 | 168 |
return "" |
| 169 | 169 |
} |
| ... | ... |
@@ -180,7 +165,7 @@ func (c *containerContext) Label(name string) string {
|
| 180 | 180 |
r := strings.NewReplacer("-", " ", "_", " ")
|
| 181 | 181 |
h := r.Replace(n[len(n)-1]) |
| 182 | 182 |
|
| 183 |
- c.addHeader(h) |
|
| 183 |
+ c.AddHeader(h) |
|
| 184 | 184 |
|
| 185 | 185 |
if c.c.Labels == nil {
|
| 186 | 186 |
return "" |
| ... | ... |
@@ -189,7 +174,7 @@ func (c *containerContext) Label(name string) string {
|
| 189 | 189 |
} |
| 190 | 190 |
|
| 191 | 191 |
func (c *containerContext) Mounts() string {
|
| 192 |
- c.addHeader(mountsHeader) |
|
| 192 |
+ c.AddHeader(mountsHeader) |
|
| 193 | 193 |
|
| 194 | 194 |
var name string |
| 195 | 195 |
var mounts []string |
| ... | ... |
@@ -95,7 +95,7 @@ func TestContainerPsContext(t *testing.T) {
|
| 95 | 95 |
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
| 96 | 96 |
} |
| 97 | 97 |
|
| 98 |
- h := ctx.fullHeader() |
|
| 98 |
+ h := ctx.FullHeader() |
|
| 99 | 99 |
if h != c.expHeader {
|
| 100 | 100 |
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
| 101 | 101 |
} |
| ... | ... |
@@ -114,7 +114,7 @@ func TestContainerPsContext(t *testing.T) {
|
| 114 | 114 |
t.Fatalf("Expected ubuntu, was %s\n", node)
|
| 115 | 115 |
} |
| 116 | 116 |
|
| 117 |
- h := ctx.fullHeader() |
|
| 117 |
+ h := ctx.FullHeader() |
|
| 118 | 118 |
if h != "SWARM ID\tNODE NAME" {
|
| 119 | 119 |
t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
| 120 | 120 |
|
| ... | ... |
@@ -129,9 +129,9 @@ func TestContainerPsContext(t *testing.T) {
|
| 129 | 129 |
} |
| 130 | 130 |
|
| 131 | 131 |
ctx = containerContext{c: c2, trunc: true}
|
| 132 |
- fullHeader := ctx.fullHeader() |
|
| 133 |
- if fullHeader != "" {
|
|
| 134 |
- t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
|
| 132 |
+ FullHeader := ctx.FullHeader() |
|
| 133 |
+ if FullHeader != "" {
|
|
| 134 |
+ t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader)
|
|
| 135 | 135 |
} |
| 136 | 136 |
|
| 137 | 137 |
} |
| ... | ... |
@@ -140,186 +140,127 @@ func TestContainerContextWrite(t *testing.T) {
|
| 140 | 140 |
unixTime := time.Now().AddDate(0, 0, -1).Unix() |
| 141 | 141 |
expectedTime := time.Unix(unixTime, 0).String() |
| 142 | 142 |
|
| 143 |
- contexts := []struct {
|
|
| 144 |
- context ContainerContext |
|
| 143 |
+ cases := []struct {
|
|
| 144 |
+ context Context |
|
| 145 | 145 |
expected string |
| 146 | 146 |
}{
|
| 147 | 147 |
// Errors |
| 148 | 148 |
{
|
| 149 |
- ContainerContext{
|
|
| 150 |
- Context: Context{
|
|
| 151 |
- Format: "{{InvalidFunction}}",
|
|
| 152 |
- }, |
|
| 153 |
- }, |
|
| 149 |
+ Context{Format: "{{InvalidFunction}}"},
|
|
| 154 | 150 |
`Template parsing error: template: :1: function "InvalidFunction" not defined |
| 155 | 151 |
`, |
| 156 | 152 |
}, |
| 157 | 153 |
{
|
| 158 |
- ContainerContext{
|
|
| 159 |
- Context: Context{
|
|
| 160 |
- Format: "{{nil}}",
|
|
| 161 |
- }, |
|
| 162 |
- }, |
|
| 154 |
+ Context{Format: "{{nil}}"},
|
|
| 163 | 155 |
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
| 164 | 156 |
`, |
| 165 | 157 |
}, |
| 166 | 158 |
// Table Format |
| 167 | 159 |
{
|
| 168 |
- ContainerContext{
|
|
| 169 |
- Context: Context{
|
|
| 170 |
- Format: "table", |
|
| 171 |
- }, |
|
| 172 |
- Size: true, |
|
| 173 |
- }, |
|
| 160 |
+ Context{Format: NewContainerFormat("table", false, true)},
|
|
| 174 | 161 |
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE |
| 175 | 162 |
containerID1 ubuntu "" 24 hours ago foobar_baz 0 B |
| 176 | 163 |
containerID2 ubuntu "" 24 hours ago foobar_bar 0 B |
| 177 | 164 |
`, |
| 178 | 165 |
}, |
| 179 | 166 |
{
|
| 180 |
- ContainerContext{
|
|
| 181 |
- Context: Context{
|
|
| 182 |
- Format: "table", |
|
| 183 |
- }, |
|
| 184 |
- }, |
|
| 167 |
+ Context{Format: NewContainerFormat("table", false, false)},
|
|
| 185 | 168 |
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
| 186 | 169 |
containerID1 ubuntu "" 24 hours ago foobar_baz |
| 187 | 170 |
containerID2 ubuntu "" 24 hours ago foobar_bar |
| 188 | 171 |
`, |
| 189 | 172 |
}, |
| 190 | 173 |
{
|
| 191 |
- ContainerContext{
|
|
| 192 |
- Context: Context{
|
|
| 193 |
- Format: "table {{.Image}}",
|
|
| 194 |
- }, |
|
| 195 |
- }, |
|
| 174 |
+ Context{Format: NewContainerFormat("table {{.Image}}", false, false)},
|
|
| 196 | 175 |
"IMAGE\nubuntu\nubuntu\n", |
| 197 | 176 |
}, |
| 198 | 177 |
{
|
| 199 |
- ContainerContext{
|
|
| 200 |
- Context: Context{
|
|
| 201 |
- Format: "table {{.Image}}",
|
|
| 202 |
- }, |
|
| 203 |
- Size: true, |
|
| 204 |
- }, |
|
| 178 |
+ Context{Format: NewContainerFormat("table {{.Image}}", false, true)},
|
|
| 205 | 179 |
"IMAGE\nubuntu\nubuntu\n", |
| 206 | 180 |
}, |
| 207 | 181 |
{
|
| 208 |
- ContainerContext{
|
|
| 209 |
- Context: Context{
|
|
| 210 |
- Format: "table {{.Image}}",
|
|
| 211 |
- Quiet: true, |
|
| 212 |
- }, |
|
| 213 |
- }, |
|
| 182 |
+ Context{Format: NewContainerFormat("table {{.Image}}", true, false)},
|
|
| 214 | 183 |
"IMAGE\nubuntu\nubuntu\n", |
| 215 | 184 |
}, |
| 216 | 185 |
{
|
| 217 |
- ContainerContext{
|
|
| 218 |
- Context: Context{
|
|
| 219 |
- Format: "table", |
|
| 220 |
- Quiet: true, |
|
| 221 |
- }, |
|
| 222 |
- }, |
|
| 186 |
+ Context{Format: NewContainerFormat("table", true, false)},
|
|
| 223 | 187 |
"containerID1\ncontainerID2\n", |
| 224 | 188 |
}, |
| 225 | 189 |
// Raw Format |
| 226 | 190 |
{
|
| 227 |
- ContainerContext{
|
|
| 228 |
- Context: Context{
|
|
| 229 |
- Format: "raw", |
|
| 230 |
- }, |
|
| 231 |
- }, |
|
| 191 |
+ Context{Format: NewContainerFormat("raw", false, false)},
|
|
| 232 | 192 |
fmt.Sprintf(`container_id: containerID1 |
| 233 | 193 |
image: ubuntu |
| 234 | 194 |
command: "" |
| 235 | 195 |
created_at: %s |
| 236 |
-status: |
|
| 196 |
+status: |
|
| 237 | 197 |
names: foobar_baz |
| 238 |
-labels: |
|
| 239 |
-ports: |
|
| 198 |
+labels: |
|
| 199 |
+ports: |
|
| 240 | 200 |
|
| 241 | 201 |
container_id: containerID2 |
| 242 | 202 |
image: ubuntu |
| 243 | 203 |
command: "" |
| 244 | 204 |
created_at: %s |
| 245 |
-status: |
|
| 205 |
+status: |
|
| 246 | 206 |
names: foobar_bar |
| 247 |
-labels: |
|
| 248 |
-ports: |
|
| 207 |
+labels: |
|
| 208 |
+ports: |
|
| 249 | 209 |
|
| 250 | 210 |
`, expectedTime, expectedTime), |
| 251 | 211 |
}, |
| 252 | 212 |
{
|
| 253 |
- ContainerContext{
|
|
| 254 |
- Context: Context{
|
|
| 255 |
- Format: "raw", |
|
| 256 |
- }, |
|
| 257 |
- Size: true, |
|
| 258 |
- }, |
|
| 213 |
+ Context{Format: NewContainerFormat("raw", false, true)},
|
|
| 259 | 214 |
fmt.Sprintf(`container_id: containerID1 |
| 260 | 215 |
image: ubuntu |
| 261 | 216 |
command: "" |
| 262 | 217 |
created_at: %s |
| 263 |
-status: |
|
| 218 |
+status: |
|
| 264 | 219 |
names: foobar_baz |
| 265 |
-labels: |
|
| 266 |
-ports: |
|
| 220 |
+labels: |
|
| 221 |
+ports: |
|
| 267 | 222 |
size: 0 B |
| 268 | 223 |
|
| 269 | 224 |
container_id: containerID2 |
| 270 | 225 |
image: ubuntu |
| 271 | 226 |
command: "" |
| 272 | 227 |
created_at: %s |
| 273 |
-status: |
|
| 228 |
+status: |
|
| 274 | 229 |
names: foobar_bar |
| 275 |
-labels: |
|
| 276 |
-ports: |
|
| 230 |
+labels: |
|
| 231 |
+ports: |
|
| 277 | 232 |
size: 0 B |
| 278 | 233 |
|
| 279 | 234 |
`, expectedTime, expectedTime), |
| 280 | 235 |
}, |
| 281 | 236 |
{
|
| 282 |
- ContainerContext{
|
|
| 283 |
- Context: Context{
|
|
| 284 |
- Format: "raw", |
|
| 285 |
- Quiet: true, |
|
| 286 |
- }, |
|
| 287 |
- }, |
|
| 237 |
+ Context{Format: NewContainerFormat("raw", true, false)},
|
|
| 288 | 238 |
"container_id: containerID1\ncontainer_id: containerID2\n", |
| 289 | 239 |
}, |
| 290 | 240 |
// Custom Format |
| 291 | 241 |
{
|
| 292 |
- ContainerContext{
|
|
| 293 |
- Context: Context{
|
|
| 294 |
- Format: "{{.Image}}",
|
|
| 295 |
- }, |
|
| 296 |
- }, |
|
| 242 |
+ Context{Format: "{{.Image}}"},
|
|
| 297 | 243 |
"ubuntu\nubuntu\n", |
| 298 | 244 |
}, |
| 299 | 245 |
{
|
| 300 |
- ContainerContext{
|
|
| 301 |
- Context: Context{
|
|
| 302 |
- Format: "{{.Image}}",
|
|
| 303 |
- }, |
|
| 304 |
- Size: true, |
|
| 305 |
- }, |
|
| 246 |
+ Context{Format: NewContainerFormat("{{.Image}}", false, true)},
|
|
| 306 | 247 |
"ubuntu\nubuntu\n", |
| 307 | 248 |
}, |
| 308 | 249 |
} |
| 309 | 250 |
|
| 310 |
- for _, context := range contexts {
|
|
| 251 |
+ for _, testcase := range cases {
|
|
| 311 | 252 |
containers := []types.Container{
|
| 312 | 253 |
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
| 313 | 254 |
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
| 314 | 255 |
} |
| 315 | 256 |
out := bytes.NewBufferString("")
|
| 316 |
- context.context.Output = out |
|
| 317 |
- context.context.Containers = containers |
|
| 318 |
- context.context.Write() |
|
| 319 |
- actual := out.String() |
|
| 320 |
- assert.Equal(t, actual, context.expected) |
|
| 321 |
- // Clean buffer |
|
| 322 |
- out.Reset() |
|
| 257 |
+ testcase.context.Output = out |
|
| 258 |
+ err := ContainerWrite(testcase.context, containers) |
|
| 259 |
+ if err != nil {
|
|
| 260 |
+ assert.Error(t, err, testcase.expected) |
|
| 261 |
+ } else {
|
|
| 262 |
+ assert.Equal(t, out.String(), testcase.expected) |
|
| 263 |
+ } |
|
| 323 | 264 |
} |
| 324 | 265 |
} |
| 325 | 266 |
|
| ... | ... |
@@ -328,75 +269,56 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
| 328 | 328 |
containers := []types.Container{}
|
| 329 | 329 |
|
| 330 | 330 |
contexts := []struct {
|
| 331 |
- context ContainerContext |
|
| 331 |
+ context Context |
|
| 332 | 332 |
expected string |
| 333 | 333 |
}{
|
| 334 | 334 |
{
|
| 335 |
- ContainerContext{
|
|
| 336 |
- Context: Context{
|
|
| 337 |
- Format: "{{.Image}}",
|
|
| 338 |
- Output: out, |
|
| 339 |
- }, |
|
| 335 |
+ Context{
|
|
| 336 |
+ Format: "{{.Image}}",
|
|
| 337 |
+ Output: out, |
|
| 340 | 338 |
}, |
| 341 | 339 |
"", |
| 342 | 340 |
}, |
| 343 | 341 |
{
|
| 344 |
- ContainerContext{
|
|
| 345 |
- Context: Context{
|
|
| 346 |
- Format: "table {{.Image}}",
|
|
| 347 |
- Output: out, |
|
| 348 |
- }, |
|
| 342 |
+ Context{
|
|
| 343 |
+ Format: "table {{.Image}}",
|
|
| 344 |
+ Output: out, |
|
| 349 | 345 |
}, |
| 350 | 346 |
"IMAGE\n", |
| 351 | 347 |
}, |
| 352 | 348 |
{
|
| 353 |
- ContainerContext{
|
|
| 354 |
- Context: Context{
|
|
| 355 |
- Format: "{{.Image}}",
|
|
| 356 |
- Output: out, |
|
| 357 |
- }, |
|
| 358 |
- Size: true, |
|
| 349 |
+ Context{
|
|
| 350 |
+ Format: NewContainerFormat("{{.Image}}", false, true),
|
|
| 351 |
+ Output: out, |
|
| 359 | 352 |
}, |
| 360 | 353 |
"", |
| 361 | 354 |
}, |
| 362 | 355 |
{
|
| 363 |
- ContainerContext{
|
|
| 364 |
- Context: Context{
|
|
| 365 |
- Format: "table {{.Image}}",
|
|
| 366 |
- Output: out, |
|
| 367 |
- }, |
|
| 368 |
- Size: true, |
|
| 356 |
+ Context{
|
|
| 357 |
+ Format: NewContainerFormat("table {{.Image}}", false, true),
|
|
| 358 |
+ Output: out, |
|
| 369 | 359 |
}, |
| 370 | 360 |
"IMAGE\n", |
| 371 | 361 |
}, |
| 372 | 362 |
{
|
| 373 |
- ContainerContext{
|
|
| 374 |
- Context: Context{
|
|
| 375 |
- Format: "table {{.Image}}\t{{.Size}}",
|
|
| 376 |
- Output: out, |
|
| 377 |
- }, |
|
| 363 |
+ Context{
|
|
| 364 |
+ Format: "table {{.Image}}\t{{.Size}}",
|
|
| 365 |
+ Output: out, |
|
| 378 | 366 |
}, |
| 379 | 367 |
"IMAGE SIZE\n", |
| 380 | 368 |
}, |
| 381 | 369 |
{
|
| 382 |
- ContainerContext{
|
|
| 383 |
- Context: Context{
|
|
| 384 |
- Format: "table {{.Image}}\t{{.Size}}",
|
|
| 385 |
- Output: out, |
|
| 386 |
- }, |
|
| 387 |
- Size: true, |
|
| 370 |
+ Context{
|
|
| 371 |
+ Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true),
|
|
| 372 |
+ Output: out, |
|
| 388 | 373 |
}, |
| 389 | 374 |
"IMAGE SIZE\n", |
| 390 | 375 |
}, |
| 391 | 376 |
} |
| 392 | 377 |
|
| 393 | 378 |
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 |
- } |
|
| 379 |
+ ContainerWrite(context.context, containers) |
|
| 380 |
+ assert.Equal(t, context.expected, out.String()) |
|
| 400 | 381 |
// Clean buffer |
| 401 | 382 |
out.Reset() |
| 402 | 383 |
} |
| ... | ... |
@@ -5,8 +5,6 @@ import ( |
| 5 | 5 |
) |
| 6 | 6 |
|
| 7 | 7 |
const ( |
| 8 |
- tableKey = "table" |
|
| 9 |
- |
|
| 10 | 8 |
imageHeader = "IMAGE" |
| 11 | 9 |
createdSinceHeader = "CREATED" |
| 12 | 10 |
createdAtHeader = "CREATED AT" |
| ... | ... |
@@ -18,22 +16,25 @@ const ( |
| 18 | 18 |
) |
| 19 | 19 |
|
| 20 | 20 |
type subContext interface {
|
| 21 |
- fullHeader() string |
|
| 22 |
- addHeader(header string) |
|
| 21 |
+ FullHeader() string |
|
| 22 |
+ AddHeader(header string) |
|
| 23 | 23 |
} |
| 24 | 24 |
|
| 25 |
-type baseSubContext struct {
|
|
| 25 |
+// HeaderContext provides the subContext interface for managing headers |
|
| 26 |
+type HeaderContext struct {
|
|
| 26 | 27 |
header []string |
| 27 | 28 |
} |
| 28 | 29 |
|
| 29 |
-func (c *baseSubContext) fullHeader() string {
|
|
| 30 |
+// FullHeader returns the header as a string |
|
| 31 |
+func (c *HeaderContext) FullHeader() string {
|
|
| 30 | 32 |
if c.header == nil {
|
| 31 | 33 |
return "" |
| 32 | 34 |
} |
| 33 | 35 |
return strings.Join(c.header, "\t") |
| 34 | 36 |
} |
| 35 | 37 |
|
| 36 |
-func (c *baseSubContext) addHeader(header string) {
|
|
| 38 |
+// AddHeader adds another column to the header |
|
| 39 |
+func (c *HeaderContext) AddHeader(header string) {
|
|
| 37 | 40 |
if c.header == nil {
|
| 38 | 41 |
c.header = []string{}
|
| 39 | 42 |
} |
| ... | ... |
@@ -12,36 +12,48 @@ import ( |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
const ( |
| 15 |
- tableFormatKey = "table" |
|
| 16 |
- rawFormatKey = "raw" |
|
| 15 |
+ // TableFormatKey is the key used to format as a table |
|
| 16 |
+ TableFormatKey = "table" |
|
| 17 |
+ // RawFormatKey is the key used to format as raw JSON |
|
| 18 |
+ RawFormatKey = "raw" |
|
| 17 | 19 |
|
| 18 | 20 |
defaultQuietFormat = "{{.ID}}"
|
| 19 | 21 |
) |
| 20 | 22 |
|
| 23 |
+// Format is the format string rendered using the Context |
|
| 24 |
+type Format string |
|
| 25 |
+ |
|
| 26 |
+// IsTable returns true if the format is a table-type format |
|
| 27 |
+func (f Format) IsTable() bool {
|
|
| 28 |
+ return strings.HasPrefix(string(f), TableFormatKey) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// Contains returns true if the format contains the substring |
|
| 32 |
+func (f Format) Contains(sub string) bool {
|
|
| 33 |
+ return strings.Contains(string(f), sub) |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 21 | 36 |
// Context contains information required by the formatter to print the output as desired. |
| 22 | 37 |
type Context struct {
|
| 23 | 38 |
// Output is the output stream to which the formatted string is written. |
| 24 | 39 |
Output io.Writer |
| 25 | 40 |
// Format is used to choose raw, table or custom format for the output. |
| 26 |
- Format string |
|
| 27 |
- // Quiet when set to true will simply print minimal information. |
|
| 28 |
- Quiet bool |
|
| 41 |
+ Format Format |
|
| 29 | 42 |
// Trunc when set to true will truncate the output of certain fields such as Container ID. |
| 30 | 43 |
Trunc bool |
| 31 | 44 |
|
| 32 | 45 |
// internal element |
| 33 |
- table bool |
|
| 34 | 46 |
finalFormat string |
| 35 | 47 |
header string |
| 36 | 48 |
buffer *bytes.Buffer |
| 37 | 49 |
} |
| 38 | 50 |
|
| 39 |
-func (c *Context) preformat() {
|
|
| 40 |
- c.finalFormat = c.Format |
|
| 51 |
+func (c *Context) preFormat() {
|
|
| 52 |
+ c.finalFormat = string(c.Format) |
|
| 41 | 53 |
|
| 42 |
- if strings.HasPrefix(c.Format, tableKey) {
|
|
| 43 |
- c.table = true |
|
| 44 |
- c.finalFormat = c.finalFormat[len(tableKey):] |
|
| 54 |
+ // TODO: handle this in the Format type |
|
| 55 |
+ if c.Format.IsTable() {
|
|
| 56 |
+ c.finalFormat = c.finalFormat[len(TableFormatKey):] |
|
| 45 | 57 |
} |
| 46 | 58 |
|
| 47 | 59 |
c.finalFormat = strings.Trim(c.finalFormat, " ") |
| ... | ... |
@@ -52,18 +64,17 @@ func (c *Context) preformat() {
|
| 52 | 52 |
func (c *Context) parseFormat() (*template.Template, error) {
|
| 53 | 53 |
tmpl, err := templates.Parse(c.finalFormat) |
| 54 | 54 |
if err != nil {
|
| 55 |
- c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
| 56 |
- c.buffer.WriteTo(c.Output) |
|
| 55 |
+ return tmpl, fmt.Errorf("Template parsing error: %v\n", err)
|
|
| 57 | 56 |
} |
| 58 | 57 |
return tmpl, err |
| 59 | 58 |
} |
| 60 | 59 |
|
| 61 |
-func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
|
| 62 |
- if c.table {
|
|
| 60 |
+func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
|
|
| 61 |
+ if c.Format.IsTable() {
|
|
| 63 | 62 |
if len(c.header) == 0 {
|
| 64 | 63 |
// 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 |
| 65 | 64 |
tmpl.Execute(bytes.NewBufferString(""), subContext)
|
| 66 |
- c.header = subContext.fullHeader() |
|
| 65 |
+ c.header = subContext.FullHeader() |
|
| 67 | 66 |
} |
| 68 | 67 |
|
| 69 | 68 |
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) |
| ... | ... |
@@ -78,13 +89,35 @@ func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
| 78 | 78 |
|
| 79 | 79 |
func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
|
| 80 | 80 |
if err := tmpl.Execute(c.buffer, subContext); err != nil {
|
| 81 |
- c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
| 82 |
- c.buffer.WriteTo(c.Output) |
|
| 83 |
- return err |
|
| 81 |
+ return fmt.Errorf("Template parsing error: %v\n", err)
|
|
| 84 | 82 |
} |
| 85 |
- if c.table && len(c.header) == 0 {
|
|
| 86 |
- c.header = subContext.fullHeader() |
|
| 83 |
+ if c.Format.IsTable() && len(c.header) == 0 {
|
|
| 84 |
+ c.header = subContext.FullHeader() |
|
| 87 | 85 |
} |
| 88 | 86 |
c.buffer.WriteString("\n")
|
| 89 | 87 |
return nil |
| 90 | 88 |
} |
| 89 |
+ |
|
| 90 |
+// SubFormat is a function type accepted by Write() |
|
| 91 |
+type SubFormat func(func(subContext) error) error |
|
| 92 |
+ |
|
| 93 |
+// Write the template to the buffer using this Context |
|
| 94 |
+func (c *Context) Write(sub subContext, f SubFormat) error {
|
|
| 95 |
+ c.buffer = bytes.NewBufferString("")
|
|
| 96 |
+ c.preFormat() |
|
| 97 |
+ |
|
| 98 |
+ tmpl, err := c.parseFormat() |
|
| 99 |
+ if err != nil {
|
|
| 100 |
+ return err |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ subFormat := func(subContext subContext) error {
|
|
| 104 |
+ return c.contextFormat(tmpl, subContext) |
|
| 105 |
+ } |
|
| 106 |
+ if err := f(subFormat); err != nil {
|
|
| 107 |
+ return err |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ c.postFormat(tmpl, sub) |
|
| 111 |
+ return nil |
|
| 112 |
+} |
| ... | ... |
@@ -1,14 +1,12 @@ |
| 1 | 1 |
package formatter |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bytes" |
|
| 5 |
- "strings" |
|
| 6 | 4 |
"time" |
| 7 | 5 |
|
| 8 | 6 |
"github.com/docker/docker/api/types" |
| 9 | 7 |
"github.com/docker/docker/pkg/stringid" |
| 10 | 8 |
"github.com/docker/docker/reference" |
| 11 |
- "github.com/docker/go-units" |
|
| 9 |
+ units "github.com/docker/go-units" |
|
| 12 | 10 |
) |
| 13 | 11 |
|
| 14 | 12 |
const ( |
| ... | ... |
@@ -25,59 +23,63 @@ const ( |
| 25 | 25 |
type ImageContext struct {
|
| 26 | 26 |
Context |
| 27 | 27 |
Digest bool |
| 28 |
- // Images |
|
| 29 |
- Images []types.Image |
|
| 30 | 28 |
} |
| 31 | 29 |
|
| 32 | 30 |
func isDangling(image types.Image) bool {
|
| 33 | 31 |
return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>" |
| 34 | 32 |
} |
| 35 | 33 |
|
| 36 |
-func (ctx ImageContext) Write() {
|
|
| 37 |
- switch ctx.Format {
|
|
| 38 |
- case tableFormatKey: |
|
| 39 |
- ctx.Format = defaultImageTableFormat |
|
| 40 |
- if ctx.Digest {
|
|
| 41 |
- ctx.Format = defaultImageTableFormatWithDigest |
|
| 34 |
+// NewImageFormat returns a format for rendering an ImageContext |
|
| 35 |
+func NewImageFormat(source string, quiet bool, digest bool) Format {
|
|
| 36 |
+ switch source {
|
|
| 37 |
+ case TableFormatKey: |
|
| 38 |
+ switch {
|
|
| 39 |
+ case quiet: |
|
| 40 |
+ return defaultQuietFormat |
|
| 41 |
+ case digest: |
|
| 42 |
+ return defaultImageTableFormatWithDigest |
|
| 43 |
+ default: |
|
| 44 |
+ return defaultImageTableFormat |
|
| 42 | 45 |
} |
| 43 |
- if ctx.Quiet {
|
|
| 44 |
- ctx.Format = defaultQuietFormat |
|
| 45 |
- } |
|
| 46 |
- case rawFormatKey: |
|
| 47 |
- if ctx.Quiet {
|
|
| 48 |
- ctx.Format = `image_id: {{.ID}}`
|
|
| 49 |
- } else {
|
|
| 50 |
- if ctx.Digest {
|
|
| 51 |
- ctx.Format = `repository: {{ .Repository }}
|
|
| 46 |
+ case RawFormatKey: |
|
| 47 |
+ switch {
|
|
| 48 |
+ case quiet: |
|
| 49 |
+ return `image_id: {{.ID}}`
|
|
| 50 |
+ case digest: |
|
| 51 |
+ return `repository: {{ .Repository }}
|
|
| 52 | 52 |
tag: {{.Tag}}
|
| 53 | 53 |
digest: {{.Digest}}
|
| 54 | 54 |
image_id: {{.ID}}
|
| 55 | 55 |
created_at: {{.CreatedAt}}
|
| 56 | 56 |
virtual_size: {{.Size}}
|
| 57 | 57 |
` |
| 58 |
- } else {
|
|
| 59 |
- ctx.Format = `repository: {{ .Repository }}
|
|
| 58 |
+ default: |
|
| 59 |
+ return `repository: {{ .Repository }}
|
|
| 60 | 60 |
tag: {{.Tag}}
|
| 61 | 61 |
image_id: {{.ID}}
|
| 62 | 62 |
created_at: {{.CreatedAt}}
|
| 63 | 63 |
virtual_size: {{.Size}}
|
| 64 | 64 |
` |
| 65 |
- } |
|
| 66 | 65 |
} |
| 67 | 66 |
} |
| 68 | 67 |
|
| 69 |
- ctx.buffer = bytes.NewBufferString("")
|
|
| 70 |
- ctx.preformat() |
|
| 71 |
- if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
|
| 72 |
- ctx.finalFormat += "\t{{.Digest}}"
|
|
| 68 |
+ format := Format(source) |
|
| 69 |
+ if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
|
|
| 70 |
+ format += "\t{{.Digest}}"
|
|
| 73 | 71 |
} |
| 72 |
+ return format |
|
| 73 |
+} |
|
| 74 | 74 |
|
| 75 |
- tmpl, err := ctx.parseFormat() |
|
| 76 |
- if err != nil {
|
|
| 77 |
- return |
|
| 75 |
+// ImageWrite writes the formatter images using the ImageContext |
|
| 76 |
+func ImageWrite(ctx ImageContext, images []types.Image) error {
|
|
| 77 |
+ render := func(format func(subContext subContext) error) error {
|
|
| 78 |
+ return imageFormat(ctx, images, format) |
|
| 78 | 79 |
} |
| 80 |
+ return ctx.Write(&imageContext{}, render)
|
|
| 81 |
+} |
|
| 79 | 82 |
|
| 80 |
- for _, image := range ctx.Images {
|
|
| 83 |
+func imageFormat(ctx ImageContext, images []types.Image, format func(subContext subContext) error) error {
|
|
| 84 |
+ for _, image := range images {
|
|
| 81 | 85 |
images := []*imageContext{}
|
| 82 | 86 |
if isDangling(image) {
|
| 83 | 87 |
images = append(images, &imageContext{
|
| ... | ... |
@@ -170,18 +172,16 @@ virtual_size: {{.Size}}
|
| 170 | 170 |
} |
| 171 | 171 |
} |
| 172 | 172 |
for _, imageCtx := range images {
|
| 173 |
- err = ctx.contextFormat(tmpl, imageCtx) |
|
| 174 |
- if err != nil {
|
|
| 175 |
- return |
|
| 173 |
+ if err := format(imageCtx); err != nil {
|
|
| 174 |
+ return err |
|
| 176 | 175 |
} |
| 177 | 176 |
} |
| 178 | 177 |
} |
| 179 |
- |
|
| 180 |
- ctx.postformat(tmpl, &imageContext{})
|
|
| 178 |
+ return nil |
|
| 181 | 179 |
} |
| 182 | 180 |
|
| 183 | 181 |
type imageContext struct {
|
| 184 |
- baseSubContext |
|
| 182 |
+ HeaderContext |
|
| 185 | 183 |
trunc bool |
| 186 | 184 |
i types.Image |
| 187 | 185 |
repo string |
| ... | ... |
@@ -190,7 +190,7 @@ type imageContext struct {
|
| 190 | 190 |
} |
| 191 | 191 |
|
| 192 | 192 |
func (c *imageContext) ID() string {
|
| 193 |
- c.addHeader(imageIDHeader) |
|
| 193 |
+ c.AddHeader(imageIDHeader) |
|
| 194 | 194 |
if c.trunc {
|
| 195 | 195 |
return stringid.TruncateID(c.i.ID) |
| 196 | 196 |
} |
| ... | ... |
@@ -198,32 +198,32 @@ func (c *imageContext) ID() string {
|
| 198 | 198 |
} |
| 199 | 199 |
|
| 200 | 200 |
func (c *imageContext) Repository() string {
|
| 201 |
- c.addHeader(repositoryHeader) |
|
| 201 |
+ c.AddHeader(repositoryHeader) |
|
| 202 | 202 |
return c.repo |
| 203 | 203 |
} |
| 204 | 204 |
|
| 205 | 205 |
func (c *imageContext) Tag() string {
|
| 206 |
- c.addHeader(tagHeader) |
|
| 206 |
+ c.AddHeader(tagHeader) |
|
| 207 | 207 |
return c.tag |
| 208 | 208 |
} |
| 209 | 209 |
|
| 210 | 210 |
func (c *imageContext) Digest() string {
|
| 211 |
- c.addHeader(digestHeader) |
|
| 211 |
+ c.AddHeader(digestHeader) |
|
| 212 | 212 |
return c.digest |
| 213 | 213 |
} |
| 214 | 214 |
|
| 215 | 215 |
func (c *imageContext) CreatedSince() string {
|
| 216 |
- c.addHeader(createdSinceHeader) |
|
| 216 |
+ c.AddHeader(createdSinceHeader) |
|
| 217 | 217 |
createdAt := time.Unix(int64(c.i.Created), 0) |
| 218 | 218 |
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) |
| 219 | 219 |
} |
| 220 | 220 |
|
| 221 | 221 |
func (c *imageContext) CreatedAt() string {
|
| 222 |
- c.addHeader(createdAtHeader) |
|
| 222 |
+ c.AddHeader(createdAtHeader) |
|
| 223 | 223 |
return time.Unix(int64(c.i.Created), 0).String() |
| 224 | 224 |
} |
| 225 | 225 |
|
| 226 | 226 |
func (c *imageContext) Size() string {
|
| 227 |
- c.addHeader(sizeHeader) |
|
| 227 |
+ c.AddHeader(sizeHeader) |
|
| 228 | 228 |
return units.HumanSizeWithPrecision(float64(c.i.Size), 3) |
| 229 | 229 |
} |
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
|
| 10 | 10 |
"github.com/docker/docker/api/types" |
| 11 | 11 |
"github.com/docker/docker/pkg/stringid" |
| 12 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 12 | 13 |
) |
| 13 | 14 |
|
| 14 | 15 |
func TestImageContext(t *testing.T) {
|
| ... | ... |
@@ -66,7 +67,7 @@ func TestImageContext(t *testing.T) {
|
| 66 | 66 |
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 |
- h := ctx.fullHeader() |
|
| 69 |
+ h := ctx.FullHeader() |
|
| 70 | 70 |
if h != c.expHeader {
|
| 71 | 71 |
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
| 72 | 72 |
} |
| ... | ... |
@@ -77,7 +78,7 @@ func TestImageContextWrite(t *testing.T) {
|
| 77 | 77 |
unixTime := time.Now().AddDate(0, 0, -1).Unix() |
| 78 | 78 |
expectedTime := time.Unix(unixTime, 0).String() |
| 79 | 79 |
|
| 80 |
- contexts := []struct {
|
|
| 80 |
+ cases := []struct {
|
|
| 81 | 81 |
context ImageContext |
| 82 | 82 |
expected string |
| 83 | 83 |
}{
|
| ... | ... |
@@ -104,7 +105,7 @@ func TestImageContextWrite(t *testing.T) {
|
| 104 | 104 |
{
|
| 105 | 105 |
ImageContext{
|
| 106 | 106 |
Context: Context{
|
| 107 |
- Format: "table", |
|
| 107 |
+ Format: NewImageFormat("table", false, false),
|
|
| 108 | 108 |
}, |
| 109 | 109 |
}, |
| 110 | 110 |
`REPOSITORY TAG IMAGE ID CREATED SIZE |
| ... | ... |
@@ -116,7 +117,7 @@ image tag2 imageID2 24 hours ago |
| 116 | 116 |
{
|
| 117 | 117 |
ImageContext{
|
| 118 | 118 |
Context: Context{
|
| 119 |
- Format: "table {{.Repository}}",
|
|
| 119 |
+ Format: NewImageFormat("table {{.Repository}}", false, false),
|
|
| 120 | 120 |
}, |
| 121 | 121 |
}, |
| 122 | 122 |
"REPOSITORY\nimage\nimage\n<none>\n", |
| ... | ... |
@@ -124,7 +125,7 @@ image tag2 imageID2 24 hours ago |
| 124 | 124 |
{
|
| 125 | 125 |
ImageContext{
|
| 126 | 126 |
Context: Context{
|
| 127 |
- Format: "table {{.Repository}}",
|
|
| 127 |
+ Format: NewImageFormat("table {{.Repository}}", false, true),
|
|
| 128 | 128 |
}, |
| 129 | 129 |
Digest: true, |
| 130 | 130 |
}, |
| ... | ... |
@@ -137,8 +138,7 @@ image <none> |
| 137 | 137 |
{
|
| 138 | 138 |
ImageContext{
|
| 139 | 139 |
Context: Context{
|
| 140 |
- Format: "table {{.Repository}}",
|
|
| 141 |
- Quiet: true, |
|
| 140 |
+ Format: NewImageFormat("table {{.Repository}}", true, false),
|
|
| 142 | 141 |
}, |
| 143 | 142 |
}, |
| 144 | 143 |
"REPOSITORY\nimage\nimage\n<none>\n", |
| ... | ... |
@@ -146,8 +146,7 @@ image <none> |
| 146 | 146 |
{
|
| 147 | 147 |
ImageContext{
|
| 148 | 148 |
Context: Context{
|
| 149 |
- Format: "table", |
|
| 150 |
- Quiet: true, |
|
| 149 |
+ Format: NewImageFormat("table", true, false),
|
|
| 151 | 150 |
}, |
| 152 | 151 |
}, |
| 153 | 152 |
"imageID1\nimageID2\nimageID3\n", |
| ... | ... |
@@ -155,8 +154,7 @@ image <none> |
| 155 | 155 |
{
|
| 156 | 156 |
ImageContext{
|
| 157 | 157 |
Context: Context{
|
| 158 |
- Format: "table", |
|
| 159 |
- Quiet: false, |
|
| 158 |
+ Format: NewImageFormat("table", false, true),
|
|
| 160 | 159 |
}, |
| 161 | 160 |
Digest: true, |
| 162 | 161 |
}, |
| ... | ... |
@@ -169,8 +167,7 @@ image tag2 <none> |
| 169 | 169 |
{
|
| 170 | 170 |
ImageContext{
|
| 171 | 171 |
Context: Context{
|
| 172 |
- Format: "table", |
|
| 173 |
- Quiet: true, |
|
| 172 |
+ Format: NewImageFormat("table", true, true),
|
|
| 174 | 173 |
}, |
| 175 | 174 |
Digest: true, |
| 176 | 175 |
}, |
| ... | ... |
@@ -180,7 +177,7 @@ image tag2 <none> |
| 180 | 180 |
{
|
| 181 | 181 |
ImageContext{
|
| 182 | 182 |
Context: Context{
|
| 183 |
- Format: "raw", |
|
| 183 |
+ Format: NewImageFormat("raw", false, false),
|
|
| 184 | 184 |
}, |
| 185 | 185 |
}, |
| 186 | 186 |
fmt.Sprintf(`repository: image |
| ... | ... |
@@ -206,7 +203,7 @@ virtual_size: 0 B |
| 206 | 206 |
{
|
| 207 | 207 |
ImageContext{
|
| 208 | 208 |
Context: Context{
|
| 209 |
- Format: "raw", |
|
| 209 |
+ Format: NewImageFormat("raw", false, true),
|
|
| 210 | 210 |
}, |
| 211 | 211 |
Digest: true, |
| 212 | 212 |
}, |
| ... | ... |
@@ -236,8 +233,7 @@ virtual_size: 0 B |
| 236 | 236 |
{
|
| 237 | 237 |
ImageContext{
|
| 238 | 238 |
Context: Context{
|
| 239 |
- Format: "raw", |
|
| 240 |
- Quiet: true, |
|
| 239 |
+ Format: NewImageFormat("raw", true, false),
|
|
| 241 | 240 |
}, |
| 242 | 241 |
}, |
| 243 | 242 |
`image_id: imageID1 |
| ... | ... |
@@ -249,7 +245,7 @@ image_id: imageID3 |
| 249 | 249 |
{
|
| 250 | 250 |
ImageContext{
|
| 251 | 251 |
Context: Context{
|
| 252 |
- Format: "{{.Repository}}",
|
|
| 252 |
+ Format: NewImageFormat("{{.Repository}}", false, false),
|
|
| 253 | 253 |
}, |
| 254 | 254 |
}, |
| 255 | 255 |
"image\nimage\n<none>\n", |
| ... | ... |
@@ -257,7 +253,7 @@ image_id: imageID3 |
| 257 | 257 |
{
|
| 258 | 258 |
ImageContext{
|
| 259 | 259 |
Context: Context{
|
| 260 |
- Format: "{{.Repository}}",
|
|
| 260 |
+ Format: NewImageFormat("{{.Repository}}", false, true),
|
|
| 261 | 261 |
}, |
| 262 | 262 |
Digest: true, |
| 263 | 263 |
}, |
| ... | ... |
@@ -265,22 +261,20 @@ image_id: imageID3 |
| 265 | 265 |
}, |
| 266 | 266 |
} |
| 267 | 267 |
|
| 268 |
- for _, context := range contexts {
|
|
| 268 |
+ for _, testcase := range cases {
|
|
| 269 | 269 |
images := []types.Image{
|
| 270 | 270 |
{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
| 271 | 271 |
{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
| 272 | 272 |
{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
| 273 | 273 |
} |
| 274 | 274 |
out := bytes.NewBufferString("")
|
| 275 |
- context.context.Output = out |
|
| 276 |
- context.context.Images = images |
|
| 277 |
- context.context.Write() |
|
| 278 |
- actual := out.String() |
|
| 279 |
- if actual != context.expected {
|
|
| 280 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 275 |
+ testcase.context.Output = out |
|
| 276 |
+ err := ImageWrite(testcase.context, images) |
|
| 277 |
+ if err != nil {
|
|
| 278 |
+ assert.Error(t, err, testcase.expected) |
|
| 279 |
+ } else {
|
|
| 280 |
+ assert.Equal(t, out.String(), testcase.expected) |
|
| 281 | 281 |
} |
| 282 |
- // Clean buffer |
|
| 283 |
- out.Reset() |
|
| 284 | 282 |
} |
| 285 | 283 |
} |
| 286 | 284 |
|
| ... | ... |
@@ -295,7 +289,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
| 295 | 295 |
{
|
| 296 | 296 |
ImageContext{
|
| 297 | 297 |
Context: Context{
|
| 298 |
- Format: "{{.Repository}}",
|
|
| 298 |
+ Format: NewImageFormat("{{.Repository}}", false, false),
|
|
| 299 | 299 |
Output: out, |
| 300 | 300 |
}, |
| 301 | 301 |
}, |
| ... | ... |
@@ -304,7 +298,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
| 304 | 304 |
{
|
| 305 | 305 |
ImageContext{
|
| 306 | 306 |
Context: Context{
|
| 307 |
- Format: "table {{.Repository}}",
|
|
| 307 |
+ Format: NewImageFormat("table {{.Repository}}", false, false),
|
|
| 308 | 308 |
Output: out, |
| 309 | 309 |
}, |
| 310 | 310 |
}, |
| ... | ... |
@@ -313,32 +307,26 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
| 313 | 313 |
{
|
| 314 | 314 |
ImageContext{
|
| 315 | 315 |
Context: Context{
|
| 316 |
- Format: "{{.Repository}}",
|
|
| 316 |
+ Format: NewImageFormat("{{.Repository}}", false, true),
|
|
| 317 | 317 |
Output: out, |
| 318 | 318 |
}, |
| 319 |
- Digest: true, |
|
| 320 | 319 |
}, |
| 321 | 320 |
"", |
| 322 | 321 |
}, |
| 323 | 322 |
{
|
| 324 | 323 |
ImageContext{
|
| 325 | 324 |
Context: Context{
|
| 326 |
- Format: "table {{.Repository}}",
|
|
| 325 |
+ Format: NewImageFormat("table {{.Repository}}", false, true),
|
|
| 327 | 326 |
Output: out, |
| 328 | 327 |
}, |
| 329 |
- Digest: true, |
|
| 330 | 328 |
}, |
| 331 | 329 |
"REPOSITORY DIGEST\n", |
| 332 | 330 |
}, |
| 333 | 331 |
} |
| 334 | 332 |
|
| 335 | 333 |
for _, context := range contexts {
|
| 336 |
- context.context.Images = images |
|
| 337 |
- context.context.Write() |
|
| 338 |
- actual := out.String() |
|
| 339 |
- if actual != context.expected {
|
|
| 340 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 341 |
- } |
|
| 334 |
+ ImageWrite(context.context, images) |
|
| 335 |
+ assert.Equal(t, out.String(), context.expected) |
|
| 342 | 336 |
// Clean buffer |
| 343 | 337 |
out.Reset() |
| 344 | 338 |
} |
| ... | ... |
@@ -1,7 +1,6 @@ |
| 1 | 1 |
package formatter |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bytes" |
|
| 5 | 4 |
"fmt" |
| 6 | 5 |
"strings" |
| 7 | 6 |
|
| ... | ... |
@@ -17,60 +16,45 @@ const ( |
| 17 | 17 |
internalHeader = "INTERNAL" |
| 18 | 18 |
) |
| 19 | 19 |
|
| 20 |
-// NetworkContext contains network specific information required by the formatter, |
|
| 21 |
-// encapsulate a Context struct. |
|
| 22 |
-type NetworkContext struct {
|
|
| 23 |
- Context |
|
| 24 |
- // Networks |
|
| 25 |
- Networks []types.NetworkResource |
|
| 26 |
-} |
|
| 27 |
- |
|
| 28 |
-func (ctx NetworkContext) Write() {
|
|
| 29 |
- switch ctx.Format {
|
|
| 30 |
- case tableFormatKey: |
|
| 31 |
- if ctx.Quiet {
|
|
| 32 |
- ctx.Format = defaultQuietFormat |
|
| 33 |
- } else {
|
|
| 34 |
- ctx.Format = defaultNetworkTableFormat |
|
| 20 |
+// NewNetworkFormat returns a Format for rendering using a network Context |
|
| 21 |
+func NewNetworkFormat(source string, quiet bool) Format {
|
|
| 22 |
+ switch source {
|
|
| 23 |
+ case TableFormatKey: |
|
| 24 |
+ if quiet {
|
|
| 25 |
+ return defaultQuietFormat |
|
| 35 | 26 |
} |
| 36 |
- case rawFormatKey: |
|
| 37 |
- if ctx.Quiet {
|
|
| 38 |
- ctx.Format = `network_id: {{.ID}}`
|
|
| 39 |
- } else {
|
|
| 40 |
- ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
|
|
| 27 |
+ return defaultNetworkTableFormat |
|
| 28 |
+ case RawFormatKey: |
|
| 29 |
+ if quiet {
|
|
| 30 |
+ return `network_id: {{.ID}}`
|
|
| 41 | 31 |
} |
| 32 |
+ return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
|
|
| 42 | 33 |
} |
| 34 |
+ return Format(source) |
|
| 35 |
+} |
|
| 43 | 36 |
|
| 44 |
- ctx.buffer = bytes.NewBufferString("")
|
|
| 45 |
- ctx.preformat() |
|
| 46 |
- |
|
| 47 |
- tmpl, err := ctx.parseFormat() |
|
| 48 |
- if err != nil {
|
|
| 49 |
- return |
|
| 50 |
- } |
|
| 51 |
- |
|
| 52 |
- for _, network := range ctx.Networks {
|
|
| 53 |
- networkCtx := &networkContext{
|
|
| 54 |
- trunc: ctx.Trunc, |
|
| 55 |
- n: network, |
|
| 56 |
- } |
|
| 57 |
- err = ctx.contextFormat(tmpl, networkCtx) |
|
| 58 |
- if err != nil {
|
|
| 59 |
- return |
|
| 37 |
+// NetworkWrite writes the context |
|
| 38 |
+func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
|
|
| 39 |
+ render := func(format func(subContext subContext) error) error {
|
|
| 40 |
+ for _, network := range networks {
|
|
| 41 |
+ networkCtx := &networkContext{trunc: ctx.Trunc, n: network}
|
|
| 42 |
+ if err := format(networkCtx); err != nil {
|
|
| 43 |
+ return err |
|
| 44 |
+ } |
|
| 60 | 45 |
} |
| 46 |
+ return nil |
|
| 61 | 47 |
} |
| 62 |
- |
|
| 63 |
- ctx.postformat(tmpl, &networkContext{})
|
|
| 48 |
+ return ctx.Write(&networkContext{}, render)
|
|
| 64 | 49 |
} |
| 65 | 50 |
|
| 66 | 51 |
type networkContext struct {
|
| 67 |
- baseSubContext |
|
| 52 |
+ HeaderContext |
|
| 68 | 53 |
trunc bool |
| 69 | 54 |
n types.NetworkResource |
| 70 | 55 |
} |
| 71 | 56 |
|
| 72 | 57 |
func (c *networkContext) ID() string {
|
| 73 |
- c.addHeader(networkIDHeader) |
|
| 58 |
+ c.AddHeader(networkIDHeader) |
|
| 74 | 59 |
if c.trunc {
|
| 75 | 60 |
return stringid.TruncateID(c.n.ID) |
| 76 | 61 |
} |
| ... | ... |
@@ -78,32 +62,32 @@ func (c *networkContext) ID() string {
|
| 78 | 78 |
} |
| 79 | 79 |
|
| 80 | 80 |
func (c *networkContext) Name() string {
|
| 81 |
- c.addHeader(nameHeader) |
|
| 81 |
+ c.AddHeader(nameHeader) |
|
| 82 | 82 |
return c.n.Name |
| 83 | 83 |
} |
| 84 | 84 |
|
| 85 | 85 |
func (c *networkContext) Driver() string {
|
| 86 |
- c.addHeader(driverHeader) |
|
| 86 |
+ c.AddHeader(driverHeader) |
|
| 87 | 87 |
return c.n.Driver |
| 88 | 88 |
} |
| 89 | 89 |
|
| 90 | 90 |
func (c *networkContext) Scope() string {
|
| 91 |
- c.addHeader(scopeHeader) |
|
| 91 |
+ c.AddHeader(scopeHeader) |
|
| 92 | 92 |
return c.n.Scope |
| 93 | 93 |
} |
| 94 | 94 |
|
| 95 | 95 |
func (c *networkContext) IPv6() string {
|
| 96 |
- c.addHeader(ipv6Header) |
|
| 96 |
+ c.AddHeader(ipv6Header) |
|
| 97 | 97 |
return fmt.Sprintf("%v", c.n.EnableIPv6)
|
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 | 100 |
func (c *networkContext) Internal() string {
|
| 101 |
- c.addHeader(internalHeader) |
|
| 101 |
+ c.AddHeader(internalHeader) |
|
| 102 | 102 |
return fmt.Sprintf("%v", c.n.Internal)
|
| 103 | 103 |
} |
| 104 | 104 |
|
| 105 | 105 |
func (c *networkContext) Labels() string {
|
| 106 |
- c.addHeader(labelsHeader) |
|
| 106 |
+ c.AddHeader(labelsHeader) |
|
| 107 | 107 |
if c.n.Labels == nil {
|
| 108 | 108 |
return "" |
| 109 | 109 |
} |
| ... | ... |
@@ -120,7 +104,7 @@ func (c *networkContext) Label(name string) string {
|
| 120 | 120 |
r := strings.NewReplacer("-", " ", "_", " ")
|
| 121 | 121 |
h := r.Replace(n[len(n)-1]) |
| 122 | 122 |
|
| 123 |
- c.addHeader(h) |
|
| 123 |
+ c.AddHeader(h) |
|
| 124 | 124 |
|
| 125 | 125 |
if c.n.Labels == nil {
|
| 126 | 126 |
return "" |
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/api/types" |
| 9 | 9 |
"github.com/docker/docker/pkg/stringid" |
| 10 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 10 | 11 |
) |
| 11 | 12 |
|
| 12 | 13 |
func TestNetworkContext(t *testing.T) {
|
| ... | ... |
@@ -62,7 +63,7 @@ func TestNetworkContext(t *testing.T) {
|
| 62 | 62 |
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
| 63 | 63 |
} |
| 64 | 64 |
|
| 65 |
- h := ctx.fullHeader() |
|
| 65 |
+ h := ctx.FullHeader() |
|
| 66 | 66 |
if h != c.expHeader {
|
| 67 | 67 |
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
| 68 | 68 |
} |
| ... | ... |
@@ -70,71 +71,45 @@ func TestNetworkContext(t *testing.T) {
|
| 70 | 70 |
} |
| 71 | 71 |
|
| 72 | 72 |
func TestNetworkContextWrite(t *testing.T) {
|
| 73 |
- contexts := []struct {
|
|
| 74 |
- context NetworkContext |
|
| 73 |
+ cases := []struct {
|
|
| 74 |
+ context Context |
|
| 75 | 75 |
expected string |
| 76 | 76 |
}{
|
| 77 | 77 |
|
| 78 | 78 |
// Errors |
| 79 | 79 |
{
|
| 80 |
- NetworkContext{
|
|
| 81 |
- Context: Context{
|
|
| 82 |
- Format: "{{InvalidFunction}}",
|
|
| 83 |
- }, |
|
| 84 |
- }, |
|
| 80 |
+ Context{Format: "{{InvalidFunction}}"},
|
|
| 85 | 81 |
`Template parsing error: template: :1: function "InvalidFunction" not defined |
| 86 | 82 |
`, |
| 87 | 83 |
}, |
| 88 | 84 |
{
|
| 89 |
- NetworkContext{
|
|
| 90 |
- Context: Context{
|
|
| 91 |
- Format: "{{nil}}",
|
|
| 92 |
- }, |
|
| 93 |
- }, |
|
| 85 |
+ Context{Format: "{{nil}}"},
|
|
| 94 | 86 |
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
| 95 | 87 |
`, |
| 96 | 88 |
}, |
| 97 | 89 |
// Table format |
| 98 | 90 |
{
|
| 99 |
- NetworkContext{
|
|
| 100 |
- Context: Context{
|
|
| 101 |
- Format: "table", |
|
| 102 |
- }, |
|
| 103 |
- }, |
|
| 91 |
+ Context{Format: NewNetworkFormat("table", false)},
|
|
| 104 | 92 |
`NETWORK ID NAME DRIVER SCOPE |
| 105 | 93 |
networkID1 foobar_baz foo local |
| 106 | 94 |
networkID2 foobar_bar bar local |
| 107 | 95 |
`, |
| 108 | 96 |
}, |
| 109 | 97 |
{
|
| 110 |
- NetworkContext{
|
|
| 111 |
- Context: Context{
|
|
| 112 |
- Format: "table", |
|
| 113 |
- Quiet: true, |
|
| 114 |
- }, |
|
| 115 |
- }, |
|
| 98 |
+ Context{Format: NewNetworkFormat("table", true)},
|
|
| 116 | 99 |
`networkID1 |
| 117 | 100 |
networkID2 |
| 118 | 101 |
`, |
| 119 | 102 |
}, |
| 120 | 103 |
{
|
| 121 |
- NetworkContext{
|
|
| 122 |
- Context: Context{
|
|
| 123 |
- Format: "table {{.Name}}",
|
|
| 124 |
- }, |
|
| 125 |
- }, |
|
| 104 |
+ Context{Format: NewNetworkFormat("table {{.Name}}", false)},
|
|
| 126 | 105 |
`NAME |
| 127 | 106 |
foobar_baz |
| 128 | 107 |
foobar_bar |
| 129 | 108 |
`, |
| 130 | 109 |
}, |
| 131 | 110 |
{
|
| 132 |
- NetworkContext{
|
|
| 133 |
- Context: Context{
|
|
| 134 |
- Format: "table {{.Name}}",
|
|
| 135 |
- Quiet: true, |
|
| 136 |
- }, |
|
| 137 |
- }, |
|
| 111 |
+ Context{Format: NewNetworkFormat("table {{.Name}}", true)},
|
|
| 138 | 112 |
`NAME |
| 139 | 113 |
foobar_baz |
| 140 | 114 |
foobar_bar |
| ... | ... |
@@ -142,11 +117,8 @@ foobar_bar |
| 142 | 142 |
}, |
| 143 | 143 |
// Raw Format |
| 144 | 144 |
{
|
| 145 |
- NetworkContext{
|
|
| 146 |
- Context: Context{
|
|
| 147 |
- Format: "raw", |
|
| 148 |
- }, |
|
| 149 |
- }, `network_id: networkID1 |
|
| 145 |
+ Context{Format: NewNetworkFormat("raw", false)},
|
|
| 146 |
+ `network_id: networkID1 |
|
| 150 | 147 |
name: foobar_baz |
| 151 | 148 |
driver: foo |
| 152 | 149 |
scope: local |
| ... | ... |
@@ -159,43 +131,32 @@ scope: local |
| 159 | 159 |
`, |
| 160 | 160 |
}, |
| 161 | 161 |
{
|
| 162 |
- NetworkContext{
|
|
| 163 |
- Context: Context{
|
|
| 164 |
- Format: "raw", |
|
| 165 |
- Quiet: true, |
|
| 166 |
- }, |
|
| 167 |
- }, |
|
| 162 |
+ Context{Format: NewNetworkFormat("raw", true)},
|
|
| 168 | 163 |
`network_id: networkID1 |
| 169 | 164 |
network_id: networkID2 |
| 170 | 165 |
`, |
| 171 | 166 |
}, |
| 172 | 167 |
// Custom Format |
| 173 | 168 |
{
|
| 174 |
- NetworkContext{
|
|
| 175 |
- Context: Context{
|
|
| 176 |
- Format: "{{.Name}}",
|
|
| 177 |
- }, |
|
| 178 |
- }, |
|
| 169 |
+ Context{Format: NewNetworkFormat("{{.Name}}", false)},
|
|
| 179 | 170 |
`foobar_baz |
| 180 | 171 |
foobar_bar |
| 181 | 172 |
`, |
| 182 | 173 |
}, |
| 183 | 174 |
} |
| 184 | 175 |
|
| 185 |
- for _, context := range contexts {
|
|
| 176 |
+ for _, testcase := range cases {
|
|
| 186 | 177 |
networks := []types.NetworkResource{
|
| 187 | 178 |
{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
|
| 188 | 179 |
{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
|
| 189 | 180 |
} |
| 190 | 181 |
out := bytes.NewBufferString("")
|
| 191 |
- context.context.Output = out |
|
| 192 |
- context.context.Networks = networks |
|
| 193 |
- context.context.Write() |
|
| 194 |
- actual := out.String() |
|
| 195 |
- if actual != context.expected {
|
|
| 196 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 182 |
+ testcase.context.Output = out |
|
| 183 |
+ err := NetworkWrite(testcase.context, networks) |
|
| 184 |
+ if err != nil {
|
|
| 185 |
+ assert.Error(t, err, testcase.expected) |
|
| 186 |
+ } else {
|
|
| 187 |
+ assert.Equal(t, out.String(), testcase.expected) |
|
| 197 | 188 |
} |
| 198 |
- // Clean buffer |
|
| 199 |
- out.Reset() |
|
| 200 | 189 |
} |
| 201 | 190 |
} |
| ... | ... |
@@ -1,7 +1,6 @@ |
| 1 | 1 |
package formatter |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bytes" |
|
| 5 | 4 |
"fmt" |
| 6 | 5 |
"strings" |
| 7 | 6 |
|
| ... | ... |
@@ -16,78 +15,63 @@ const ( |
| 16 | 16 |
// Status header ? |
| 17 | 17 |
) |
| 18 | 18 |
|
| 19 |
-// VolumeContext contains volume specific information required by the formatter, |
|
| 20 |
-// encapsulate a Context struct. |
|
| 21 |
-type VolumeContext struct {
|
|
| 22 |
- Context |
|
| 23 |
- // Volumes |
|
| 24 |
- Volumes []*types.Volume |
|
| 25 |
-} |
|
| 26 |
- |
|
| 27 |
-func (ctx VolumeContext) Write() {
|
|
| 28 |
- switch ctx.Format {
|
|
| 29 |
- case tableFormatKey: |
|
| 30 |
- if ctx.Quiet {
|
|
| 31 |
- ctx.Format = defaultVolumeQuietFormat |
|
| 32 |
- } else {
|
|
| 33 |
- ctx.Format = defaultVolumeTableFormat |
|
| 19 |
+// NewVolumeFormat returns a format for use with a volume Context |
|
| 20 |
+func NewVolumeFormat(source string, quiet bool) Format {
|
|
| 21 |
+ switch source {
|
|
| 22 |
+ case TableFormatKey: |
|
| 23 |
+ if quiet {
|
|
| 24 |
+ return defaultVolumeQuietFormat |
|
| 34 | 25 |
} |
| 35 |
- case rawFormatKey: |
|
| 36 |
- if ctx.Quiet {
|
|
| 37 |
- ctx.Format = `name: {{.Name}}`
|
|
| 38 |
- } else {
|
|
| 39 |
- ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
|
|
| 26 |
+ return defaultVolumeTableFormat |
|
| 27 |
+ case RawFormatKey: |
|
| 28 |
+ if quiet {
|
|
| 29 |
+ return `name: {{.Name}}`
|
|
| 40 | 30 |
} |
| 31 |
+ return `name: {{.Name}}\ndriver: {{.Driver}}\n`
|
|
| 41 | 32 |
} |
| 33 |
+ return Format(source) |
|
| 34 |
+} |
|
| 42 | 35 |
|
| 43 |
- ctx.buffer = bytes.NewBufferString("")
|
|
| 44 |
- ctx.preformat() |
|
| 45 |
- |
|
| 46 |
- tmpl, err := ctx.parseFormat() |
|
| 47 |
- if err != nil {
|
|
| 48 |
- return |
|
| 49 |
- } |
|
| 50 |
- |
|
| 51 |
- for _, volume := range ctx.Volumes {
|
|
| 52 |
- volumeCtx := &volumeContext{
|
|
| 53 |
- v: volume, |
|
| 54 |
- } |
|
| 55 |
- err = ctx.contextFormat(tmpl, volumeCtx) |
|
| 56 |
- if err != nil {
|
|
| 57 |
- return |
|
| 36 |
+// VolumeWrite writes formatted volumes using the Context |
|
| 37 |
+func VolumeWrite(ctx Context, volumes []*types.Volume) error {
|
|
| 38 |
+ render := func(format func(subContext subContext) error) error {
|
|
| 39 |
+ for _, volume := range volumes {
|
|
| 40 |
+ if err := format(&volumeContext{v: volume}); err != nil {
|
|
| 41 |
+ return err |
|
| 42 |
+ } |
|
| 58 | 43 |
} |
| 44 |
+ return nil |
|
| 59 | 45 |
} |
| 60 |
- |
|
| 61 |
- ctx.postformat(tmpl, &networkContext{})
|
|
| 46 |
+ return ctx.Write(&volumeContext{}, render)
|
|
| 62 | 47 |
} |
| 63 | 48 |
|
| 64 | 49 |
type volumeContext struct {
|
| 65 |
- baseSubContext |
|
| 50 |
+ HeaderContext |
|
| 66 | 51 |
v *types.Volume |
| 67 | 52 |
} |
| 68 | 53 |
|
| 69 | 54 |
func (c *volumeContext) Name() string {
|
| 70 |
- c.addHeader(nameHeader) |
|
| 55 |
+ c.AddHeader(nameHeader) |
|
| 71 | 56 |
return c.v.Name |
| 72 | 57 |
} |
| 73 | 58 |
|
| 74 | 59 |
func (c *volumeContext) Driver() string {
|
| 75 |
- c.addHeader(driverHeader) |
|
| 60 |
+ c.AddHeader(driverHeader) |
|
| 76 | 61 |
return c.v.Driver |
| 77 | 62 |
} |
| 78 | 63 |
|
| 79 | 64 |
func (c *volumeContext) Scope() string {
|
| 80 |
- c.addHeader(scopeHeader) |
|
| 65 |
+ c.AddHeader(scopeHeader) |
|
| 81 | 66 |
return c.v.Scope |
| 82 | 67 |
} |
| 83 | 68 |
|
| 84 | 69 |
func (c *volumeContext) Mountpoint() string {
|
| 85 |
- c.addHeader(mountpointHeader) |
|
| 70 |
+ c.AddHeader(mountpointHeader) |
|
| 86 | 71 |
return c.v.Mountpoint |
| 87 | 72 |
} |
| 88 | 73 |
|
| 89 | 74 |
func (c *volumeContext) Labels() string {
|
| 90 |
- c.addHeader(labelsHeader) |
|
| 75 |
+ c.AddHeader(labelsHeader) |
|
| 91 | 76 |
if c.v.Labels == nil {
|
| 92 | 77 |
return "" |
| 93 | 78 |
} |
| ... | ... |
@@ -105,7 +89,7 @@ func (c *volumeContext) Label(name string) string {
|
| 105 | 105 |
r := strings.NewReplacer("-", " ", "_", " ")
|
| 106 | 106 |
h := r.Replace(n[len(n)-1]) |
| 107 | 107 |
|
| 108 |
- c.addHeader(h) |
|
| 108 |
+ c.AddHeader(h) |
|
| 109 | 109 |
|
| 110 | 110 |
if c.v.Labels == nil {
|
| 111 | 111 |
return "" |
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/api/types" |
| 9 | 9 |
"github.com/docker/docker/pkg/stringid" |
| 10 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 10 | 11 |
) |
| 11 | 12 |
|
| 12 | 13 |
func TestVolumeContext(t *testing.T) {
|
| ... | ... |
@@ -48,7 +49,7 @@ func TestVolumeContext(t *testing.T) {
|
| 48 | 48 |
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 |
- h := ctx.fullHeader() |
|
| 51 |
+ h := ctx.FullHeader() |
|
| 52 | 52 |
if h != c.expHeader {
|
| 53 | 53 |
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
| 54 | 54 |
} |
| ... | ... |
@@ -56,71 +57,45 @@ func TestVolumeContext(t *testing.T) {
|
| 56 | 56 |
} |
| 57 | 57 |
|
| 58 | 58 |
func TestVolumeContextWrite(t *testing.T) {
|
| 59 |
- contexts := []struct {
|
|
| 60 |
- context VolumeContext |
|
| 59 |
+ cases := []struct {
|
|
| 60 |
+ context Context |
|
| 61 | 61 |
expected string |
| 62 | 62 |
}{
|
| 63 | 63 |
|
| 64 | 64 |
// Errors |
| 65 | 65 |
{
|
| 66 |
- VolumeContext{
|
|
| 67 |
- Context: Context{
|
|
| 68 |
- Format: "{{InvalidFunction}}",
|
|
| 69 |
- }, |
|
| 70 |
- }, |
|
| 66 |
+ Context{Format: "{{InvalidFunction}}"},
|
|
| 71 | 67 |
`Template parsing error: template: :1: function "InvalidFunction" not defined |
| 72 | 68 |
`, |
| 73 | 69 |
}, |
| 74 | 70 |
{
|
| 75 |
- VolumeContext{
|
|
| 76 |
- Context: Context{
|
|
| 77 |
- Format: "{{nil}}",
|
|
| 78 |
- }, |
|
| 79 |
- }, |
|
| 71 |
+ Context{Format: "{{nil}}"},
|
|
| 80 | 72 |
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
| 81 | 73 |
`, |
| 82 | 74 |
}, |
| 83 | 75 |
// Table format |
| 84 | 76 |
{
|
| 85 |
- VolumeContext{
|
|
| 86 |
- Context: Context{
|
|
| 87 |
- Format: "table", |
|
| 88 |
- }, |
|
| 89 |
- }, |
|
| 77 |
+ Context{Format: NewVolumeFormat("table", false)},
|
|
| 90 | 78 |
`DRIVER NAME |
| 91 | 79 |
foo foobar_baz |
| 92 | 80 |
bar foobar_bar |
| 93 | 81 |
`, |
| 94 | 82 |
}, |
| 95 | 83 |
{
|
| 96 |
- VolumeContext{
|
|
| 97 |
- Context: Context{
|
|
| 98 |
- Format: "table", |
|
| 99 |
- Quiet: true, |
|
| 100 |
- }, |
|
| 101 |
- }, |
|
| 84 |
+ Context{Format: NewVolumeFormat("table", true)},
|
|
| 102 | 85 |
`foobar_baz |
| 103 | 86 |
foobar_bar |
| 104 | 87 |
`, |
| 105 | 88 |
}, |
| 106 | 89 |
{
|
| 107 |
- VolumeContext{
|
|
| 108 |
- Context: Context{
|
|
| 109 |
- Format: "table {{.Name}}",
|
|
| 110 |
- }, |
|
| 111 |
- }, |
|
| 90 |
+ Context{Format: NewVolumeFormat("table {{.Name}}", false)},
|
|
| 112 | 91 |
`NAME |
| 113 | 92 |
foobar_baz |
| 114 | 93 |
foobar_bar |
| 115 | 94 |
`, |
| 116 | 95 |
}, |
| 117 | 96 |
{
|
| 118 |
- VolumeContext{
|
|
| 119 |
- Context: Context{
|
|
| 120 |
- Format: "table {{.Name}}",
|
|
| 121 |
- Quiet: true, |
|
| 122 |
- }, |
|
| 123 |
- }, |
|
| 97 |
+ Context{Format: NewVolumeFormat("table {{.Name}}", true)},
|
|
| 124 | 98 |
`NAME |
| 125 | 99 |
foobar_baz |
| 126 | 100 |
foobar_bar |
| ... | ... |
@@ -128,11 +103,8 @@ foobar_bar |
| 128 | 128 |
}, |
| 129 | 129 |
// Raw Format |
| 130 | 130 |
{
|
| 131 |
- VolumeContext{
|
|
| 132 |
- Context: Context{
|
|
| 133 |
- Format: "raw", |
|
| 134 |
- }, |
|
| 135 |
- }, `name: foobar_baz |
|
| 131 |
+ Context{Format: NewVolumeFormat("raw", false)},
|
|
| 132 |
+ `name: foobar_baz |
|
| 136 | 133 |
driver: foo |
| 137 | 134 |
|
| 138 | 135 |
name: foobar_bar |
| ... | ... |
@@ -141,43 +113,32 @@ driver: bar |
| 141 | 141 |
`, |
| 142 | 142 |
}, |
| 143 | 143 |
{
|
| 144 |
- VolumeContext{
|
|
| 145 |
- Context: Context{
|
|
| 146 |
- Format: "raw", |
|
| 147 |
- Quiet: true, |
|
| 148 |
- }, |
|
| 149 |
- }, |
|
| 144 |
+ Context{Format: NewVolumeFormat("raw", true)},
|
|
| 150 | 145 |
`name: foobar_baz |
| 151 | 146 |
name: foobar_bar |
| 152 | 147 |
`, |
| 153 | 148 |
}, |
| 154 | 149 |
// Custom Format |
| 155 | 150 |
{
|
| 156 |
- VolumeContext{
|
|
| 157 |
- Context: Context{
|
|
| 158 |
- Format: "{{.Name}}",
|
|
| 159 |
- }, |
|
| 160 |
- }, |
|
| 151 |
+ Context{Format: NewVolumeFormat("{{.Name}}", false)},
|
|
| 161 | 152 |
`foobar_baz |
| 162 | 153 |
foobar_bar |
| 163 | 154 |
`, |
| 164 | 155 |
}, |
| 165 | 156 |
} |
| 166 | 157 |
|
| 167 |
- for _, context := range contexts {
|
|
| 158 |
+ for _, testcase := range cases {
|
|
| 168 | 159 |
volumes := []*types.Volume{
|
| 169 | 160 |
{Name: "foobar_baz", Driver: "foo"},
|
| 170 | 161 |
{Name: "foobar_bar", Driver: "bar"},
|
| 171 | 162 |
} |
| 172 | 163 |
out := bytes.NewBufferString("")
|
| 173 |
- context.context.Output = out |
|
| 174 |
- context.context.Volumes = volumes |
|
| 175 |
- context.context.Write() |
|
| 176 |
- actual := out.String() |
|
| 177 |
- if actual != context.expected {
|
|
| 178 |
- t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
|
| 164 |
+ testcase.context.Output = out |
|
| 165 |
+ err := VolumeWrite(testcase.context, volumes) |
|
| 166 |
+ if err != nil {
|
|
| 167 |
+ assert.Error(t, err, testcase.expected) |
|
| 168 |
+ } else {
|
|
| 169 |
+ assert.Equal(t, out.String(), testcase.expected) |
|
| 179 | 170 |
} |
| 180 |
- // Clean buffer |
|
| 181 |
- out.Reset() |
|
| 182 | 171 |
} |
| 183 | 172 |
} |
| ... | ... |
@@ -64,27 +64,22 @@ func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
|
| 64 | 64 |
return err |
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
- f := opts.format |
|
| 68 |
- if len(f) == 0 {
|
|
| 67 |
+ format := opts.format |
|
| 68 |
+ if len(format) == 0 {
|
|
| 69 | 69 |
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
|
| 70 |
- f = dockerCli.ConfigFile().ImagesFormat |
|
| 70 |
+ format = dockerCli.ConfigFile().ImagesFormat |
|
| 71 | 71 |
} else {
|
| 72 |
- f = "table" |
|
| 72 |
+ format = "table" |
|
| 73 | 73 |
} |
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 |
- imagesCtx := formatter.ImageContext{
|
|
| 76 |
+ imageCtx := formatter.ImageContext{
|
|
| 77 | 77 |
Context: formatter.Context{
|
| 78 | 78 |
Output: dockerCli.Out(), |
| 79 |
- Format: f, |
|
| 80 |
- Quiet: opts.quiet, |
|
| 79 |
+ Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests), |
|
| 81 | 80 |
Trunc: !opts.noTrunc, |
| 82 | 81 |
}, |
| 83 | 82 |
Digest: opts.showDigests, |
| 84 |
- Images: images, |
|
| 85 | 83 |
} |
| 86 |
- |
|
| 87 |
- imagesCtx.Write() |
|
| 88 |
- |
|
| 89 |
- return nil |
|
| 84 |
+ return formatter.ImageWrite(imageCtx, images) |
|
| 90 | 85 |
} |
| ... | ... |
@@ -50,35 +50,27 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 50 | 50 |
|
| 51 | 51 |
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 52 | 52 |
client := dockerCli.Client() |
| 53 |
- |
|
| 54 | 53 |
options := types.NetworkListOptions{Filters: opts.filter.Value()}
|
| 55 | 54 |
networkResources, err := client.NetworkList(context.Background(), options) |
| 56 | 55 |
if err != nil {
|
| 57 | 56 |
return err |
| 58 | 57 |
} |
| 59 | 58 |
|
| 60 |
- f := opts.format |
|
| 61 |
- if len(f) == 0 {
|
|
| 59 |
+ format := opts.format |
|
| 60 |
+ if len(format) == 0 {
|
|
| 62 | 61 |
if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
|
| 63 |
- f = dockerCli.ConfigFile().NetworksFormat |
|
| 62 |
+ format = dockerCli.ConfigFile().NetworksFormat |
|
| 64 | 63 |
} else {
|
| 65 |
- f = "table" |
|
| 64 |
+ format = "table" |
|
| 66 | 65 |
} |
| 67 | 66 |
} |
| 68 | 67 |
|
| 69 | 68 |
sort.Sort(byNetworkName(networkResources)) |
| 70 | 69 |
|
| 71 |
- networksCtx := formatter.NetworkContext{
|
|
| 72 |
- Context: formatter.Context{
|
|
| 73 |
- Output: dockerCli.Out(), |
|
| 74 |
- Format: f, |
|
| 75 |
- Quiet: opts.quiet, |
|
| 76 |
- Trunc: !opts.noTrunc, |
|
| 77 |
- }, |
|
| 78 |
- Networks: networkResources, |
|
| 70 |
+ networksCtx := formatter.Context{
|
|
| 71 |
+ Output: dockerCli.Out(), |
|
| 72 |
+ Format: formatter.NewNetworkFormat(format, opts.quiet), |
|
| 73 |
+ Trunc: !opts.noTrunc, |
|
| 79 | 74 |
} |
| 80 |
- |
|
| 81 |
- networksCtx.Write() |
|
| 82 |
- |
|
| 83 |
- return nil |
|
| 75 |
+ return formatter.NetworkWrite(networksCtx, networkResources) |
|
| 84 | 76 |
} |
| ... | ... |
@@ -56,29 +56,22 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 56 | 56 |
return err |
| 57 | 57 |
} |
| 58 | 58 |
|
| 59 |
- f := opts.format |
|
| 60 |
- if len(f) == 0 {
|
|
| 59 |
+ format := opts.format |
|
| 60 |
+ if len(format) == 0 {
|
|
| 61 | 61 |
if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet {
|
| 62 |
- f = dockerCli.ConfigFile().VolumesFormat |
|
| 62 |
+ format = dockerCli.ConfigFile().VolumesFormat |
|
| 63 | 63 |
} else {
|
| 64 |
- f = "table" |
|
| 64 |
+ format = "table" |
|
| 65 | 65 |
} |
| 66 | 66 |
} |
| 67 | 67 |
|
| 68 | 68 |
sort.Sort(byVolumeName(volumes.Volumes)) |
| 69 | 69 |
|
| 70 |
- volumeCtx := formatter.VolumeContext{
|
|
| 71 |
- Context: formatter.Context{
|
|
| 72 |
- Output: dockerCli.Out(), |
|
| 73 |
- Format: f, |
|
| 74 |
- Quiet: opts.quiet, |
|
| 75 |
- }, |
|
| 76 |
- Volumes: volumes.Volumes, |
|
| 70 |
+ volumeCtx := formatter.Context{
|
|
| 71 |
+ Output: dockerCli.Out(), |
|
| 72 |
+ Format: formatter.NewVolumeFormat(format, opts.quiet), |
|
| 77 | 73 |
} |
| 78 |
- |
|
| 79 |
- volumeCtx.Write() |
|
| 80 |
- |
|
| 81 |
- return nil |
|
| 74 |
+ return formatter.VolumeWrite(volumeCtx, volumes.Volumes) |
|
| 82 | 75 |
} |
| 83 | 76 |
|
| 84 | 77 |
var listDescription = ` |