2021-03-30 12:59:03 +00:00
|
|
|
package cache
|
|
|
|
|
|
|
|
import (
|
|
|
|
"compress/gzip"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
|
2021-09-05 07:01:13 +00:00
|
|
|
cdcompression "github.com/containerd/containerd/archive/compression"
|
2021-03-30 12:59:03 +00:00
|
|
|
"github.com/containerd/containerd/content"
|
|
|
|
"github.com/containerd/containerd/errdefs"
|
|
|
|
"github.com/containerd/containerd/images"
|
|
|
|
"github.com/containerd/containerd/images/converter"
|
|
|
|
"github.com/containerd/containerd/labels"
|
2021-09-05 07:01:13 +00:00
|
|
|
"github.com/klauspost/compress/zstd"
|
2021-09-07 22:57:02 +00:00
|
|
|
"github.com/moby/buildkit/identity"
|
2021-03-30 12:59:03 +00:00
|
|
|
"github.com/moby/buildkit/util/compression"
|
2021-07-26 09:47:58 +00:00
|
|
|
digest "github.com/opencontainers/go-digest"
|
2021-07-26 08:53:30 +00:00
|
|
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
2021-07-26 02:00:48 +00:00
|
|
|
"github.com/pkg/errors"
|
2021-03-30 12:59:03 +00:00
|
|
|
)
|
|
|
|
|
2021-09-07 22:57:02 +00:00
|
|
|
// needsConversion indicates whether a conversion is needed for the specified descriptor to
|
2021-07-26 02:00:48 +00:00
|
|
|
// be the compressionType.
|
2021-09-07 22:57:02 +00:00
|
|
|
func needsConversion(ctx context.Context, cs content.Store, desc ocispecs.Descriptor, compressionType compression.Type) (bool, error) {
|
|
|
|
mediaType := desc.MediaType
|
2021-03-30 12:59:03 +00:00
|
|
|
switch compressionType {
|
|
|
|
case compression.Uncompressed:
|
2021-09-05 07:01:13 +00:00
|
|
|
if !images.IsLayerType(mediaType) || compression.FromMediaType(mediaType) == compression.Uncompressed {
|
2021-07-26 02:00:48 +00:00
|
|
|
return false, nil
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
|
|
|
case compression.Gzip:
|
2021-09-07 22:57:02 +00:00
|
|
|
esgz, err := isEStargz(ctx, cs, desc.Digest)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if (!images.IsLayerType(mediaType) || compression.FromMediaType(mediaType) == compression.Gzip) && !esgz {
|
2021-09-05 07:01:13 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
case compression.Zstd:
|
|
|
|
if !images.IsLayerType(mediaType) || compression.FromMediaType(mediaType) == compression.Zstd {
|
2021-07-26 02:00:48 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
case compression.EStargz:
|
2021-09-07 22:57:02 +00:00
|
|
|
esgz, err := isEStargz(ctx, cs, desc.Digest)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if !images.IsLayerType(mediaType) || esgz {
|
2021-07-26 02:00:48 +00:00
|
|
|
return false, nil
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
|
|
|
default:
|
2021-07-26 02:00:48 +00:00
|
|
|
return false, fmt.Errorf("unknown compression type during conversion: %q", compressionType)
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
2021-07-26 02:00:48 +00:00
|
|
|
return true, nil
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
|
|
|
|
2021-07-26 02:00:48 +00:00
|
|
|
// getConverter returns converter function according to the specified compression type.
|
|
|
|
// If no conversion is needed, this returns nil without error.
|
2021-09-07 22:57:02 +00:00
|
|
|
func getConverter(ctx context.Context, cs content.Store, desc ocispecs.Descriptor, compressionType compression.Type) (converter.ConvertFunc, error) {
|
|
|
|
if needs, err := needsConversion(ctx, cs, desc, compressionType); err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "failed to determine conversion needs")
|
2021-07-26 02:00:48 +00:00
|
|
|
} else if !needs {
|
2021-03-30 12:59:03 +00:00
|
|
|
// No conversion. No need to return an error here.
|
|
|
|
return nil, nil
|
|
|
|
}
|
2021-09-05 07:01:13 +00:00
|
|
|
|
|
|
|
c := conversion{target: compressionType}
|
|
|
|
|
|
|
|
from := compression.FromMediaType(desc.MediaType)
|
|
|
|
switch from {
|
|
|
|
case compression.Uncompressed:
|
|
|
|
case compression.Gzip, compression.Zstd:
|
2021-09-07 22:57:02 +00:00
|
|
|
c.decompress = func(ctx context.Context, desc ocispecs.Descriptor) (r io.ReadCloser, err error) {
|
|
|
|
ra, err := cs.ReaderAt(ctx, desc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
esgz, err := isEStargz(ctx, cs, desc.Digest)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if esgz {
|
|
|
|
r, err = decompressEStargz(io.NewSectionReader(ra, 0, ra.Size()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
r, err = cdcompression.DecompressStream(io.NewSectionReader(ra, 0, ra.Size()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &readCloser{r, ra.Close}, nil
|
|
|
|
}
|
2021-09-05 07:01:13 +00:00
|
|
|
default:
|
|
|
|
return nil, errors.Errorf("unsupported source compression type %q from mediatype %q", from, desc.MediaType)
|
|
|
|
}
|
|
|
|
|
2021-07-26 02:00:48 +00:00
|
|
|
switch compressionType {
|
|
|
|
case compression.Uncompressed:
|
|
|
|
case compression.Gzip:
|
2021-09-05 07:01:13 +00:00
|
|
|
c.compress = func(w io.Writer) (io.WriteCloser, error) {
|
|
|
|
return gzip.NewWriter(w), nil
|
|
|
|
}
|
|
|
|
case compression.Zstd:
|
|
|
|
c.compress = func(w io.Writer) (io.WriteCloser, error) {
|
|
|
|
return zstd.NewWriter(w)
|
|
|
|
}
|
2021-07-26 02:00:48 +00:00
|
|
|
case compression.EStargz:
|
2021-09-07 22:57:02 +00:00
|
|
|
compressorFunc, finalize := compressEStargz()
|
2021-09-05 07:01:13 +00:00
|
|
|
c.compress = func(w io.Writer) (io.WriteCloser, error) {
|
|
|
|
return compressorFunc(w, ocispecs.MediaTypeImageLayerGzip)
|
|
|
|
}
|
|
|
|
c.finalize = finalize
|
2021-07-26 02:00:48 +00:00
|
|
|
default:
|
2021-09-05 07:01:13 +00:00
|
|
|
return nil, errors.Errorf("unknown target compression type during conversion: %q", compressionType)
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
2021-09-05 07:01:13 +00:00
|
|
|
|
|
|
|
return (&c).convert, nil
|
2021-07-26 02:00:48 +00:00
|
|
|
}
|
2021-03-30 12:59:03 +00:00
|
|
|
|
2021-09-05 07:01:13 +00:00
|
|
|
type conversion struct {
|
|
|
|
target compression.Type
|
2021-09-07 22:57:02 +00:00
|
|
|
decompress func(context.Context, ocispecs.Descriptor) (io.ReadCloser, error)
|
2021-09-05 07:01:13 +00:00
|
|
|
compress func(w io.Writer) (io.WriteCloser, error)
|
|
|
|
finalize func(context.Context, content.Store) (map[string]string, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *conversion) convert(ctx context.Context, cs content.Store, desc ocispecs.Descriptor) (*ocispecs.Descriptor, error) {
|
|
|
|
// prepare the source and destination
|
2021-09-07 22:57:02 +00:00
|
|
|
labelz := make(map[string]string)
|
|
|
|
ref := fmt.Sprintf("convert-from-%s-to-%s-%s", desc.Digest, c.target.String(), identity.NewID())
|
2021-09-05 07:01:13 +00:00
|
|
|
w, err := cs.Writer(ctx, content.WithRef(ref))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer w.Close()
|
|
|
|
if err := w.Truncate(0); err != nil { // Old written data possibly remains
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
var zw io.WriteCloser = w
|
|
|
|
var compress io.WriteCloser
|
|
|
|
if c.compress != nil {
|
|
|
|
zw, err = c.compress(zw)
|
2021-07-26 02:00:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer zw.Close()
|
2021-09-05 07:01:13 +00:00
|
|
|
compress = zw
|
|
|
|
}
|
2021-07-26 02:00:48 +00:00
|
|
|
|
2021-09-05 07:01:13 +00:00
|
|
|
// convert this layer
|
|
|
|
diffID := digest.Canonical.Digester()
|
2021-09-07 22:57:02 +00:00
|
|
|
var rdr io.Reader
|
|
|
|
if c.decompress == nil {
|
|
|
|
ra, err := cs.ReaderAt(ctx, desc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer ra.Close()
|
|
|
|
rdr = io.NewSectionReader(ra, 0, ra.Size())
|
|
|
|
} else {
|
|
|
|
rc, err := c.decompress(ctx, desc)
|
2021-07-26 02:00:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-09-05 07:01:13 +00:00
|
|
|
defer rc.Close()
|
|
|
|
rdr = rc
|
|
|
|
}
|
|
|
|
if _, err := io.Copy(zw, io.TeeReader(rdr, diffID.Hash())); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if compress != nil {
|
|
|
|
if err := compress.Close(); err != nil { // Flush the writer
|
|
|
|
return nil, err
|
2021-07-26 02:00:48 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-05 07:01:13 +00:00
|
|
|
labelz[labels.LabelUncompressed] = diffID.Digest().String() // update diffID label
|
|
|
|
if err = w.Commit(ctx, 0, "", content.WithLabels(labelz)); err != nil && !errdefs.IsAlreadyExists(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-09-07 22:57:02 +00:00
|
|
|
info, err := cs.Info(ctx, w.Digest())
|
2021-09-05 07:01:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
|
|
|
|
2021-09-05 07:01:13 +00:00
|
|
|
newDesc := desc
|
|
|
|
newDesc.MediaType = c.target.DefaultMediaType()
|
|
|
|
newDesc.Digest = info.Digest
|
|
|
|
newDesc.Size = info.Size
|
2021-09-07 22:57:02 +00:00
|
|
|
newDesc.Annotations = nil
|
2021-09-05 07:01:13 +00:00
|
|
|
if c.finalize != nil {
|
|
|
|
a, err := c.finalize(ctx, cs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "failed finalize compression")
|
|
|
|
}
|
|
|
|
for k, v := range a {
|
|
|
|
if newDesc.Annotations == nil {
|
|
|
|
newDesc.Annotations = make(map[string]string)
|
|
|
|
}
|
|
|
|
newDesc.Annotations[k] = v
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-05 07:01:13 +00:00
|
|
|
return &newDesc, nil
|
2021-03-30 12:59:03 +00:00
|
|
|
}
|
2021-09-07 22:57:02 +00:00
|
|
|
|
|
|
|
type readCloser struct {
|
|
|
|
io.ReadCloser
|
|
|
|
closeFunc func() error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rc *readCloser) Close() error {
|
|
|
|
err1 := rc.ReadCloser.Close()
|
|
|
|
err2 := rc.closeFunc()
|
|
|
|
if err1 != nil {
|
|
|
|
return errors.Wrapf(err1, "failed to close: %v", err2)
|
|
|
|
}
|
|
|
|
return err2
|
|
|
|
}
|