Browse Source

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 8 years ago
parent
commit
016eea004b
7 changed files with 61 additions and 0 deletions
  1. 6 0
      api/swagger.yaml
  2. 6 0
      api/types/types.go
  3. 8 0
      daemon/image_inspect.go
  4. 3 0
      daemon/image_tag.go
  5. 1 0
      docs/api/version-history.md
  6. 19 0
      image/store.go
  7. 18 0
      image/store_test.go

+ 6 - 0
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"

+ 6 - 0
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:

+ 8 - 0
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)

+ 3 - 0
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
 }

+ 1 - 0
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
 

+ 19 - 0
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()

+ 18 - 0
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) {