Browse Source

Include digest and image identifier in image.Info

These fields help with detecting when

 - references with different tags point at the same image (e.g.,
 `latest` vs whatever)
 - the same reference fetched at different times refer to different
   images (e.g., `latest` now vs `latest` before)

NB I have not attempted to use the fields as above -- I've just made
sure they are populated.
Michael Bridgen 2 years ago
parent
commit
ce32a3a76e
5 changed files with 92 additions and 33 deletions
  1. 8
    3
      daemon/daemon_test.go
  2. 24
    18
      image/image.go
  3. 48
    6
      image/image_test.go
  4. 10
    3
      registry/client.go
  5. 2
    3
      registry/registry_test.go

+ 8
- 3
daemon/daemon_test.go View File

@@ -321,6 +321,11 @@ func TestDaemon_JobStatusWithNoCache(t *testing.T) {
321 321
 	w.ForJobSucceeded(d, id)
322 322
 }
323 323
 
324
+func makeImageInfo(ref string, t time.Time) image.Info {
325
+	r, _ := image.ParseRef(ref)
326
+	return image.Info{ID: r, CreatedAt: t}
327
+}
328
+
324 329
 func mockDaemon(t *testing.T) (*Daemon, func(), *cluster.Mock, *mockEventWriter) {
325 330
 	logger := log.NewNopLogger()
326 331
 
@@ -397,9 +402,9 @@ func mockDaemon(t *testing.T) (*Daemon, func(), *cluster.Mock, *mockEventWriter)
397 402
 
398 403
 	var imageRegistry registry.Registry
399 404
 	{
400
-		img1, _ := image.ParseInfo(currentHelloImage, time.Now())
401
-		img2, _ := image.ParseInfo(newHelloImage, time.Now().Add(1*time.Second))
402
-		img3, _ := image.ParseInfo("another/service:latest", time.Now().Add(1*time.Second))
405
+		img1 := makeImageInfo(currentHelloImage, time.Now())
406
+		img2 := makeImageInfo(newHelloImage, time.Now().Add(1*time.Second))
407
+		img3 := makeImageInfo("another/service:latest", time.Now().Add(1*time.Second))
403 408
 		imageRegistry = registry.NewMockRegistry([]image.Info{
404 409
 			img1,
405 410
 			img2,

+ 24
- 18
image/image.go View File

@@ -221,32 +221,49 @@ func (i Ref) WithNewTag(t string) Ref {
221 221
 	return img
222 222
 }
223 223
 
224
-// Info has the metadata we are able to determine about an image, from
225
-// its registry.
224
+// Info has the metadata we are able to determine about an image ref,
225
+// from its registry.
226 226
 type Info struct {
227
-	ID        Ref
227
+	// the reference to this image; probably a tagged image name
228
+	ID Ref
229
+	// the digest we got when fetching the metadata, which will be
230
+	// different each time a manifest is uploaded for the reference
231
+	Digest string
232
+	// an identifier for the *image* this reference points to; this
233
+	// will be the same for references that point at the same image
234
+	// (but does not necessarily equal Docker's image ID)
235
+	ImageID string
236
+	// the time at which the image pointed at was created
228 237
 	CreatedAt time.Time
229 238
 }
230 239
 
240
+// MarshalJSON returns the Info value in JSON (as bytes). It is
241
+// implemented so that we can omit the `CreatedAt` value when it's
242
+// zero, which would otherwise be tricky for e.g., JavaScript to
243
+// detect.
231 244
 func (im Info) MarshalJSON() ([]byte, error) {
245
+	type InfoAlias Info // alias to shed existing MarshalJSON implementation
232 246
 	var t string
233 247
 	if !im.CreatedAt.IsZero() {
234 248
 		t = im.CreatedAt.UTC().Format(time.RFC3339Nano)
235 249
 	}
236 250
 	encode := struct {
237
-		ID        Ref
251
+		InfoAlias
238 252
 		CreatedAt string `json:",omitempty"`
239
-	}{im.ID, t}
253
+	}{InfoAlias(im), t}
240 254
 	return json.Marshal(encode)
241 255
 }
242 256
 
257
+// UnmarshalJSON populates an Info from JSON (as bytes). It's the
258
+// companion to MarshalJSON above.
243 259
 func (im *Info) UnmarshalJSON(b []byte) error {
260
+	type InfoAlias Info
244 261
 	unencode := struct {
245
-		ID        Ref
262
+		InfoAlias
246 263
 		CreatedAt string `json:",omitempty"`
247 264
 	}{}
248 265
 	json.Unmarshal(b, &unencode)
249
-	im.ID = unencode.ID
266
+	*im = Info(unencode.InfoAlias)
250 267
 	if unencode.CreatedAt == "" {
251 268
 		im.CreatedAt = time.Time{}
252 269
 	} else {
@@ -259,17 +276,6 @@ func (im *Info) UnmarshalJSON(b []byte) error {
259 276
 	return nil
260 277
 }
261 278
 
262
-func ParseInfo(s string, createdAt time.Time) (Info, error) {
263
-	id, err := ParseRef(s)
264
-	if err != nil {
265
-		return Info{}, err
266
-	}
267
-	return Info{
268
-		ID:        id,
269
-		CreatedAt: createdAt,
270
-	}, nil
271
-}
272
-
273 279
 // ByCreatedDesc is a shim used to sort image info by creation date
274 280
 type ByCreatedDesc []Info
275 281
 

+ 48
- 6
image/image_test.go View File

@@ -3,6 +3,7 @@ package image
3 3
 import (
4 4
 	"encoding/json"
5 5
 	"fmt"
6
+	"reflect"
6 7
 	"sort"
7 8
 	"strconv"
8 9
 	"testing"
@@ -137,15 +138,56 @@ func TestRefSerialization(t *testing.T) {
137 138
 	}
138 139
 }
139 140
 
141
+func mustMakeInfo(ref string, created time.Time) Info {
142
+	r, err := ParseRef(ref)
143
+	if err != nil {
144
+		panic(err)
145
+	}
146
+	return Info{ID: r, CreatedAt: created}
147
+}
148
+
149
+func TestImageInfoSerialisation(t *testing.T) {
150
+	t0 := time.Now().UTC() // UTC so it has nil location, otherwise it won't compare
151
+	info := mustMakeInfo("my/image:tag", t0)
152
+	info.Digest = "sha256:digest"
153
+	info.ImageID = "sha256:layerID"
154
+	bytes, err := json.Marshal(info)
155
+	if err != nil {
156
+		t.Fatal(err)
157
+	}
158
+	var info1 Info
159
+	if err = json.Unmarshal(bytes, &info1); err != nil {
160
+		t.Fatal(err)
161
+	}
162
+	if !reflect.DeepEqual(info, info1) {
163
+		t.Errorf("roundtrip serialisation failed:\n original: %#v\nroundtripped: %#v", info, info1)
164
+	}
165
+}
166
+
167
+func TestImageInfoCreatedAtZero(t *testing.T) {
168
+	info := mustMakeInfo("my/image:tag", time.Now())
169
+	info = Info{ID: info.ID}
170
+	bytes, err := json.Marshal(info)
171
+	if err != nil {
172
+		t.Fatal(err)
173
+	}
174
+	var info1 map[string]interface{}
175
+	if err = json.Unmarshal(bytes, &info1); err != nil {
176
+		t.Fatal(err)
177
+	}
178
+	if _, ok := info1["CreatedAt"]; ok {
179
+		t.Errorf("serialised Info included zero time field; expected it to be omitted\n%s", string(bytes))
180
+	}
181
+}
182
+
140 183
 func TestImage_OrderByCreationDate(t *testing.T) {
141
-	fmt.Printf("testTime: %s\n", testTime)
142 184
 	time0 := testTime.Add(time.Second)
143 185
 	time2 := testTime.Add(-time.Second)
144
-	imA, _ := ParseInfo("my/Image:3", testTime)
145
-	imB, _ := ParseInfo("my/Image:1", time0)
146
-	imC, _ := ParseInfo("my/Image:4", time2)
147
-	imD, _ := ParseInfo("my/Image:0", time.Time{}) // test nil
148
-	imE, _ := ParseInfo("my/Image:2", testTime)    // test equal
186
+	imA := mustMakeInfo("my/Image:3", testTime)
187
+	imB := mustMakeInfo("my/Image:1", time0)
188
+	imC := mustMakeInfo("my/Image:4", time2)
189
+	imD := mustMakeInfo("my/Image:0", time.Time{}) // test nil
190
+	imE := mustMakeInfo("my/Image:2", testTime)    // test equal
149 191
 	imgs := []Info{imA, imB, imC, imD, imE}
150 192
 	sort.Sort(ByCreatedDesc(imgs))
151 193
 	for i, im := range imgs {

+ 10
- 3
registry/client.go View File

@@ -65,14 +65,16 @@ func (a *Remote) Manifest(ctx context.Context, ref string) (image.Info, error) {
65 65
 	if err != nil {
66 66
 		return image.Info{}, err
67 67
 	}
68
-	manifest, fetchErr := manifests.Get(ctx, digest.Digest(ref), distribution.WithTagOption{ref})
68
+	var manifestDigest digest.Digest
69
+	digestOpt := client.ReturnContentDigest(&manifestDigest)
70
+	manifest, fetchErr := manifests.Get(ctx, digest.Digest(ref), digestOpt, distribution.WithTagOption{ref})
69 71
 
70 72
 interpret:
71 73
 	if fetchErr != nil {
72 74
 		return image.Info{}, err
73 75
 	}
74 76
 
75
-	info := image.Info{ID: a.repo.ToRef(ref)}
77
+	info := image.Info{ID: a.repo.ToRef(ref), Digest: manifestDigest.String()}
76 78
 
77 79
 	// TODO(michael): can we type switch? Not sure how dependable the
78 80
 	// underlying types are.
@@ -90,6 +92,9 @@ interpret:
90 92
 		if err = json.Unmarshal([]byte(man.History[0].V1Compatibility), &v1); err != nil {
91 93
 			return image.Info{}, err
92 94
 		}
95
+		// This is not the ImageID that Docker uses, but assumed to
96
+		// identify the image as it's the topmost layer.
97
+		info.ImageID = v1.ID
93 98
 		info.CreatedAt = v1.Created
94 99
 	case *schema2.DeserializedManifest:
95 100
 		var man schema2.Manifest = deserialised.Manifest
@@ -106,13 +111,15 @@ interpret:
106 111
 		if err = json.Unmarshal(configBytes, &config); err != nil {
107 112
 			return image.Info{}, err
108 113
 		}
114
+		// This _is_ what Docker uses as its Image ID.
115
+		info.ImageID = man.Config.Digest.String()
109 116
 		info.CreatedAt = config.Created
110 117
 	case *manifestlist.DeserializedManifestList:
111 118
 		var list manifestlist.ManifestList = deserialised.ManifestList
112 119
 		// TODO(michael): is it valid to just pick the first one that matches?
113 120
 		for _, m := range list.Manifests {
114 121
 			if m.Platform.OS == "linux" && m.Platform.Architecture == "amd64" {
115
-				manifest, fetchErr = manifests.Get(ctx, m.Digest)
122
+				manifest, fetchErr = manifests.Get(ctx, m.Digest, digestOpt)
116 123
 				goto interpret
117 124
 			}
118 125
 		}

+ 2
- 3
registry/registry_test.go View File

@@ -34,9 +34,8 @@ var (
34 34
 var (
35 35
 	testTags = []string{testTagStr, "anotherTag"}
36 36
 	mClient  = NewMockClient(
37
-		func(repository image.Ref) (image.Info, error) {
38
-			img, _ := image.ParseInfo(testImageStr, time.Time{})
39
-			return img, nil
37
+		func(_ image.Ref) (image.Info, error) {
38
+			return image.Info{ID: id, CreatedAt: time.Time{}}, nil
40 39
 		},
41 40
 		func(repository image.Name) ([]string, error) {
42 41
 			return testTags, nil

Loading…
Cancel
Save