diff --git a/cli/command/formatter/volume.go b/cli/command/formatter/volume.go index 75b6ad007f94..16e46ae70a20 100644 --- a/cli/command/formatter/volume.go +++ b/cli/command/formatter/volume.go @@ -8,6 +8,7 @@ import ( "slices" "strconv" "strings" + "time" "github.com/docker/go-units" "github.com/moby/moby/api/types/volume" @@ -75,6 +76,7 @@ func newVolumeContext() *volumeContext { "Links": linksHeader, "Size": SizeHeader, "Status": statusHeader, + "CreatedAt": CreatedAtHeader, } return &volumeCtx } @@ -149,6 +151,21 @@ func (c *volumeContext) Availability() string { return string(c.v.ClusterVolume.Spec.Availability) } +// CreatedAt returns the time the volume was created, formatted the same way as +// the "CreatedAt" field of other list commands (e.g. "docker ps", "docker network ls"). +// It returns an empty string if the daemon did not report a creation time, and +// the raw value as reported by the daemon if it cannot be parsed. +func (c *volumeContext) CreatedAt() string { + if c.v.CreatedAt == "" { + return "" + } + createdAt, err := time.Parse(time.RFC3339Nano, c.v.CreatedAt) + if err != nil { + return c.v.CreatedAt + } + return createdAt.String() +} + func (c *volumeContext) Status() string { if c.v.ClusterVolume == nil { return "N/A" diff --git a/cli/command/formatter/volume_test.go b/cli/command/formatter/volume_test.go index b85a75c387c8..0f9af5ab2a9e 100644 --- a/cli/command/formatter/volume_test.go +++ b/cli/command/formatter/volume_test.go @@ -43,6 +43,15 @@ func TestVolumeContext(t *testing.T) { {volumeContext{ v: volume.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, }, "label1=value1,label2=value2", ctx.Labels}, + {volumeContext{ + v: volume.Volume{}, + }, "", ctx.CreatedAt}, + {volumeContext{ + v: volume.Volume{CreatedAt: "2016-06-07T20:31:11.853781916Z"}, + }, "2016-06-07 20:31:11.853781916 +0000 UTC", ctx.CreatedAt}, + {volumeContext{ + v: volume.Volume{CreatedAt: "not-a-timestamp"}, + }, "not-a-timestamp", ctx.CreatedAt}, } for _, c := range cases { @@ -143,12 +152,12 @@ foobar_bar func TestVolumeContextWriteJSON(t *testing.T) { volumes := []volume.Volume{ - {Driver: "foo", Name: "foobar_baz"}, + {Driver: "foo", Name: "foobar_baz", CreatedAt: "2016-06-07T20:31:11.853781916Z"}, {Driver: "bar", Name: "foobar_bar"}, } expectedJSONs := []map[string]any{ - {"Availability": "N/A", "Driver": "foo", "Group": "N/A", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A", "Status": "N/A"}, - {"Availability": "N/A", "Driver": "bar", "Group": "N/A", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A", "Status": "N/A"}, + {"Availability": "N/A", "CreatedAt": "2016-06-07 20:31:11.853781916 +0000 UTC", "Driver": "foo", "Group": "N/A", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A", "Status": "N/A"}, + {"Availability": "N/A", "CreatedAt": "", "Driver": "bar", "Group": "N/A", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A", "Status": "N/A"}, } out := bytes.NewBufferString("") err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes) diff --git a/docs/reference/commandline/volume_ls.md b/docs/reference/commandline/volume_ls.md index 8420080a7347..6b4d7e2dc1c8 100644 --- a/docs/reference/commandline/volume_ls.md +++ b/docs/reference/commandline/volume_ls.md @@ -160,6 +160,7 @@ Valid placeholders for the Go template are listed below: | `.Mountpoint` | The mount point of the volume on the host | | `.Labels` | All labels assigned to the volume | | `.Label` | Value of a specific label for this volume. For example `{{.Label "project.version"}}` | +| `.CreatedAt` | Time when the volume was created | When using the `--format` option, the `volume ls` command will either output the data exactly as the template declares or, when using the