c8d/image: Simplify presentImages and better "platform not found" error#48276
c8d/image: Simplify presentImages and better "platform not found" error#48276thaJeztah merged 7 commits intomoby:masterfrom
presentImages and better "platform not found" error#48276Conversation
5338fd4 to
ff766a2
Compare
a00af73 to
b729268
Compare
|
|
||
| return &childManifests[0], nil | ||
| if best == nil { | ||
| return nil, &errPlatformNotFound{imageRef: imageFamiliarName(img)} |
There was a problem hiding this comment.
should we not set the wanted platform here?
There was a problem hiding this comment.
Nevermind, saw that we're doing this in the caller. Wonder if we could move that.
There was a problem hiding this comment.
We should, but we can't set a specific ocispec.Platform here because the input is a platform matcher (which can match multiple platforms).
The expectation is that the caller can fill this on its own, like:
moby/daemon/containerd/image_history.go
Lines 31 to 36 in 063ee50
There was a problem hiding this comment.
I see. I don't love having to remember to do that, but oh well 🤷♀️ Maybe we could mention that in getBestPresentImageManifest's godoc?
There was a problem hiding this comment.
Hmm I got a funny idea, let me try it out 🙈
There was a problem hiding this comment.
So, I changed matchRequestedOrDefault to return a wrapper that also has the requested platform bundled in and use that to populate the error when it's created, so the caller doesn't need to do it.
WDYT?
|
|
||
| func (e *errPlatformNotFound) NotFound() {} | ||
| func (e *errPlatformNotFound) Error() string { | ||
| msg := "image with reference " + e.imageRef + " was found but does not match the specified platform" |
There was a problem hiding this comment.
Perhaps requested instead of specified would work for this? Thinking about situations where no explicit platform was requested, so the default was used (or don't we produce the error in any of those cases)?
Also considering if match -> provide (or support); which may be more accurate for multi-arch 🤔
There was a problem hiding this comment.
It would! But that would be different from the graphdrivers error returned in the same case. So that could break potential error string matching.
IMO, until we provide a better way for consumers to check for this error, we should stick to the existing wording.
There was a problem hiding this comment.
IMO, until we provide a better way for consumers to check for this error, we should stick to the existing wording.
Hmm.. right; you mean distinction between "image present, but not platform" vs "image not present"? FWIW; I was more concerned about the No such errors, because those prefixes were used in docker itself to match error-types; for the errors we're modifying here, we never had that as a contract, so I'm much more OK with changing the error format.
There was a problem hiding this comment.
Yeah. We might not have had that as a contract explicitly, but unfortunately Go stringly-typed errors forced that upon us implicitly I think 😅
There's probably not much of consumers doing string matching for this specific error, but I found at least one instance: https://github.com/buildpacks/pack/blob/5d0687f636fc6ab0ac5aef4afe0274965943f574/pkg/image/fetcher.go#L125
There was a problem hiding this comment.
but unfortunately Go stringly-typed errors forced that upon us implicitly I think 😅
For exported, sentinel errors, for sure (but also the reason we chose to use the "interface-matching" approach in errdefs). I think in this case, the error itself is never returned to the client as a go-error that can be matched, as the API is in between, so at most the client receives a typed error (errdefs type that is) with an error message that should be considered informational, but in itself not something to control flow.
but I found at least one instance:
:sad_panda: yeah, that's not great. At least it looks like that code already calls it out for being brittle and temporary, and it looks like the issue linked to that comment was closed / addressed buildpacks/pack#2079. That said, I agree we may have to look if the current logic of an image either being "there" or "not there" (errdefs.NotFound) is still sufficient to handle logic on the client side, or if we need more specific errors (that also make it accross the API).
I'm ok with keeping the error-message as-is for now, but we should indeed make sure that we don't paint ourself (again) into a corner where error-strings become an undocumented API, and we must preserve error-messages (like we had with the No such prefix).
There was a problem hiding this comment.
Perhaps we should add a section to the API docs describing that error-message MUST be considered informational, and can change at any time, so string-matching being highly discouraged.
| if e.wanted.OS != "" { | ||
| msg += ": wanted " + platforms.Format(e.wanted) | ||
| } |
There was a problem hiding this comment.
Playing around a bit as I was wondering if putting this after a : delimiter may feel like it's a separate error that was wrapped. I guess we could included it between braces;
image with reference foo:bar was found but does not match the requested platform (linux/amd64)
Only downside of that would be if we'd have windows ( the requested platform (windows(10.0.17763)/amd64))
There was a problem hiding this comment.
The wanted: part is also present in the graphdrivers error, see above comment.
| if len(presentImages) == 0 { | ||
| return nil, errdefs.NotFound(errors.New("no matching image found")) | ||
| return nil, &errPlatformNotFound{ | ||
| imageRef: img.Name, | ||
| } |
There was a problem hiding this comment.
Is this situation is only if the manifest index does not provide a usable platform, or also if it does (but the content isn't present)?
There was a problem hiding this comment.
To add to the above; the reason I was wondering is because I know we have logic on the client-side where we have special handling for not found errors; when doing a docker run (or docker create), and we get a 404, the CLI initiates a docker pull (image not found; let's try pulling it).
Very likely not for this PR, but something we need to start looking at, because with multi-arch there will be more permutations of that;
- The image is missing as a whole; we'll try pulling the image (manifest-index) and the desired platform.
- We pulled the manifest-index, and it has an image for the given platform, but it's not yet pulled. In this case, we can pull the missing platform (but keep the manifest-index at the same version)
- We pulled the manifest-index, but it does not contain the given platform
The last one is the tricky one, at least with the current logic not being granular enough;
- We can either bail out (we have the manifest-index, but unfortunately it doesn't have this platform)
- We can try pulling the manifest-index to see if perhaps a current version of it does have the platform (maybe the image was updated since we pulled the manifest-index?)
Either of the above are "valid", but we don't have a lot of granularity for this currently. The last one also can be confusing, as pulling the manifest-index may mean that any image (for other platform) may now become "dangling" as they no longer match the digest of the newly pulled manifest-index.
|
|
||
| // ReadConfig gets the image config and unmarshals it into the provided struct. | ||
| // The provided struct should be a pointer to the config struct or its subset. | ||
| func (m *ImageManifest) ReadConfig(ctx context.Context, outConfig any) error { |
There was a problem hiding this comment.
Adding any may require this file to have a go:build comment (#46941) (although I don't think this file would be imported by consumers)
Looks like this is only used internally; so we may not need to export it; more so because having to pass the type to read feels a bit "internal"; i.e., if it's encapsulated in the ImageManifest type, then somewhat expecting that to know what it returns; being able to unmarshal to something else is mostly an internal optimisation.
There was a problem hiding this comment.
I can change it to interface{}.
This struct already has most of the methods marked as exported (to signify that they're not "internal" and should be used by the consumers of the ImageManifest).
I think we could just unexport the whole ImageManifest instead.. WDYT?
There was a problem hiding this comment.
Perhaps "both"; we could indeed un-export the ImageManifest, but that would still keep the methods exported (i.e., matching it to an interface would still expose them); un-exporting the method as well makes it clear it's internal (at least currently).
There was a problem hiding this comment.
Unexporting the method itself is more of an indicator that the method is "internal" to the ImageManifest itself though, which is not the case here as it's used outside of the struct itself by the ImageService.
There was a problem hiding this comment.
Maybe. It's just that an interface could still be matched if it would ever be returned, so doesn't really remain internal; e.g. the errPlatformNotFound is not exported, but still satisfies the errdefs.NotFound interface.
It shouldn't be really problematic (just don't do that!), just thinking if un-exporting it makes it even more clear and it for sure can help looking at code, knowing that it cannot be used outside of the package, without having to go search if it possibly was exported to match some interface .. somewhere..
The `refOrId` parameter is only needed to construct an error in case where the doesn't have the matching platform. Move the responsibility of creating the error message to the caller. Signed-off-by: Paweł Gronowski <[email protected]>
Return a similar error as the graphdrivers implementation when an image was found, but the requested platform is not present locally or in the image. The message doesn't include the "actual" platform, as it doesn't make sense with the multi-platform images. With graphdrivers all images were single platform. Signed-off-by: Paweł Gronowski <[email protected]>
All two usages only care about the "first" result from the slice sorted according to the platform preference. Signed-off-by: Paweł Gronowski <[email protected]>
Only two fields from the whole image config are used, so only unmarshal these. Signed-off-by: Paweł Gronowski <[email protected]>
When `getBestPresentImageManifest` fails with `errPlatformNotFound` - fill the requested platform. Signed-off-by: Paweł Gronowski <[email protected]>
Refactor a pattern where a passed `*ocispec.Platform` was used to create a platform matcher that matches the passed platform if not nil and uses a default host platform otherwise into a separate function. Also add some basic unit tests for its behavior. Signed-off-by: Paweł Gronowski <[email protected]>
Allowing it to obtain a specific `ocispec.Platform` used to create this platform matcher. Signed-off-by: Paweł Gronowski <[email protected]>
f1e4051 to
ee766ae
Compare
- What I did
A minor refactor, best to review commit by commit.
Summary:
presentImagestogetBestPresentImageManifest- all usages were only using the first sorted resulterrPlatformNotFoundwhich produces a better error message in case where the image exists, but the requested platform is not presentmatchRequestedOrDefaultfunction to refactor a common pattern of creating a platform matcher from*ocispec.Platform- How to verify it
- Description for the changelog
- A picture of a cute animal (not mandatory but encouraged)