Skip to content

Commit 797156a

Browse files
committed
added import/export support for OCI compatible image manifest version of cache manifest (opt-in on export, inferred on import) moby/buildkit moby#2251
Signed-off-by: Kang, Matthew <[email protected]>
1 parent 81d19ad commit 797156a

File tree

5 files changed

+156
-32
lines changed

5 files changed

+156
-32
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`)
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`)
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: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,19 @@ const (
4343
ExporterResponseManifestDesc = "cache.manifest"
4444
)
4545

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
46+
func NewExporter(ingester content.Ingester, ref string, oci bool, imageManifest bool, compressionConfig compression.Config) Exporter {
47+
cc := v1.NewCacheChains()
48+
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, imageManifest: imageManifest, ref: ref, comp: compressionConfig}
5349
}
5450

55-
func NewExporter(ingester content.Ingester, ref string, oci bool, compressionConfig compression.Config) Exporter {
56-
cc := v1.NewCacheChains()
57-
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, ref: ref, comp: compressionConfig}
51+
type contentCacheExporter struct {
52+
solver.CacheExporterTarget
53+
chains *v1.CacheChains
54+
ingester content.Ingester
55+
oci bool
56+
imageManifest bool
57+
ref string
58+
comp compression.Config
5859
}
5960

6061
func (ce *contentCacheExporter) Name() string {
@@ -75,20 +76,23 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
7576
}
7677

7778
// own type because oci type can't be pushed and docker type doesn't have annotations
78-
type manifestList struct {
79+
type abstractManifest struct {
7980
specs.Versioned
8081

81-
MediaType string `json:"mediaType,omitempty"`
82-
82+
MediaType string `json:"mediaType,omitempty"`
83+
Config *ocispecs.Descriptor `json:"config,omitempty"`
8384
// Manifests references platform specific manifests.
84-
Manifests []ocispecs.Descriptor `json:"manifests"`
85+
Manifests []ocispecs.Descriptor `json:"manifests,omitempty"`
86+
Layers []ocispecs.Descriptor `json:"layers,omitempty"`
8587
}
8688

87-
var mfst manifestList
89+
var mfst abstractManifest
8890
mfst.SchemaVersion = 2
8991
mfst.MediaType = images.MediaTypeDockerSchema2ManifestList
90-
if ce.oci {
92+
if ce.oci && !ce.imageManifest {
9193
mfst.MediaType = ocispecs.MediaTypeImageIndex
94+
} else if ce.imageManifest {
95+
mfst.MediaType = ocispecs.MediaTypeImageManifest
9296
}
9397

9498
for _, l := range config.Layers {
@@ -101,10 +105,16 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
101105
return nil, layerDone(errors.Wrap(err, "error writing layer blob"))
102106
}
103107
layerDone(nil)
104-
mfst.Manifests = append(mfst.Manifests, dgstPair.Descriptor)
108+
if ce.imageManifest {
109+
mfst.Layers = append(mfst.Layers, dgstPair.Descriptor)
110+
} else {
111+
mfst.Manifests = append(mfst.Manifests, dgstPair.Descriptor)
112+
}
105113
}
106114

107-
mfst.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ce.oci, mfst.Manifests...)
115+
if !ce.imageManifest {
116+
mfst.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ce.oci, mfst.Manifests...)
117+
}
108118

109119
dt, err := json.Marshal(config)
110120
if err != nil {
@@ -122,7 +132,11 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
122132
}
123133
configDone(nil)
124134

125-
mfst.Manifests = append(mfst.Manifests, desc)
135+
if ce.imageManifest {
136+
mfst.Config = &desc
137+
} else {
138+
mfst.Manifests = append(mfst.Manifests, desc)
139+
}
126140

127141
dt, err = json.Marshal(mfst)
128142
if err != nil {
@@ -135,7 +149,12 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
135149
Size: int64(len(dt)),
136150
MediaType: mfst.MediaType,
137151
}
138-
mfstDone := progress.OneOff(ctx, fmt.Sprintf("writing manifest %s", dgst))
152+
153+
mfstLog := fmt.Sprintf("writing cache manifest %s", dgst)
154+
if ce.imageManifest {
155+
mfstLog = fmt.Sprintf("writing cache image manifest %s", dgst)
156+
}
157+
mfstDone := progress.OneOff(ctx, mfstLog)
139158
if err := content.WriteBlob(ctx, ce.ingester, dgst.String(), bytes.NewReader(dt), desc); err != nil {
140159
return nil, mfstDone(errors.Wrap(err, "error writing manifest blob"))
141160
}
@@ -145,5 +164,6 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
145164
}
146165
res[ExporterResponseManifestDesc] = string(descJSON)
147166
mfstDone(nil)
167+
148168
return res, nil
149169
}

cache/remotecache/import.go

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package remotecache
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"sync"
89
"time"
910

11+
"github.com/moby/buildkit/util/progress"
12+
"github.com/opencontainers/image-spec/specs-go"
13+
1014
"github.com/containerd/containerd/content"
1115
"github.com/containerd/containerd/images"
1216
v1 "github.com/moby/buildkit/cache/remotecache/v1"
@@ -21,6 +25,27 @@ import (
2125
"golang.org/x/sync/errgroup"
2226
)
2327

28+
type ManifestType int
29+
30+
const (
31+
NotInferred ManifestType = iota
32+
ManifestList
33+
ImageManifest
34+
)
35+
36+
func (data ManifestType) String() string {
37+
switch data {
38+
case NotInferred:
39+
return "Not Inferred"
40+
case ManifestList:
41+
return "Manifest List"
42+
case ImageManifest:
43+
return "Image Manifest"
44+
default:
45+
return "Not Inferred"
46+
}
47+
}
48+
2449
// ResolveCacheImporterFunc returns importer and descriptor.
2550
type ResolveCacheImporterFunc func(ctx context.Context, g session.Group, attrs map[string]string) (Importer, ocispecs.Descriptor, error)
2651

@@ -47,24 +72,52 @@ func (ci *contentCacheImporter) Resolve(ctx context.Context, desc ocispecs.Descr
4772
return nil, err
4873
}
4974

50-
var mfst ocispecs.Index
51-
if err := json.Unmarshal(dt, &mfst); err != nil {
75+
manifestType, err := inferManifestType(ctx, dt)
76+
if err != nil {
5277
return nil, err
5378
}
5479

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

83+
allLayers := v1.DescriptorProvider{}
5784
var configDesc ocispecs.Descriptor
5885

59-
for _, m := range mfst.Manifests {
60-
if m.MediaType == v1.CacheConfigMediaTypeV0 {
61-
configDesc = m
62-
continue
86+
if manifestType == ManifestList {
87+
var mfst ocispecs.Index
88+
if err := json.Unmarshal(dt, &mfst); err != nil {
89+
return nil, err
90+
}
91+
92+
for _, m := range mfst.Manifests {
93+
if m.MediaType == v1.CacheConfigMediaTypeV0 {
94+
configDesc = m
95+
continue
96+
}
97+
allLayers[m.Digest] = v1.DescriptorProviderPair{
98+
Descriptor: m,
99+
Provider: ci.provider,
100+
}
63101
}
64-
allLayers[m.Digest] = v1.DescriptorProviderPair{
65-
Descriptor: m,
66-
Provider: ci.provider,
102+
} else if manifestType == ImageManifest {
103+
var mfst ocispecs.Manifest
104+
if err := json.Unmarshal(dt, &mfst); err != nil {
105+
return nil, err
67106
}
107+
108+
for _, m := range mfst.Layers {
109+
if m.MediaType == v1.CacheConfigMediaTypeV0 {
110+
configDesc = m
111+
continue
112+
}
113+
allLayers[m.Digest] = v1.DescriptorProviderPair{
114+
Descriptor: m,
115+
Provider: ci.provider,
116+
}
117+
}
118+
} else {
119+
err = errors.Wrapf(err, "Unsupported or uninferrable manifest type")
120+
return nil, err
68121
}
69122

70123
if dsls, ok := ci.provider.(DistributionSourceLabelSetter); ok {
@@ -97,6 +150,37 @@ func (ci *contentCacheImporter) Resolve(ctx context.Context, desc ocispecs.Descr
97150
return solver.NewCacheManager(ctx, id, keysStorage, resultStorage), nil
98151
}
99152

153+
// extends support for "new"-style image-manifest style remote cache manifests and determining downstream
154+
// handling based on inference of document structure (is this a new or old cache manifest type?)
155+
func inferManifestType(ctx context.Context, dt []byte) (ManifestType, error) {
156+
// this is a loose schema superset of both OCI Index and Manifest in order to
157+
// be able to poke at the structure of the imported cache manifest
158+
type OpenManifest struct {
159+
specs.Versioned
160+
161+
MediaType string `json:"mediaType,omitempty"`
162+
Config map[string]interface{} `json:"config,omitempty"`
163+
// Manifests references platform specific manifests.
164+
Manifests []map[string]interface{} `json:"manifests,omitempty"`
165+
Layers []map[string]interface{} `json:"layers,omitempty"`
166+
}
167+
168+
var openManifest OpenManifest
169+
if err := json.Unmarshal(dt, &openManifest); err != nil {
170+
return NotInferred, err
171+
}
172+
173+
if len(openManifest.Manifests) == 0 && len(openManifest.Layers) > 0 {
174+
return ImageManifest, nil
175+
}
176+
177+
if len(openManifest.Layers) == 0 && len(openManifest.Manifests) > 0 {
178+
return ManifestList, nil
179+
}
180+
181+
return NotInferred, nil
182+
}
183+
100184
func readBlob(ctx context.Context, provider content.Provider, desc ocispecs.Descriptor) ([]byte, error) {
101185
maxBlobSize := int64(1 << 20)
102186
if desc.Size > maxBlobSize {

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)