mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00

Delay loading the attestation data immediately, and only compute it upon request. We do this using a deferred function which allows to define the computation in the same place as before, but perform the computation later. With this patch, we ensure that the attestation data is only pulled from the remote if it is actually referenced in the format string - otherwise, we can skip it, for improved performance. Signed-off-by: Justin Chadwell <me@jedevc.com>
407 lines
9.2 KiB
Go
407 lines
9.2 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"
|
|
)
|
|
|
|
const (
|
|
annotationReference = "vnd.docker.reference.digest"
|
|
)
|
|
|
|
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()
|
|
|
|
ref, ok := desc.Annotations[annotationReference]
|
|
if ok {
|
|
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()
|
|
} else {
|
|
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
|
|
}
|