Skip to content

Commit b20e82c

Browse files
committed
Add new "bashbrew remote arches" command
This command will, given a remote image reference, look up the list of platforms from it and match them to supported bashbrew architectures (providing content descriptors for each). Also, refactor registry code to be more correct: previously, this couldn't fetch from Docker without `DOCKERHUB_PUBLIC_PROXY` (see `registry-1.docker.io` change) and was ignoring content digests. Now it works correctly with or without `DOCKERHUB_PUBLIC_PROXY`, verifies the size of every object it pulls, verifies the digest, _and_ should continue working with the in-progress Moby containerd-integration (where the local image ID becomes the digest of the manifest or index instead of the digest of the config blob as it is today).
1 parent 0feb2b9 commit b20e82c

File tree

10 files changed

+467
-145
lines changed

10 files changed

+467
-145
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
!go.sum
77
!manifest/
88
!pkg/
9+
!registry/
910
!scripts/

architecture/oci-platform.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
package architecture
22

3-
import "path"
3+
import (
4+
"path"
5+
6+
"github.com/containerd/containerd/platforms"
7+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
8+
)
49

510
// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md#image-index-property-descriptions
611
// see "platform" (under "manifests")
7-
type OCIPlatform struct {
8-
OS string `json:"os"`
9-
Architecture string `json:"architecture"`
10-
Variant string `json:"variant,omitempty"`
11-
12-
//OSVersion string `json:"os.version,omitempty"`
13-
//OSFeatures []string `json:"os.features,omitempty"`
14-
}
12+
type OCIPlatform ocispec.Platform
1513

1614
var SupportedArches = map[string]OCIPlatform{
1715
"amd64": {OS: "linux", Architecture: "amd64"},
@@ -36,3 +34,18 @@ func (p OCIPlatform) String() string {
3634
p.Variant,
3735
)
3836
}
37+
38+
func Normalize(p ocispec.Platform) ocispec.Platform {
39+
p = platforms.Normalize(p)
40+
if p.Architecture == "arm64" && p.Variant == "" {
41+
// 😭 https://github.com/containerd/containerd/blob/1c90a442489720eec95342e1789ee8a5e1b9536f/platforms/database.go#L98 (inconsistent normalization of "linux/arm -> linux/arm/v7" vs "linux/arm64/v8 -> linux/arm64")
42+
p.Variant = "v8"
43+
// TODO get pedantic about amd64 variants too? (in our defense, those variants didn't exist when we defined our "amd64", unlike "arm64v8" 👀)
44+
}
45+
return p
46+
}
47+
48+
func (p OCIPlatform) Is(q OCIPlatform) bool {
49+
// (assumes "p" and "q" are both already bashbrew normalized, like one of the SupportedArches above)
50+
return p.OS == q.OS && p.Architecture == q.Architecture && p.Variant == q.Variant
51+
}

architecture/oci-platform_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"testing"
55

66
"github.com/docker-library/bashbrew/architecture"
7+
8+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
79
)
810

911
func TestString(t *testing.T) {
@@ -21,3 +23,45 @@ func TestString(t *testing.T) {
2123
})
2224
}
2325
}
26+
27+
func TestIs(t *testing.T) {
28+
tests := map[bool][][2]architecture.OCIPlatform{
29+
true: {
30+
{architecture.SupportedArches["amd64"], architecture.SupportedArches["amd64"]},
31+
{architecture.SupportedArches["arm32v5"], architecture.SupportedArches["arm32v5"]},
32+
{architecture.SupportedArches["arm32v6"], architecture.SupportedArches["arm32v6"]},
33+
{architecture.SupportedArches["arm32v7"], architecture.SupportedArches["arm32v7"]},
34+
{architecture.SupportedArches["arm64v8"], architecture.OCIPlatform{OS: "linux", Architecture: "arm64", Variant: "v8"}},
35+
{architecture.SupportedArches["windows-amd64"], architecture.OCIPlatform{OS: "windows", Architecture: "amd64", OSVersion: "1.2.3.4"}},
36+
},
37+
false: {
38+
{architecture.SupportedArches["amd64"], architecture.OCIPlatform{OS: "linux", Architecture: "amd64", Variant: "v4"}},
39+
{architecture.SupportedArches["amd64"], architecture.SupportedArches["arm64v8"]},
40+
{architecture.SupportedArches["amd64"], architecture.SupportedArches["i386"]},
41+
{architecture.SupportedArches["amd64"], architecture.SupportedArches["windows-amd64"]},
42+
{architecture.SupportedArches["arm32v7"], architecture.SupportedArches["arm32v6"]},
43+
{architecture.SupportedArches["arm32v7"], architecture.SupportedArches["arm64v8"]},
44+
{architecture.SupportedArches["arm64v8"], architecture.OCIPlatform{OS: "linux", Architecture: "arm64", Variant: "v9"}},
45+
},
46+
}
47+
for expected, test := range tests {
48+
for _, platforms := range test {
49+
t.Run(platforms[0].String()+" vs "+platforms[1].String(), func(t *testing.T) {
50+
if got := platforms[0].Is(platforms[1]); got != expected {
51+
t.Errorf("expected %v; got %v", expected, got)
52+
}
53+
})
54+
}
55+
}
56+
}
57+
58+
func TestNormalize(t *testing.T) {
59+
for arch, expected := range architecture.SupportedArches {
60+
t.Run(arch, func(t *testing.T) {
61+
normal := architecture.OCIPlatform(architecture.Normalize(ocispec.Platform(expected)))
62+
if !expected.Is(normal) {
63+
t.Errorf("expected %#v; got %#v", expected, normal)
64+
}
65+
})
66+
}
67+
}

cmd/bashbrew/cmd-push.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func cmdPush(c *cli.Context) error {
3939
}
4040

4141
// we can't use "r.Tags()" here because it will include SharedTags, which we never want to push directly (see "cmd-put-shared.go")
42+
TagsLoop:
4243
for i, tag := range entry.Tags {
4344
if uniq && i > 0 {
4445
break
@@ -47,10 +48,12 @@ func cmdPush(c *cli.Context) error {
4748

4849
if !force {
4950
localImageId, _ := dockerInspect("{{.Id}}", tag)
50-
registryImageId := fetchRegistryImageId(tag)
51-
if registryImageId != "" && localImageId == registryImageId {
52-
fmt.Fprintf(os.Stderr, "skipping %s (remote image matches local)\n", tag)
53-
continue
51+
registryImageIds := fetchRegistryImageIds(tag)
52+
for _, registryImageId := range registryImageIds {
53+
if localImageId == registryImageId {
54+
fmt.Fprintf(os.Stderr, "skipping %s (remote image matches local)\n", tag)
55+
continue TagsLoop
56+
}
5457
}
5558
}
5659
fmt.Printf("Pushing %s\n", tag)

cmd/bashbrew/cmd-remote-arches.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"sort"
8+
9+
"github.com/docker-library/bashbrew/registry"
10+
11+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
12+
"github.com/urfave/cli"
13+
)
14+
15+
func cmdRemoteArches(c *cli.Context) error {
16+
args := c.Args()
17+
if len(args) < 1 {
18+
return fmt.Errorf("expected at least one argument")
19+
}
20+
doJson := c.Bool("json")
21+
ctx := context.Background()
22+
for _, arg := range args {
23+
img, err := registry.Resolve(ctx, arg)
24+
if err != nil {
25+
return err
26+
}
27+
28+
arches, err := img.Architectures(ctx)
29+
if err != nil {
30+
return err
31+
}
32+
33+
if doJson {
34+
ret := struct {
35+
Ref string `json:"ref"`
36+
Desc ocispec.Descriptor `json:"desc"`
37+
Arches map[string][]ocispec.Descriptor `json:"arches"`
38+
}{
39+
Ref: img.ImageRef,
40+
Desc: img.Desc,
41+
Arches: map[string][]ocispec.Descriptor{},
42+
}
43+
for arch, imgs := range arches {
44+
for _, obj := range imgs {
45+
ret.Arches[arch] = append(ret.Arches[arch], obj.Desc)
46+
}
47+
}
48+
out, err := json.Marshal(ret)
49+
if err != nil {
50+
return err
51+
}
52+
fmt.Println(string(out))
53+
} else {
54+
fmt.Printf("%s -> %s\n", img.ImageRef, img.Desc.Digest)
55+
56+
// Go.....
57+
keys := []string{}
58+
for arch := range arches {
59+
keys = append(keys, arch)
60+
}
61+
sort.Strings(keys)
62+
for _, arch := range keys {
63+
for _, obj := range arches[arch] {
64+
fmt.Printf(" %s -> %s\n", arch, obj.Desc.Digest)
65+
}
66+
}
67+
}
68+
}
69+
return nil
70+
}

cmd/bashbrew/main.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ func main() {
239239
Name: "target-namespace",
240240
Usage: `target namespace to act into ("docker tag namespace/repo:tag target-namespace/repo:tag", "docker push target-namespace/repo:tag")`,
241241
},
242+
243+
"json": cli.BoolFlag{
244+
Name: "json",
245+
Usage: "output machine-readable JSON instead of human-readable text",
246+
},
242247
}
243248

244249
app.Commands = []cli.Command{
@@ -395,6 +400,22 @@ func main() {
395400

396401
Category: "plumbing",
397402
},
403+
{
404+
Name: "remote",
405+
Usage: "query registries for bashbrew-related data",
406+
Before: subcommandBeforeFactory("remote"),
407+
Category: "plumbing",
408+
Subcommands: []cli.Command{
409+
{
410+
Name: "arches",
411+
Usage: "returns a list of bashbrew architectures and content descriptors for the specified image(s)",
412+
Flags: []cli.Flag{
413+
commonFlags["json"],
414+
},
415+
Action: cmdRemoteArches,
416+
},
417+
},
418+
},
398419
}
399420

400421
err := app.Run(os.Args)

0 commit comments

Comments
 (0)