github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/util/imagetools/create.go (about)

     1  package imagetools
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"maps"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"github.com/containerd/containerd/content"
    12  	"github.com/containerd/containerd/errdefs"
    13  	"github.com/containerd/containerd/images"
    14  	"github.com/containerd/containerd/platforms"
    15  	"github.com/containerd/containerd/remotes"
    16  	"github.com/distribution/reference"
    17  	"github.com/docker/buildx/util/buildflags"
    18  	"github.com/moby/buildkit/exporter/containerimage/exptypes"
    19  	"github.com/moby/buildkit/util/contentutil"
    20  	"github.com/opencontainers/go-digest"
    21  	"github.com/opencontainers/image-spec/specs-go"
    22  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    23  	"github.com/pkg/errors"
    24  	"golang.org/x/sync/errgroup"
    25  )
    26  
    27  type Source struct {
    28  	Desc ocispec.Descriptor
    29  	Ref  reference.Named
    30  }
    31  
    32  func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann []string) ([]byte, ocispec.Descriptor, error) {
    33  	eg, ctx := errgroup.WithContext(ctx)
    34  
    35  	dts := make([][]byte, len(srcs))
    36  	for i := range dts {
    37  		func(i int) {
    38  			eg.Go(func() error {
    39  				dt, err := r.GetDescriptor(ctx, srcs[i].Ref.String(), srcs[i].Desc)
    40  				if err != nil {
    41  					return err
    42  				}
    43  				dts[i] = dt
    44  
    45  				if srcs[i].Desc.MediaType == "" {
    46  					mt, err := detectMediaType(dt)
    47  					if err != nil {
    48  						return err
    49  					}
    50  					srcs[i].Desc.MediaType = mt
    51  				}
    52  
    53  				mt := srcs[i].Desc.MediaType
    54  
    55  				switch mt {
    56  				case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
    57  					p := srcs[i].Desc.Platform
    58  					if srcs[i].Desc.Platform == nil {
    59  						p = &ocispec.Platform{}
    60  					}
    61  					if p.OS == "" || p.Architecture == "" {
    62  						if err := r.loadPlatform(ctx, p, srcs[i].Ref.String(), dt); err != nil {
    63  							return err
    64  						}
    65  					}
    66  					srcs[i].Desc.Platform = p
    67  				case images.MediaTypeDockerSchema1Manifest:
    68  					return errors.Errorf("schema1 manifests are not allowed in manifest lists")
    69  				}
    70  
    71  				return nil
    72  			})
    73  		}(i)
    74  	}
    75  
    76  	if err := eg.Wait(); err != nil {
    77  		return nil, ocispec.Descriptor{}, err
    78  	}
    79  
    80  	// on single source, return original bytes
    81  	if len(srcs) == 1 && len(ann) == 0 {
    82  		if mt := srcs[0].Desc.MediaType; mt == images.MediaTypeDockerSchema2ManifestList || mt == ocispec.MediaTypeImageIndex {
    83  			return dts[0], srcs[0].Desc, nil
    84  		}
    85  	}
    86  
    87  	m := map[digest.Digest]int{}
    88  	newDescs := make([]ocispec.Descriptor, 0, len(srcs))
    89  
    90  	addDesc := func(d ocispec.Descriptor) {
    91  		idx, ok := m[d.Digest]
    92  		if ok {
    93  			old := newDescs[idx]
    94  			if old.MediaType == "" {
    95  				old.MediaType = d.MediaType
    96  			}
    97  			if d.Platform != nil {
    98  				old.Platform = d.Platform
    99  			}
   100  			if old.Annotations == nil {
   101  				old.Annotations = map[string]string{}
   102  			}
   103  			for k, v := range d.Annotations {
   104  				old.Annotations[k] = v
   105  			}
   106  			newDescs[idx] = old
   107  		} else {
   108  			m[d.Digest] = len(newDescs)
   109  			newDescs = append(newDescs, d)
   110  		}
   111  	}
   112  
   113  	for i, src := range srcs {
   114  		switch src.Desc.MediaType {
   115  		case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
   116  			var mfst ocispec.Index
   117  			if err := json.Unmarshal(dts[i], &mfst); err != nil {
   118  				return nil, ocispec.Descriptor{}, errors.WithStack(err)
   119  			}
   120  			for _, d := range mfst.Manifests {
   121  				addDesc(d)
   122  			}
   123  		default:
   124  			addDesc(src.Desc)
   125  		}
   126  	}
   127  
   128  	dockerMfsts := 0
   129  	for _, desc := range newDescs {
   130  		if strings.HasPrefix(desc.MediaType, "application/vnd.docker.") {
   131  			dockerMfsts++
   132  		}
   133  	}
   134  
   135  	var mt string
   136  	if dockerMfsts == len(newDescs) {
   137  		// all manifests are Docker types, use Docker manifest list
   138  		mt = images.MediaTypeDockerSchema2ManifestList
   139  	} else {
   140  		// otherwise, use OCI index
   141  		mt = ocispec.MediaTypeImageIndex
   142  	}
   143  
   144  	// annotations are only allowed on OCI indexes
   145  	indexAnnotation := make(map[string]string)
   146  	if mt == ocispec.MediaTypeImageIndex {
   147  		annotations, err := buildflags.ParseAnnotations(ann)
   148  		if err != nil {
   149  			return nil, ocispec.Descriptor{}, err
   150  		}
   151  		for k, v := range annotations {
   152  			switch k.Type {
   153  			case exptypes.AnnotationIndex:
   154  				indexAnnotation[k.Key] = v
   155  			case exptypes.AnnotationManifestDescriptor:
   156  				for i := 0; i < len(newDescs); i++ {
   157  					if newDescs[i].Annotations == nil {
   158  						newDescs[i].Annotations = map[string]string{}
   159  					}
   160  					if k.Platform == nil || k.PlatformString() == platforms.Format(*newDescs[i].Platform) {
   161  						newDescs[i].Annotations[k.Key] = v
   162  					}
   163  				}
   164  			case exptypes.AnnotationManifest, "":
   165  				return nil, ocispec.Descriptor{}, errors.Errorf("%q annotations are not supported yet", k.Type)
   166  			case exptypes.AnnotationIndexDescriptor:
   167  				return nil, ocispec.Descriptor{}, errors.Errorf("%q annotations are invalid while creating an image", k.Type)
   168  			}
   169  		}
   170  	}
   171  
   172  	idxBytes, err := json.MarshalIndent(ocispec.Index{
   173  		MediaType: mt,
   174  		Versioned: specs.Versioned{
   175  			SchemaVersion: 2,
   176  		},
   177  		Manifests:   newDescs,
   178  		Annotations: indexAnnotation,
   179  	}, "", "  ")
   180  	if err != nil {
   181  		return nil, ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal index")
   182  	}
   183  
   184  	return idxBytes, ocispec.Descriptor{
   185  		MediaType: mt,
   186  		Size:      int64(len(idxBytes)),
   187  		Digest:    digest.FromBytes(idxBytes),
   188  	}, nil
   189  }
   190  
   191  func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispec.Descriptor, dt []byte) error {
   192  	ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
   193  
   194  	ref = reference.TagNameOnly(ref)
   195  	p, err := r.resolver().Pusher(ctx, ref.String())
   196  	if err != nil {
   197  		return err
   198  	}
   199  	cw, err := p.Push(ctx, desc)
   200  	if err != nil {
   201  		if errdefs.IsAlreadyExists(err) {
   202  			return nil
   203  		}
   204  		return err
   205  	}
   206  
   207  	err = content.Copy(ctx, cw, bytes.NewReader(dt), desc.Size, desc.Digest)
   208  	if errdefs.IsAlreadyExists(err) {
   209  		return nil
   210  	}
   211  	return err
   212  }
   213  
   214  func (r *Resolver) Copy(ctx context.Context, src *Source, dest reference.Named) error {
   215  	ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto")
   216  
   217  	dest = reference.TagNameOnly(dest)
   218  	p, err := r.resolver().Pusher(ctx, dest.String())
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	srcRef := reference.TagNameOnly(src.Ref)
   224  	f, err := r.resolver().Fetcher(ctx, srcRef.String())
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	refspec := reference.TrimNamed(src.Ref).String()
   230  	u, err := url.Parse("dummy://" + refspec)
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	desc := src.Desc
   236  	desc.Annotations = maps.Clone(desc.Annotations)
   237  	if desc.Annotations == nil {
   238  		desc.Annotations = make(map[string]string)
   239  	}
   240  
   241  	source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/")
   242  	desc.Annotations["containerd.io/distribution.source."+source] = repo
   243  
   244  	err = contentutil.CopyChain(ctx, contentutil.FromPusher(p), contentutil.FromFetcher(f), desc)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	return nil
   249  }
   250  
   251  func (r *Resolver) loadPlatform(ctx context.Context, p2 *ocispec.Platform, in string, dt []byte) error {
   252  	var manifest ocispec.Manifest
   253  	if err := json.Unmarshal(dt, &manifest); err != nil {
   254  		return errors.WithStack(err)
   255  	}
   256  
   257  	dt, err := r.GetDescriptor(ctx, in, manifest.Config)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	var p ocispec.Platform
   263  	if err := json.Unmarshal(dt, &p); err != nil {
   264  		return errors.WithStack(err)
   265  	}
   266  
   267  	p = platforms.Normalize(p)
   268  
   269  	if p2.Architecture == "" {
   270  		p2.Architecture = p.Architecture
   271  		if p2.Variant == "" {
   272  			p2.Variant = p.Variant
   273  		}
   274  	}
   275  	if p2.OS == "" {
   276  		p2.OS = p.OS
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  func detectMediaType(dt []byte) (string, error) {
   283  	var mfst struct {
   284  		MediaType string          `json:"mediaType"`
   285  		Config    json.RawMessage `json:"config"`
   286  		FSLayers  []string        `json:"fsLayers"`
   287  	}
   288  
   289  	if err := json.Unmarshal(dt, &mfst); err != nil {
   290  		return "", errors.WithStack(err)
   291  	}
   292  
   293  	if mfst.MediaType != "" {
   294  		return mfst.MediaType, nil
   295  	}
   296  	if mfst.Config != nil {
   297  		return images.MediaTypeDockerSchema2Manifest, nil
   298  	}
   299  	if len(mfst.FSLayers) > 0 {
   300  		return images.MediaTypeDockerSchema1Manifest, nil
   301  	}
   302  
   303  	return images.MediaTypeDockerSchema2ManifestList, nil
   304  }