Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Include digest and image identifier in image.Info
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
squaremo committed Nov 30, 2017
1 parent a07617d commit 390d1d1
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 33 deletions.
11 changes: 8 additions & 3 deletions daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ func TestDaemon_JobStatusWithNoCache(t *testing.T) {
w.ForJobSucceeded(d, id)
}

func makeImageInfo(ref string, t time.Time) image.Info {
r, _ := image.ParseRef(ref)
return image.Info{ID: r, CreatedAt: t}
}

func mockDaemon(t *testing.T) (*Daemon, func(), *cluster.Mock, *mockEventWriter) {
logger := log.NewNopLogger()

Expand Down Expand Up @@ -397,9 +402,9 @@ func mockDaemon(t *testing.T) (*Daemon, func(), *cluster.Mock, *mockEventWriter)

var imageRegistry registry.Registry
{
img1, _ := image.ParseInfo(currentHelloImage, time.Now())
img2, _ := image.ParseInfo(newHelloImage, time.Now().Add(1*time.Second))
img3, _ := image.ParseInfo("another/service:latest", time.Now().Add(1*time.Second))
img1 := makeImageInfo(currentHelloImage, time.Now())
img2 := makeImageInfo(newHelloImage, time.Now().Add(1*time.Second))
img3 := makeImageInfo("another/service:latest", time.Now().Add(1*time.Second))
imageRegistry = registry.NewMockRegistry([]image.Info{
img1,
img2,
Expand Down
42 changes: 24 additions & 18 deletions image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,32 +221,49 @@ func (i Ref) WithNewTag(t string) Ref {
return img
}

// Info has the metadata we are able to determine about an image, from
// its registry.
// Info has the metadata we are able to determine about an image ref,
// from its registry.
type Info struct {
ID Ref
// the reference to this image; probably a tagged image name
ID Ref
// the digest we got when fetching the metadata, which will be
// different each time a manifest is uploaded for the reference
Digest string
// an identifier for the *image* this reference points to; this
// will be the same for references that point at the same image
// (but does not necessarily equal Docker's image ID)
ImageID string
// the time at which the image pointed at was created
CreatedAt time.Time
}

// MarshalJSON returns the Info value in JSON (as bytes). It is
// implemented so that we can omit the `CreatedAt` value when it's
// zero, which would otherwise be tricky for e.g., JavaScript to
// detect.
func (im Info) MarshalJSON() ([]byte, error) {
type InfoAlias Info // alias to shed existing MarshalJSON implementation
var t string
if !im.CreatedAt.IsZero() {
t = im.CreatedAt.UTC().Format(time.RFC3339Nano)
}
encode := struct {
ID Ref
InfoAlias
CreatedAt string `json:",omitempty"`
}{im.ID, t}
}{InfoAlias(im), t}
return json.Marshal(encode)
}

// UnmarshalJSON populates an Info from JSON (as bytes). It's the
// companion to MarshalJSON above.
func (im *Info) UnmarshalJSON(b []byte) error {
type InfoAlias Info
unencode := struct {
ID Ref
InfoAlias
CreatedAt string `json:",omitempty"`
}{}
json.Unmarshal(b, &unencode)
im.ID = unencode.ID
*im = Info(unencode.InfoAlias)
if unencode.CreatedAt == "" {
im.CreatedAt = time.Time{}
} else {
Expand All @@ -259,17 +276,6 @@ func (im *Info) UnmarshalJSON(b []byte) error {
return nil
}

func ParseInfo(s string, createdAt time.Time) (Info, error) {
id, err := ParseRef(s)
if err != nil {
return Info{}, err
}
return Info{
ID: id,
CreatedAt: createdAt,
}, nil
}

// ByCreatedDesc is a shim used to sort image info by creation date
type ByCreatedDesc []Info

Expand Down
54 changes: 48 additions & 6 deletions image/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package image
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"testing"
Expand Down Expand Up @@ -137,15 +138,56 @@ func TestRefSerialization(t *testing.T) {
}
}

func mustMakeInfo(ref string, created time.Time) Info {
r, err := ParseRef(ref)
if err != nil {
panic(err)
}
return Info{ID: r, CreatedAt: created}
}

func TestImageInfoSerialisation(t *testing.T) {
t0 := time.Now().UTC() // UTC so it has nil location, otherwise it won't compare
info := mustMakeInfo("my/image:tag", t0)
info.Digest = "sha256:digest"
info.ImageID = "sha256:layerID"
bytes, err := json.Marshal(info)
if err != nil {
t.Fatal(err)
}
var info1 Info
if err = json.Unmarshal(bytes, &info1); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(info, info1) {
t.Errorf("roundtrip serialisation failed:\n original: %#v\nroundtripped: %#v", info, info1)
}
}

func TestImageInfoCreatedAtZero(t *testing.T) {
info := mustMakeInfo("my/image:tag", time.Now())
info = Info{ID: info.ID}
bytes, err := json.Marshal(info)
if err != nil {
t.Fatal(err)
}
var info1 map[string]interface{}
if err = json.Unmarshal(bytes, &info1); err != nil {
t.Fatal(err)
}
if _, ok := info1["CreatedAt"]; ok {
t.Errorf("serialised Info included zero time field; expected it to be omitted\n%s", string(bytes))
}
}

func TestImage_OrderByCreationDate(t *testing.T) {
fmt.Printf("testTime: %s\n", testTime)
time0 := testTime.Add(time.Second)
time2 := testTime.Add(-time.Second)
imA, _ := ParseInfo("my/Image:3", testTime)
imB, _ := ParseInfo("my/Image:1", time0)
imC, _ := ParseInfo("my/Image:4", time2)
imD, _ := ParseInfo("my/Image:0", time.Time{}) // test nil
imE, _ := ParseInfo("my/Image:2", testTime) // test equal
imA := mustMakeInfo("my/Image:3", testTime)
imB := mustMakeInfo("my/Image:1", time0)
imC := mustMakeInfo("my/Image:4", time2)
imD := mustMakeInfo("my/Image:0", time.Time{}) // test nil
imE := mustMakeInfo("my/Image:2", testTime) // test equal
imgs := []Info{imA, imB, imC, imD, imE}
sort.Sort(ByCreatedDesc(imgs))
for i, im := range imgs {
Expand Down
13 changes: 10 additions & 3 deletions registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ func (a *Remote) Manifest(ctx context.Context, ref string) (image.Info, error) {
if err != nil {
return image.Info{}, err
}
manifest, fetchErr := manifests.Get(ctx, digest.Digest(ref), distribution.WithTagOption{ref})
var manifestDigest digest.Digest
digestOpt := client.ReturnContentDigest(&manifestDigest)
manifest, fetchErr := manifests.Get(ctx, digest.Digest(ref), digestOpt, distribution.WithTagOption{ref})

interpret:
if fetchErr != nil {
return image.Info{}, err
}

info := image.Info{ID: a.repo.ToRef(ref)}
info := image.Info{ID: a.repo.ToRef(ref), Digest: manifestDigest.String()}

// TODO(michael): can we type switch? Not sure how dependable the
// underlying types are.
Expand All @@ -90,6 +92,9 @@ interpret:
if err = json.Unmarshal([]byte(man.History[0].V1Compatibility), &v1); err != nil {
return image.Info{}, err
}
// This is not the ImageID that Docker uses, but assumed to
// identify the image as it's the topmost layer.
info.ImageID = v1.ID
info.CreatedAt = v1.Created
case *schema2.DeserializedManifest:
var man schema2.Manifest = deserialised.Manifest
Expand All @@ -106,13 +111,15 @@ interpret:
if err = json.Unmarshal(configBytes, &config); err != nil {
return image.Info{}, err
}
// This _is_ what Docker uses as its Image ID.
info.ImageID = man.Config.Digest.String()
info.CreatedAt = config.Created
case *manifestlist.DeserializedManifestList:
var list manifestlist.ManifestList = deserialised.ManifestList
// TODO(michael): is it valid to just pick the first one that matches?
for _, m := range list.Manifests {
if m.Platform.OS == "linux" && m.Platform.Architecture == "amd64" {
manifest, fetchErr = manifests.Get(ctx, m.Digest)
manifest, fetchErr = manifests.Get(ctx, m.Digest, digestOpt)
goto interpret
}
}
Expand Down
5 changes: 2 additions & 3 deletions registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ var (
var (
testTags = []string{testTagStr, "anotherTag"}
mClient = NewMockClient(
func(repository image.Ref) (image.Info, error) {
img, _ := image.ParseInfo(testImageStr, time.Time{})
return img, nil
func(_ image.Ref) (image.Info, error) {
return image.Info{ID: id, CreatedAt: time.Time{}}, nil
},
func(repository image.Name) ([]string, error) {
return testTags, nil
Expand Down

0 comments on commit 390d1d1

Please sign in to comment.