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

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>
418 lines
9.5 KiB
Go
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
|
|
}
|