mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 01:53:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			283 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package bundle
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"compress/gzip"
 | 
						|
	"context"
 | 
						|
	"encoding/json"
 | 
						|
	"io"
 | 
						|
 | 
						|
	"github.com/containerd/containerd/v2/core/content"
 | 
						|
	"github.com/containerd/containerd/v2/core/content/proxy"
 | 
						|
	imgarchive "github.com/containerd/containerd/v2/core/images/archive"
 | 
						|
	"github.com/docker/buildx/localstate"
 | 
						|
	controlapi "github.com/moby/buildkit/api/services/control"
 | 
						|
	"github.com/moby/buildkit/client"
 | 
						|
	"github.com/moby/buildkit/util/contentutil"
 | 
						|
	"github.com/opencontainers/go-digest"
 | 
						|
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	HistoryRecordMediaTypeV0 = "application/vnd.buildkit.historyrecord.v0"
 | 
						|
	RefDescriptorMediaType   = "vnd.export-build.descriptor.mediatype"
 | 
						|
)
 | 
						|
 | 
						|
type Record struct {
 | 
						|
	*controlapi.BuildHistoryRecord
 | 
						|
 | 
						|
	DefaultPlatform string
 | 
						|
	LocalState      *localstate.State      `json:"localState,omitempty"`
 | 
						|
	StateGroup      *localstate.StateGroup `json:"stateGroup,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
func Export(ctx context.Context, c []*client.Client, w io.Writer, records []*Record) error {
 | 
						|
	var store content.Store
 | 
						|
	for _, c := range c {
 | 
						|
		s := proxy.NewContentStore(c.ContentClient())
 | 
						|
		if store == nil {
 | 
						|
			store = s
 | 
						|
			break
 | 
						|
		}
 | 
						|
		store = &nsFallbackStore{
 | 
						|
			main: store,
 | 
						|
			fb:   s,
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if store == nil {
 | 
						|
		return errors.New("no buildkit client found")
 | 
						|
	}
 | 
						|
 | 
						|
	mp := contentutil.NewMultiProvider(store)
 | 
						|
 | 
						|
	desc, err := export(ctx, mp, records)
 | 
						|
	if err != nil {
 | 
						|
		return errors.Wrap(err, "failed to export")
 | 
						|
	}
 | 
						|
 | 
						|
	gz := gzip.NewWriter(w)
 | 
						|
	defer gz.Close()
 | 
						|
 | 
						|
	if err := imgarchive.Export(ctx, mp, gz, imgarchive.WithManifest(desc), imgarchive.WithSkipDockerManifest()); err != nil {
 | 
						|
		return errors.Wrap(err, "failed to create dockerbuild archive")
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func export(ctx context.Context, mp *contentutil.MultiProvider, records []*Record) (ocispecs.Descriptor, error) {
 | 
						|
	if len(records) == 1 {
 | 
						|
		desc, err := exportRecord(ctx, mp, records[0])
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to export record")
 | 
						|
		}
 | 
						|
		return desc, nil
 | 
						|
	}
 | 
						|
 | 
						|
	var idx ocispecs.Index
 | 
						|
	idx.MediaType = ocispecs.MediaTypeImageIndex
 | 
						|
	idx.SchemaVersion = 2
 | 
						|
 | 
						|
	for _, r := range records {
 | 
						|
		desc, err := exportRecord(ctx, mp, r)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to export record")
 | 
						|
		}
 | 
						|
		if desc.Annotations == nil {
 | 
						|
			desc.Annotations = make(map[string]string)
 | 
						|
		}
 | 
						|
		desc.Annotations["vnd.buildkit.history.reference"] = r.Ref
 | 
						|
		idx.Manifests = append(idx.Manifests, desc)
 | 
						|
	}
 | 
						|
 | 
						|
	desc, err := writeJSON(ctx, mp, idx.MediaType, idx)
 | 
						|
	if err != nil {
 | 
						|
		return ocispecs.Descriptor{}, errors.Wrap(err, "failed to write index")
 | 
						|
	}
 | 
						|
 | 
						|
	return desc, nil
 | 
						|
}
 | 
						|
 | 
						|
func writeJSON(ctx context.Context, mp *contentutil.MultiProvider, mt string, data any) (ocispecs.Descriptor, error) {
 | 
						|
	dt, err := json.MarshalIndent(data, "", "  ")
 | 
						|
	if err != nil {
 | 
						|
		return ocispecs.Descriptor{}, errors.Wrap(err, "failed to marshal data")
 | 
						|
	}
 | 
						|
 | 
						|
	desc := ocispecs.Descriptor{
 | 
						|
		MediaType: mt,
 | 
						|
		Size:      int64(len(dt)),
 | 
						|
		Digest:    digest.FromBytes(dt),
 | 
						|
	}
 | 
						|
 | 
						|
	buf := contentutil.NewBuffer()
 | 
						|
	if err := content.WriteBlob(ctx, buf, "blob-"+desc.Digest.String(), bytes.NewReader(dt), desc); err != nil {
 | 
						|
		return ocispecs.Descriptor{}, errors.Wrap(err, "failed to write blob")
 | 
						|
	}
 | 
						|
 | 
						|
	mp.Add(desc.Digest, buf)
 | 
						|
	return desc, nil
 | 
						|
}
 | 
						|
 | 
						|
func sanitizeCacheImports(v string) (string, error) {
 | 
						|
	type cacheImport struct {
 | 
						|
		Type  string            `json:"Type"`
 | 
						|
		Attrs map[string]string `json:"Attrs"`
 | 
						|
	}
 | 
						|
	var arr []cacheImport
 | 
						|
	if err := json.Unmarshal([]byte(v), &arr); err != nil {
 | 
						|
		return "", errors.Wrap(err, "failed to unmarshal cache imports")
 | 
						|
	}
 | 
						|
	for i := range arr {
 | 
						|
		m := map[string]string{}
 | 
						|
		for k, v := range arr[i].Attrs {
 | 
						|
			if k == "scope" || k == "ref" {
 | 
						|
				m[k] = v
 | 
						|
			}
 | 
						|
		}
 | 
						|
		arr[i].Attrs = m
 | 
						|
	}
 | 
						|
	dt, err := json.Marshal(arr)
 | 
						|
	if err != nil {
 | 
						|
		return "", errors.Wrap(err, "failed to marshal cache imports")
 | 
						|
	}
 | 
						|
	return string(dt), nil
 | 
						|
}
 | 
						|
 | 
						|
func sanitizeRecord(rec *controlapi.BuildHistoryRecord) {
 | 
						|
	for k, v := range rec.FrontendAttrs {
 | 
						|
		if k == "cache-imports" {
 | 
						|
			v, err := sanitizeCacheImports(v)
 | 
						|
			if err != nil {
 | 
						|
				rec.FrontendAttrs[k] = ""
 | 
						|
			} else {
 | 
						|
				rec.FrontendAttrs[k] = v
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func exportRecord(ctx context.Context, mp *contentutil.MultiProvider, record *Record) (ocispecs.Descriptor, error) {
 | 
						|
	var mfst ocispecs.Manifest
 | 
						|
	mfst.MediaType = ocispecs.MediaTypeImageManifest
 | 
						|
	mfst.SchemaVersion = 2
 | 
						|
 | 
						|
	sanitizeRecord(record.BuildHistoryRecord)
 | 
						|
 | 
						|
	visited := map[string]struct{}{}
 | 
						|
 | 
						|
	if trace := record.Trace; trace != nil {
 | 
						|
		desc, err := loadDescriptor(ctx, mp, trace, visited)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to load trace descriptor")
 | 
						|
		}
 | 
						|
		desc, err = sanitizeTrace(ctx, mp, *desc)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to sanitize trace")
 | 
						|
		}
 | 
						|
		record.Trace.Digest = desc.Digest.String()
 | 
						|
		record.Trace.Size = desc.Size
 | 
						|
		mfst.Layers = append(mfst.Layers, *desc)
 | 
						|
	}
 | 
						|
 | 
						|
	config, err := writeJSON(ctx, mp, HistoryRecordMediaTypeV0, record)
 | 
						|
	if err != nil {
 | 
						|
		return ocispecs.Descriptor{}, errors.Wrap(err, "failed to write history record")
 | 
						|
	}
 | 
						|
 | 
						|
	mfst.Config = config
 | 
						|
 | 
						|
	if logs := record.Logs; logs != nil {
 | 
						|
		desc, err := loadDescriptor(ctx, mp, logs, visited)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to load logs descriptor")
 | 
						|
		}
 | 
						|
		mfst.Layers = append(mfst.Layers, *desc)
 | 
						|
	}
 | 
						|
 | 
						|
	if res := record.Result; res != nil {
 | 
						|
		results, err := loadResult(ctx, mp, res, visited)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to load result")
 | 
						|
		}
 | 
						|
		mfst.Layers = append(mfst.Layers, results...)
 | 
						|
	}
 | 
						|
 | 
						|
	if exterr := record.ExternalError; exterr != nil {
 | 
						|
		desc, err := loadDescriptor(ctx, mp, exterr, visited)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to load external error descriptor")
 | 
						|
		}
 | 
						|
		mfst.Layers = append(mfst.Layers, *desc)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, res := range record.Results {
 | 
						|
		results, err := loadResult(ctx, mp, res, visited)
 | 
						|
		if err != nil {
 | 
						|
			return ocispecs.Descriptor{}, errors.Wrap(err, "failed to load result")
 | 
						|
		}
 | 
						|
		mfst.Layers = append(mfst.Layers, results...)
 | 
						|
	}
 | 
						|
 | 
						|
	desc, err := writeJSON(ctx, mp, mfst.MediaType, mfst)
 | 
						|
	if err != nil {
 | 
						|
		return ocispecs.Descriptor{}, errors.Wrap(err, "failed to write manifest")
 | 
						|
	}
 | 
						|
 | 
						|
	return desc, nil
 | 
						|
}
 | 
						|
 | 
						|
func loadResult(ctx context.Context, ip content.InfoProvider, in *controlapi.BuildResultInfo, visited map[string]struct{}) ([]ocispecs.Descriptor, error) {
 | 
						|
	var out []ocispecs.Descriptor
 | 
						|
	for _, attest := range in.Attestations {
 | 
						|
		desc, err := loadDescriptor(ctx, ip, attest, visited)
 | 
						|
		if err != nil {
 | 
						|
			return nil, errors.Wrap(err, "failed to load attestation descriptor")
 | 
						|
		}
 | 
						|
		if desc != nil {
 | 
						|
			out = append(out, *desc)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for _, r := range in.Results {
 | 
						|
		desc, err := loadDescriptor(ctx, ip, r, visited)
 | 
						|
		if err != nil {
 | 
						|
			return nil, errors.Wrap(err, "failed to load result descriptor")
 | 
						|
		}
 | 
						|
		if desc != nil {
 | 
						|
			if desc.Annotations == nil {
 | 
						|
				desc.Annotations = make(map[string]string)
 | 
						|
			}
 | 
						|
			// Override media type to avoid containerd to walk children. Also
 | 
						|
			// keep original media type in annotations.
 | 
						|
			desc.Annotations[RefDescriptorMediaType] = desc.MediaType
 | 
						|
			desc.MediaType = "application/json"
 | 
						|
			out = append(out, *desc)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return out, nil
 | 
						|
}
 | 
						|
 | 
						|
func loadDescriptor(ctx context.Context, ip content.InfoProvider, in *controlapi.Descriptor, visited map[string]struct{}) (*ocispecs.Descriptor, error) {
 | 
						|
	if _, ok := visited[in.Digest]; ok {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
	visited[in.Digest] = struct{}{}
 | 
						|
 | 
						|
	dgst, err := digest.Parse(in.Digest)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to parse digest")
 | 
						|
	}
 | 
						|
 | 
						|
	if _, err := ip.Info(ctx, dgst); err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to get info")
 | 
						|
	}
 | 
						|
 | 
						|
	return &ocispecs.Descriptor{
 | 
						|
		MediaType:   in.MediaType,
 | 
						|
		Digest:      dgst,
 | 
						|
		Size:        in.Size,
 | 
						|
		Annotations: in.Annotations,
 | 
						|
	}, nil
 | 
						|
}
 |