Skip to content

Commit

Permalink
added import/export support for OCI compatible image manifest version…
Browse files Browse the repository at this point in the history
… of cache manifest (opt-in on export, inferred on import) moby/buildkit moby#2251

Signed-off-by: Matt Kang <impulsecss@gmail.com>
  • Loading branch information
kattmang committed May 3, 2023
1 parent 01148a5 commit 864270f
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 67 deletions.
15 changes: 15 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ issues:
- linters:
- revive
text: "stutters"
- linters:
- revive
text: "empty-block"
- linters:
- revive
text: "superfluous-else"
- linters:
- revive
text: "unused-parameter"
- linters:
- revive
text: "redefines-builtin-id"
- linters:
- revive
text: "if-return"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ buildctl build ... \
* `min`: only export layers for the resulting image
* `max`: export all the layers of all intermediate steps
* `ref=<ref>`: specify repository reference to store cache, e.g. `docker.io/user/image:tag`
* `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`)
* `oci-mediatypes=<true|false>`: whether to use OCI mediatypes in exported manifests (default: `true`, since BuildKit `v0.8`)
* `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`
* `compression-level=<value>`: choose compression level for gzip, estargz (0-9) and zstd (0-22)
Expand All @@ -414,6 +415,7 @@ The directory layout conforms to OCI Image Spec v1.0.
* `max`: export all the layers of all intermediate steps
* `dest=<path>`: destination directory for cache exporter
* `tag=<tag>`: specify custom tag of image to write to local index (default: `latest`)
* `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`)
* `oci-mediatypes=<true|false>`: whether to use OCI mediatypes in exported manifests (default `true`, since BuildKit `v0.8`)
* `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`.
* `compression-level=<value>`: compression level for gzip, estargz (0-9) and zstd (0-22)
Expand Down
126 changes: 96 additions & 30 deletions cache/remotecache/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/moby/buildkit/util/progress"
"github.com/moby/buildkit/util/progress/logs"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
Expand All @@ -43,18 +42,77 @@ const (
ExporterResponseManifestDesc = "cache.manifest"
)

type contentCacheExporter struct {
solver.CacheExporterTarget
chains *v1.CacheChains
ingester content.Ingester
oci bool
ref string
comp compression.Config
func NewExporter(ingester content.Ingester, ref string, oci bool, imageManifest bool, compressionConfig compression.Config) Exporter {
cc := v1.NewCacheChains()
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, imageManifest: imageManifest, ref: ref, comp: compressionConfig}
}

func NewExporter(ingester content.Ingester, ref string, oci bool, compressionConfig compression.Config) Exporter {
cc := v1.NewCacheChains()
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, ref: ref, comp: compressionConfig}
type ExportableCache struct {
exportedManifest ocispecs.Manifest
exportedIndex ocispecs.Index
cacheType int
}

func (ec *ExportableCache) GetMediaType() string {
if ec.cacheType == 0 {
return ec.exportedIndex.MediaType
}
return ec.exportedManifest.MediaType
}

func (ec *ExportableCache) SetSchemaVersion(version int) {
if ec.cacheType == 0 {
ec.exportedIndex.SchemaVersion = version
} else {
ec.exportedManifest.SchemaVersion = version
}
}
func (ec *ExportableCache) SetMediaType(mediaType string) {
if ec.cacheType == 0 {
ec.exportedIndex.MediaType = mediaType
} else {
ec.exportedManifest.MediaType = mediaType
}
}

func (ec *ExportableCache) AddCacheBlob(blob ocispecs.Descriptor) {
if ec.cacheType == 0 {
ec.exportedIndex.Manifests = append(ec.exportedIndex.Manifests, blob)
} else {
ec.exportedManifest.Layers = append(ec.exportedManifest.Layers, blob)
}
}

func (ec *ExportableCache) FinalizeCache(ctx context.Context, ce *contentCacheExporter) {
// Nothing needed here for Manifest-type cache manifests
if ec.cacheType == 0 {
ec.exportedIndex.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ce.oci, ec.exportedIndex.Manifests...)
}
}

func (ec *ExportableCache) SetConfig(config ocispecs.Descriptor) {
if ec.cacheType == 0 {
ec.exportedIndex.Manifests = append(ec.exportedIndex.Manifests, config)
} else {
ec.exportedManifest.Config = config
}
}

func (ec *ExportableCache) GetCacheJSON() ([]byte, error) {
if ec.cacheType == 0 {
return json.Marshal(ec.exportedIndex)
}
return json.Marshal(ec.exportedManifest)
}

type contentCacheExporter struct {
solver.CacheExporterTarget
chains *v1.CacheChains
ingester content.Ingester
oci bool
imageManifest bool
ref string
comp compression.Config
}

func (ce *contentCacheExporter) Name() string {
Expand All @@ -74,21 +132,23 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
return nil, err
}

// own type because oci type can't be pushed and docker type doesn't have annotations
type manifestList struct {
specs.Versioned

MediaType string `json:"mediaType,omitempty"`

// Manifests references platform specific manifests.
Manifests []ocispecs.Descriptor `json:"manifests"`
cache := ExportableCache{}
if ce.imageManifest {
cache.cacheType = 1
} else {
cache.cacheType = 0
}
cache.SetSchemaVersion(2)

var mfst manifestList
mfst.SchemaVersion = 2
mfst.MediaType = images.MediaTypeDockerSchema2ManifestList
if ce.oci {
mfst.MediaType = ocispecs.MediaTypeImageIndex
if ce.oci && !ce.imageManifest {
cache.SetMediaType(ocispecs.MediaTypeImageIndex)
} else if ce.imageManifest {
if !ce.oci {
return nil, errors.Errorf("invalid configuration for remote cache")
}
cache.SetMediaType(ocispecs.MediaTypeImageManifest)
} else {
cache.SetMediaType(images.MediaTypeDockerSchema2ManifestList)
}

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

mfst.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ce.oci, mfst.Manifests...)
cache.FinalizeCache(ctx, ce)

dt, err := json.Marshal(config)
if err != nil {
Expand All @@ -122,9 +182,9 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
}
configDone(nil)

mfst.Manifests = append(mfst.Manifests, desc)
cache.SetConfig(desc)

dt, err = json.Marshal(mfst)
dt, err = cache.GetCacheJSON()
if err != nil {
return nil, errors.Wrap(err, "failed to marshal manifest")
}
Expand All @@ -133,9 +193,14 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
desc = ocispecs.Descriptor{
Digest: dgst,
Size: int64(len(dt)),
MediaType: mfst.MediaType,
MediaType: cache.GetMediaType(),
}

mfstLog := fmt.Sprintf("writing cache manifest %s", dgst)
if ce.imageManifest {
mfstLog = fmt.Sprintf("writing cache image manifest %s", dgst)
}
mfstDone := progress.OneOff(ctx, fmt.Sprintf("writing manifest %s", dgst))
mfstDone := progress.OneOff(ctx, mfstLog)
if err := content.WriteBlob(ctx, ce.ingester, dgst.String(), bytes.NewReader(dt), desc); err != nil {
return nil, mfstDone(errors.Wrap(err, "error writing manifest blob"))
}
Expand All @@ -145,5 +210,6 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string
}
res[ExporterResponseManifestDesc] = string(descJSON)
mfstDone(nil)

return res, nil
}
53 changes: 43 additions & 10 deletions cache/remotecache/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package remotecache
import (
"context"
"encoding/json"
"fmt"
"io"
"sync"
"time"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/imageutil"
"github.com/moby/buildkit/util/progress"
"github.com/moby/buildkit/worker"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
Expand Down Expand Up @@ -47,24 +49,55 @@ func (ci *contentCacheImporter) Resolve(ctx context.Context, desc ocispecs.Descr
return nil, err
}

var mfst ocispecs.Index
if err := json.Unmarshal(dt, &mfst); err != nil {
manifestType, err := imageutil.DetectManifestBlobMediaType(dt)
if err != nil {
return nil, err
}

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

allLayers := v1.DescriptorProvider{}
var configDesc ocispecs.Descriptor

for _, m := range mfst.Manifests {
if m.MediaType == v1.CacheConfigMediaTypeV0 {
configDesc = m
continue
switch manifestType {
case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex:
var mfst ocispecs.Index
if err := json.Unmarshal(dt, &mfst); err != nil {
return nil, err
}

for _, m := range mfst.Manifests {
if m.MediaType == v1.CacheConfigMediaTypeV0 {
configDesc = m
continue
}
allLayers[m.Digest] = v1.DescriptorProviderPair{
Descriptor: m,
Provider: ci.provider,
}
}
case images.MediaTypeDockerSchema2Manifest, ocispecs.MediaTypeImageManifest:
var mfst ocispecs.Manifest
if err := json.Unmarshal(dt, &mfst); err != nil {
return nil, err
}

if mfst.Config.MediaType == v1.CacheConfigMediaTypeV0 {
configDesc = mfst.Config
} else {
err = errors.Wrapf(err, "Image Manifest cache is missing expected cache Config mediatype")
return nil, err
}
allLayers[m.Digest] = v1.DescriptorProviderPair{
Descriptor: m,
Provider: ci.provider,
for _, m := range mfst.Layers {
allLayers[m.Digest] = v1.DescriptorProviderPair{
Descriptor: m,
Provider: ci.provider,
}
}
default:
err = errors.Wrapf(err, "Unsupported or uninferrable manifest type")
return nil, err
}

if dsls, ok := ci.provider.(DistributionSourceLabelSetter); ok {
Expand Down
11 changes: 10 additions & 1 deletion cache/remotecache/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
attrDigest = "digest"
attrSrc = "src"
attrDest = "dest"
attrImageManifest = "image-manifest"
attrOCIMediatypes = "oci-mediatypes"
contentStoreIDPrefix = "local:"
)
Expand Down Expand Up @@ -50,12 +51,20 @@ func ResolveCacheExporterFunc(sm *session.Manager) remotecache.ResolveCacheExpor
}
ociMediatypes = b
}
imageManifest := false
if v, ok := attrs[attrImageManifest]; ok {
b, err := strconv.ParseBool(v)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s", attrImageManifest)
}
imageManifest = b
}
csID := contentStoreIDPrefix + store
cs, err := getContentStore(ctx, sm, g, csID)
if err != nil {
return nil, err
}
return &exporter{remotecache.NewExporter(cs, "", ociMediatypes, compressionConfig)}, nil
return &exporter{remotecache.NewExporter(cs, "", ociMediatypes, imageManifest, compressionConfig)}, nil
}
}

Expand Down
11 changes: 10 additions & 1 deletion cache/remotecache/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func canonicalizeRef(rawRef string) (reference.Named, error) {

const (
attrRef = "ref"
attrImageManifest = "image-manifest"
attrOCIMediatypes = "oci-mediatypes"
attrInsecure = "registry.insecure"
)
Expand Down Expand Up @@ -67,6 +68,14 @@ func ResolveCacheExporterFunc(sm *session.Manager, hosts docker.RegistryHosts) r
}
ociMediatypes = b
}
imageManifest := false
if v, ok := attrs[attrImageManifest]; ok {
b, err := strconv.ParseBool(v)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s", attrImageManifest)
}
imageManifest = b
}
insecure := false
if v, ok := attrs[attrInsecure]; ok {
b, err := strconv.ParseBool(v)
Expand All @@ -82,7 +91,7 @@ func ResolveCacheExporterFunc(sm *session.Manager, hosts docker.RegistryHosts) r
if err != nil {
return nil, err
}
return &exporter{remotecache.NewExporter(contentutil.FromPusher(pusher), refString, ociMediatypes, compressionConfig)}, nil
return &exporter{remotecache.NewExporter(contentutil.FromPusher(pusher), refString, ociMediatypes, imageManifest, compressionConfig)}, nil
}
}

Expand Down
Loading

0 comments on commit 864270f

Please sign in to comment.