Skip to content
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

proxy: Make OpenImageOptional work with oci-archive: #2114

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

cgwalters
Copy link
Contributor

By adding the standard ENOENT to our list of errors.

I hit this while working on
coreos/rpm-ostree#4598
which is a tool that builds base images and wants to query if the image exists beforehand.

@cgwalters cgwalters force-pushed the open-optional-ociarchive branch from acaf245 to 21aadaf Compare September 15, 2023 12:55
@@ -235,7 +235,8 @@ func isDockerManifestUnknownError(err error) bool {
// TODO drive this into containers/image properly
func isNotFoundImageError(err error) bool {
return isDockerManifestUnknownError(err) ||
errors.Is(err, ocilayout.ImageNotFoundError{})
errors.Is(err, ocilayout.ImageNotFoundError{}) ||
errors.Is(err, os.ErrNotExist)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK self-review...perhaps the real fix is in c/image to map enoent there to ocilayout.ImageNotFoundError ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says

// ImageNotFoundError is used when the OCI structure, in principle, exists and seems valid enough,
// but nothing matches the “image” part of the provided reference.

Copy link
Contributor

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first impression is to wonder why this needs special handling.

Obviously “an image not found on the registry” is something that can only be detected by talking to the registry with full credentials, i.e. inside Skopeo. Similarly “a tag not found inside an OCI archive” requires an OCI archive implementation.

But if the archive just doesn’t exist… it might make sense for Skopeo to treat that uniformly, for callers that provide an user-specified transport:image syntax without understanding the details of that… and then we get to discuss semantics of what “uniformly” means.

But rpm-ostree compose image seems to have its own format=oci option, so the transport:image value is not user-provided.


Maybe a different way to ask this: Would we be expected to similarly return 0 for docker://nxdomain.invalid/something?) If not, what’s the difference?

Right now the documentation of OpenImageOptional says “If the image does not exist, zero
is returned.”.

What is the criteria for classifying errors as “image does not exist” vs. “other error opening an image”? Does the documentation of OpenImageOptional need clarifying, or updating?


*shrug* rpm-ostree is the only known user, so pragmatically speaking it gets to shape the proxy API however it is useful for rpm-ostree; but I’d like the Skopeo-repo documentation of that API to be reasonably clear about what are the properties that need maintaining. (“The ostree-rs-ext tests continue to pass” is one way to document those properties, I guess. I find that inelegant but I can live with that.)

@@ -235,7 +235,8 @@ func isDockerManifestUnknownError(err error) bool {
// TODO drive this into containers/image properly
func isNotFoundImageError(err error) bool {
return isDockerManifestUnknownError(err) ||
errors.Is(err, ocilayout.ImageNotFoundError{})
errors.Is(err, ocilayout.ImageNotFoundError{}) ||
errors.Is(err, os.ErrNotExist)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says

// ImageNotFoundError is used when the OCI structure, in principle, exists and seems valid enough,
// but nothing matches the “image” part of the provided reference.

@@ -235,7 +235,8 @@ func isDockerManifestUnknownError(err error) bool {
// TODO drive this into containers/image properly
func isNotFoundImageError(err error) bool {
return isDockerManifestUnknownError(err) ||
errors.Is(err, ocilayout.ImageNotFoundError{})
errors.Is(err, ocilayout.ImageNotFoundError{}) ||
errors.Is(err, os.ErrNotExist)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not all that precise — e.g. I think it matches if /var/tmp does not exist.

(See the top-level comment first — what is the condition isNotFoundImageError should actually be checking for? Assuming that being precise about it matters at all…)

@cgwalters
Copy link
Contributor Author

cgwalters commented Sep 15, 2023

Maybe a different way to ask this: Would we be expected to similarly return 0 for docker://nxdomain.invalid/something?) If not, what’s the difference?
Right now the documentation of OpenImageOptional says “If the image does not exist, zero
is returned.”.
What is the criteria for classifying errors as “image does not exist” vs. “other error opening an image”?

For registries, it's really that the server returns HTTP 404 and that's how it works now. Giving something that doesn't expose the registry API (for example) we correctly error out:

$ sudo env rpm-ostree compose image --initialize-mode if-not-exists --format=registry  fedora-tier-1.yaml example.com/blah
error: Failed to invoke skopeo proxy method OpenImageOptional: remote error: pinging container registry example.com: StatusCode: 404, <!doctype html>
<html>
<head>
    <title>Example D...

Other cases like failure to make a TCP connection, DNS doesn't resolve all should error out. Only the "HTTP server returned 404" or semantically equivalent should return (in Rust terms) Ok(None).

This is not all that precise — e.g. I think it matches if /var/tmp does not exist.

Yes, agree. In this scenario, that's suboptimal but still okay. The case I care about most are registry and oci (which actually has a distinct bug).

@cgwalters
Copy link
Contributor Author

How about this variant instead? (requires a patch to c/image)

From 63f4e4665b74e2d0d7b58e4fffa7fe665b85590f Mon Sep 17 00:00:00 2001
From: Colin Walters <[email protected]>
Date: Fri, 15 Sep 2023 08:53:45 -0400
Subject: [PATCH] proxy: Make `OpenImageOptional` work with `oci-archive:`

By adding the standard ENOENT to our list of errors.

I hit this while working on
https://github.com/coreos/rpm-ostree/pull/4598
which is a tool that builds base images and wants to query
if the image exists beforehand.

Signed-off-by: Colin Walters <[email protected]>
---
 cmd/skopeo/proxy.go                                  | 12 +++++++++++-
 integration/proxy_test.go                            |  7 +++++++
 .../containers/image/v5/oci/archive/oci_src.go       |  7 ++++++-
 3 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/cmd/skopeo/proxy.go b/cmd/skopeo/proxy.go
index aab85365..40c8e85f 100644
--- a/cmd/skopeo/proxy.go
+++ b/cmd/skopeo/proxy.go
@@ -73,6 +73,7 @@ import (
 
 	"github.com/containers/image/v5/image"
 	"github.com/containers/image/v5/manifest"
+	ociarchive "github.com/containers/image/v5/oci/archive"
 	ocilayout "github.com/containers/image/v5/oci/layout"
 	"github.com/containers/image/v5/pkg/blobinfocache"
 	"github.com/containers/image/v5/transports"
@@ -229,13 +230,22 @@ func isDockerManifestUnknownError(err error) bool {
 	return ec.ErrorCode() == dockerdistributionapi.ErrorCodeManifestUnknown
 }
 
+func isOciImageNotFound(err error) bool {
+	var lerr ocilayout.ImageNotFoundError
+	if errors.As(err, &lerr) {
+		return true
+	}
+	var aerr ociarchive.ImageNotFoundError
+	return errors.As(err, &aerr)
+}
+
 // isNotFoundImageError heuristically attempts to determine whether an error
 // is saying the remote source couldn't find the image (as opposed to an
 // authentication error, an I/O error etc.)
 // TODO drive this into containers/image properly
 func isNotFoundImageError(err error) bool {
 	return isDockerManifestUnknownError(err) ||
-		errors.Is(err, ocilayout.ImageNotFoundError{})
+		isOciImageNotFound(err)
 }
 
 func (h *proxyHandler) openImageImpl(args []any, allowNotFound bool) (retReplyBuf replyBuf, retErr error) {
diff --git a/integration/proxy_test.go b/integration/proxy_test.go
index 826128da..28949eff 100644
--- a/integration/proxy_test.go
+++ b/integration/proxy_test.go
@@ -354,4 +354,11 @@ func (s *proxySuite) TestProxy() {
 		err = fmt.Errorf("Testing optional image %s: %v", knownNotExtantImage, err)
 	}
 	assert.NoError(t, err)
+
+	nonExistentArchive := "oci-archive:/enoent"
+	err = runTestOpenImageOptionalNotFound(p, nonExistentArchive)
+	if err != nil {
+		err = fmt.Errorf("Testing optional image %s: %v", nonExistentArchive, err)
+	}
+	assert.NoError(t, err)
 }
diff --git a/vendor/github.com/containers/image/v5/oci/archive/oci_src.go b/vendor/github.com/containers/image/v5/oci/archive/oci_src.go
index 6c9ee334..819659e7 100644
--- a/vendor/github.com/containers/image/v5/oci/archive/oci_src.go
+++ b/vendor/github.com/containers/image/v5/oci/archive/oci_src.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 
 	"github.com/containers/image/v5/internal/imagesource"
 	"github.com/containers/image/v5/internal/imagesource/impl"
@@ -41,7 +42,11 @@ type ociArchiveImageSource struct {
 func newImageSource(ctx context.Context, sys *types.SystemContext, ref ociArchiveReference) (private.ImageSource, error) {
 	tempDirRef, err := createUntarTempDir(sys, ref)
 	if err != nil {
-		return nil, fmt.Errorf("creating temp directory: %w", err)
+		if errors.Is(err, fs.ErrNotExist) {
+			return nil, ImageNotFoundError{ref: ref}
+		} else {
+			return nil, fmt.Errorf("creating temp directory: %w", err)
+		}
 	}
 
 	unpackedSrc, err := tempDirRef.ociRefExtracted.NewImageSource(ctx, sys)
-- 
2.40.1

@cgwalters
Copy link
Contributor Author

(not going to lie, it took me at least 20 minutes of confusion to realize that there's two copies of ImageNotFoundError in c/image and that the previous code was always buggy and also needed to use errors.As)

@cgwalters
Copy link
Contributor Author

cgwalters commented Sep 15, 2023

Tangential to this of course is my very strong opinion that downcasting and parsing errors should be an extremely rare event in code, and in e.g. languages like Rust the right thing to do is exactly how we model this API in the Rust bindings:

fn open_image_optional(&self, imgref: &str) -> Result<Option<OpenedImage>>

The common "not found" case is the Option - it's not an error! More on this in e.g. https://users.rust-lang.org/t/why-i-use-anyhow-error-even-in-libraries/68592

@cgwalters
Copy link
Contributor Author

But rpm-ostree compose image seems to have its own format=oci option, so the transport:image value is not user-provided.

Yes, they are distinct options but they are very much intended to exactly map to the c/image transports and work the same way from a user PoV. (I just take the liberty to add a nicer registry alias instead of docker:// which just looks weird/legacy IMO)

@mtrmac
Copy link
Contributor

mtrmac commented Sep 15, 2023

How about this variant instead? (requires a patch to c/image)

@@ -41,7 +42,11 @@ type ociArchiveImageSource struct {
 func newImageSource(ctx context.Context, sys *types.SystemContext, ref ociArchiveReference) (private.ImageSource, error) {
 	tempDirRef, err := createUntarTempDir(sys, ref)
 	if err != nil {
-		return nil, fmt.Errorf("creating temp directory: %w", err)
+		if errors.Is(err, fs.ErrNotExist) {
+			return nil, ImageNotFoundError{ref: ref}
+		} else {
+			return nil, fmt.Errorf("creating temp directory: %w", err)
+		}

This still matches the /var/tmp case; pushing the detection code down inside createUntarTempDir would allow precisely targeting the right os.Open.

@cgwalters
Copy link
Contributor Author

Introducing a new error type would maintain the existing API. (And introducing a new precisely-targeted error is, I think the cleanest solution.)

Hmm. So with oci: transports, we can distinguish between "oci directory exists, has index.json etc." and "oci directory doesn't exist". But with oci-archive there's no such thing...either the file exists, or it doesn't - and ISTM that for oci-archive, this does semantically map to ImageNotFoundError. Right?

@mtrmac
Copy link
Contributor

mtrmac commented Sep 15, 2023

The common "not found" case is the Option - it's not an error!

(Many things in Go are awkward and unsafe, with no sum types, no optionality… The complete lack of compiler support for errors, and things like errors.Is vs. errors.As, with no compile-time checking, is also just sad.)

I think that works for a frequent expected cause of a specific operation; the c/image “transport” abstraction makes this harder (in a sense, that’s exactly what this PR is hitting — after abstracting to the NewImageSource interface method we can no longer discuss what exactly failed / how exactly it failed, without talking about concepts specific to a particular transport).

c/image can just about manage to provide a common types.ImageReference / transport.ImageName concept, but I think providing a common “image container” that can deal with registries, c/storage, oci-archive (a filesystem of dir: subdirectories?) is just not feasible. And “image not found in a container” vs “container not found at all” is that kind of a situation.

Ultimately, API stability does not allow us to change NewImageSource return values anyway…

By adding the standard ENOENT to our list of errors.

I hit this while working on
coreos/rpm-ostree#4598
which is a tool that builds base images and wants to query
if the image exists beforehand.

Signed-off-by: Colin Walters <[email protected]>
@cgwalters cgwalters force-pushed the open-optional-ociarchive branch from 21aadaf to b80b222 Compare September 15, 2023 17:49
@mtrmac
Copy link
Contributor

mtrmac commented Sep 15, 2023

Hmm. So with oci: transports, we can distinguish between "oci directory exists, has index.json etc." and "oci directory doesn't exist". But with oci-archive there's no such thing...either the file exists, or it doesn't

No, oci-archive can also contain multiple images. So, oci-archive:existing-path:non-matching-reference is the current ImageNotFoundError, and oci-archive:/dev/null/this/does/not/exist:whatever is a different failure mode.

@cgwalters
Copy link
Contributor Author

cgwalters commented Sep 15, 2023

No, oci-archive can also contain multiple images. So, oci-archive:existing-path:non-matching-reference is the current ImageNotFoundError, and oci-archive:/dev/null/this/does/not/exist:whatever is a different failure mode.

Thanks, I didn't know that. But...I guess then I'm arguing that at least for all use cases I can think of, I really do want to treat "oci-archive file doesn't exist" here the same as ImageNotFoundError.

Unlike oci directories which require initialization, to me kind of the point of oci-archive is that it's just a single file that you can just...manage as a file. Requiring users of rpm-ostree compose image to create an "empty" oci-archive file would just be weird and defeat the purpose of the ergonomics here.

@cgwalters
Copy link
Contributor Author

I've updated the main PR here so far to:

In c/image:

  • Move the error check further down
  • Necessarily reorder the open invocation first

In skopeo:

  • Add a test case for the "parent directory doesn't exist" correctly erroring

cgwalters added a commit to cgwalters/image that referenced this pull request Sep 18, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to `ImageNotFound`, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/image that referenced this pull request Sep 18, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to `ImageNotFound`, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.
@cgwalters
Copy link
Contributor Author

Moved the c/image changes to containers/image#2123

cgwalters added a commit to cgwalters/image that referenced this pull request Sep 18, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
@mtrmac
Copy link
Contributor

mtrmac commented Sep 18, 2023

Requiring users of rpm-ostree compose image to create an "empty" oci-archive file would just be weird and defeat the purpose of the ergonomics here.

(The archives are not editable that way — they can either be read or written. But I do think that when reading a multi-image archive, differentiating the two failures should be possible.)

cgwalters added a commit to cgwalters/image that referenced this pull request Sep 18, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/image that referenced this pull request Sep 18, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/image that referenced this pull request Sep 19, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/image that referenced this pull request Sep 19, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/image that referenced this pull request Sep 19, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
cgwalters added a commit to cgwalters/image that referenced this pull request Sep 19, 2023
This is for containers/skopeo#2114
which is in turn a dependency of coreos/rpm-ostree#4598

Basically I want to map ENOENT to a clear error, because the build
tooling wants to treat "target image is not present" differently
from "DNS lookup failed" or "we got EPERM".

There's a bit of code motion here because we need to move
the `os.Open()` call before creating a temporary directory.

Signed-off-by: Colin Walters <[email protected]>
@mtrmac mtrmac added the kind/feature A request for, or a PR adding, new functionality label Oct 16, 2023
Copy link

A friendly reminder that this PR had no activity for 30 days.

@mtrmac
Copy link
Contributor

mtrmac commented Jun 24, 2024

@cgwalters the c/image error type PR has merged; do you want to update this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature A request for, or a PR adding, new functionality stale-pr
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants