mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +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
|
|
}
|