Skip to content

Commit bdbeb76

Browse files
committed
Add a Reference wrapper type to ociref.Reference to handle our "Docker references" logic more cleanly
This also allows us to add explicit JSON round-tripping which can handle normalizing/denormalizing for us, so our output strings no longer contain `docker.io/[library/]`. 🎉
1 parent cf871c9 commit bdbeb76

File tree

8 files changed

+433
-359
lines changed

8 files changed

+433
-359
lines changed

.test/builds.json

Lines changed: 147 additions & 147 deletions
Large diffs are not rendered by default.

.test/cache-builds.json

Lines changed: 182 additions & 182 deletions
Large diffs are not rendered by default.

.test/lookup-test.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"size": 946,
1010
"annotations": {
1111
"com.docker.official-images.bashbrew.arch": "windows-amd64",
12-
"org.opencontainers.image.ref.name": "docker.io/tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23"
12+
"org.opencontainers.image.ref.name": "tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23"
1313
},
1414
"platform": {
1515
"architecture": "amd64",
@@ -19,7 +19,7 @@
1919
}
2020
],
2121
"annotations": {
22-
"org.opencontainers.image.ref.name": "docker.io/tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23"
22+
"org.opencontainers.image.ref.name": "tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23"
2323
}
2424
},
2525
{
@@ -32,7 +32,7 @@
3232
"size": 861,
3333
"annotations": {
3434
"com.docker.official-images.bashbrew.arch": "amd64",
35-
"org.opencontainers.image.ref.name": "docker.io/tianon/test@sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
35+
"org.opencontainers.image.ref.name": "tianon/test@sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
3636
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
3737
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:amd64/hello-world",
3838
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
@@ -49,7 +49,7 @@
4949
"size": 946,
5050
"annotations": {
5151
"com.docker.official-images.bashbrew.arch": "windows-amd64",
52-
"org.opencontainers.image.ref.name": "docker.io/tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23"
52+
"org.opencontainers.image.ref.name": "tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23"
5353
},
5454
"platform": {
5555
"architecture": "amd64",
@@ -63,7 +63,7 @@
6363
"size": 946,
6464
"annotations": {
6565
"com.docker.official-images.bashbrew.arch": "windows-amd64",
66-
"org.opencontainers.image.ref.name": "docker.io/tianon/test@sha256:3a0bd0fb5ad6dd6528dc78726b3df78e980b39b379e99c5a508904ec17cfafe5"
66+
"org.opencontainers.image.ref.name": "tianon/test@sha256:3a0bd0fb5ad6dd6528dc78726b3df78e980b39b379e99c5a508904ec17cfafe5"
6767
},
6868
"platform": {
6969
"architecture": "amd64",
@@ -73,7 +73,7 @@
7373
}
7474
],
7575
"annotations": {
76-
"org.opencontainers.image.ref.name": "docker.io/tianon/test@sha256:347290ddd775c1b85a3e381b09edde95242478eb65153e9b17225356f4c072ac"
76+
"org.opencontainers.image.ref.name": "tianon/test@sha256:347290ddd775c1b85a3e381b09edde95242478eb65153e9b17225356f4c072ac"
7777
}
7878
}
7979
]

cmd/builds/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ var (
5555
)
5656

5757
func resolveIndex(ctx context.Context, img string, diskCacheForSure bool) (*ocispec.Index, error) {
58-
ref, err := registry.ParseRefNormalized(img)
58+
ref, err := registry.ParseRef(img)
5959
if err != nil {
6060
return nil, err
6161
}
@@ -90,7 +90,7 @@ func resolveIndex(ctx context.Context, img string, diskCacheForSure bool) (*ocis
9090
if diskCacheForSure {
9191
saveCacheMutex.Lock()
9292
if saveCache != nil {
93-
saveCache.Indexes[refString] = index
93+
saveCache.Indexes[ref] = index
9494
}
9595
saveCacheMutex.Unlock()
9696
}
@@ -137,7 +137,7 @@ func resolveArchIndex(ctx context.Context, img string, arch string, diskCacheFor
137137
}
138138

139139
type cacheFileContents struct {
140-
Indexes map[string]*ocispec.Index `json:"indexes"`
140+
Indexes map[registry.Reference]*ocispec.Index `json:"indexes"`
141141
}
142142

143143
var (
@@ -152,7 +152,7 @@ func loadCacheFromFile() error {
152152

153153
// now that we know we have a file we want cache to go into (and come from), let's initialize the "saveCache" (which will be written when the whole process is done / we're successful, and *only* caches staging images)
154154
saveCacheMutex.Lock()
155-
saveCache = &cacheFileContents{Indexes: map[string]*ocispec.Index{}}
155+
saveCache = &cacheFileContents{Indexes: map[registry.Reference]*ocispec.Index{}}
156156
saveCacheMutex.Unlock()
157157

158158
f, err := os.Open(cacheFile)
@@ -180,7 +180,7 @@ func loadCacheFromFile() error {
180180
panic(err)
181181
}
182182
if index2 != index {
183-
panic("index2 != index??? " + img)
183+
panic("index2 != index??? " + img.String())
184184
}
185185
}
186186

cmd/lookup/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func main() {
1616
defer stop()
1717

1818
for _, img := range os.Args[1:] {
19-
ref, err := registry.ParseRefNormalized(img)
19+
ref, err := registry.ParseRef(img)
2020
if err != nil {
2121
panic(err)
2222
}

registry/ref.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,56 @@ import (
1010
"cuelabs.dev/go/oci/ociregistry/ociref"
1111
)
1212

13-
// parse a ref like `hello-world:latest` into an [ociref.Reference] object, with Docker Hub canonicalization applied: `docker.io/library/hello-world:latest`
13+
// parse a string ref like `hello-world:latest` directly into a [Reference] object, with Docker Hub canonicalization applied: `docker.io/library/hello-world:latest`
1414
//
15-
// See also [ociref.ParseRelative]
16-
//
17-
// NOTE: this explicitly does *not* normalize Tag to `:latest` because it's useful to be able to parse a reference and know it did not specify either tag or digest (and `if ref.Tag == "" { ref.Tag = "latest" }` is really trivial code outside this for that case)
18-
func ParseRefNormalized(img string) (ociref.Reference, error) {
19-
ref, err := ociref.ParseRelative(img)
15+
// See also [Reference.Normalize] and [ociref.ParseRelative] (which are the underlying implementation details of this method).
16+
func ParseRef(img string) (Reference, error) {
17+
r, err := ociref.ParseRelative(img)
2018
if err != nil {
21-
return ociref.Reference{}, err
19+
return Reference{}, err
2220
}
21+
ref := Reference(r)
22+
ref.Normalize()
23+
return ref, nil
24+
}
25+
26+
// copy ociref.Reference so we can add methods (especially for JSON round-trip, but also Docker-isms like the implied default [Reference.Host] and `library/` prefix for DOI)
27+
type Reference ociref.Reference
28+
29+
// normalize Docker Hub refs like `hello-world:latest`: `docker.io/library/hello-world:latest`
30+
//
31+
// NOTE: this explicitly does *not* normalize Tag to `:latest` because it's useful to be able to parse a reference and know it did not specify either tag or digest (and `if ref.Tag == "" { ref.Tag = "latest" }` is really trivial code outside this for that case)
32+
func (ref *Reference) Normalize() {
2333
if dockerHubHosts[ref.Host] {
2434
// normalize Docker Hub host value
2535
ref.Host = dockerHubCanonical
2636
// normalize Docker Official Images to library/ prefix
2737
if !strings.Contains(ref.Repository, "/") {
2838
ref.Repository = "library/" + ref.Repository
2939
}
40+
// add an error return and return an error if we have more than one "/" in Repository? probably not worth embedding that many "Hub" implementation details this low (since it'll error appropriately on use of such invalid references anyhow)
3041
}
31-
return ref, nil
42+
}
43+
44+
// like [ociref.Reference.String], but with Docker Hub "denormalization" applied (no explicit `docker.io` host, no `library/` prefix for DOI)
45+
func (ref Reference) String() string {
46+
if ref.Host == dockerHubCanonical {
47+
ref.Host = ""
48+
ref.Repository = strings.TrimPrefix(ref.Repository, "library/")
49+
}
50+
return ociref.Reference(ref).String()
51+
}
52+
53+
// implements [encoding.TextMarshaler] (especially for [Reference]-in-JSON)
54+
func (ref Reference) MarshalText() ([]byte, error) {
55+
return []byte(ref.String()), nil
56+
}
57+
58+
// implements [encoding.TextUnmarshaler] (especially for [Reference]-from-JSON)
59+
func (ref *Reference) UnmarshalText(text []byte) error {
60+
r, err := ParseRef(string(text))
61+
if err == nil {
62+
*ref = r
63+
}
64+
return err
3265
}

registry/ref_test.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
package registry_test
22

33
import (
4+
"encoding/json"
5+
"strings"
46
"testing"
57

68
"github.com/docker-library/meta-scripts/registry"
9+
10+
"cuelabs.dev/go/oci/ociregistry/ociref"
711
)
812

9-
func TestParseRefNormalized(t *testing.T) {
13+
func toJson(t *testing.T, v any) string {
14+
t.Helper()
15+
b, err := json.Marshal(v)
16+
if err != nil {
17+
t.Fatal("unexpected JSON error", err)
18+
}
19+
return string(b)
20+
}
21+
22+
func fromJson(t *testing.T, j string, v any) {
23+
t.Helper()
24+
err := json.Unmarshal([]byte(j), v)
25+
if err != nil {
26+
t.Fatal("unexpected JSON error", err)
27+
}
28+
}
29+
30+
func TestParseRef(t *testing.T) {
1031
t.Parallel()
1132

1233
for _, o := range []struct {
@@ -28,17 +49,38 @@ func TestParseRefNormalized(t *testing.T) {
2849
{"registry.hub.docker.com/library/hello-world", "docker.io/library/hello-world"},
2950
} {
3051
o := o // https://github.com/golang/go/issues/60078
52+
dockerOut := strings.TrimPrefix(strings.TrimPrefix(o.out, "docker.io/library/"), "docker.io/")
53+
3154
t.Run(o.in, func(t *testing.T) {
32-
ref, err := registry.ParseRefNormalized(o.in)
55+
ref, err := registry.ParseRef(o.in)
3356
if err != nil {
3457
t.Fatal("unexpected error", err)
35-
return
3658
}
3759

38-
out := ref.String()
60+
out := ociref.Reference(ref).String()
3961
if out != o.out {
4062
t.Fatalf("expected %q, got %q", o.out, out)
41-
return
63+
}
64+
65+
out = ref.String()
66+
if out != dockerOut {
67+
t.Fatalf("expected %q, got %q", dockerOut, out)
68+
}
69+
})
70+
71+
t.Run(o.in+" JSON", func(t *testing.T) {
72+
json := toJson(t, o.in) // "hello-world:latest" (string straight to JSON so we can unmarshal it as a Reference)
73+
var ref registry.Reference
74+
fromJson(t, json, &ref)
75+
out := ociref.Reference(ref).String()
76+
if out != o.out {
77+
t.Fatalf("expected %q, got %q", o.out, out)
78+
}
79+
80+
json = toJson(t, ref) // "hello-world:latest" (take our reference and convert it to JSON so we can verify it goes out correctly)
81+
fromJson(t, json, &out) // back to a string
82+
if out != dockerOut {
83+
t.Fatalf("expected %q, got %q", dockerOut, out)
4284
}
4385
})
4486
}

registry/synthesize-index.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import (
99
"github.com/docker-library/bashbrew/architecture"
1010

1111
"cuelabs.dev/go/oci/ociregistry"
12-
"cuelabs.dev/go/oci/ociregistry/ociref"
1312
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1413
)
1514

1615
// returns a synthesized [ocispec.Index] object for the given reference that includes automatically pulling up [ocispec.Platform] objects for entries missing them plus annotations for bashbrew architecture ([AnnotationBashbrewArch]) and where to find the "upstream" object if it needs to be copied/pulled ([ocispec.AnnotationRefName])
17-
func SynthesizeIndex(ctx context.Context, ref ociref.Reference) (*ocispec.Index, error) {
16+
func SynthesizeIndex(ctx context.Context, ref Reference) (*ocispec.Index, error) {
1817
// consider making this a full ociregistry.Interface object? GetManifest(digest) not returning an object with that digest would certainly be Weird though so maybe that's a misguided idea (with very minimal actual benefit, at least right now)
1918

2019
client, err := Client(ref.Host, nil)
@@ -137,8 +136,8 @@ func SynthesizeIndex(ctx context.Context, ref ociref.Reference) (*ocispec.Index,
137136
return &index, nil
138137
}
139138

140-
// given a (potentially `nil`) map of annotations, add [ocispec.AnnotationRefName] including the supplied [ociref.Reference] (but with [ociref.Reference.Digest] set to a new value)
141-
func setRefAnnotation(annotations *map[string]string, ref ociref.Reference, digest ociregistry.Digest) {
139+
// given a (potentially `nil`) map of annotations, add [ocispec.AnnotationRefName] including the supplied [Reference] (but with [Reference.Digest] set to a new value)
140+
func setRefAnnotation(annotations *map[string]string, ref Reference, digest ociregistry.Digest) {
142141
if *annotations == nil {
143142
// "assignment to nil map" 🙃
144143
*annotations = map[string]string{}
@@ -148,7 +147,7 @@ func setRefAnnotation(annotations *map[string]string, ref ociref.Reference, dige
148147
}
149148

150149
// given a manifest descriptor (and optionally an existing [ociregistry.BlobReader] on the manifest object itself), make sure it has a valid [ocispec.Platform] object if possible, querying down into the [ocispec.Image] ("config" blob) if necessary
151-
func normalizeManifestPlatform(ctx context.Context, m *ocispec.Descriptor, r ociregistry.BlobReader, client ociregistry.Interface, ref ociref.Reference) error {
150+
func normalizeManifestPlatform(ctx context.Context, m *ocispec.Descriptor, r ociregistry.BlobReader, client ociregistry.Interface, ref Reference) error {
152151
if m.Platform == nil || m.Platform.OS == "" || m.Platform.Architecture == "" {
153152
// if missing (or obviously invalid) "platform", we need to (maybe) reach downwards and synthesize
154153
m.Platform = nil

0 commit comments

Comments
 (0)