Skip to content

errdefs: implement "IsXXX" utilities using containerd/errdefs #49378

Draft
thaJeztah wants to merge 2 commits intomoby:masterfrom
thaJeztah:c8d_errdefs
Draft

errdefs: implement "IsXXX" utilities using containerd/errdefs #49378
thaJeztah wants to merge 2 commits intomoby:masterfrom
thaJeztah:c8d_errdefs

Conversation

@thaJeztah
Copy link
Copy Markdown
Member

@thaJeztah thaJeztah commented Jan 31, 2025

This makes sure that errors originating from containerd are detected as
errdefs types without converting them.

- What I did

- How I did it

- How to verify it

- Description for the changelog

Update errdefs package helpers to also match errors produced by containerd

- A picture of a cute animal (not mandatory but encouraged)

@thaJeztah thaJeztah added status/2-code-review kind/refactor PR's that refactor, or clean-up code impact/go-sdk Noteworthy (compatibility changes) in the Go SDK labels Jan 31, 2025
@thaJeztah thaJeztah self-assigned this Jan 31, 2025
@thaJeztah
Copy link
Copy Markdown
Member Author

I have a branch to also update uses of containerd's errdefs package, but pushing this one first to verify compatibility.

@thaJeztah
Copy link
Copy Markdown
Member Author

Hm... something is not working right here;

=== Failed
=== FAIL: volume/service TestServiceCreate (0.00s)
    service_test.go:31: assertion failed: expression is false: errdefs.IsNotFound(err): create v1: volume driver not found: notexist

That looks to be this error that it's not matching;

type driverNotFoundError string
func (e driverNotFoundError) Error() string {
return "volume driver not found: " + string(e)
}
func (driverNotFoundError) NotFound() {}

@thaJeztah
Copy link
Copy Markdown
Member Author

So, that error is matched, but it gets wrapped in an OpError here;

vd, err := s.drivers.CreateDriver(driverName)
if err != nil {
return nil, false, &OpErr{Op: "create", Name: name, Err: err}
}

And that seems to implement the Causer interface, but not Unwrap, which makes me think that containerd's errdefs pacakge may not handle both;

// OpErr is the error type returned by functions in the store package. It describes
// the operation, volume name, and error.
type OpErr struct {
// Err is the error that occurred during the operation.
Err error
// Op is the operation which caused the error, such as "create", or "list".
Op string
// Name is the name of the resource being requested for this op, typically the volume name or the driver name.
Name string
// Refs is the list of references associated with the resource.
Refs []string
}
// Error satisfies the built-in error interface type.
func (e *OpErr) Error() string {
if e == nil {
return "<nil>"
}
s := e.Op
if e.Name != "" {
s = s + " " + e.Name
}
s = s + ": " + e.Err.Error()
if len(e.Refs) > 0 {
s = s + " - " + "[" + strings.Join(e.Refs, ", ") + "]"
}
return s
}
// Cause returns the error the caused this error
func (e *OpErr) Cause() error {
return e.Err
}

@thaJeztah
Copy link
Copy Markdown
Member Author

thaJeztah commented Jan 31, 2025

Yup, that's it; it fails without Unwrap(), and apparently doesn't check for Causer() interface; this test works after adding the Unwrap

type driverNotFoundError string

func (e driverNotFoundError) Error() string {
	return "volume driver not found: " + string(e)
}

func (driverNotFoundError) NotFound() {}

// OpErr is the error type returned by functions in the store package. It describes
// the operation, volume name, and error.
type OpErr struct {
	// Err is the error that occurred during the operation.
	Err error
	// Op is the operation which caused the error, such as "create", or "list".
	Op string
	// Name is the name of the resource being requested for this op, typically the volume name or the driver name.
	Name string
	// Refs is the list of references associated with the resource.
	Refs []string
}

// Error satisfies the built-in error interface type.
func (e *OpErr) Error() string {
	if e == nil {
		return "<nil>"
	}
	s := e.Op
	if e.Name != "" {
		s = s + " " + e.Name
	}

	s = s + ": " + e.Err.Error()
	if len(e.Refs) > 0 {
		s = s + " - " + "[" + strings.Join(e.Refs, ", ") + "]"
	}
	return s
}

// Cause returns the error the caused this error
func (e *OpErr) Cause() error {
	return e.Err
}

// Unwrap returns the error the caused this error
func (e *OpErr) Unwrap() error {
	return e.Err
}

func TestDriverNotFound(t *testing.T) {
	err := driverNotFoundError("test")

	if !IsNotFound(err) {
		t.Fatalf("expected driver not found, got %T", err)
	}

	err2 := &OpErr{Op: "create", Name: "notexist", Err: err}

	if !IsNotFound(err2) {
		t.Fatalf("expected driver not found, got %T", err2)
	}
}

func isInterface[T any](err error) bool {
for {
switch x := err.(type) {
case T:
return true
case customMessage:
err = x.err
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if isInterface[T](err) {
return true
}
}
return false
default:
return false
}
}
}

@thaJeztah
Copy link
Copy Markdown
Member Author

Integration tests:

=== Failed
=== FAIL: amd64.integration.container TestCreateLinkToNonExistingContainer (0.08s)
    create_test.go:163: assertion failed: error is Error response from daemon: could not get container for no-such-container: No such container: no-such-container (errdefs.errNotFound), not errdefs.IsInvalidParameter

=== FAIL: amd64.integration.container TestCreateVolumesFromNonExistingContainer (0.07s)
    create_test.go:588: assertion failed: error is Error response from daemon: No such container: nosuchcontainer (errdefs.errNotFound), not errdefs.IsInvalidParameter

=== FAIL: amd64.integration.container TestPIDModeContainer/non-existing_container (0.01s)
    pidmode_linux_test.go:42: assertion failed: error is Error response from daemon: No such container: nosuchcontainer (errdefs.errNotFound), not errdefs.IsInvalidParameter
    --- FAIL: TestPIDModeContainer/non-existing_container (0.01s)

=== FAIL: amd64.integration.container TestPIDModeContainer/non-running_container (0.05s)
    pidmode_linux_test.go:54: assertion failed: error is Error response from daemon: failed to join PID namespace: container 0cce948e718bfef781a0a2ede607631e735a930c1cb97814d1786c394aff97d2 is not running (errdefs.errConflict), not errdefs.IsSystem: should produce a System error when starting an existing container from an invalid state
    --- FAIL: TestPIDModeContainer/non-running_container (0.05s)

=== FAIL: amd64.integration.container TestPIDModeContainer (0.70s)

=== FAIL: amd64.integration.container TestWaitBlocked/test-wait-blocked-exit-random (10.30s)
    wait_test.go:95: assertion failed: 99 (tc.expectedCode int64) != 137 (waitRes.StatusCode int64)
    --- FAIL: TestWaitBlocked/test-wait-blocked-exit-random (10.30s)

=== FAIL: amd64.integration.container TestWaitBlocked (0.09s)

Unit tests:

=== Failed
=== FAIL: volume/service TestServiceCreate (0.00s)
    service_test.go:31: assertion failed: expression is false: errdefs.IsNotFound(err): create v1: volume driver not found: notexist

=== FAIL: volume/service TestServiceGet (0.00s)
time="2025-01-31T19:53:41Z" level=warning msg="could not determine size of volume" error="lstat fake: no such file or directory" volume=test
time="2025-01-31T19:53:41Z" level=warning msg="could not determine size of volume" error="lstat fake: no such file or directory" volume=test3
    service_test.go:150: assertion failed: expression is false: errdefs.IsConflict(err): get test: provided volume driver does not match stored driver

This one is probably flaky;

=== Failed
=== FAIL: amd64.integration.networking TestAccessPublishedPortFromAnotherNetwork/IPv4/Host (0.53s)
    port_mapping_linux_test.go:877: assertion failed: string "" does not contain "foobar": Payload was not received by the server container
    --- FAIL: TestAccessPublishedPortFromAnotherNetwork/IPv4/Host (0.53s)

@thaJeztah thaJeztah changed the title errdefs: touch-up godoc for helpers, and implement "IsXXX" utilities using containerd/errdefs errdefs: implement "IsXXX" utilities using containerd/errdefs Feb 4, 2025
@thaJeztah thaJeztah marked this pull request as draft February 4, 2025 16:53
@thaJeztah
Copy link
Copy Markdown
Member Author

thaJeztah commented Feb 4, 2025

This one is still failing (on Windows at least; Linux is still running)

=== Failed
=== FAIL: github.com/docker/docker/integration/container TestCreateVolumesFromNonExistingContainer (0.33s)
    create_test.go:588: assertion failed: error is Error response from daemon: No such container: nosuchcontainer (errdefs.errNotFound), not errdefs.IsInvalidParameter

That one would be hitting this code-path, but I see that any error from failing to find the container would be wrapped in an InvalidParameter;

moby/daemon/volumes.go

Lines 101 to 110 in e7da72a

for _, v := range hostConfig.VolumesFrom {
containerID, mode, err := parser.ParseVolumesFrom(v)
if err != nil {
return errdefs.InvalidParameter(err)
}
c, err := daemon.GetContainer(containerID)
if err != nil {
return errdefs.InvalidParameter(err)
}

The only other thing in that code that looks possibly incorrect is that the errors returning from the volume driver are returned as-is, which could mean that it's producing a 404 if a volume isn't found;

moby/daemon/volumes.go

Lines 125 to 131 in e7da72a

if len(cp.Source) == 0 {
v, err := daemon.volumes.Get(ctx, cp.Name, volumeopts.WithGetDriver(cp.Driver), volumeopts.WithGetReference(container.ID))
if err != nil {
return err
}
cp.Volume = &volumeWrapper{v: v, s: daemon.volumes}
}

@thaJeztah
Copy link
Copy Markdown
Member Author

These still fail on Linux as well. I'm starting to wonder if the containerd errdefs's IsXXX functions do a "deep" search for errors, so continues unwrapping (therefore errdefs.InvalidParameter(errdefs.NotFound(errors.New("foo")))) being unwrapped to errdefs.NotFound(), which is not the intent of such cases;

=== Failed
=== FAIL: amd64.integration.container TestCreateLinkToNonExistingContainer (0.02s)
    create_test.go:163: assertion failed: error is Error response from daemon: could not get container for no-such-container: No such container: no-such-container (errdefs.errNotFound), not errdefs.IsInvalidParameter

=== FAIL: amd64.integration.container TestCreateVolumesFromNonExistingContainer (0.02s)
    create_test.go:588: assertion failed: error is Error response from daemon: No such container: nosuchcontainer (errdefs.errNotFound), not errdefs.IsInvalidParameter

=== FAIL: amd64.integration.container TestPIDModeContainer/non-existing_container (0.00s)
    pidmode_linux_test.go:42: assertion failed: error is Error response from daemon: No such container: nosuchcontainer (errdefs.errNotFound), not errdefs.IsInvalidParameter
    --- FAIL: TestPIDModeContainer/non-existing_container (0.00s)

=== FAIL: amd64.integration.container TestPIDModeContainer/non-running_container (0.03s)
    pidmode_linux_test.go:54: assertion failed: error is Error response from daemon: failed to join PID namespace: container 1f06447d6a759aef6cc07b393fc2ad6e3ab04cfbe671eecbc4414279b56316a4 is not running (errdefs.errConflict), not errdefs.IsSystem: should produce a System error when starting an existing container from an invalid state
    --- FAIL: TestPIDModeContainer/non-running_container (0.03s)

Comment thread errdefs/helpers_test.go
Comment on lines +288 to +302
func TestIntentionallyShadowed(t *testing.T) {
var (
rootErr = errors.New("root error")

// Some code resulted in a "not found" error
notFoundErr = NotFound(rootErr)

// But a "not found" error should be considered a "invalid parameter",
// so it's intentionally wrapped.
actualErr = InvalidParameter(notFoundErr)
)

if !IsInvalidParameter(actualErr) {
t.Errorf("expected invalid parameter error, got %T", actualErr)
}
if IsNotFound(actualErr) {
t.Errorf("not found error should've been masked by an invalid parameter error, but got %T", actualErr)
}
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed; this test passes with our errdefs, but fails with containerd's implementation 🤔

thaJeztah added 2 commits May 6, 2025 11:08
This makes sure that errors originating from containerd are detected as
errdefs types without converting them.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/go-sdk impact/go-sdk Noteworthy (compatibility changes) in the Go SDK kind/refactor PR's that refactor, or clean-up code status/2-code-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant