Skip to content

Commit 995d908

Browse files
authored
Merge pull request moby#3724 from kattmang/master
Import/export support for OCI compatible image manifest version of cache manifest (opt-in on export, inferred on import)
2 parents 81d19ad + 29fd071 commit 995d908

File tree

7 files changed

+335
-43
lines changed

7 files changed

+335
-43
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ buildctl build ... \
388388
* `min`: only export layers for the resulting image
389389
* `max`: export all the layers of all intermediate steps
390390
* `ref=<ref>`: specify repository reference to store cache, e.g. `docker.io/user/image:tag`
391+
* `image-manifest=<true|false>`: whether to export cache manifest as an OCI-compatible image manifest rather than a manifest list/index (default: `false`, must be used with `oci-mediatypes=true`)
391392
* `oci-mediatypes=<true|false>`: whether to use OCI mediatypes in exported manifests (default: `true`, since BuildKit `v0.8`)
392393
* `compression=<uncompressed|gzip|estargz|zstd>`: choose compression type for layers newly created and cached, gzip is default value. estargz and zstd should be used with `oci-mediatypes=true`
393394
* `compression-level=<value>`: choose compression level for gzip, estargz (0-9) and zstd (0-22)
@@ -414,6 +415,7 @@ The directory layout conforms to OCI Image Spec v1.0.
414415
* `max`: export all the layers of all intermediate steps
415416
* `dest=<path>`: destination directory for cache exporter
416417
* `tag=<tag>`: specify custom tag of image to write to local index (default: `latest`)
418+
* `image-manifest=<true|false>`: whether to export cache manifest as an OCI-compatible image manifest rather than a manifest list/index (default: `false`, must be used with `oci-mediatypes=true`)
417419
* `oci-mediatypes=<true|false>`: whether to use OCI mediatypes in exported manifests (default `true`, since BuildKit `v0.8`)
418420
* `compression=<uncompressed|gzip|estargz|zstd>`: choose compression type for layers newly created and cached, gzip is default value. estargz and zstd should be used with `oci-mediatypes=true`.
419421
* `compression-level=<value>`: compression level for gzip, estargz (0-9) and zstd (0-22)

cache/remotecache/export.go

Lines changed: 136 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"github.com/moby/buildkit/util/progress"
1717
"github.com/moby/buildkit/util/progress/logs"
1818
digest "github.com/opencontainers/go-digest"
19-
specs "github.com/opencontainers/image-spec/specs-go"
19+
"github.com/opencontainers/image-spec/specs-go"
2020
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
2121
"github.com/pkg/errors"
2222
)
@@ -37,24 +37,135 @@ type Config struct {
3737
Compression compression.Config
3838
}
3939

40+
type CacheType int
41+
4042
const (
4143
// ExportResponseManifestDesc is a key for the map returned from Exporter.Finalize.
4244
// The map value is a JSON string of an OCI desciptor of a manifest.
4345
ExporterResponseManifestDesc = "cache.manifest"
4446
)
4547

46-
type contentCacheExporter struct {
47-
solver.CacheExporterTarget
48-
chains *v1.CacheChains
49-
ingester content.Ingester
50-
oci bool
51-
ref string
52-
comp compression.Config
48+
const (
49+
NotSet CacheType = iota
50+
ManifestList
51+
ImageManifest
52+
)
53+
54+
func (data CacheType) String() string {
55+
switch data {
56+
case ManifestList:
57+
return "Manifest List"
58+
case ImageManifest:
59+
return "Image Manifest"
60+
default:
61+
return "Not Set"
62+
}
5363
}
5464

55-
func NewExporter(ingester content.Ingester, ref string, oci bool, compressionConfig compression.Config) Exporter {
65+
func NewExporter(ingester content.Ingester, ref string, oci bool, imageManifest bool, compressionConfig compression.Config) Exporter {
5666
cc := v1.NewCacheChains()
57-
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, ref: ref, comp: compressionConfig}
67+
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, imageManifest: imageManifest, ref: ref, comp: compressionConfig}
68+
}
69+
70+
type ExportableCache struct {
71+
// This cache describes two distinct styles of exportable cache, one is an Index (or Manifest List) of blobs,
72+
// or as an artifact using the OCI image manifest format.
73+
ExportedManifest ocispecs.Manifest
74+
ExportedIndex ocispecs.Index
75+
CacheType CacheType
76+
OCI bool
77+
}
78+
79+
func NewExportableCache(oci bool, imageManifest bool) (*ExportableCache, error) {
80+
var mediaType string
81+
82+
if imageManifest {
83+
mediaType = ocispecs.MediaTypeImageManifest
84+
if !oci {
85+
return nil, errors.Errorf("invalid configuration for remote cache")
86+
}
87+
} else {
88+
if oci {
89+
mediaType = ocispecs.MediaTypeImageIndex
90+
} else {
91+
mediaType = images.MediaTypeDockerSchema2ManifestList
92+
}
93+
}
94+
95+
cacheType := ManifestList
96+
if imageManifest {
97+
cacheType = ImageManifest
98+
}
99+
100+
schemaVersion := specs.Versioned{SchemaVersion: 2}
101+
switch cacheType {
102+
case ManifestList:
103+
return &ExportableCache{ExportedIndex: ocispecs.Index{
104+
MediaType: mediaType,
105+
Versioned: schemaVersion,
106+
},
107+
CacheType: cacheType,
108+
OCI: oci,
109+
}, nil
110+
case ImageManifest:
111+
return &ExportableCache{ExportedManifest: ocispecs.Manifest{
112+
MediaType: mediaType,
113+
Versioned: schemaVersion,
114+
},
115+
CacheType: cacheType,
116+
OCI: oci,
117+
}, nil
118+
default:
119+
return nil, errors.Errorf("exportable cache type not set")
120+
}
121+
}
122+
123+
func (ec *ExportableCache) MediaType() string {
124+
if ec.CacheType == ManifestList {
125+
return ec.ExportedIndex.MediaType
126+
}
127+
return ec.ExportedManifest.MediaType
128+
}
129+
130+
func (ec *ExportableCache) AddCacheBlob(blob ocispecs.Descriptor) {
131+
if ec.CacheType == ManifestList {
132+
ec.ExportedIndex.Manifests = append(ec.ExportedIndex.Manifests, blob)
133+
} else {
134+
ec.ExportedManifest.Layers = append(ec.ExportedManifest.Layers, blob)
135+
}
136+
}
137+
138+
func (ec *ExportableCache) FinalizeCache(ctx context.Context) {
139+
if ec.CacheType == ManifestList {
140+
ec.ExportedIndex.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ec.OCI, ec.ExportedIndex.Manifests...)
141+
} else {
142+
ec.ExportedManifest.Layers = compression.ConvertAllLayerMediaTypes(ctx, ec.OCI, ec.ExportedManifest.Layers...)
143+
}
144+
}
145+
146+
func (ec *ExportableCache) SetConfig(config ocispecs.Descriptor) {
147+
if ec.CacheType == ManifestList {
148+
ec.ExportedIndex.Manifests = append(ec.ExportedIndex.Manifests, config)
149+
} else {
150+
ec.ExportedManifest.Config = config
151+
}
152+
}
153+
154+
func (ec *ExportableCache) MarshalJSON() ([]byte, error) {
155+
if ec.CacheType == ManifestList {
156+
return json.Marshal(ec.ExportedIndex)
157+
}
158+
return json.Marshal(ec.ExportedManifest)
159+
}
160+
161+
type contentCacheExporter struct {
162+
solver.CacheExporterTarget
163+
chains *v1.CacheChains
164+
ingester content.Ingester
165+
oci bool
166+
imageManifest bool
167+
ref string
168+
comp compression.Config
58169
}
59170

60171
func (ce *contentCacheExporter) Name() string {
@@ -74,21 +185,9 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
74185
return nil, err
75186
}
76187

77-
// own type because oci type can't be pushed and docker type doesn't have annotations
78-
type manifestList struct {
79-
specs.Versioned
80-
81-
MediaType string `json:"mediaType,omitempty"`
82-
83-
// Manifests references platform specific manifests.
84-
Manifests []ocispecs.Descriptor `json:"manifests"`
85-
}
86-
87-
var mfst manifestList
88-
mfst.SchemaVersion = 2
89-
mfst.MediaType = images.MediaTypeDockerSchema2ManifestList
90-
if ce.oci {
91-
mfst.MediaType = ocispecs.MediaTypeImageIndex
188+
cache, err := NewExportableCache(ce.oci, ce.imageManifest)
189+
if err != nil {
190+
return nil, err
92191
}
93192

94193
for _, l := range config.Layers {
@@ -101,10 +200,10 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
101200
return nil, layerDone(errors.Wrap(err, "error writing layer blob"))
102201
}
103202
layerDone(nil)
104-
mfst.Manifests = append(mfst.Manifests, dgstPair.Descriptor)
203+
cache.AddCacheBlob(dgstPair.Descriptor)
105204
}
106205

107-
mfst.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ce.oci, mfst.Manifests...)
206+
cache.FinalizeCache(ctx)
108207

109208
dt, err := json.Marshal(config)
110209
if err != nil {
@@ -122,9 +221,9 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
122221
}
123222
configDone(nil)
124223

125-
mfst.Manifests = append(mfst.Manifests, desc)
224+
cache.SetConfig(desc)
126225

127-
dt, err = json.Marshal(mfst)
226+
dt, err = cache.MarshalJSON()
128227
if err != nil {
129228
return nil, errors.Wrap(err, "failed to marshal manifest")
130229
}
@@ -133,9 +232,14 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
133232
desc = ocispecs.Descriptor{
134233
Digest: dgst,
135234
Size: int64(len(dt)),
136-
MediaType: mfst.MediaType,
235+
MediaType: cache.MediaType(),
137236
}
138-
mfstDone := progress.OneOff(ctx, fmt.Sprintf("writing manifest %s", dgst))
237+
238+
mfstLog := fmt.Sprintf("writing cache manifest %s", dgst)
239+
if ce.imageManifest {
240+
mfstLog = fmt.Sprintf("writing cache image manifest %s", dgst)
241+
}
242+
mfstDone := progress.OneOff(ctx, mfstLog)
139243
if err := content.WriteBlob(ctx, ce.ingester, dgst.String(), bytes.NewReader(dt), desc); err != nil {
140244
return nil, mfstDone(errors.Wrap(err, "error writing manifest blob"))
141245
}
@@ -145,5 +249,6 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
145249
}
146250
res[ExporterResponseManifestDesc] = string(descJSON)
147251
mfstDone(nil)
252+
148253
return res, nil
149254
}

cache/remotecache/import.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package remotecache
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"sync"
89
"time"
@@ -14,6 +15,7 @@ import (
1415
"github.com/moby/buildkit/solver"
1516
"github.com/moby/buildkit/util/bklog"
1617
"github.com/moby/buildkit/util/imageutil"
18+
"github.com/moby/buildkit/util/progress"
1719
"github.com/moby/buildkit/worker"
1820
digest "github.com/opencontainers/go-digest"
1921
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -47,24 +49,52 @@ func (ci *contentCacheImporter) Resolve(ctx context.Context, desc ocispecs.Descr
4749
return nil, err
4850
}
4951

50-
var mfst ocispecs.Index
51-
if err := json.Unmarshal(dt, &mfst); err != nil {
52+
manifestType, err := imageutil.DetectManifestBlobMediaType(dt)
53+
if err != nil {
5254
return nil, err
5355
}
5456

55-
allLayers := v1.DescriptorProvider{}
57+
layerDone := progress.OneOff(ctx, fmt.Sprintf("inferred cache manifest type: %s", manifestType))
58+
layerDone(nil)
5659

60+
allLayers := v1.DescriptorProvider{}
5761
var configDesc ocispecs.Descriptor
5862

59-
for _, m := range mfst.Manifests {
60-
if m.MediaType == v1.CacheConfigMediaTypeV0 {
61-
configDesc = m
62-
continue
63+
switch manifestType {
64+
case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
65+
var mfst ocispecs.Index
66+
if err := json.Unmarshal(dt, &mfst); err != nil {
67+
return nil, err
6368
}
64-
allLayers[m.Digest] = v1.DescriptorProviderPair{
65-
Descriptor: m,
66-
Provider: ci.provider,
69+
70+
for _, m := range mfst.Manifests {
71+
if m.MediaType == v1.CacheConfigMediaTypeV0 {
72+
configDesc = m
73+
continue
74+
}
75+
allLayers[m.Digest] = v1.DescriptorProviderPair{
76+
Descriptor: m,
77+
Provider: ci.provider,
78+
}
6779
}
80+
case images.MediaTypeDockerSchema2Manifest, ocispecs.MediaTypeImageManifest:
81+
var mfst ocispecs.Manifest
82+
if err := json.Unmarshal(dt, &mfst); err != nil {
83+
return nil, err
84+
}
85+
86+
if mfst.Config.MediaType == v1.CacheConfigMediaTypeV0 {
87+
configDesc = mfst.Config
88+
}
89+
for _, m := range mfst.Layers {
90+
allLayers[m.Digest] = v1.DescriptorProviderPair{
91+
Descriptor: m,
92+
Provider: ci.provider,
93+
}
94+
}
95+
default:
96+
err = errors.Wrapf(err, "unsupported or uninferrable manifest type")
97+
return nil, err
6898
}
6999

70100
if dsls, ok := ci.provider.(DistributionSourceLabelSetter); ok {

cache/remotecache/local/local.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
attrDigest = "digest"
2020
attrSrc = "src"
2121
attrDest = "dest"
22+
attrImageManifest = "image-manifest"
2223
attrOCIMediatypes = "oci-mediatypes"
2324
contentStoreIDPrefix = "local:"
2425
)
@@ -50,12 +51,20 @@ func ResolveCacheExporterFunc(sm *session.Manager) remotecache.ResolveCacheExpor
5051
}
5152
ociMediatypes = b
5253
}
54+
imageManifest := false
55+
if v, ok := attrs[attrImageManifest]; ok {
56+
b, err := strconv.ParseBool(v)
57+
if err != nil {
58+
return nil, errors.Wrapf(err, "failed to parse %s", attrImageManifest)
59+
}
60+
imageManifest = b
61+
}
5362
csID := contentStoreIDPrefix + store
5463
cs, err := getContentStore(ctx, sm, g, csID)
5564
if err != nil {
5665
return nil, err
5766
}
58-
return &exporter{remotecache.NewExporter(cs, "", ociMediatypes, compressionConfig)}, nil
67+
return &exporter{remotecache.NewExporter(cs, "", ociMediatypes, imageManifest, compressionConfig)}, nil
5968
}
6069
}
6170

cache/remotecache/registry/registry.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func canonicalizeRef(rawRef string) (reference.Named, error) {
3636

3737
const (
3838
attrRef = "ref"
39+
attrImageManifest = "image-manifest"
3940
attrOCIMediatypes = "oci-mediatypes"
4041
attrInsecure = "registry.insecure"
4142
)
@@ -67,6 +68,14 @@ func ResolveCacheExporterFunc(sm *session.Manager, hosts docker.RegistryHosts) r
6768
}
6869
ociMediatypes = b
6970
}
71+
imageManifest := false
72+
if v, ok := attrs[attrImageManifest]; ok {
73+
b, err := strconv.ParseBool(v)
74+
if err != nil {
75+
return nil, errors.Wrapf(err, "failed to parse %s", attrImageManifest)
76+
}
77+
imageManifest = b
78+
}
7079
insecure := false
7180
if v, ok := attrs[attrInsecure]; ok {
7281
b, err := strconv.ParseBool(v)
@@ -82,7 +91,7 @@ func ResolveCacheExporterFunc(sm *session.Manager, hosts docker.RegistryHosts) r
8291
if err != nil {
8392
return nil, err
8493
}
85-
return &exporter{remotecache.NewExporter(contentutil.FromPusher(pusher), refString, ociMediatypes, compressionConfig)}, nil
94+
return &exporter{remotecache.NewExporter(contentutil.FromPusher(pusher), refString, ociMediatypes, imageManifest, compressionConfig)}, nil
8695
}
8796
}
8897

0 commit comments

Comments
 (0)