Signed-off-by: Daniel Nephin <dnephin@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -45,6 +45,12 @@ type ImageInspect struct {
|
| 45 | 45 |
VirtualSize int64 |
| 46 | 46 |
GraphDriver GraphDriverData |
| 47 | 47 |
RootFS RootFS |
| 48 |
+ Metadata ImageMetadata |
|
| 49 |
+} |
|
| 50 |
+ |
|
| 51 |
+// ImageMetadata contains engine-local data about the image |
|
| 52 |
+type ImageMetadata struct {
|
|
| 53 |
+ LastTagTime time.Time `json:",omitempty"` |
|
| 48 | 54 |
} |
| 49 | 55 |
|
| 50 | 56 |
// Container contains response of Engine API: |
| ... | ... |
@@ -61,6 +61,11 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) {
|
| 61 | 61 |
comment = img.History[len(img.History)-1].Comment |
| 62 | 62 |
} |
| 63 | 63 |
|
| 64 |
+ lastUpdated, err := daemon.stores[platform].imageStore.GetLastUpdated(img.ID()) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ return nil, err |
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 64 | 69 |
imageInspect := &types.ImageInspect{
|
| 65 | 70 |
ID: img.ID().String(), |
| 66 | 71 |
RepoTags: repoTags, |
| ... | ... |
@@ -79,6 +84,9 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) {
|
| 79 | 79 |
Size: size, |
| 80 | 80 |
VirtualSize: size, // TODO: field unused, deprecate |
| 81 | 81 |
RootFS: rootFSToAPIType(img.RootFS), |
| 82 |
+ Metadata: types.ImageMetadata{
|
|
| 83 |
+ LastTagTime: lastUpdated, |
|
| 84 |
+ }, |
|
| 82 | 85 |
} |
| 83 | 86 |
|
| 84 | 87 |
imageInspect.GraphDriver.Name = daemon.GraphDriverName(platform) |
| ... | ... |
@@ -32,6 +32,9 @@ func (daemon *Daemon) TagImageWithReference(imageID image.ID, platform string, n |
| 32 | 32 |
return err |
| 33 | 33 |
} |
| 34 | 34 |
|
| 35 |
+ if err := daemon.stores[platform].imageStore.SetLastUpdated(imageID); err != nil {
|
|
| 36 |
+ return err |
|
| 37 |
+ } |
|
| 35 | 38 |
daemon.LogImageEvent(imageID.String(), reference.FamiliarString(newTag), "tag") |
| 36 | 39 |
return nil |
| 37 | 40 |
} |
| ... | ... |
@@ -25,6 +25,7 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 25 | 25 |
* `POST /session` is a new endpoint that can be used for running interactive long-running protocols between client and |
| 26 | 26 |
the daemon. This endpoint is experimental and only available if the daemon is started with experimental features |
| 27 | 27 |
enabled. |
| 28 |
+* `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. |
|
| 28 | 29 |
|
| 29 | 30 |
## v1.30 API changes |
| 30 | 31 |
|
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"runtime" |
| 7 | 7 |
"strings" |
| 8 | 8 |
"sync" |
| 9 |
+ "time" |
|
| 9 | 10 |
|
| 10 | 11 |
"github.com/Sirupsen/logrus" |
| 11 | 12 |
"github.com/docker/distribution/digestset" |
| ... | ... |
@@ -23,6 +24,8 @@ type Store interface {
|
| 23 | 23 |
Search(partialID string) (ID, error) |
| 24 | 24 |
SetParent(id ID, parent ID) error |
| 25 | 25 |
GetParent(id ID) (ID, error) |
| 26 |
+ SetLastUpdated(id ID) error |
|
| 27 |
+ GetLastUpdated(id ID) (time.Time, error) |
|
| 26 | 28 |
Children(id ID) []ID |
| 27 | 29 |
Map() map[ID]*Image |
| 28 | 30 |
Heads() map[ID]*Image |
| ... | ... |
@@ -259,6 +262,22 @@ func (is *store) GetParent(id ID) (ID, error) {
|
| 259 | 259 |
return ID(d), nil // todo: validate? |
| 260 | 260 |
} |
| 261 | 261 |
|
| 262 |
+// SetLastUpdated time for the image ID to the current time |
|
| 263 |
+func (is *store) SetLastUpdated(id ID) error {
|
|
| 264 |
+ lastUpdated := []byte(time.Now().Format(time.RFC3339Nano)) |
|
| 265 |
+ return is.fs.SetMetadata(id.Digest(), "lastUpdated", lastUpdated) |
|
| 266 |
+} |
|
| 267 |
+ |
|
| 268 |
+// GetLastUpdated time for the image ID |
|
| 269 |
+func (is *store) GetLastUpdated(id ID) (time.Time, error) {
|
|
| 270 |
+ bytes, err := is.fs.GetMetadata(id.Digest(), "lastUpdated") |
|
| 271 |
+ if err != nil || len(bytes) == 0 {
|
|
| 272 |
+ // No lastUpdated time |
|
| 273 |
+ return time.Time{}, nil
|
|
| 274 |
+ } |
|
| 275 |
+ return time.Parse(time.RFC3339Nano, string(bytes)) |
|
| 276 |
+} |
|
| 277 |
+ |
|
| 262 | 278 |
func (is *store) Children(id ID) []ID {
|
| 263 | 279 |
is.RLock() |
| 264 | 280 |
defer is.RUnlock() |
| ... | ... |
@@ -149,6 +149,24 @@ func defaultImageStore(t *testing.T) (Store, func()) {
|
| 149 | 149 |
return store, cleanup |
| 150 | 150 |
} |
| 151 | 151 |
|
| 152 |
+func TestGetAndSetLastUpdated(t *testing.T) {
|
|
| 153 |
+ store, cleanup := defaultImageStore(t) |
|
| 154 |
+ defer cleanup() |
|
| 155 |
+ |
|
| 156 |
+ id, err := store.Create([]byte(`{"comment": "abc1", "rootfs": {"type": "layers"}}`))
|
|
| 157 |
+ assert.NoError(t, err) |
|
| 158 |
+ |
|
| 159 |
+ updated, err := store.GetLastUpdated(id) |
|
| 160 |
+ assert.NoError(t, err) |
|
| 161 |
+ assert.Equal(t, updated.IsZero(), true) |
|
| 162 |
+ |
|
| 163 |
+ assert.NoError(t, store.SetLastUpdated(id)) |
|
| 164 |
+ |
|
| 165 |
+ updated, err = store.GetLastUpdated(id) |
|
| 166 |
+ assert.NoError(t, err) |
|
| 167 |
+ assert.Equal(t, updated.IsZero(), false) |
|
| 168 |
+} |
|
| 169 |
+ |
|
| 152 | 170 |
type mockLayerGetReleaser struct{}
|
| 153 | 171 |
|
| 154 | 172 |
func (ls *mockLayerGetReleaser) Get(layer.ChainID) (layer.Layer, error) {
|