diff --git a/api/swagger.yaml b/api/swagger.yaml index b4c1bd8b15..fd0c4c1715 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -949,6 +949,12 @@ definitions: type: "string" BaseLayer: type: "string" + Metadata: + type: "object" + properties: + LastTagTime: + type: "string" + format: "dateTime" ImageSummary: type: "object" diff --git a/api/types/types.go b/api/types/types.go index 2473497c89..c96df27332 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -45,6 +45,12 @@ type ImageInspect struct { VirtualSize int64 GraphDriver GraphDriverData RootFS RootFS + Metadata ImageMetadata +} + +// ImageMetadata contains engine-local data about the image +type ImageMetadata struct { + LastTagTime time.Time `json:",omitempty"` } // Container contains response of Engine API: diff --git a/daemon/image_inspect.go b/daemon/image_inspect.go index 672a4cc012..3baf265dab 100644 --- a/daemon/image_inspect.go +++ b/daemon/image_inspect.go @@ -61,6 +61,11 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) { comment = img.History[len(img.History)-1].Comment } + lastUpdated, err := daemon.stores[platform].imageStore.GetLastUpdated(img.ID()) + if err != nil { + return nil, err + } + imageInspect := &types.ImageInspect{ ID: img.ID().String(), RepoTags: repoTags, @@ -79,6 +84,9 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) { Size: size, VirtualSize: size, // TODO: field unused, deprecate RootFS: rootFSToAPIType(img.RootFS), + Metadata: types.ImageMetadata{ + LastTagTime: lastUpdated, + }, } imageInspect.GraphDriver.Name = daemon.GraphDriverName(platform) diff --git a/daemon/image_tag.go b/daemon/image_tag.go index 199ca9ad84..5f28daed0a 100644 --- a/daemon/image_tag.go +++ b/daemon/image_tag.go @@ -32,6 +32,9 @@ func (daemon *Daemon) TagImageWithReference(imageID image.ID, platform string, n return err } + if err := daemon.stores[platform].imageStore.SetLastUpdated(imageID); err != nil { + return err + } daemon.LogImageEvent(imageID.String(), reference.FamiliarString(newTag), "tag") return nil } diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 1d19d56dce..aa089ed0a1 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -25,6 +25,7 @@ keywords: "API, Docker, rcli, REST, documentation" * `POST /session` is a new endpoint that can be used for running interactive long-running protocols between client and the daemon. This endpoint is experimental and only available if the daemon is started with experimental features enabled. +* `GET /images/(name)/get` now includes an `ImageMetadata` field which contains image metadata that is local to the engine and not part of the image config. ## v1.30 API changes diff --git a/image/store.go b/image/store.go index 541b07bba1..b215d977f8 100644 --- a/image/store.go +++ b/image/store.go @@ -6,6 +6,7 @@ import ( "runtime" "strings" "sync" + "time" "github.com/Sirupsen/logrus" "github.com/docker/distribution/digestset" @@ -23,6 +24,8 @@ type Store interface { Search(partialID string) (ID, error) SetParent(id ID, parent ID) error GetParent(id ID) (ID, error) + SetLastUpdated(id ID) error + GetLastUpdated(id ID) (time.Time, error) Children(id ID) []ID Map() map[ID]*Image Heads() map[ID]*Image @@ -259,6 +262,22 @@ func (is *store) GetParent(id ID) (ID, error) { return ID(d), nil // todo: validate? } +// SetLastUpdated time for the image ID to the current time +func (is *store) SetLastUpdated(id ID) error { + lastUpdated := []byte(time.Now().Format(time.RFC3339Nano)) + return is.fs.SetMetadata(id.Digest(), "lastUpdated", lastUpdated) +} + +// GetLastUpdated time for the image ID +func (is *store) GetLastUpdated(id ID) (time.Time, error) { + bytes, err := is.fs.GetMetadata(id.Digest(), "lastUpdated") + if err != nil || len(bytes) == 0 { + // No lastUpdated time + return time.Time{}, nil + } + return time.Parse(time.RFC3339Nano, string(bytes)) +} + func (is *store) Children(id ID) []ID { is.RLock() defer is.RUnlock() diff --git a/image/store_test.go b/image/store_test.go index 13318cf221..fc6d461d99 100644 --- a/image/store_test.go +++ b/image/store_test.go @@ -149,6 +149,24 @@ func defaultImageStore(t *testing.T) (Store, func()) { return store, cleanup } +func TestGetAndSetLastUpdated(t *testing.T) { + store, cleanup := defaultImageStore(t) + defer cleanup() + + id, err := store.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`)) + assert.NoError(t, err) + + updated, err := store.GetLastUpdated(id) + assert.NoError(t, err) + assert.Equal(t, updated.IsZero(), true) + + assert.NoError(t, store.SetLastUpdated(id)) + + updated, err = store.GetLastUpdated(id) + assert.NoError(t, err) + assert.Equal(t, updated.IsZero(), false) +} + type mockLayerGetReleaser struct{} func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) {