buildx/util/imagetools/loader.go
Justin Chadwell 642f28f439 imagetools: process com.docker.reference.* annotations
To give us the option later down the road of producing recommended OCI
names in BuildKit (using com instead of vnd, woops), we need to update
Buildx to be able to process both.

Ideally, if a Buildx/BuildKit release hadn't been made we could just
switch over, but since we have, we'd need to support both (at least for
a while, eventually we could consider deprecating+removing the vnd
variant).

Signed-off-by: Justin Chadwell <me@jedevc.com>
2023-02-14 11:24:40 +00:00

418 lines
9.5 KiB
Go

package imagetools
// TODO: replace with go-imageinspect library when public
import (
"context"
"encoding/json"
"sort"
"strings"
"sync"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
"github.com/docker/distribution/reference"
"github.com/moby/buildkit/util/contentutil"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
var (
annotationReferences = []string{
"com.docker.reference.digest",
"vnd.docker.reference.digest", // TODO: deprecate/remove after migration to new annotation
}
)
type contentCache interface {
content.Provider
content.Ingester
}
type loader struct {
resolver remotes.Resolver
cache contentCache
}
type manifest struct {
desc ocispec.Descriptor
manifest ocispec.Manifest
}
type index struct {
desc ocispec.Descriptor
index ocispec.Index
}
type asset struct {
config *ocispec.Image
sbom *sbomStub
provenance *provenanceStub
deferredSbom func() (*sbomStub, error)
deferredProvenance func() (*provenanceStub, error)
}
type result struct {
mu sync.Mutex
indexes map[digest.Digest]index
manifests map[digest.Digest]manifest
images map[string]digest.Digest
refs map[digest.Digest][]digest.Digest
platforms []string
assets map[string]asset
}
func newLoader(resolver remotes.Resolver) *loader {
return &loader{
resolver: resolver,
cache: contentutil.NewBuffer(),
}
}
func (l *loader) Load(ctx context.Context, ref string) (*result, error) {
named, err := parseRef(ref)
if err != nil {
return nil, err
}
_, desc, err := l.resolver.Resolve(ctx, named.String())
if err != nil {
return nil, err
}
canonical, err := reference.WithDigest(named, desc.Digest)
if err != nil {
return nil, err
}
fetcher, err := l.resolver.Fetcher(ctx, canonical.String())
if err != nil {
return nil, err
}
r := &result{
indexes: make(map[digest.Digest]index),
manifests: make(map[digest.Digest]manifest),
images: make(map[string]digest.Digest),
refs: make(map[digest.Digest][]digest.Digest),
assets: make(map[string]asset),
}
if err := l.fetch(ctx, fetcher, desc, r); err != nil {
return nil, err
}
for platform, dgst := range r.images {
r.platforms = append(r.platforms, platform)
mfst, ok := r.manifests[dgst]
if !ok {
return nil, errors.Errorf("image %s not found", platform)
}
var a asset
annotations := make(map[string]string, len(mfst.manifest.Annotations)+len(mfst.desc.Annotations))
for k, v := range mfst.desc.Annotations {
annotations[k] = v
}
for k, v := range mfst.manifest.Annotations {
annotations[k] = v
}
if err := l.scanConfig(ctx, fetcher, mfst.manifest.Config, &a); err != nil {
return nil, err
}
refs, ok := r.refs[dgst]
if ok {
if err := l.scanSBOM(ctx, fetcher, r, refs, &a); err != nil {
return nil, err
}
}
if err := l.scanProvenance(ctx, fetcher, r, refs, &a); err != nil {
return nil, err
}
r.assets[platform] = a
}
sort.Strings(r.platforms)
return r, nil
}
func (l *loader) fetch(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor, r *result) error {
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc)
if err != nil {
return err
}
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
var mfst ocispec.Manifest
dt, err := content.ReadBlob(ctx, l.cache, desc)
if err != nil {
return err
}
if err := json.Unmarshal(dt, &mfst); err != nil {
return err
}
r.mu.Lock()
r.manifests[desc.Digest] = manifest{
desc: desc,
manifest: mfst,
}
r.mu.Unlock()
found := false
for _, annotationReference := range annotationReferences {
ref, ok := desc.Annotations[annotationReference]
if !ok {
continue
}
refdgst, err := digest.Parse(ref)
if err != nil {
return err
}
r.mu.Lock()
r.refs[refdgst] = append(r.refs[refdgst], desc.Digest)
r.mu.Unlock()
found = true
break
}
if !found {
p := desc.Platform
if p == nil {
p, err = l.readPlatformFromConfig(ctx, fetcher, mfst.Config)
if err != nil {
return err
}
}
r.mu.Lock()
r.images[platforms.Format(platforms.Normalize(*p))] = desc.Digest
r.mu.Unlock()
}
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var idx ocispec.Index
dt, err := content.ReadBlob(ctx, l.cache, desc)
if err != nil {
return err
}
if err := json.Unmarshal(dt, &idx); err != nil {
return err
}
r.mu.Lock()
r.indexes[desc.Digest] = index{
desc: desc,
index: idx,
}
r.mu.Unlock()
eg, ctx := errgroup.WithContext(ctx)
for _, d := range idx.Manifests {
d := d
eg.Go(func() error {
return l.fetch(ctx, fetcher, d, r)
})
}
if err := eg.Wait(); err != nil {
return err
}
default:
}
return nil
}
func (l *loader) readPlatformFromConfig(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor) (*ocispec.Platform, error) {
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc)
if err != nil {
return nil, err
}
dt, err := content.ReadBlob(ctx, l.cache, desc)
if err != nil {
return nil, err
}
var config ocispec.Image
if err := json.Unmarshal(dt, &config); err != nil {
return nil, err
}
return &ocispec.Platform{
OS: config.OS,
Architecture: config.Architecture,
Variant: config.Variant,
}, nil
}
func (l *loader) scanConfig(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor, as *asset) error {
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc)
if err != nil {
return err
}
dt, err := content.ReadBlob(ctx, l.cache, desc)
if err != nil {
return err
}
return json.Unmarshal(dt, &as.config)
}
type sbomStub struct {
SPDX interface{} `json:",omitempty"`
AdditionalSPDXs []interface{} `json:",omitempty"`
}
func (l *loader) scanSBOM(ctx context.Context, fetcher remotes.Fetcher, r *result, refs []digest.Digest, as *asset) error {
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
as.deferredSbom = func() (*sbomStub, error) {
var sbom *sbomStub
for _, dgst := range refs {
mfst, ok := r.manifests[dgst]
if !ok {
return nil, errors.Errorf("referenced image %s not found", dgst)
}
for _, layer := range mfst.manifest.Layers {
if layer.MediaType == "application/vnd.in-toto+json" && layer.Annotations["in-toto.io/predicate-type"] == "https://spdx.dev/Document" {
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, layer)
if err != nil {
return nil, err
}
dt, err := content.ReadBlob(ctx, l.cache, layer)
if err != nil {
return nil, err
}
var spdx struct {
Predicate interface{} `json:"predicate"`
}
if err := json.Unmarshal(dt, &spdx); err != nil {
return nil, err
}
if sbom == nil {
sbom = &sbomStub{}
sbom.SPDX = spdx.Predicate
} else {
sbom.AdditionalSPDXs = append(sbom.AdditionalSPDXs, spdx.Predicate)
}
}
}
}
return sbom, nil
}
return nil
}
type provenanceStub struct {
SLSA interface{} `json:",omitempty"`
}
func (l *loader) scanProvenance(ctx context.Context, fetcher remotes.Fetcher, r *result, refs []digest.Digest, as *asset) error {
ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
as.deferredProvenance = func() (*provenanceStub, error) {
var provenance *provenanceStub
for _, dgst := range refs {
mfst, ok := r.manifests[dgst]
if !ok {
return nil, errors.Errorf("referenced image %s not found", dgst)
}
for _, layer := range mfst.manifest.Layers {
if layer.MediaType == "application/vnd.in-toto+json" && strings.HasPrefix(layer.Annotations["in-toto.io/predicate-type"], "https://slsa.dev/provenance/") {
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, layer)
if err != nil {
return nil, err
}
dt, err := content.ReadBlob(ctx, l.cache, layer)
if err != nil {
return nil, err
}
var slsa struct {
Predicate interface{} `json:"predicate"`
}
if err := json.Unmarshal(dt, &slsa); err != nil {
return nil, err
}
provenance = &provenanceStub{
SLSA: slsa.Predicate,
}
break
}
}
}
return provenance, nil
}
return nil
}
func (r *result) Configs() map[string]*ocispec.Image {
if len(r.assets) == 0 {
return nil
}
res := make(map[string]*ocispec.Image)
for p, a := range r.assets {
if a.config == nil {
continue
}
res[p] = a.config
}
return res
}
func (r *result) Provenance() (map[string]provenanceStub, error) {
if len(r.assets) == 0 {
return nil, nil
}
res := make(map[string]provenanceStub)
for p, a := range r.assets {
if a.deferredProvenance == nil {
continue
}
if a.provenance == nil {
provenance, err := a.deferredProvenance()
if err != nil {
return nil, err
}
if provenance == nil {
continue
}
a.provenance = provenance
}
res[p] = *a.provenance
}
return res, nil
}
func (r *result) SBOM() (map[string]sbomStub, error) {
if len(r.assets) == 0 {
return nil, nil
}
res := make(map[string]sbomStub)
for p, a := range r.assets {
if a.deferredSbom == nil {
continue
}
if a.sbom == nil {
sbom, err := a.deferredSbom()
if err != nil {
return nil, err
}
if sbom == nil {
continue
}
a.sbom = sbom
}
res[p] = *a.sbom
}
return res, nil
}