github.com/moby/docker@v26.1.3+incompatible/builder/builder-next/exporter/mobyexporter/export.go (about)

     1  package mobyexporter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/containerd/containerd/content"
    10  	"github.com/containerd/containerd/leases"
    11  	"github.com/containerd/log"
    12  	distref "github.com/distribution/reference"
    13  	"github.com/docker/docker/image"
    14  	"github.com/docker/docker/internal/compatcontext"
    15  	"github.com/docker/docker/layer"
    16  	"github.com/moby/buildkit/exporter"
    17  	"github.com/moby/buildkit/exporter/containerimage"
    18  	"github.com/moby/buildkit/exporter/containerimage/exptypes"
    19  	"github.com/moby/buildkit/util/leaseutil"
    20  	"github.com/opencontainers/go-digest"
    21  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  // Differ can make a moby layer from a snapshot
    26  type Differ interface {
    27  	EnsureLayer(ctx context.Context, key string) ([]layer.DiffID, error)
    28  }
    29  
    30  type ImageTagger interface {
    31  	TagImage(ctx context.Context, imageID image.ID, newTag distref.Named) error
    32  }
    33  
    34  // Opt defines a struct for creating new exporter
    35  type Opt struct {
    36  	ImageStore   image.Store
    37  	Differ       Differ
    38  	ImageTagger  ImageTagger
    39  	ContentStore content.Store
    40  	LeaseManager leases.Manager
    41  }
    42  
    43  type imageExporter struct {
    44  	opt Opt
    45  }
    46  
    47  // New creates a new moby imagestore exporter
    48  func New(opt Opt) (exporter.Exporter, error) {
    49  	im := &imageExporter{opt: opt}
    50  	return im, nil
    51  }
    52  
    53  func (e *imageExporter) Resolve(ctx context.Context, id int, opt map[string]string) (exporter.ExporterInstance, error) {
    54  	i := &imageExporterInstance{
    55  		imageExporter: e,
    56  		id:            id,
    57  	}
    58  	for k, v := range opt {
    59  		switch exptypes.ImageExporterOptKey(k) {
    60  		case exptypes.OptKeyName:
    61  			for _, v := range strings.Split(v, ",") {
    62  				ref, err := distref.ParseNormalizedNamed(v)
    63  				if err != nil {
    64  					return nil, err
    65  				}
    66  				i.targetNames = append(i.targetNames, ref)
    67  			}
    68  		default:
    69  			if i.meta == nil {
    70  				i.meta = make(map[string][]byte)
    71  			}
    72  			i.meta[k] = []byte(v)
    73  		}
    74  	}
    75  	return i, nil
    76  }
    77  
    78  type imageExporterInstance struct {
    79  	*imageExporter
    80  	id          int
    81  	targetNames []distref.Named
    82  	meta        map[string][]byte
    83  }
    84  
    85  func (e *imageExporterInstance) ID() int {
    86  	return e.id
    87  }
    88  
    89  func (e *imageExporterInstance) Name() string {
    90  	return "exporting to image"
    91  }
    92  
    93  func (e *imageExporterInstance) Config() *exporter.Config {
    94  	return exporter.NewConfig()
    95  }
    96  
    97  func (e *imageExporterInstance) Export(ctx context.Context, inp *exporter.Source, inlineCache exptypes.InlineCache, sessionID string) (map[string]string, exporter.DescriptorReference, error) {
    98  	if len(inp.Refs) > 1 {
    99  		return nil, nil, fmt.Errorf("exporting multiple references to image store is currently unsupported")
   100  	}
   101  
   102  	ref := inp.Ref
   103  	if ref != nil && len(inp.Refs) == 1 {
   104  		return nil, nil, fmt.Errorf("invalid exporter input: Ref and Refs are mutually exclusive")
   105  	}
   106  
   107  	// only one loop
   108  	for _, v := range inp.Refs {
   109  		ref = v
   110  	}
   111  
   112  	var config []byte
   113  	switch len(inp.Refs) {
   114  	case 0:
   115  		config = inp.Metadata[exptypes.ExporterImageConfigKey]
   116  	case 1:
   117  		ps, err := exptypes.ParsePlatforms(inp.Metadata)
   118  		if err != nil {
   119  			return nil, nil, fmt.Errorf("cannot export image, failed to parse platforms: %w", err)
   120  		}
   121  		if len(ps.Platforms) != len(inp.Refs) {
   122  			return nil, nil, errors.Errorf("number of platforms does not match references %d %d", len(ps.Platforms), len(inp.Refs))
   123  		}
   124  		config = inp.Metadata[fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, ps.Platforms[0].ID)]
   125  	}
   126  
   127  	var diffs []digest.Digest
   128  	if ref != nil {
   129  		layersDone := oneOffProgress(ctx, "exporting layers")
   130  
   131  		if err := ref.Finalize(ctx); err != nil {
   132  			return nil, nil, layersDone(err)
   133  		}
   134  
   135  		if err := ref.Extract(ctx, nil); err != nil {
   136  			return nil, nil, err
   137  		}
   138  
   139  		diffIDs, err := e.opt.Differ.EnsureLayer(ctx, ref.ID())
   140  		if err != nil {
   141  			return nil, nil, layersDone(err)
   142  		}
   143  
   144  		diffs = make([]digest.Digest, len(diffIDs))
   145  		for i := range diffIDs {
   146  			diffs[i] = digest.Digest(diffIDs[i])
   147  		}
   148  
   149  		_ = layersDone(nil)
   150  	}
   151  
   152  	if len(config) == 0 {
   153  		var err error
   154  		config, err = emptyImageConfig()
   155  		if err != nil {
   156  			return nil, nil, err
   157  		}
   158  	}
   159  
   160  	history, err := parseHistoryFromConfig(config)
   161  	if err != nil {
   162  		return nil, nil, err
   163  	}
   164  
   165  	diffs, history = normalizeLayersAndHistory(diffs, history, ref)
   166  
   167  	var inlineCacheEntry *exptypes.InlineCacheEntry
   168  	if inlineCache != nil {
   169  		inlineCacheResult, err := inlineCache(ctx)
   170  		if err != nil {
   171  			return nil, nil, err
   172  		}
   173  		if inlineCacheResult != nil {
   174  			if ref != nil {
   175  				inlineCacheEntry, _ = inlineCacheResult.FindRef(ref.ID())
   176  			} else {
   177  				inlineCacheEntry = inlineCacheResult.Ref
   178  			}
   179  		}
   180  	}
   181  	config, err = patchImageConfig(config, diffs, history, inlineCacheEntry)
   182  	if err != nil {
   183  		return nil, nil, err
   184  	}
   185  
   186  	configDigest := digest.FromBytes(config)
   187  
   188  	configDone := oneOffProgress(ctx, fmt.Sprintf("writing image %s", configDigest))
   189  	id, err := e.opt.ImageStore.Create(config)
   190  	if err != nil {
   191  		return nil, nil, configDone(err)
   192  	}
   193  	_ = configDone(nil)
   194  
   195  	var names []string
   196  	for _, targetName := range e.targetNames {
   197  		names = append(names, targetName.String())
   198  		if e.opt.ImageTagger != nil {
   199  			tagDone := oneOffProgress(ctx, "naming to "+targetName.String())
   200  			if err := e.opt.ImageTagger.TagImage(ctx, image.ID(digest.Digest(id)), targetName); err != nil {
   201  				return nil, nil, tagDone(err)
   202  			}
   203  			_ = tagDone(nil)
   204  		}
   205  	}
   206  
   207  	resp := map[string]string{
   208  		exptypes.ExporterImageConfigDigestKey: configDigest.String(),
   209  		exptypes.ExporterImageDigestKey:       id.String(),
   210  	}
   211  	if len(names) > 0 {
   212  		resp["image.name"] = strings.Join(names, ",")
   213  	}
   214  
   215  	descRef, err := e.newTempReference(ctx, config)
   216  	if err != nil {
   217  		return nil, nil, fmt.Errorf("failed to create a temporary descriptor reference: %w", err)
   218  	}
   219  
   220  	return resp, descRef, nil
   221  }
   222  
   223  func (e *imageExporterInstance) newTempReference(ctx context.Context, config []byte) (exporter.DescriptorReference, error) {
   224  	lm := e.opt.LeaseManager
   225  
   226  	dgst := digest.FromBytes(config)
   227  	leaseCtx, done, err := leaseutil.WithLease(ctx, lm, leaseutil.MakeTemporary)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	unlease := func(ctx context.Context) error {
   233  		err := done(compatcontext.WithoutCancel(ctx))
   234  		if err != nil {
   235  			log.G(ctx).WithError(err).Error("failed to delete descriptor reference lease")
   236  		}
   237  		return err
   238  	}
   239  
   240  	desc := ocispec.Descriptor{
   241  		Digest:    dgst,
   242  		MediaType: "application/vnd.docker.container.image.v1+json",
   243  		Size:      int64(len(config)),
   244  	}
   245  
   246  	if err := content.WriteBlob(leaseCtx, e.opt.ContentStore, desc.Digest.String(), bytes.NewReader(config), desc); err != nil {
   247  		unlease(leaseCtx)
   248  		return nil, fmt.Errorf("failed to save temporary image config: %w", err)
   249  	}
   250  
   251  	return containerimage.NewDescriptorReference(desc, unlease), nil
   252  }