Do not re-tag non-distributable blob descriptors

Before this change buildkit was changing the media type for
non-distributable layers to normal layers.
It was also clearing out the urls to get those blobs.

Now the layer mediatype and URL's are preserved.
If a layer blob is seen more than once, if it has extra URL's they will
be appended to the stored value.

On export there is now a new exporter option to preserve the
non-distributable data values.
All URL's seen by buildkit will be added to the exported content.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
master
Brian Goff 2022-01-14 22:15:50 +00:00
parent 2f996517bc
commit 78bb7137ee
13 changed files with 245 additions and 43 deletions

1
cache/blobs.go vendored
View File

@ -306,6 +306,7 @@ func (sr *immutableRef) setBlob(ctx context.Context, compressionType compression
sr.queueBlob(desc.Digest)
sr.queueMediaType(desc.MediaType)
sr.queueBlobSize(desc.Size)
sr.appendURLs(desc.URLs)
if err := sr.commitMetadata(); err != nil {
return err
}

1
cache/manager.go vendored
View File

@ -283,6 +283,7 @@ func (cm *cacheManager) GetByBlob(ctx context.Context, desc ocispecs.Descriptor,
rec.queueBlobOnly(blobOnly)
rec.queueMediaType(desc.MediaType)
rec.queueBlobSize(desc.Size)
rec.appendURLs(desc.URLs)
rec.queueCommitted(true)
if err := rec.commitMetadata(); err != nil {

59
cache/manager_test.go vendored
View File

@ -1508,6 +1508,65 @@ func checkVariantsCoverage(ctx context.Context, t *testing.T, variants idxToVari
require.Equal(t, 0, len(got))
}
// Make sure that media type and urls are persisted for non-distributable blobs.
func TestNondistributableBlobs(t *testing.T) {
t.Parallel()
ctx := namespaces.WithNamespace(context.Background(), "buildkit-test")
tmpdir, err := ioutil.TempDir("", "cachemanager")
require.NoError(t, err)
defer os.RemoveAll(tmpdir)
snapshotter, err := native.NewSnapshotter(filepath.Join(tmpdir, "snapshots"))
require.NoError(t, err)
co, cleanup, err := newCacheManager(ctx, cmOpt{
snapshotter: snapshotter,
snapshotterName: "native",
})
require.NoError(t, err)
defer cleanup()
cm := co.manager
ctx, done, err := leaseutil.WithLease(ctx, co.lm, leaseutil.MakeTemporary)
require.NoError(t, err)
defer done(context.TODO())
contentBuffer := contentutil.NewBuffer()
descHandlers := DescHandlers(map[digest.Digest]*DescHandler{})
data, desc, err := mapToBlob(map[string]string{"foo": "bar"}, false)
require.NoError(t, err)
// Pretend like this is non-distributable
desc.MediaType = ocispecs.MediaTypeImageLayerNonDistributable
desc.URLs = []string{"https://buildkit.moby.dev/foo"}
cw, err := contentBuffer.Writer(ctx)
require.NoError(t, err)
_, err = cw.Write(data)
require.NoError(t, err)
err = cw.Commit(ctx, 0, cw.Digest())
require.NoError(t, err)
descHandlers[desc.Digest] = &DescHandler{
Provider: func(_ session.Group) content.Provider { return contentBuffer },
}
ref, err := cm.GetByBlob(ctx, desc, nil, descHandlers)
require.NoError(t, err)
remotes, err := ref.GetRemotes(ctx, true, compression.Config{}, false, nil)
require.NoError(t, err)
desc2 := remotes[0].Descriptors[0]
require.Equal(t, desc.MediaType, desc2.MediaType)
require.Equal(t, desc.URLs, desc2.URLs)
}
func checkInfo(ctx context.Context, t *testing.T, cs content.Store, info content.Info) {
if info.Labels == nil {
return

45
cache/metadata.go vendored
View File

@ -37,6 +37,7 @@ const keyMediaType = "cache.mediatype"
const keyImageRefs = "cache.imageRefs"
const keyDeleted = "cache.deleted"
const keyBlobSize = "cache.blobsize" // the packed blob size as specified in the oci descriptor
const keyURLs = "cache.layer.urls"
// Indexes
const blobchainIndex = "blobchainid:"
@ -281,6 +282,17 @@ func (md *cacheMetadata) queueBlob(str digest.Digest) error {
return md.queueValue(keyBlob, str, "")
}
func (md *cacheMetadata) appendURLs(urls []string) error {
if len(urls) == 0 {
return nil
}
return md.appendStringSlice(keyURLs, urls...)
}
func (md *cacheMetadata) getURLs() []string {
return md.GetStringSlice(keyURLs)
}
func (md *cacheMetadata) getBlob() digest.Digest {
return digest.Digest(md.GetString(keyBlob))
}
@ -468,6 +480,18 @@ func (md *cacheMetadata) GetString(key string) string {
return str
}
func (md *cacheMetadata) GetStringSlice(key string) []string {
v := md.si.Get(key)
if v == nil {
return nil
}
var val []string
if err := v.Unmarshal(&val); err != nil {
return nil
}
return val
}
func (md *cacheMetadata) setTime(key string, value time.Time, index string) error {
return md.setValue(key, value.UnixNano(), index)
}
@ -512,7 +536,7 @@ func (md *cacheMetadata) getInt64(key string) (int64, bool) {
return i, true
}
func (md *cacheMetadata) appendStringSlice(key string, value string) error {
func (md *cacheMetadata) appendStringSlice(key string, values ...string) error {
return md.si.GetAndSetValue(key, func(v *metadata.Value) (*metadata.Value, error) {
var slice []string
if v != nil {
@ -520,12 +544,25 @@ func (md *cacheMetadata) appendStringSlice(key string, value string) error {
return nil, err
}
}
idx := make(map[string]struct{}, len(values))
for _, v := range values {
idx[v] = struct{}{}
}
for _, existing := range slice {
if existing == value {
return nil, metadata.ErrSkipSetValue
if _, ok := idx[existing]; ok {
delete(idx, existing)
}
}
slice = append(slice, value)
if len(idx) == 0 {
return nil, metadata.ErrSkipSetValue
}
for value := range idx {
slice = append(slice, value)
}
v, err := metadata.NewValue(slice)
if err != nil {
return nil, err

2
cache/refs.go vendored
View File

@ -600,6 +600,7 @@ func (sr *immutableRef) ociDesc(ctx context.Context, dhs DescHandlers) (ocispecs
Size: sr.getBlobSize(),
MediaType: sr.getMediaType(),
Annotations: make(map[string]string),
URLs: sr.getURLs(),
}
if blobDesc, err := getBlobDesc(ctx, sr.cm.ContentStore, desc.Digest); err == nil {
@ -748,6 +749,7 @@ func getBlobDesc(ctx context.Context, cs content.Store, dgst digest.Digest) (oci
if !ok {
return ocispecs.Descriptor{}, fmt.Errorf("no media type is stored for %q", info.Digest)
}
desc := ocispecs.Descriptor{
Digest: info.Digest,
Size: info.Size,

1
cache/remote.go vendored
View File

@ -224,6 +224,7 @@ func (sr *immutableRef) getRemote(ctx context.Context, createIfNeeded bool, comp
newDesc.MediaType = blobDesc.MediaType
newDesc.Digest = blobDesc.Digest
newDesc.Size = blobDesc.Size
newDesc.URLs = blobDesc.URLs
newDesc.Annotations = nil
for _, k := range addAnnotations {
newDesc.Annotations[k] = desc.Annotations[k]

View File

@ -44,6 +44,10 @@ const (
keyCompressionLevel = "compression-level"
keyBuildInfo = "buildinfo"
ociTypes = "oci-mediatypes"
// propagateNondistLayersKey is an exporter option which can be used to mark a layer as non-distributable if the layer reference was
// already found to use a non-distributable media type.
// When this option is not set, the exporter will change the media type of the layer to a distributable one.
propagateNondistLayersKey = "propagate-nondist-layers"
)
type Opt struct {
@ -181,6 +185,12 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp
return nil, err
}
i.buildInfoMode = bimode
case propagateNondistLayersKey:
b, err := strconv.ParseBool(v)
if err != nil {
return nil, errors.Wrapf(err, "non-bool value %s specified for %s", v, k)
}
i.propagateNondistLayers = b
default:
if i.meta == nil {
i.meta = make(map[string][]byte)
@ -197,19 +207,20 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp
type imageExporterInstance struct {
*imageExporter
targetName string
push bool
pushByDigest bool
unpack bool
insecure bool
ociTypes bool
nameCanonical bool
danglingPrefix string
layerCompression compression.Type
forceCompression bool
compressionLevel *int
buildInfoMode buildinfo.ExportMode
meta map[string][]byte
targetName string
push bool
pushByDigest bool
unpack bool
insecure bool
ociTypes bool
nameCanonical bool
danglingPrefix string
layerCompression compression.Type
forceCompression bool
compressionLevel *int
buildInfoMode buildinfo.ExportMode
meta map[string][]byte
propagateNondistLayers bool
}
func (e *imageExporterInstance) Name() string {
@ -244,7 +255,7 @@ func (e *imageExporterInstance) Export(ctx context.Context, src exporter.Source,
}
defer done(context.TODO())
desc, err := e.opt.ImageWriter.Commit(ctx, src, e.ociTypes, e.compression(), e.buildInfoMode, sessionID)
desc, err := e.opt.ImageWriter.Commit(ctx, src, e.ociTypes, e.compression(), e.buildInfoMode, e.propagateNondistLayers, sessionID)
if err != nil {
return nil, err
}

View File

@ -21,6 +21,7 @@ import (
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/buildinfo"
"github.com/moby/buildkit/util/compression"
"github.com/moby/buildkit/util/convert"
"github.com/moby/buildkit/util/progress"
"github.com/moby/buildkit/util/system"
"github.com/moby/buildkit/util/tracing"
@ -48,7 +49,7 @@ type ImageWriter struct {
opt WriterOpt
}
func (ic *ImageWriter) Commit(ctx context.Context, inp exporter.Source, oci bool, comp compression.Config, buildInfoMode buildinfo.ExportMode, sessionID string) (*ocispecs.Descriptor, error) {
func (ic *ImageWriter) Commit(ctx context.Context, inp exporter.Source, oci bool, comp compression.Config, buildInfoMode buildinfo.ExportMode, propagateNonDist bool, sessionID string) (*ocispecs.Descriptor, error) {
platformsBytes, ok := inp.Metadata[exptypes.ExporterPlatformsKey]
if len(inp.Refs) > 0 && !ok {
@ -56,6 +57,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp exporter.Source, oci bool
}
if len(inp.Refs) == 0 {
// TODO: pass through non-dist layers?
remotes, err := ic.exportLayers(ctx, comp, session.NewGroup(sessionID), inp.Ref)
if err != nil {
return nil, err
@ -66,7 +68,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp exporter.Source, oci bool
buildInfo = inp.Metadata[exptypes.ExporterBuildInfo]
}
mfstDesc, configDesc, err := ic.commitDistributionManifest(ctx, inp.Ref, inp.Metadata[exptypes.ExporterImageConfigKey], &remotes[0], oci, inp.Metadata[exptypes.ExporterInlineCache], buildInfo)
mfstDesc, configDesc, err := ic.commitDistributionManifest(ctx, inp.Ref, inp.Metadata[exptypes.ExporterImageConfigKey], &remotes[0], oci, inp.Metadata[exptypes.ExporterInlineCache], buildInfo, propagateNonDist)
if err != nil {
return nil, err
}
@ -94,6 +96,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp exporter.Source, oci bool
refs = append(refs, r)
}
// TODO: Pass through non-distributable layers
remotes, err := ic.exportLayers(ctx, comp, session.NewGroup(sessionID), refs...)
if err != nil {
return nil, err
@ -133,7 +136,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp exporter.Source, oci bool
buildInfo = inp.Metadata[fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, p.ID)]
}
desc, _, err := ic.commitDistributionManifest(ctx, r, config, &remotes[remotesMap[p.ID]], oci, inlineCache, buildInfo)
desc, _, err := ic.commitDistributionManifest(ctx, r, config, &remotes[remotesMap[p.ID]], oci, inlineCache, buildInfo, propagateNonDist)
if err != nil {
return nil, err
}
@ -202,7 +205,7 @@ func (ic *ImageWriter) exportLayers(ctx context.Context, comp compression.Config
return out, err
}
func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache.ImmutableRef, config []byte, remote *solver.Remote, oci bool, inlineCache []byte, buildInfo []byte) (*ocispecs.Descriptor, *ocispecs.Descriptor, error) {
func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache.ImmutableRef, config []byte, remote *solver.Remote, oci bool, inlineCache []byte, buildInfo []byte, propagateNonDist bool) (*ocispecs.Descriptor, *ocispecs.Descriptor, error) {
if len(config) == 0 {
var err error
config, err = emptyImageConfig()
@ -278,6 +281,10 @@ func (ic *ImageWriter) commitDistributionManifest(ctx context.Context, ref cache
} else {
desc.Annotations = nil
}
if !propagateNonDist {
desc.MediaType = convert.LayerToDistributable(oci, desc.MediaType)
desc.URLs = nil
}
mfst.Layers = append(mfst.Layers, desc)
labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = desc.Digest.String()
}

View File

@ -38,6 +38,10 @@ const (
keyForceCompression = "force-compression"
keyCompressionLevel = "compression-level"
keyBuildInfo = "buildinfo"
// propagateNondistLayersKey is an exporter option which can be used to mark a layer as non-distributable if the layer reference was
// already found to use a non-distributable media type.
// When this option is not set, the exporter will change the media type of the layer to a distributable one.
propagateNondistLayersKey = "propagate-nondist-layers"
)
type Opt struct {
@ -119,6 +123,12 @@ func (e *imageExporter) Resolve(ctx context.Context, opt map[string]string) (exp
return nil, err
}
i.buildInfoMode = bimode
case propagateNondistLayersKey:
b, err := strconv.ParseBool(v)
if err != nil {
return nil, errors.Wrapf(err, "non-bool value specified for %s", k)
}
i.propagateNonDist = b
default:
if i.meta == nil {
i.meta = make(map[string][]byte)
@ -147,6 +157,7 @@ type imageExporterInstance struct {
forceCompression bool
compressionLevel *int
buildInfoMode buildinfo.ExportMode
propagateNonDist bool
}
func (e *imageExporterInstance) Name() string {
@ -185,7 +196,7 @@ func (e *imageExporterInstance) Export(ctx context.Context, src exporter.Source,
}
defer done(context.TODO())
desc, err := e.opt.ImageWriter.Commit(ctx, src, e.ociTypes, e.compression(), e.buildInfoMode, sessionID)
desc, err := e.opt.ImageWriter.Commit(ctx, src, e.ociTypes, e.compression(), e.buildInfoMode, e.propagateNonDist, sessionID)
if err != nil {
return nil, err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
@ -14,10 +15,18 @@ import (
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/util/progress"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil"
fstypes "github.com/tonistiigi/fsutil/types"
)
const (
// propagateNondistLayersKey is an exporter option which can be used to mark a layer as non-distributable if the layer reference was
// already found to use a non-distributable media type.
// When this option is not set, the exporter will change the media type of the layer to a distributable one.
propagateNondistLayersKey = "propagate-nondist-layers"
)
type Opt struct {
SessionManager *session.Manager
}
@ -34,11 +43,22 @@ func New(opt Opt) (exporter.Exporter, error) {
func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exporter.ExporterInstance, error) {
li := &localExporterInstance{localExporter: e}
v, ok := opt[propagateNondistLayersKey]
if ok {
b, err := strconv.ParseBool(v)
if err != nil {
return nil, errors.Wrapf(err, "non-bool value for %s: %s", propagateNondistLayersKey, v)
}
li.propagateNonDist = b
}
return li, nil
}
type localExporterInstance struct {
*localExporter
propagateNonDist bool
}
func (e *localExporterInstance) Name() string {

View File

@ -116,11 +116,11 @@ func (ct Type) IsMediaType(mt string) bool {
func FromMediaType(mediaType string) Type {
switch toOCILayerType[mediaType] {
case ocispecs.MediaTypeImageLayer:
case ocispecs.MediaTypeImageLayer, ocispecs.MediaTypeImageLayerNonDistributable:
return Uncompressed
case ocispecs.MediaTypeImageLayerGzip:
case ocispecs.MediaTypeImageLayerGzip, ocispecs.MediaTypeImageLayerNonDistributableGzip:
return Gzip
case mediaTypeImageLayerZstd:
case mediaTypeImageLayerZstd, ocispecs.MediaTypeImageLayerNonDistributableZstd:
return Zstd
default:
return UnknownCompression
@ -193,25 +193,30 @@ func detectCompressionType(cr *io.SectionReader) (Type, error) {
}
var toDockerLayerType = map[string]string{
ocispecs.MediaTypeImageLayer: images.MediaTypeDockerSchema2Layer,
images.MediaTypeDockerSchema2Layer: images.MediaTypeDockerSchema2Layer,
ocispecs.MediaTypeImageLayerGzip: images.MediaTypeDockerSchema2LayerGzip,
images.MediaTypeDockerSchema2LayerGzip: images.MediaTypeDockerSchema2LayerGzip,
images.MediaTypeDockerSchema2LayerForeign: images.MediaTypeDockerSchema2Layer,
images.MediaTypeDockerSchema2LayerForeignGzip: images.MediaTypeDockerSchema2LayerGzip,
mediaTypeImageLayerZstd: mediaTypeDockerSchema2LayerZstd,
mediaTypeDockerSchema2LayerZstd: mediaTypeDockerSchema2LayerZstd,
ocispecs.MediaTypeImageLayer: images.MediaTypeDockerSchema2Layer,
images.MediaTypeDockerSchema2Layer: images.MediaTypeDockerSchema2Layer,
ocispecs.MediaTypeImageLayerGzip: images.MediaTypeDockerSchema2LayerGzip,
images.MediaTypeDockerSchema2LayerGzip: images.MediaTypeDockerSchema2LayerGzip,
images.MediaTypeDockerSchema2LayerForeign: images.MediaTypeDockerSchema2LayerForeign,
images.MediaTypeDockerSchema2LayerForeignGzip: images.MediaTypeDockerSchema2LayerForeignGzip,
ocispecs.MediaTypeImageLayerNonDistributable: images.MediaTypeDockerSchema2LayerForeign,
ocispecs.MediaTypeImageLayerNonDistributableGzip: images.MediaTypeDockerSchema2LayerForeignGzip,
mediaTypeImageLayerZstd: mediaTypeDockerSchema2LayerZstd,
mediaTypeDockerSchema2LayerZstd: mediaTypeDockerSchema2LayerZstd,
}
var toOCILayerType = map[string]string{
ocispecs.MediaTypeImageLayer: ocispecs.MediaTypeImageLayer,
images.MediaTypeDockerSchema2Layer: ocispecs.MediaTypeImageLayer,
ocispecs.MediaTypeImageLayerGzip: ocispecs.MediaTypeImageLayerGzip,
images.MediaTypeDockerSchema2LayerGzip: ocispecs.MediaTypeImageLayerGzip,
images.MediaTypeDockerSchema2LayerForeign: ocispecs.MediaTypeImageLayer,
images.MediaTypeDockerSchema2LayerForeignGzip: ocispecs.MediaTypeImageLayerGzip,
mediaTypeImageLayerZstd: mediaTypeImageLayerZstd,
mediaTypeDockerSchema2LayerZstd: mediaTypeImageLayerZstd,
ocispecs.MediaTypeImageLayer: ocispecs.MediaTypeImageLayer,
ocispecs.MediaTypeImageLayerNonDistributable: ocispecs.MediaTypeImageLayerNonDistributable,
ocispecs.MediaTypeImageLayerNonDistributableGzip: ocispecs.MediaTypeImageLayerNonDistributableGzip,
ocispecs.MediaTypeImageLayerNonDistributableZstd: ocispecs.MediaTypeImageLayerNonDistributableZstd,
images.MediaTypeDockerSchema2Layer: ocispecs.MediaTypeImageLayer,
ocispecs.MediaTypeImageLayerGzip: ocispecs.MediaTypeImageLayerGzip,
images.MediaTypeDockerSchema2LayerGzip: ocispecs.MediaTypeImageLayerGzip,
images.MediaTypeDockerSchema2LayerForeign: ocispecs.MediaTypeImageLayerNonDistributable,
images.MediaTypeDockerSchema2LayerForeignGzip: ocispecs.MediaTypeImageLayerNonDistributableGzip,
mediaTypeImageLayerZstd: mediaTypeImageLayerZstd,
mediaTypeDockerSchema2LayerZstd: mediaTypeImageLayerZstd,
}
func convertLayerMediaType(mediaType string, oci bool) string {

37
util/convert/layer.go Normal file
View File

@ -0,0 +1,37 @@
package convert
import (
"github.com/containerd/containerd/images"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)
// LayertoDistributable changes the passed in media type to the "distributable" version of the media type.
func LayerToDistributable(oci bool, mt string) string {
if !images.IsNonDistributable(mt) {
// Layer is already a distributable media type (or this is not even a layer).
// No conversion needed
return mt
}
if oci {
switch mt {
case ocispecs.MediaTypeImageLayerNonDistributable:
return ocispecs.MediaTypeImageLayer
case ocispecs.MediaTypeImageLayerNonDistributableGzip:
return ocispecs.MediaTypeImageLayerGzip
case ocispecs.MediaTypeImageLayerNonDistributableZstd:
return ocispecs.MediaTypeImageLayerZstd
default:
return mt
}
}
switch mt {
case images.MediaTypeDockerSchema2LayerForeign:
return images.MediaTypeDockerSchema2Layer
case images.MediaTypeDockerSchema2LayerForeignGzip:
return images.MediaTypeDockerSchema2LayerGzip
default:
return mt
}
}

View File

@ -225,7 +225,17 @@ func (p *provider) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (cont
func filterLayerBlobs(metadata map[digest.Digest]ocispecs.Descriptor, mu sync.Locker) images.HandlerFunc {
return func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) {
switch desc.MediaType {
case ocispecs.MediaTypeImageLayer, images.MediaTypeDockerSchema2Layer, ocispecs.MediaTypeImageLayerGzip, images.MediaTypeDockerSchema2LayerGzip, images.MediaTypeDockerSchema2LayerForeign, images.MediaTypeDockerSchema2LayerForeignGzip:
case
ocispecs.MediaTypeImageLayer,
ocispecs.MediaTypeImageLayerNonDistributable,
images.MediaTypeDockerSchema2Layer,
images.MediaTypeDockerSchema2LayerForeign,
ocispecs.MediaTypeImageLayerGzip,
images.MediaTypeDockerSchema2LayerGzip,
ocispecs.MediaTypeImageLayerNonDistributableGzip,
images.MediaTypeDockerSchema2LayerForeignGzip,
ocispecs.MediaTypeImageLayerZstd,
ocispecs.MediaTypeImageLayerNonDistributableZstd:
return nil, images.ErrSkipDesc
default:
if metadata != nil {