Browse code

Set a LastUpdated time in image metadata when an image tag is updated.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Daniel Nephin authored on 2017/03/03 05:47:02
Showing 7 changed files
... ...
@@ -949,6 +949,12 @@ definitions:
949 949
               type: "string"
950 950
           BaseLayer:
951 951
             type: "string"
952
+      Metadata:
953
+        type: "object"
954
+        properties:
955
+          LastTagTime:
956
+            type: "string"
957
+            format: "dateTime"
952 958
 
953 959
   ImageSummary:
954 960
     type: "object"
... ...
@@ -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) {