diff --git a/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index 1ba9c5874c..80204e43a6 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/api/swagger.yaml @@ -1048,6 +1048,10 @@ definitions: type: "string" description: "Mount path of the volume on the host." x-nullable: false + CreatedAt: + type: "string" + format: "dateTime" + description: "Time volume was created." Status: type: "object" description: | @@ -1101,6 +1105,7 @@ definitions: com.example.some-label: "some-value" com.example.some-other-label: "some-other-value" Scope: "local" + CreatedAt: "2016-06-07T20:31:11.853781916Z" Network: type: "object" diff --git a/components/engine/api/types/volume.go b/components/engine/api/types/volume.go index da4f8ebd9c..a69b0cfb17 100644 --- a/components/engine/api/types/volume.go +++ b/components/engine/api/types/volume.go @@ -7,6 +7,9 @@ package types // swagger:model Volume type Volume struct { + // Time volume was created. + CreatedAt string `json:"CreatedAt,omitempty"` + // Name of the volume driver used by the volume. // Required: true Driver string `json:"Driver"` diff --git a/components/engine/daemon/volumes.go b/components/engine/daemon/volumes.go index 26204338eb..6f24f05910 100644 --- a/components/engine/daemon/volumes.go +++ b/components/engine/daemon/volumes.go @@ -7,6 +7,7 @@ import ( "path/filepath" "reflect" "strings" + "time" "github.com/Sirupsen/logrus" dockererrors "github.com/docker/docker/api/errors" @@ -28,9 +29,11 @@ type mounts []container.Mount // volumeToAPIType converts a volume.Volume to the type used by the Engine API func volumeToAPIType(v volume.Volume) *types.Volume { + createdAt, _ := v.CreatedAt() tv := &types.Volume{ - Name: v.Name(), - Driver: v.DriverName(), + Name: v.Name(), + Driver: v.DriverName(), + CreatedAt: createdAt.Format(time.RFC3339), } if v, ok := v.(volume.DetailedVolume); ok { tv.Labels = v.Labels() diff --git a/components/engine/integration-cli/docker_api_volumes_test.go b/components/engine/integration-cli/docker_api_volumes_test.go index 3cc03dfb36..f354856d32 100644 --- a/components/engine/integration-cli/docker_api_volumes_test.go +++ b/components/engine/integration-cli/docker_api_volumes_test.go @@ -2,8 +2,11 @@ package main import ( "encoding/json" + "fmt" "net/http" "path/filepath" + "strings" + "time" "github.com/docker/docker/api/types" volumetypes "github.com/docker/docker/api/types/volume" @@ -69,6 +72,8 @@ func (s *DockerSuite) TestVolumesAPIInspect(c *check.C) { config := volumetypes.VolumesCreateBody{ Name: "test", } + // sampling current time minus a minute so to now have false positive in case of delays + now := time.Now().Truncate(time.Minute) status, b, err := request.SockRequest("POST", "/volumes/create", config, daemonHost()) c.Assert(err, check.IsNil) c.Assert(status, check.Equals, http.StatusCreated, check.Commentf(string(b))) @@ -87,4 +92,12 @@ func (s *DockerSuite) TestVolumesAPIInspect(c *check.C) { c.Assert(status, checker.Equals, http.StatusOK, check.Commentf(string(b))) c.Assert(json.Unmarshal(b, &vol), checker.IsNil) c.Assert(vol.Name, checker.Equals, config.Name) + + // comparing CreatedAt field time for the new volume to now. Removing a minute from both to avoid false positive + testCreatedAt, err := time.Parse(time.RFC3339, strings.TrimSpace(vol.CreatedAt)) + c.Assert(err, check.IsNil) + testCreatedAt = testCreatedAt.Truncate(time.Minute) + if !testCreatedAt.Equal(now) { + c.Assert(fmt.Errorf("Time Volume is CreatedAt not equal to current time"), check.NotNil) + } } diff --git a/components/engine/volume/drivers/adapter.go b/components/engine/volume/drivers/adapter.go index 62ef7dfe60..0ec68dad5f 100644 --- a/components/engine/volume/drivers/adapter.go +++ b/components/engine/volume/drivers/adapter.go @@ -4,6 +4,7 @@ import ( "errors" "path/filepath" "strings" + "time" "github.com/Sirupsen/logrus" "github.com/docker/docker/volume" @@ -82,6 +83,7 @@ func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) { name: v.Name, driverName: a.Name(), eMount: v.Mountpoint, + createdAt: v.CreatedAt, status: v.Status, baseHostPath: a.baseHostPath, }, nil @@ -124,13 +126,15 @@ type volumeAdapter struct { name string baseHostPath string driverName string - eMount string // ephemeral host volume path + eMount string // ephemeral host volume path + createdAt time.Time // time the directory was created status map[string]interface{} } type proxyVolume struct { Name string Mountpoint string + CreatedAt time.Time Status map[string]interface{} } @@ -168,6 +172,9 @@ func (a *volumeAdapter) Unmount(id string) error { return err } +func (a *volumeAdapter) CreatedAt() (time.Time, error) { + return a.createdAt, nil +} func (a *volumeAdapter) Status() map[string]interface{} { out := make(map[string]interface{}, len(a.status)) for k, v := range a.status { diff --git a/components/engine/volume/local/local_unix.go b/components/engine/volume/local/local_unix.go index fb08862cef..5bba5b7068 100644 --- a/components/engine/volume/local/local_unix.go +++ b/components/engine/volume/local/local_unix.go @@ -8,8 +8,11 @@ package local import ( "fmt" "net" + "os" "path/filepath" "strings" + "syscall" + "time" "github.com/pkg/errors" @@ -85,3 +88,12 @@ func (v *localVolume) mount() error { err := mount.Mount(v.opts.MountDevice, v.path, v.opts.MountType, mountOpts) return errors.Wrapf(err, "error while mounting volume with options: %s", v.opts) } + +func (v *localVolume) CreatedAt() (time.Time, error) { + fileInfo, err := os.Stat(v.path) + if err != nil { + return time.Time{}, err + } + sec, nsec := fileInfo.Sys().(*syscall.Stat_t).Ctim.Unix() + return time.Unix(sec, nsec), nil +} diff --git a/components/engine/volume/local/local_windows.go b/components/engine/volume/local/local_windows.go index 1bdb368a0f..6f5d2223ae 100644 --- a/components/engine/volume/local/local_windows.go +++ b/components/engine/volume/local/local_windows.go @@ -5,8 +5,11 @@ package local import ( "fmt" + "os" "path/filepath" "strings" + "syscall" + "time" ) type optsConfig struct{} @@ -32,3 +35,12 @@ func setOpts(v *localVolume, opts map[string]string) error { func (v *localVolume) mount() error { return nil } + +func (v *localVolume) CreatedAt() (time.Time, error) { + fileInfo, err := os.Stat(v.path) + if err != nil { + return time.Time{}, err + } + ft := fileInfo.Sys().(*syscall.Win32FileAttributeData).CreationTime + return time.Unix(0, ft.Nanoseconds()), nil +} diff --git a/components/engine/volume/testutils/testutils.go b/components/engine/volume/testutils/testutils.go index 2dbac02fdb..d54ba44be6 100644 --- a/components/engine/volume/testutils/testutils.go +++ b/components/engine/volume/testutils/testutils.go @@ -2,6 +2,7 @@ package testutils import ( "fmt" + "time" "github.com/docker/docker/volume" ) @@ -27,6 +28,9 @@ func (NoopVolume) Unmount(_ string) error { return nil } // Status proivdes low-level details about the volume func (NoopVolume) Status() map[string]interface{} { return nil } +// CreatedAt provides the time the volume (directory) was created at +func (NoopVolume) CreatedAt() (time.Time, error) { return time.Now(), nil } + // FakeVolume is a fake volume with a random name type FakeVolume struct { name string @@ -56,6 +60,9 @@ func (FakeVolume) Unmount(_ string) error { return nil } // Status proivdes low-level details about the volume func (FakeVolume) Status() map[string]interface{} { return nil } +// CreatedAt provides the time the volume (directory) was created at +func (FakeVolume) CreatedAt() (time.Time, error) { return time.Now(), nil } + // FakeDriver is a driver that generates fake volumes type FakeDriver struct { name string diff --git a/components/engine/volume/volume.go b/components/engine/volume/volume.go index 5135605281..ef362370e5 100644 --- a/components/engine/volume/volume.go +++ b/components/engine/volume/volume.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" "syscall" + "time" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/pkg/idtools" @@ -64,6 +65,8 @@ type Volume interface { Mount(id string) (string, error) // Unmount unmounts the volume when it is no longer in use. Unmount(id string) error + // CreatedAt returns Volume Creation time + CreatedAt() (time.Time, error) // Status returns low-level status information about a volume Status() map[string]interface{} }