diff --git a/.dockerignore b/.dockerignore index d39ae3eb..e9e1abbb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,4 +6,5 @@ !go.sum !manifest/ !pkg/ +!registry/ !scripts/ diff --git a/architecture/oci-platform.go b/architecture/oci-platform.go index 727a9028..d8ecfa8d 100644 --- a/architecture/oci-platform.go +++ b/architecture/oci-platform.go @@ -1,17 +1,15 @@ package architecture -import "path" +import ( + "path" + + "github.com/containerd/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) // https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md#image-index-property-descriptions // see "platform" (under "manifests") -type OCIPlatform struct { - OS string `json:"os"` - Architecture string `json:"architecture"` - Variant string `json:"variant,omitempty"` - - //OSVersion string `json:"os.version,omitempty"` - //OSFeatures []string `json:"os.features,omitempty"` -} +type OCIPlatform ocispec.Platform var SupportedArches = map[string]OCIPlatform{ "amd64": {OS: "linux", Architecture: "amd64"}, @@ -36,3 +34,18 @@ func (p OCIPlatform) String() string { p.Variant, ) } + +func Normalize(p ocispec.Platform) ocispec.Platform { + p = platforms.Normalize(p) + if p.Architecture == "arm64" && p.Variant == "" { + // 😭 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") + p.Variant = "v8" + // TODO get pedantic about amd64 variants too? (in our defense, those variants didn't exist when we defined our "amd64", unlike "arm64v8" 👀) + } + return p +} + +func (p OCIPlatform) Is(q OCIPlatform) bool { + // (assumes "p" and "q" are both already bashbrew normalized, like one of the SupportedArches above) + return p.OS == q.OS && p.Architecture == q.Architecture && p.Variant == q.Variant +} diff --git a/architecture/oci-platform_test.go b/architecture/oci-platform_test.go index ef89b40e..80866ad7 100644 --- a/architecture/oci-platform_test.go +++ b/architecture/oci-platform_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/docker-library/bashbrew/architecture" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) func TestString(t *testing.T) { @@ -21,3 +23,45 @@ func TestString(t *testing.T) { }) } } + +func TestIs(t *testing.T) { + tests := map[bool][][2]architecture.OCIPlatform{ + true: { + {architecture.SupportedArches["amd64"], architecture.SupportedArches["amd64"]}, + {architecture.SupportedArches["arm32v5"], architecture.SupportedArches["arm32v5"]}, + {architecture.SupportedArches["arm32v6"], architecture.SupportedArches["arm32v6"]}, + {architecture.SupportedArches["arm32v7"], architecture.SupportedArches["arm32v7"]}, + {architecture.SupportedArches["arm64v8"], architecture.OCIPlatform{OS: "linux", Architecture: "arm64", Variant: "v8"}}, + {architecture.SupportedArches["windows-amd64"], architecture.OCIPlatform{OS: "windows", Architecture: "amd64", OSVersion: "1.2.3.4"}}, + }, + false: { + {architecture.SupportedArches["amd64"], architecture.OCIPlatform{OS: "linux", Architecture: "amd64", Variant: "v4"}}, + {architecture.SupportedArches["amd64"], architecture.SupportedArches["arm64v8"]}, + {architecture.SupportedArches["amd64"], architecture.SupportedArches["i386"]}, + {architecture.SupportedArches["amd64"], architecture.SupportedArches["windows-amd64"]}, + {architecture.SupportedArches["arm32v7"], architecture.SupportedArches["arm32v6"]}, + {architecture.SupportedArches["arm32v7"], architecture.SupportedArches["arm64v8"]}, + {architecture.SupportedArches["arm64v8"], architecture.OCIPlatform{OS: "linux", Architecture: "arm64", Variant: "v9"}}, + }, + } + for expected, test := range tests { + for _, platforms := range test { + t.Run(platforms[0].String()+" vs "+platforms[1].String(), func(t *testing.T) { + if got := platforms[0].Is(platforms[1]); got != expected { + t.Errorf("expected %v; got %v", expected, got) + } + }) + } + } +} + +func TestNormalize(t *testing.T) { + for arch, expected := range architecture.SupportedArches { + t.Run(arch, func(t *testing.T) { + normal := architecture.OCIPlatform(architecture.Normalize(ocispec.Platform(expected))) + if !expected.Is(normal) { + t.Errorf("expected %#v; got %#v", expected, normal) + } + }) + } +} diff --git a/cmd/bashbrew/cmd-push.go b/cmd/bashbrew/cmd-push.go index 1fc9cf3a..fe3e1ebd 100644 --- a/cmd/bashbrew/cmd-push.go +++ b/cmd/bashbrew/cmd-push.go @@ -39,6 +39,7 @@ func cmdPush(c *cli.Context) error { } // we can't use "r.Tags()" here because it will include SharedTags, which we never want to push directly (see "cmd-put-shared.go") + TagsLoop: for i, tag := range entry.Tags { if uniq && i > 0 { break @@ -47,10 +48,12 @@ func cmdPush(c *cli.Context) error { if !force { localImageId, _ := dockerInspect("{{.Id}}", tag) - registryImageId := fetchRegistryImageId(tag) - if registryImageId != "" && localImageId == registryImageId { - fmt.Fprintf(os.Stderr, "skipping %s (remote image matches local)\n", tag) - continue + registryImageIds := fetchRegistryImageIds(tag) + for _, registryImageId := range registryImageIds { + if localImageId == registryImageId { + fmt.Fprintf(os.Stderr, "skipping %s (remote image matches local)\n", tag) + continue TagsLoop + } } } fmt.Printf("Pushing %s\n", tag) diff --git a/cmd/bashbrew/cmd-remote-arches.go b/cmd/bashbrew/cmd-remote-arches.go new file mode 100644 index 00000000..ade304fc --- /dev/null +++ b/cmd/bashbrew/cmd-remote-arches.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/docker-library/bashbrew/registry" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/urfave/cli" +) + +func cmdRemoteArches(c *cli.Context) error { + args := c.Args() + if len(args) < 1 { + return fmt.Errorf("expected at least one argument") + } + doJson := c.Bool("json") + ctx := context.Background() + for _, arg := range args { + img, err := registry.Resolve(ctx, arg) + if err != nil { + return err + } + + arches, err := img.Architectures(ctx) + if err != nil { + return err + } + + if doJson { + ret := struct { + Ref string `json:"ref"` + Desc ocispec.Descriptor `json:"desc"` + Arches map[string][]ocispec.Descriptor `json:"arches"` + }{ + Ref: img.ImageRef, + Desc: img.Desc, + Arches: map[string][]ocispec.Descriptor{}, + } + for arch, imgs := range arches { + for _, obj := range imgs { + ret.Arches[arch] = append(ret.Arches[arch], obj.Desc) + } + } + out, err := json.Marshal(ret) + if err != nil { + return err + } + fmt.Println(string(out)) + } else { + fmt.Printf("%s -> %s\n", img.ImageRef, img.Desc.Digest) + + // Go..... + keys := []string{} + for arch := range arches { + keys = append(keys, arch) + } + sort.Strings(keys) + for _, arch := range keys { + for _, obj := range arches[arch] { + fmt.Printf(" %s -> %s\n", arch, obj.Desc.Digest) + } + } + } + } + return nil +} diff --git a/cmd/bashbrew/main.go b/cmd/bashbrew/main.go index 53645afd..8fc6efec 100644 --- a/cmd/bashbrew/main.go +++ b/cmd/bashbrew/main.go @@ -239,6 +239,11 @@ func main() { Name: "target-namespace", Usage: `target namespace to act into ("docker tag namespace/repo:tag target-namespace/repo:tag", "docker push target-namespace/repo:tag")`, }, + + "json": cli.BoolFlag{ + Name: "json", + Usage: "output machine-readable JSON instead of human-readable text", + }, } app.Commands = []cli.Command{ @@ -395,6 +400,22 @@ func main() { Category: "plumbing", }, + { + Name: "remote", + Usage: "query registries for bashbrew-related data", + Before: subcommandBeforeFactory("remote"), + Category: "plumbing", + Subcommands: []cli.Command{ + { + Name: "arches", + Usage: "returns a list of bashbrew architectures and content descriptors for the specified image(s)", + Flags: []cli.Flag{ + commonFlags["json"], + }, + Action: cmdRemoteArches, + }, + }, + }, } err := app.Run(os.Args) diff --git a/cmd/bashbrew/registry.go b/cmd/bashbrew/registry.go index e35f98e9..fe693198 100644 --- a/cmd/bashbrew/registry.go +++ b/cmd/bashbrew/registry.go @@ -2,63 +2,48 @@ package main import ( "context" - "encoding/json" - "net/url" - "os" - "github.com/containerd/containerd/images" - "github.com/containerd/containerd/reference/docker" - "github.com/containerd/containerd/remotes" - dockerremote "github.com/containerd/containerd/remotes/docker" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/docker-library/bashbrew/registry" ) -var registryImageIdCache = map[string]string{} +var registryImageIdsCache = map[string][]string{} // assumes the provided image name is NOT a manifest list (used for testing whether we need to "bashbrew push" or whether the remote image is already up-to-date) // this does NOT handle authentication, and will return the empty string for repositories which require it (causing "bashbrew push" to simply shell out to "docker push" which will handle authentication appropriately) -func fetchRegistryImageId(image string) string { +func fetchRegistryImageIds(image string) []string { ctx := context.Background() - ref, resolver, err := fetchRegistryResolveHelper(image) + img, err := registry.Resolve(ctx, image) if err != nil { - return "" - } - - name, desc, err := resolver.Resolve(ctx, ref) - if err != nil { - return "" - } - - if desc.MediaType != images.MediaTypeDockerSchema2Manifest && desc.MediaType != ocispec.MediaTypeImageManifest { - return "" + return nil } - digest := desc.Digest.String() - if id, ok := registryImageIdCache[digest]; ok { - return id + digest := img.Desc.Digest.String() + if ids, ok := registryImageIdsCache[digest]; ok { + return ids } - fetcher, err := resolver.Fetcher(ctx, name) + manifests, err := img.Manifests(ctx) if err != nil { - return "" + return nil } - r, err := fetcher.Fetch(ctx, desc) - if err != nil { - return "" + ids := []string{} + if img.IsImageIndex() { + ids = append(ids, digest) } - defer r.Close() - - var manifest ocispec.Manifest - if err := json.NewDecoder(r).Decode(&manifest); err != nil { - return "" + for _, manifestDesc := range manifests { + ids = append(ids, manifestDesc.Digest.String()) + manifest, err := img.At(manifestDesc).Manifest(ctx) + if err != nil { + continue + } + ids = append(ids, manifest.Config.Digest.String()) } - id := manifest.Config.Digest.String() - if id != "" { - registryImageIdCache[digest] = id + if len(ids) > 0 { + registryImageIdsCache[digest] = ids } - return id + return ids } var registryManifestListCache = map[string][]string{} @@ -67,46 +52,22 @@ var registryManifestListCache = map[string][]string{} func fetchRegistryManiestListDigests(image string) []string { ctx := context.Background() - ref, resolver, err := fetchRegistryResolveHelper(image) - if err != nil { - return nil - } - - name, desc, err := resolver.Resolve(ctx, ref) + img, err := registry.Resolve(ctx, image) if err != nil { return nil } - digest := desc.Digest.String() - if desc.MediaType == images.MediaTypeDockerSchema2Manifest || desc.MediaType == ocispec.MediaTypeImageManifest { - return []string{digest} - } - - if desc.MediaType != images.MediaTypeDockerSchema2ManifestList && desc.MediaType != ocispec.MediaTypeImageIndex { - return nil - } - + digest := img.Desc.Digest.String() if digests, ok := registryManifestListCache[digest]; ok { return digests } - fetcher, err := resolver.Fetcher(ctx, name) - if err != nil { - return nil - } - - r, err := fetcher.Fetch(ctx, desc) + manifests, err := img.Manifests(ctx) if err != nil { return nil } - defer r.Close() - - var manifestList ocispec.Index - if err := json.NewDecoder(r).Decode(&manifestList); err != nil { - return nil - } digests := []string{} - for _, manifest := range manifestList.Manifests { + for _, manifest := range manifests { if manifest.Digest != "" { digests = append(digests, manifest.Digest.String()) } @@ -116,30 +77,3 @@ func fetchRegistryManiestListDigests(image string) []string { } return digests } - -func fetchRegistryResolveHelper(image string) (string, remotes.Resolver, error) { - ref, err := docker.ParseAnyReference(image) - if err != nil { - return "", nil, err - } - if namedRef, ok := ref.(docker.Named); ok { - // add ":latest" if necessary - namedRef = docker.TagNameOnly(namedRef) - ref = namedRef - } - return ref.String(), dockerremote.NewResolver(dockerremote.ResolverOptions{ - Host: func(host string) (string, error) { - if host == "docker.io" { - if publicProxy := os.Getenv("DOCKERHUB_PUBLIC_PROXY"); publicProxy != "" { - if publicProxyURL, err := url.Parse(publicProxy); err == nil { - // TODO Scheme (also not sure if "host:port" will be satisfactory to containerd here, but 🤷) - return publicProxyURL.Host, nil - } else { - return "", err - } - } - } - return host, nil - }, - }), nil -} diff --git a/go.mod b/go.mod index e9c9e919..de613189 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/docker-library/bashbrew go 1.18 require ( - github.com/containerd/containerd v1.5.11 + github.com/containerd/containerd v1.6.9 github.com/go-git/go-git/v5 v5.4.2 - github.com/opencontainers/image-spec v1.0.2 + github.com/opencontainers/image-spec v1.1.0-rc2.0.20221013174636-8159c8264e2e github.com/sirupsen/logrus v1.8.1 github.com/urfave/cli v1.22.5 pault.ag/go/debian v0.12.0 @@ -13,7 +13,7 @@ require ( ) require ( - github.com/Microsoft/go-winio v0.5.1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect @@ -28,16 +28,15 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/locker v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect - golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect - golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect - google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 // indirect - google.golang.org/grpc v1.42.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect + google.golang.org/grpc v1.47.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 21c19e3a..607bcf79 100644 --- a/go.sum +++ b/go.sum @@ -5,9 +5,8 @@ github.com/DataDog/zstd v1.4.8/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwS github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= -github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 h1:XcF0cTDJeiuZ5NU8w7WUDge0HRwwNRmxj/GGk6KSA6g= github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= @@ -19,18 +18,16 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/containerd v1.5.11 h1:+biZCY9Kns9t2J8L9hOqubjvNQBr1ULdmR7kL+omKoY= -github.com/containerd/containerd v1.5.11/go.mod h1:FJl/l1urLXpO3oKDx2No2ouBno2GSI56nTl02HfHeZY= +github.com/containerd/containerd v1.6.9 h1:IN/r8DUes/B5lEGTNfIiUkfZBtIQJGx2ai703dV6lRA= +github.com/containerd/containerd v1.6.9/go.mod h1:XVicUvkxOrftE2Q1YWUXgZwkkAxwQYNOFzYWvfVfEfQ= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -44,8 +41,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -81,8 +77,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -113,8 +110,8 @@ github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221013174636-8159c8264e2e h1:s/Yjbl65/SrXqrMXDSP7eeC1vGZP3mOpya4rNeTwTKY= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221013174636-8159c8264e2e/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -133,7 +130,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -155,8 +151,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -168,11 +164,12 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI= -golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -190,7 +187,9 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -199,14 +198,14 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -221,17 +220,17 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 h1:DN5b3HU13J4sMd/QjDx34U6afpaexKTDdop+26pdjdk= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -243,8 +242,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -257,8 +257,8 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 00000000..775df83b --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,237 @@ +package registry + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/url" + "os" + "unicode" + + // thanks, go-digest... + _ "crypto/sha256" + _ "crypto/sha512" + + "github.com/docker-library/bashbrew/architecture" + + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/reference/docker" + "github.com/containerd/containerd/remotes" + dockerremote "github.com/containerd/containerd/remotes/docker" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type ResolvedObject struct { + Desc ocispec.Descriptor + + ImageRef string + resolver remotes.Resolver + fetcher remotes.Fetcher +} + +func (obj ResolvedObject) fetchJSON(ctx context.Context, v interface{}) error { + // prevent go-digest panics later + if err := obj.Desc.Digest.Validate(); err != nil { + return err + } + + // (perhaps use a containerd content store?? they do validation of all content they ingest, and then there's a cache) + + r, err := obj.fetcher.Fetch(ctx, obj.Desc) + if err != nil { + return err + } + defer r.Close() + + // make sure we can't possibly read (much) more than we're supposed to + limited := &io.LimitedReader{ + R: r, + N: obj.Desc.Size + 1, // +1 to allow us to detect if we read too much (see verification below) + } + + // copy all read data into the digest verifier so we can validate afterwards + verifier := obj.Desc.Digest.Verifier() + tee := io.TeeReader(limited, verifier) + + // decode directly! (mostly avoids double memory hit for big objects) + // (TODO protect against malicious objects somehow?) + if err := json.NewDecoder(tee).Decode(v); err != nil { + return err + } + + // read anything leftover ... + bs, err := io.ReadAll(tee) + if err != nil { + return err + } + // ... and make sure it was just whitespace, if anything + for _, b := range bs { + if !unicode.IsSpace(rune(b)) { + return fmt.Errorf("unexpected non-whitespace at the end of %q: %+v\n", obj.Desc.Digest.String(), rune(b)) + } + } + + // after reading *everything*, we should have exactly one byte left in our LimitedReader (anything else is an error) + if limited.N < 1 { + return fmt.Errorf("size of %q is bigger than it should be (%d)", obj.Desc.Digest.String(), obj.Desc.Size) + } else if limited.N > 1 { + return fmt.Errorf("size of %q is %d bytes smaller than it should be (%d)", obj.Desc.Digest.String(), limited.N-1, obj.Desc.Size) + } + + // and finally, let's verify our checksum + if !verifier.Verified() { + return fmt.Errorf("digest of %q not correct", obj.Desc.Digest.String()) + } + + return nil +} + +func get[T any](ctx context.Context, obj ResolvedObject) (*T, error) { + var ret T + if err := obj.fetchJSON(ctx, &ret); err != nil { + return nil, err + } + return &ret, nil +} + +// At returns a new object pointing to the given descriptor (still within the context of the same repository as the original resolved object) +func (obj ResolvedObject) At(desc ocispec.Descriptor) *ResolvedObject { + obj.Desc = desc + return &obj +} + +// Index assumes the given object is an "index" or "manifest list" and fetches/returns the parsed index JSON +func (obj ResolvedObject) Index(ctx context.Context) (*ocispec.Index, error) { + if !obj.IsImageIndex() { + return nil, fmt.Errorf("unknown media type: %q", obj.Desc.MediaType) + } + return get[ocispec.Index](ctx, obj) +} + +// Manifests returns a list of "content descriptors" that corresponds to either this object (if it is a single-image manifest) or all the manifests of the index/manifest list this object represents +func (obj ResolvedObject) Manifests(ctx context.Context) ([]ocispec.Descriptor, error) { + if obj.IsImageManifest() { + return []ocispec.Descriptor{obj.Desc}, nil + } + index, err := obj.Index(ctx) + if err != nil { + return nil, err + } + return index.Manifests, nil +} + +// Architectures returns a map of "bashbrew architecture" strings to a list of members of the object (as either a manifest or an index) which match the given "bashbrew architecture" (either in an explicit "platform" object or by reading all the way down into the image "config" object for the platform fields) +func (obj ResolvedObject) Architectures(ctx context.Context) (map[string][]ResolvedObject, error) { + manifests, err := obj.Manifests(ctx) + if err != nil { + return nil, err + } + + ret := map[string][]ResolvedObject{} + for _, manifestDesc := range manifests { + obj := obj.At(manifestDesc) + + if obj.Desc.Platform == nil || obj.Desc.Platform.OS == "" || obj.Desc.Platform.Architecture == "" { + manifest, err := obj.Manifest(ctx) + if err != nil { + return nil, err // TODO should we really return this, or should we ignore it? + } + config, err := obj.At(manifest.Config).ConfigBlob(ctx) + if err != nil { + return nil, err // TODO should we really return this, or should we ignore it? + } + obj.Desc.Platform = &config.Platform + } + + objPlat := architecture.Normalize(*obj.Desc.Platform) + obj.Desc.Platform = &objPlat + + for arch, plat := range architecture.SupportedArches { + if plat.Is(architecture.OCIPlatform(objPlat)) { + ret[arch] = append(ret[arch], *obj) + } + } + } + return ret, nil +} + +// Manifest assumes the given object is a (single-image) "manifest" (see [ResolvedObject.At]) and fetches/returns the parsed manifest JSON +func (obj ResolvedObject) Manifest(ctx context.Context) (*ocispec.Manifest, error) { + if !obj.IsImageManifest() { + return nil, fmt.Errorf("unknown media type: %q", obj.Desc.MediaType) + } + return get[ocispec.Manifest](ctx, obj) +} + +// ConfigBlob assumes the given object is a "config" blob (see [ResolvedObject.At]) and fetches/returns the parsed config object +func (obj ResolvedObject) ConfigBlob(ctx context.Context) (*ocispec.Image, error) { + if obj.Desc.MediaType != "application/vnd.oci.image.config.v1+json" && obj.Desc.MediaType != "application/vnd.docker.container.image.v1+json" { + return nil, fmt.Errorf("unknown media type: %q", obj.Desc.MediaType) + } + return get[ocispec.Image](ctx, obj) +} + +func (obj ResolvedObject) IsImageManifest() bool { + return obj.Desc.MediaType == ocispec.MediaTypeImageManifest || obj.Desc.MediaType == images.MediaTypeDockerSchema2Manifest +} + +func (obj ResolvedObject) IsImageIndex() bool { + return obj.Desc.MediaType == ocispec.MediaTypeImageIndex || obj.Desc.MediaType == images.MediaTypeDockerSchema2ManifestList +} + +// Resolve returns an object which can be used to query a registry for manifest objects or certain blobs with type checking helpers +func Resolve(ctx context.Context, image string) (*ResolvedObject, error) { + var ( + obj = ResolvedObject{ + ImageRef: image, + } + err error + ) + + obj.ImageRef, obj.resolver, err = resolverHelper(obj.ImageRef) + if err != nil { + return nil, err + } + + obj.ImageRef, obj.Desc, err = obj.resolver.Resolve(ctx, obj.ImageRef) + if err != nil { + return nil, err + } + + obj.fetcher, err = obj.resolver.Fetcher(ctx, obj.ImageRef) + if err != nil { + return nil, err + } + + return &obj, nil +} + +func resolverHelper(image string) (string, remotes.Resolver, error) { + ref, err := docker.ParseAnyReference(image) + if err != nil { + return "", nil, err + } + if namedRef, ok := ref.(docker.Named); ok { + // add ":latest" if necessary + namedRef = docker.TagNameOnly(namedRef) + ref = namedRef + } + return ref.String(), dockerremote.NewResolver(dockerremote.ResolverOptions{ + // TODO port this to "Hosts:" (especially so we can return Scheme correctly) but requires reimplementing some of https://github.com/containerd/containerd/blob/v1.6.9/remotes/docker/resolver.go#L161-L184 😞 + Host: func(host string) (string, error) { + if host == "docker.io" { + if publicProxy := os.Getenv("DOCKERHUB_PUBLIC_PROXY"); publicProxy != "" { + if publicProxyURL, err := url.Parse(publicProxy); err == nil { + // TODO Scheme (also not sure if "host:port" will be satisfactory to containerd here, but 🤷) + return publicProxyURL.Host, nil + } else { + return "", err + } + } + return "registry-1.docker.io", nil // https://github.com/containerd/containerd/blob/1c90a442489720eec95342e1789ee8a5e1b9536f/remotes/docker/registry.go#L193 + } + return host, nil + }, + }), nil +}