-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Import/export support for OCI compatible image manifest version of cache manifest (opt-in on export, inferred on import) #3724
Conversation
Also, looking for maintainer feedback on how we can get this change into the next upcoming release branch for Buildkit/Docker CLI support. |
Converted to draft as I see some integ tests failures on tests I wasn't able to test locally due to work network limitations. EDIT: error appears to be for all/most to be related to cache manifest push tests (on the old image index format):
Image Index cache manifest works fine on my box to my local cache with the PR commit
|
Fixed in amended commit, existing integ tests are passing now. Humbly looking for guidance on where to add integ tests for the new ImageManifest type :) |
Linter errors fixed as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look at the current integration tests, eg. testBasicCacheImportExport
, testCacheImportExport
and add variants where there is opt-in to this version.
cache/remotecache/export.go
Outdated
@@ -75,20 +76,23 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string | |||
} | |||
|
|||
// own type because oci type can't be pushed and docker type doesn't have annotations | |||
type manifestList struct { | |||
type abstractManifest struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use different types for this and don't try to combine them together. I believe missing MediaType
issue was fixed in latest oci spec so probably custom types are not needed anymore at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think ocispecs.Manifest and ocispecs.Index would work respectively. I think if that's the case though, for the rest of the export code I would either need to:
- create an wrapper struct around
Manifest
andIndex
(seems to still have the same concern you have here) and implement functions that map the generated cache descriptors to the appropriate spec/oci object akaWrapperObj.addCacheDescriptor()
OR
- Have two object references for
Manifest
andIndex
and interchange which is used when the same "flat" way the code is structured now (bifurcation/ifs where the content descriptors are actually being set)
Is the concern more around the possibility of generating a nonconforming OCI Index/Manifest that has a superset of fields? If so, going with Approach 2 would create a new problem of writing to the wrong Object altogether and at the end of Finalize
encode an empty object to string. I kind of prefer Approach 1 as the wrapping struct could have a setCacheFormat()
, which would remove all of the if ce.imageManifest
references in Finalize
. I only worry that with Approach 1 we may be abstracting in the wrong direction (should Finalize become be a switchboard to what generated descriptors go where or should it only concern itself about the generation of the descriptors?)
I'm going to go with Approach 1 as I can formalize it in my head better but open to alternatives.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we probably would want to do option 3: use the ocispecs.Index struct, and simply set the mediaType depending on whether oci-mediatypes has been appropriately configured: see
buildkit/exporter/containerimage/writer.go
Lines 170 to 180 in 3187d2d
idx := ocispecs.Index{ | |
MediaType: ocispecs.MediaTypeImageIndex, | |
Annotations: opts.Annotations.Platform(nil).Index, | |
Versioned: specs.Versioned{ | |
SchemaVersion: 2, | |
}, | |
} | |
if !opts.OCITypes { | |
idx.MediaType = images.MediaTypeDockerSchema2ManifestList | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jedevc I'm not sure how Index
can be adapted to support both Index
and Manifest
(Manifest
is the format that being introduced into both import.go
and export.go
here). Maybe I'm missing something here.
EDIT: point taken on oci-mediatypes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See new commit: I went ahead with option one. I also added checks on oci-mediatype compatibility with image manifest: #3724 (comment)
cache/remotecache/export.go
Outdated
@@ -122,7 +132,11 @@ func (ce *contentCacheExporter) Finalize(ctx context.Context) (map[string]string | |||
} | |||
configDone(nil) | |||
|
|||
mfst.Manifests = append(mfst.Manifests, desc) | |||
if ce.imageManifest { | |||
mfst.Config = &desc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the mediatype correct in here for the config descriptor? @sudo-bmitch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this still looks correct to me. We should definitely avoid changing the config media type here if at all possible since some registries have a hardcoded list of allowed config media types (as demonstrated in #3610) - if we keep it the same, we won't have the same problem, and this new way of storing cache should "just work".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that looks right. We've added guidance in image-spec for packaging artifacts (like the build cache) in the image manifest. My advice is to stick with the first example for now, which I believe you're already doing: https://github.com/opencontainers/image-spec/blob/933f91770d22839aca9c0ae46abc01321ca4ee70/manifest.md#guidelines-for-artifact-usage
cache/remotecache/import.go
Outdated
} | ||
|
||
for _, m := range mfst.Layers { | ||
if m.MediaType == v1.CacheConfigMediaTypeV0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not look correct. It is not layer mediatype that is CacheConfigMediaTypeV0
but the config's.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, yeah. This indeed popped when I added integ tests :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in new commit.
cache/remotecache/import.go
Outdated
@@ -97,6 +150,37 @@ func (ci *contentCacheImporter) Resolve(ctx context.Context, desc ocispecs.Descr | |||
return solver.NewCacheManager(ctx, id, keysStorage, resultStorage), nil | |||
} | |||
|
|||
// extends support for "new"-style image-manifest style remote cache manifests and determining downstream | |||
// handling based on inference of document structure (is this a new or old cache manifest type?) | |||
func inferManifestType(ctx context.Context, dt []byte) (ManifestType, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have a function called DetectManifestMediaType
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh, I feel very lucky it already exists :) Good learning for me to see a better quality implementation as well. I also like that it uses the containerd/oci mediatypes which seems more direct and extensible than my fake-enum approach.
Thanks for the feedback and pointers on new integ tests, @tonistiigi .Very insightful and will address yours and others' feedback items point by point after vacation in April! |
cache/remotecache/export.go
Outdated
mfst.SchemaVersion = 2 | ||
mfst.MediaType = images.MediaTypeDockerSchema2ManifestList | ||
if ce.oci { | ||
if ce.oci && !ce.imageManifest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should still respect the value of oci-mediatypes
here, even when image-manifest=true
.
The default for oci-mediatypes
is false, so it will still work out of the box with ECR, but users may use other registries and want to use OCI media types instead, so we should give them the option to enable this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably oci-mediatypes=false
and image-manifest=true
would be invalid configuration and should just error.
What would be a better name for image-manifest=true
? Is it oci-artifact-manifest
oci-artifact
? I'm quite confused about what they mean, but image-manifest
seems too generic and doesn't convey the meaning that this uses a config with custom mediatyoe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Funnily enough in my initial work for this I did use oci-artifact
as the key name here, but after working with some colleagues more closely associated with the OCI image-spec 1.1 work ( tagging @michaelb990 and @jlbutler ), we found that the term artifact
has become semantically loaded. More specifically there's two artifacts in people's mind in the space:
- small "a" artifacts: image manifest + layers with a nonstandardized
config.mediaType
(this aligns with the cache manifest approach in this PR) - capital "A" Artifacts: artifact manifest + blobs (currently being pulled out / made optional in 1.1). This is an approach we could take for cache manifest if/when this manifest type becomes more prevalent.
As I work through this happy to consider other names for the key.
Will think through oci-mediatypes: I think I lean on erroring here though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it more: ImageManifest mode could support docker's image manifest version (which I don't think makes sense given registry support is wide enough for OCI manifest), so I am going to error if oci-mediatypes=false and image-manifest=true as invalid and down the road that can be implemented as needed/warranted. I also will update the readme to instruct client users to need to toggle both for Image Manifest remote caching to work.
Added Client and Dockerfile Integ tests, and addressed feedback, primarily the largest thing being a bugfix to the import on imageManifest being broken on config mediatype being checked on layers instead of Config (thanks @tonistiigi ). Also tore out my manifest type inference code in favor of |
one second, rebuilding branch. EDIT: FIxed, ready for review |
cache/remotecache/export.go
Outdated
type ExportableCache struct { | ||
exportedManifest ocispecs.Manifest | ||
exportedIndex ocispecs.Index | ||
cacheType int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add actual type for this with iota
const values.
cache/remotecache/export.go
Outdated
cc := v1.NewCacheChains() | ||
return &contentCacheExporter{CacheExporterTarget: cc, chains: cc, ingester: ingester, oci: oci, ref: ref, comp: compressionConfig} | ||
type ExportableCache struct { | ||
exportedManifest ocispecs.Manifest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment in here that there are two styles of exports. Index of blobs and OCI artifact manifest.
cache/remotecache/export.go
Outdated
|
||
// Manifests references platform specific manifests. | ||
Manifests []ocispecs.Descriptor `json:"manifests"` | ||
cache := ExportableCache{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iiuc the public functions SetMediaType
and SetSchemaVersion
are not needed. You can just make a constructor NewExportableCache(type)
and everything is set up internally in that function.
For the oci
check, this can be before NewExportableCache
is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember when I was writing this I had it like that and I changed it to this (I have no idea why and on reflection this just makes more sense).
cache/remotecache/export.go
Outdated
} | ||
|
||
func (ec *ExportableCache) FinalizeCache(ctx context.Context, ce *contentCacheExporter) { | ||
// Nothing needed here for Manifest-type cache manifests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure ConvertAllLayerMediaTypes
is not needed? Is it guaranteed to be OCI already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my tests I think it "accidentally" was OCI compatible, but I think you're right in that there's no guarantee that whatever the solver gives is going to be a consistent OCI Layer
type. We should call this regardless of cache type to ensure the entire cache manifest is internally consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment in here was not removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Literally had my IDE open to remove it as I got this message :)
cache/remotecache/export.go
Outdated
} | ||
} | ||
|
||
func (ec *ExportableCache) GetCacheJSON() ([]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Call this MarshalJSON()
.
cache/remotecache/export.go
Outdated
@@ -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(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After removing SetMediaType
, Get
prefix is not needed here.
cache/remotecache/import.go
Outdated
if mfst.Config.MediaType == v1.CacheConfigMediaTypeV0 { | ||
configDesc = mfst.Config | ||
} else { | ||
err = errors.Wrapf(err, "Image Manifest cache is missing expected cache Config mediatype") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lowercase errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err
is nil
in here. This needs to be errors.Errorf
. Include the mediatype that doesn't match in the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
} | ||
default: | ||
err = errors.Wrapf(err, "Unsupported or uninferrable manifest type") | ||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how is the inline
case still hit in line 112
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would work fine if the ref
'd manifest happens to be a Manifest List/Index and had an inline cache in its config (can that even happen?), but it would fail to hit the inline
case if the ref
is an Image Manifest because of line 88. I propose just ripping out lines 88 through 91 and letting any malform failures happen in importInlineCache
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, my apologies for not seeing the integ test failure indicating this issue: I did see some integ tests fail without any changes from my part so I thought that was attributable to other changes in master. Very good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just tested in my local, ripping out 88-91 does indeed fix the integ tests failing below.
Also rebase to remove the merge commit from the commit chain. |
badd8b2
to
ba2c7dc
Compare
I've addressed comments and rebased. |
Integ tests look clean from my repo: https://github.com/kattmang/buildkit-cache-manifest-oci/actions/runs/4868808356/jobs/8682618226 |
Just to confirm it looks like the single test failure here is for a test unrelated to this change, but want to see what if you've seen this one: Happy to pull from the latest master and resubmit. EDIT: looks like reruns did the trick and is part of a subset of tests that are flakey. Corresponded over slack with @tonistiigi and appears related to #3401 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested this and seemed to work correctly
cache/remotecache/export.go
Outdated
CacheType: cacheType, | ||
} | ||
} | ||
return &ExportableCache{ExportedManifest: ocispecs.Manifest{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add switch for the cacheType with error case for unknown value.
cache/remotecache/export.go
Outdated
CacheType CacheType | ||
} | ||
|
||
func NewExportableCache(cacheType CacheType, mediaType string, schemaVersion specs.Versioned) *ExportableCache { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is schemaVersion a parameter? It is always 2
and can't be configured.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of mediatype this should take oci
parameter so that the type switch only happens once. And caller can't make a mistake setting incompatible cacheType and mediaType.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I take this to mean we should pull in scope from Finalize where we determine mediatype and cachetype more into the Cache object itself, which makes sense.
configDesc = m | ||
continue | ||
switch manifestType { | ||
case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be follow-up, but I guess manifest list of "cache image manifests" should also be a valid configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm not sure I should tackle this because I don't know how such a cache would/could be exported.
|
||
dt2, err = os.ReadFile(filepath.Join(destDir, "unique")) | ||
require.NoError(t, err) | ||
require.Equal(t, string(dt), string(dt2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, you should also test in here that the manifest in the registry is an image manifest and it has a config with correct mediatype. Otherwise, this test would pass without any of your changes as well because image-manifest=true
would just be ignored.
… of cache manifest (opt-in on export, inferred on import) moby/buildkit moby#2251 Signed-off-by: Kang, Matthew <impulsecss@gmail.com>
Made the integ test change and the ExportableCache refactor @tonistiigi . LMK if we're good here. |
cache/remotecache/export.go
Outdated
|
||
func NewExportableCache(oci bool, imageManifest bool) (*ExportableCache, error) { | ||
var mediaType string | ||
if oci && !imageManifest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't read very well.
var mediaType string
cacheType := ManifestList
if imageManifest {
cacheType = ImageManifest
if oci {} else {}
} else {
if oci {} else {}
}
cache/remotecache/export.go
Outdated
const ( | ||
// ExportResponseManifestDesc is a key for the map returned from Exporter.Finalize. | ||
// The map value is a JSON string of an OCI desciptor of a manifest. | ||
ExporterResponseManifestDesc = "cache.manifest" | ||
CacheManifestSchemaVersion = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be public.
If you combine the schemaVersion to a single definition, then this doesn't need to be defined as constant at all.
cache/remotecache/export.go
Outdated
func (ec *ExportableCache) FinalizeCache(ctx context.Context, ce *contentCacheExporter) { | ||
// Nothing needed here for Manifest-type cache manifests | ||
if ec.CacheType == ManifestList { | ||
ec.ExportedIndex.Manifests = compression.ConvertAllLayerMediaTypes(ctx, ce.oci, ec.ExportedIndex.Manifests...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just store the .oci
in NewExportableCache
so it can be read from ec
.
5ebb4b3
to
a7f450c
Compare
cache/remotecache/export.go
Outdated
} | ||
|
||
func (ec *ExportableCache) FinalizeCache(ctx context.Context, ce *contentCacheExporter) { | ||
// Nothing needed here for Manifest-type cache manifests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment in here was not removed
cache/remotecache/export.go
Outdated
ExportedManifest ocispecs.Manifest | ||
ExportedIndex ocispecs.Index | ||
CacheType CacheType | ||
Oci bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if it is public then OCI
cache/remotecache/export.go
Outdated
|
||
func (data CacheType) String() string { | ||
switch data { | ||
case NotSet: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: case NotSet:
is not needed here
Signed-off-by: Matt Kang <impulsecss@gmail.com>
Thanks for your hard work in the review @tonistiigi ! Excited to see if we can fold this into 0.12. |
Can't wait for the new release. If you want to test or use it early, you can use the master tag https://hub.docker.com/r/moby/buildkit/tags |
This gives us access to moby/buildkit#3724 which (hopefully) unblocks errors such as https://github.com/firezone/firezone/actions/runs/6425322959/job/17447712176?pr=2258#step:6:158 when trying to pull the cache. --------- Signed-off-by: Thomas Eizinger <thomas@eizinger.io> Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
Import/export support for OCI compatible image manifest version of cache manifest (opt-in on export, inferred on import)
This feature addition is an extension to the remote and local cache feature. This generally follows @joadrp's Proposal in #2251 while also taking in Tonis' comment on thread regarding an opt-in key for exporting with this new cache manifest format. #2251 (comment)
To summarize, this commit adds:
export-cache
key for both local and registry typesimage-manifest
bool key with default False (opt-in) to the contentCacheExporter.Tested to both
local
andregistry
, and tested for regression to the old manifest type, using ECR as the registry destination (At ECR we do NOT support the Image Index format as it isn't strict to the OCI spec for valid mediaTypes in the manifest list).Open to additional testing suggestions.