214 lines
6.5 KiB
Go
214 lines
6.5 KiB
Go
package registry
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"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"
|
|
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 !images.IsConfigType(obj.Desc.MediaType) {
|
|
return nil, fmt.Errorf("unknown media type: %q", obj.Desc.MediaType)
|
|
}
|
|
return get[ocispec.Image](ctx, obj)
|
|
}
|
|
|
|
func (obj ResolvedObject) IsImageManifest() bool {
|
|
return images.IsManifestType(obj.Desc.MediaType)
|
|
}
|
|
|
|
func (obj ResolvedObject) IsImageIndex() bool {
|
|
return images.IsIndexType(obj.Desc.MediaType)
|
|
}
|
|
|
|
// 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
|
|
)
|
|
|
|
ref, err := docker.ParseAnyReference(obj.ImageRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if namedRef, ok := ref.(docker.Named); ok {
|
|
// add ":latest" if necessary
|
|
namedRef = docker.TagNameOnly(namedRef)
|
|
ref = namedRef
|
|
}
|
|
obj.ImageRef = ref.String()
|
|
|
|
obj.resolver = NewDockerAuthResolver()
|
|
|
|
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
|
|
}
|