1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Add volume --format flag to ls

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Vincent Demeester 2016-08-04 14:59:55 +02:00
parent a8aaafc4a3
commit a488ad1a09
No known key found for this signature in database
GPG key ID: 083CC6FD6EB699A3
7 changed files with 402 additions and 17 deletions

View file

@ -136,6 +136,12 @@ func (cli *DockerCli) NetworksFormat() string {
return cli.configFile.NetworksFormat
}
// VolumesFormat returns the format string specified in the configuration.
// String contains columns and format specification, for example {{ID}}\t{{Name}}
func (cli *DockerCli) VolumesFormat() string {
return cli.configFile.VolumesFormat
}
func (cli *DockerCli) setRawTerminal() error {
if os.Getenv("NORAW") == "" {
if cli.isTerminalIn {

View file

@ -0,0 +1,114 @@
package formatter
import (
"bytes"
"fmt"
"strings"
"github.com/docker/engine-api/types"
)
const (
defaultVolumeQuietFormat = "{{.Name}}"
defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
mountpointHeader = "MOUNTPOINT"
// Status header ?
)
// VolumeContext contains volume specific information required by the formatter,
// encapsulate a Context struct.
type VolumeContext struct {
Context
// Volumes
Volumes []*types.Volume
}
func (ctx VolumeContext) Write() {
switch ctx.Format {
case tableFormatKey:
if ctx.Quiet {
ctx.Format = defaultVolumeQuietFormat
} else {
ctx.Format = defaultVolumeTableFormat
}
case rawFormatKey:
if ctx.Quiet {
ctx.Format = `name: {{.Name}}`
} else {
ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
}
}
ctx.buffer = bytes.NewBufferString("")
ctx.preformat()
tmpl, err := ctx.parseFormat()
if err != nil {
return
}
for _, volume := range ctx.Volumes {
volumeCtx := &volumeContext{
v: volume,
}
err = ctx.contextFormat(tmpl, volumeCtx)
if err != nil {
return
}
}
ctx.postformat(tmpl, &networkContext{})
}
type volumeContext struct {
baseSubContext
v *types.Volume
}
func (c *volumeContext) Name() string {
c.addHeader(nameHeader)
return c.v.Name
}
func (c *volumeContext) Driver() string {
c.addHeader(driverHeader)
return c.v.Driver
}
func (c *volumeContext) Scope() string {
c.addHeader(scopeHeader)
return c.v.Scope
}
func (c *volumeContext) Mountpoint() string {
c.addHeader(mountpointHeader)
return c.v.Mountpoint
}
func (c *volumeContext) Labels() string {
c.addHeader(labelsHeader)
if c.v.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.v.Labels {
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
func (c *volumeContext) Label(name string) string {
n := strings.Split(name, ".")
r := strings.NewReplacer("-", " ", "_", " ")
h := r.Replace(n[len(n)-1])
c.addHeader(h)
if c.v.Labels == nil {
return ""
}
return c.v.Labels[name]
}

View file

@ -0,0 +1,183 @@
package formatter
import (
"bytes"
"strings"
"testing"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/engine-api/types"
)
func TestVolumeContext(t *testing.T) {
volumeName := stringid.GenerateRandomID()
var ctx volumeContext
cases := []struct {
volumeCtx volumeContext
expValue string
expHeader string
call func() string
}{
{volumeContext{
v: &types.Volume{Name: volumeName},
}, volumeName, nameHeader, ctx.Name},
{volumeContext{
v: &types.Volume{Driver: "driver_name"},
}, "driver_name", driverHeader, ctx.Driver},
{volumeContext{
v: &types.Volume{Scope: "local"},
}, "local", scopeHeader, ctx.Scope},
{volumeContext{
v: &types.Volume{Mountpoint: "mountpoint"},
}, "mountpoint", mountpointHeader, ctx.Mountpoint},
{volumeContext{
v: &types.Volume{},
}, "", labelsHeader, ctx.Labels},
{volumeContext{
v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
}
for _, c := range cases {
ctx = c.volumeCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
h := ctx.fullHeader()
if h != c.expHeader {
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
}
}
}
func TestVolumeContextWrite(t *testing.T) {
contexts := []struct {
context VolumeContext
expected string
}{
// Errors
{
VolumeContext{
Context: Context{
Format: "{{InvalidFunction}}",
},
},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
VolumeContext{
Context: Context{
Format: "{{nil}}",
},
},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
VolumeContext{
Context: Context{
Format: "table",
},
},
`DRIVER NAME
foo foobar_baz
bar foobar_bar
`,
},
{
VolumeContext{
Context: Context{
Format: "table",
Quiet: true,
},
},
`foobar_baz
foobar_bar
`,
},
{
VolumeContext{
Context: Context{
Format: "table {{.Name}}",
},
},
`NAME
foobar_baz
foobar_bar
`,
},
{
VolumeContext{
Context: Context{
Format: "table {{.Name}}",
Quiet: true,
},
},
`NAME
foobar_baz
foobar_bar
`,
},
// Raw Format
{
VolumeContext{
Context: Context{
Format: "raw",
},
}, `name: foobar_baz
driver: foo
name: foobar_bar
driver: bar
`,
},
{
VolumeContext{
Context: Context{
Format: "raw",
Quiet: true,
},
},
`name: foobar_baz
name: foobar_bar
`,
},
// Custom Format
{
VolumeContext{
Context: Context{
Format: "{{.Name}}",
},
},
`foobar_baz
foobar_bar
`,
},
}
for _, context := range contexts {
volumes := []*types.Volume{
{Name: "foobar_baz", Driver: "foo"},
{Name: "foobar_bar", Driver: "bar"},
}
out := bytes.NewBufferString("")
context.context.Output = out
context.context.Volumes = volumes
context.context.Write()
actual := out.String()
if actual != context.expected {
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
}
// Clean buffer
out.Reset()
}
}

View file

@ -1,13 +1,12 @@
package volume
import (
"fmt"
"sort"
"text/tabwriter"
"golang.org/x/net/context"
"github.com/docker/docker/api/client"
"github.com/docker/docker/api/client/formatter"
"github.com/docker/docker/cli"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
@ -24,6 +23,7 @@ func (r byVolumeName) Less(i, j int) bool {
type listOptions struct {
quiet bool
format string
filter []string
}
@ -43,6 +43,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names")
flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template")
flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')")
return cmd
@ -65,24 +66,28 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
return err
}
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
if !opts.quiet {
for _, warn := range volumes.Warnings {
fmt.Fprintln(dockerCli.Err(), warn)
f := opts.format
if len(f) == 0 {
if len(dockerCli.VolumesFormat()) > 0 && !opts.quiet {
f = dockerCli.VolumesFormat()
} else {
f = "table"
}
fmt.Fprintf(w, "DRIVER \tVOLUME NAME")
fmt.Fprintf(w, "\n")
}
sort.Sort(byVolumeName(volumes.Volumes))
for _, vol := range volumes.Volumes {
if opts.quiet {
fmt.Fprintln(w, vol.Name)
continue
}
fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name)
volumeCtx := formatter.VolumeContext{
Context: formatter.Context{
Output: dockerCli.Out(),
Format: f,
Quiet: opts.quiet,
},
Volumes: volumes.Volumes,
}
w.Flush()
volumeCtx.Write()
return nil
}

View file

@ -27,6 +27,7 @@ type ConfigFile struct {
PsFormat string `json:"psFormat,omitempty"`
ImagesFormat string `json:"imagesFormat,omitempty"`
NetworksFormat string `json:"networksFormat,omitempty"`
VolumesFormat string `json:"volumesFormat,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
Filename string `json:"-"` // Note: for internal use only

View file

@ -23,6 +23,7 @@ Options:
- dangling=<boolean> a volume if referenced or not
- driver=<string> a volume's driver name
- name=<string> a volume's name
--format string Pretty-print volumes using a Go template
--help Print usage
-q, --quiet Only display volume names
```
@ -82,6 +83,36 @@ The following filter matches all volumes with a name containing the `rose` strin
DRIVER VOLUME NAME
local rosemary
## Formatting
The formatting options (`--format`) pretty-prints volumes output
using a Go template.
Valid placeholders for the Go template are listed below:
Placeholder | Description
--------------|------------------------------------------------------------------------------------------
`.Name` | Network name
`.Driver` | Network driver
`.Scope` | Network scope (local, global)
`.Mountpoint` | Whether the network is internal or not.
`.Labels` | All labels assigned to the volume.
`.Label` | Value of a specific label for this volume. For example `{{.Label "project.version"}}`
When using the `--format` option, the `volume ls` command will either
output the data exactly as the template declares or, when using the
`table` directive, includes column headers as well.
The following example uses a template without headers and outputs the
`Name` and `Driver` entries separated by a colon for all volumes:
```bash
$ docker volume ls --format "{{.Name}}: {{.Driver}}"
vol1: local
vol2: local
vol3: local
```
## Related information
* [volume create](volume_create.md)

View file

@ -1,7 +1,10 @@
package main
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/docker/docker/pkg/integration/checker"
@ -65,20 +68,62 @@ func (s *DockerSuite) TestVolumeCliInspectMulti(c *check.C) {
func (s *DockerSuite) TestVolumeCliLs(c *check.C) {
prefix, _ := getPrefixAndSlashFromDaemonPlatform()
out, _ := dockerCmd(c, "volume", "create", "--name", "aaa")
dockerCmd(c, "volume", "create", "--name", "aaa")
dockerCmd(c, "volume", "create", "--name", "test")
dockerCmd(c, "volume", "create", "--name", "soo")
dockerCmd(c, "run", "-v", "soo:"+prefix+"/foo", "busybox", "ls", "/")
out, _ = dockerCmd(c, "volume", "ls")
out, _ := dockerCmd(c, "volume", "ls")
outArr := strings.Split(strings.TrimSpace(out), "\n")
c.Assert(len(outArr), check.Equals, 4, check.Commentf("\n%s", out))
assertVolList(c, out, []string{"aaa", "soo", "test"})
}
func (s *DockerSuite) TestVolumeLsFormat(c *check.C) {
dockerCmd(c, "volume", "create", "--name", "aaa")
dockerCmd(c, "volume", "create", "--name", "test")
dockerCmd(c, "volume", "create", "--name", "soo")
out, _ := dockerCmd(c, "volume", "ls", "--format", "{{.Name}}")
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
expected := []string{"aaa", "soo", "test"}
var names []string
for _, l := range lines {
names = append(names, l)
}
c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
}
func (s *DockerSuite) TestVolumeLsFormatDefaultFormat(c *check.C) {
dockerCmd(c, "volume", "create", "--name", "aaa")
dockerCmd(c, "volume", "create", "--name", "test")
dockerCmd(c, "volume", "create", "--name", "soo")
config := `{
"volumesFormat": "{{ .Name }} default"
}`
d, err := ioutil.TempDir("", "integration-cli-")
c.Assert(err, checker.IsNil)
defer os.RemoveAll(d)
err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644)
c.Assert(err, checker.IsNil)
out, _ := dockerCmd(c, "--config", d, "volume", "ls")
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
expected := []string{"aaa default", "soo default", "test default"}
var names []string
for _, l := range lines {
names = append(names, l)
}
c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
}
// assertVolList checks volume retrieved with ls command
// equals to expected volume list
// note: out should be `volume ls [option]` result