From c5ea0ae3b24a3b34f0bffa40e0509913e9b35000 Mon Sep 17 00:00:00 2001 From: uditDewan Date: Sun, 2 Aug 2026 17:47:03 -0400 Subject: [PATCH] cli/command/formatter: add CreatedAt to volume list output Containers, images and networks all expose a "CreatedAt" placeholder in their list output, but volumes did not, even though the API returns a CreatedAt field for them. This made the creation time unavailable to `docker volume ls --format`, including `--format json`. Add a CreatedAt method to the volume context. The daemon returns the value as an RFC3339 string, which is parsed and rendered with the same formatting used by the other list commands, so that a template such as `{{.CreatedAt}}` produces consistent output across commands. The field is optional in the API, so an empty string is returned when the daemon does not report a creation time; a value that cannot be parsed is passed through as-is. Fixes #3871 Signed-off-by: uditDewan --- cli/command/formatter/volume.go | 17 +++++++++++++++++ cli/command/formatter/volume_test.go | 15 ++++++++++++--- docs/reference/commandline/volume_ls.md | 1 + 3 files changed, 30 insertions(+), 3 deletions(-) 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