buildx/util/imagetools/create.go
Justin Chadwell d3412f1039 imagetools: refactor combining repository logic
This patch modifies the existing combining code in imagetools create to
provide better support for multiple repositories down the road.
Specifically, the code should no longer rely on a single repository
being used for all sources and tags, and should resolve descriptors in
their relevant repositories.

Signed-off-by: Justin Chadwell <me@jedevc.com>
2022-07-29 14:21:27 +01:00

226 lines
5.2 KiB
Go

package imagetools
import (
"bytes"
"context"
"encoding/json"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/platforms"
"github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
type Source struct {
Desc ocispec.Descriptor
Ref reference.Named
}
func (r *Resolver) Combine(ctx context.Context, srcs []*Source) ([]byte, ocispec.Descriptor, error) {
eg, ctx := errgroup.WithContext(ctx)
dts := make([][]byte, len(srcs))
for i := range dts {
func(i int) {
eg.Go(func() error {
dt, err := r.GetDescriptor(ctx, srcs[i].Ref.String(), srcs[i].Desc)
if err != nil {
return err
}
dts[i] = dt
if srcs[i].Desc.MediaType == "" {
mt, err := detectMediaType(dt)
if err != nil {
return err
}
srcs[i].Desc.MediaType = mt
}
mt := srcs[i].Desc.MediaType
switch mt {
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
p := srcs[i].Desc.Platform
if srcs[i].Desc.Platform == nil {
p = &ocispec.Platform{}
}
if p.OS == "" || p.Architecture == "" {
if err := r.loadPlatform(ctx, p, srcs[i].Ref.String(), dt); err != nil {
return err
}
}
srcs[i].Desc.Platform = p
case images.MediaTypeDockerSchema1Manifest:
return errors.Errorf("schema1 manifests are not allowed in manifest lists")
}
return nil
})
}(i)
}
if err := eg.Wait(); err != nil {
return nil, ocispec.Descriptor{}, err
}
// on single source, return original bytes
if len(srcs) == 1 {
if mt := srcs[0].Desc.MediaType; mt == images.MediaTypeDockerSchema2ManifestList || mt == ocispec.MediaTypeImageIndex {
return dts[0], srcs[0].Desc, nil
}
}
m := map[digest.Digest]int{}
newDescs := make([]ocispec.Descriptor, 0, len(srcs))
addDesc := func(d ocispec.Descriptor) {
idx, ok := m[d.Digest]
if ok {
old := newDescs[idx]
if old.MediaType == "" {
old.MediaType = d.MediaType
}
if d.Platform != nil {
old.Platform = d.Platform
}
if old.Annotations == nil {
old.Annotations = map[string]string{}
}
for k, v := range d.Annotations {
old.Annotations[k] = v
}
newDescs[idx] = old
} else {
m[d.Digest] = len(newDescs)
newDescs = append(newDescs, d)
}
}
for i, src := range srcs {
switch src.Desc.MediaType {
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var mfst ocispec.Index
if err := json.Unmarshal(dts[i], &mfst); err != nil {
return nil, ocispec.Descriptor{}, errors.WithStack(err)
}
for _, d := range mfst.Manifests {
addDesc(d)
}
default:
addDesc(src.Desc)
}
}
mt := images.MediaTypeDockerSchema2ManifestList //ocispec.MediaTypeImageIndex
idx := struct {
// MediaType is reserved in the OCI spec but
// excluded from go types.
MediaType string `json:"mediaType,omitempty"`
ocispec.Index
}{
MediaType: mt,
Index: ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
Manifests: newDescs,
},
}
idxBytes, err := json.MarshalIndent(idx, "", " ")
if err != nil {
return nil, ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal index")
}
return idxBytes, ocispec.Descriptor{
MediaType: mt,
Size: int64(len(idxBytes)),
Digest: digest.FromBytes(idxBytes),
}, nil
}
func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispec.Descriptor, dt []byte) error {
ref = reference.TagNameOnly(ref)
p, err := r.resolver().Pusher(ctx, ref.String())
if err != nil {
return err
}
cw, err := p.Push(ctx, desc)
if err != nil {
if errdefs.IsAlreadyExists(err) {
return nil
}
return err
}
err = content.Copy(ctx, cw, bytes.NewReader(dt), desc.Size, desc.Digest)
if errdefs.IsAlreadyExists(err) {
return nil
}
return err
}
func (r *Resolver) loadPlatform(ctx context.Context, p2 *ocispec.Platform, in string, dt []byte) error {
var manifest ocispec.Manifest
if err := json.Unmarshal(dt, &manifest); err != nil {
return errors.WithStack(err)
}
dt, err := r.GetDescriptor(ctx, in, manifest.Config)
if err != nil {
return err
}
var p ocispec.Platform
if err := json.Unmarshal(dt, &p); err != nil {
return errors.WithStack(err)
}
p = platforms.Normalize(p)
if p2.Architecture == "" {
p2.Architecture = p.Architecture
if p2.Variant == "" {
p2.Variant = p.Variant
}
}
if p2.OS == "" {
p2.OS = p.OS
}
return nil
}
func detectMediaType(dt []byte) (string, error) {
var mfst struct {
MediaType string `json:"mediaType"`
Config json.RawMessage `json:"config"`
FSLayers []string `json:"fsLayers"`
}
if err := json.Unmarshal(dt, &mfst); err != nil {
return "", errors.WithStack(err)
}
if mfst.MediaType != "" {
return mfst.MediaType, nil
}
if mfst.Config != nil {
return images.MediaTypeDockerSchema2Manifest, nil
}
if len(mfst.FSLayers) > 0 {
return images.MediaTypeDockerSchema1Manifest, nil
}
return images.MediaTypeDockerSchema2ManifestList, nil
}