From b9711abff3ed169c7f494b59f3f6d42db7a26d73 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 20 May 2019 13:39:50 -0700 Subject: [PATCH] Add errdefs package Providers should use this package so the virtual kubelet core controllers can understand the errors produced from the provider code. --- errdefs/invalid.go | 65 +++++++++++++++++++++++++++++++ errdefs/invalid_test.go | 80 +++++++++++++++++++++++++++++++++++++++ errdefs/notfound.go | 65 +++++++++++++++++++++++++++++++ errdefs/notfound_test.go | 80 +++++++++++++++++++++++++++++++++++++++ errdefs/wrapped.go | 10 +++++ providers/provider.go | 4 ++ vkubelet/podcontroller.go | 4 ++ 7 files changed, 308 insertions(+) create mode 100644 errdefs/invalid.go create mode 100644 errdefs/invalid_test.go create mode 100644 errdefs/notfound.go create mode 100644 errdefs/notfound_test.go create mode 100644 errdefs/wrapped.go diff --git a/errdefs/invalid.go b/errdefs/invalid.go new file mode 100644 index 000000000..d2ffa0460 --- /dev/null +++ b/errdefs/invalid.go @@ -0,0 +1,65 @@ +package errdefs + +import ( + "errors" + "fmt" +) + +// InvalidInput is an error interface which denotes whether the opration failed due +// to a the resource not being found. +type ErrInvalidInput interface { + InvalidInput() bool + error +} + +type invalidInputError struct { + error +} + +func (e *invalidInputError) InvalidInput() bool { + return true +} + +func (e *invalidInputError) Cause() error { + return e.error +} + +// AsInvalidInput wraps the passed in error to make it of type ErrInvalidInput +// +// Callers should make sure the passed in error has exactly the error message +// it wants as this function does not decorate the message. +func AsInvalidInput(err error) error { + if err == nil { + return nil + } + return &invalidInputError{err} +} + +// InvalidInput makes an ErrInvalidInput from the provided error message +func InvalidInput(msg string) error { + return &invalidInputError{errors.New(msg)} +} + +// InvalidInputf makes an ErrInvalidInput from the provided error format and args +func InvalidInputf(format string, args ...interface{}) error { + return &invalidInputError{fmt.Errorf(format, args...)} +} + +// IsInvalidInput determines if the passed in error is of type ErrInvalidInput +// +// This will traverse the causal chain (`Cause() error`), until it finds an error +// which implements the `InvalidInput` interface. +func IsInvalidInput(err error) bool { + if err == nil { + return false + } + if e, ok := err.(ErrInvalidInput); ok { + return e.InvalidInput() + } + + if e, ok := err.(causal); ok { + return IsInvalidInput(e) + } + + return false +} diff --git a/errdefs/invalid_test.go b/errdefs/invalid_test.go new file mode 100644 index 000000000..1a3002b45 --- /dev/null +++ b/errdefs/invalid_test.go @@ -0,0 +1,80 @@ +package errdefs + +import ( + "errors" + "fmt" + "testing" + + "gotest.tools/assert" + "gotest.tools/assert/cmp" +) + +type testingInvalidInputError bool + +func (e testingInvalidInputError) Error() string { + return fmt.Sprintf("%v", bool(e)) +} + +func (e testingInvalidInputError) InvalidInput() bool { + return bool(e) +} + +func TestIsInvalidInput(t *testing.T) { + type testCase struct { + name string + err error + xMsg string + xInvalidInput bool + } + + for _, c := range []testCase{ + { + name: "InvalidInputf", + err: InvalidInputf("%s not found", "foo"), + xMsg: "foo not found", + xInvalidInput: true, + }, + { + name: "AsInvalidInput", + err: AsInvalidInput(errors.New("this is a test")), + xMsg: "this is a test", + xInvalidInput: true, + }, + { + name: "AsInvalidInputWithNil", + err: AsInvalidInput(nil), + xMsg: "", + xInvalidInput: false, + }, + { + name: "nilError", + err: nil, + xMsg: "", + xInvalidInput: false, + }, + { + name: "customInvalidInputFalse", + err: testingInvalidInputError(false), + xMsg: "false", + xInvalidInput: false, + }, + { + name: "customInvalidInputTrue", + err: testingInvalidInputError(true), + xMsg: "true", + xInvalidInput: true, + }, + } { + t.Run(c.name, func(t *testing.T) { + assert.Check(t, cmp.Equal(IsInvalidInput(c.err), c.xInvalidInput)) + if c.err != nil { + assert.Check(t, cmp.Equal(c.err.Error(), c.xMsg)) + } + }) + } +} + +func TestInvalidInputCause(t *testing.T) { + err := errors.New("test") + assert.Equal(t, (&invalidInputError{err}).Cause(), err) +} diff --git a/errdefs/notfound.go b/errdefs/notfound.go new file mode 100644 index 000000000..c55274dc9 --- /dev/null +++ b/errdefs/notfound.go @@ -0,0 +1,65 @@ +package errdefs + +import ( + "errors" + "fmt" +) + +// NotFound is an error interface which denotes whether the opration failed due +// to a the resource not being found. +type ErrNotFound interface { + NotFound() bool + error +} + +type notFoundError struct { + error +} + +func (e *notFoundError) NotFound() bool { + return true +} + +func (e *notFoundError) Cause() error { + return e.error +} + +// AsNotFound wraps the passed in error to make it of type ErrNotFound +// +// Callers should make sure the passed in error has exactly the error message +// it wants as this function does not decorate the message. +func AsNotFound(err error) error { + if err == nil { + return nil + } + return ¬FoundError{err} +} + +// NotFound makes an ErrNotFound from the provided error message +func NotFound(msg string) error { + return ¬FoundError{errors.New(msg)} +} + +// NotFoundf makes an ErrNotFound from the provided error format and args +func NotFoundf(format string, args ...interface{}) error { + return ¬FoundError{fmt.Errorf(format, args...)} +} + +// IsNotFound determines if the passed in error is of type ErrNotFound +// +// This will traverse the causal chain (`Cause() error`), until it finds an error +// which implements the `NotFound` interface. +func IsNotFound(err error) bool { + if err == nil { + return false + } + if e, ok := err.(ErrNotFound); ok { + return e.NotFound() + } + + if e, ok := err.(causal); ok { + return IsNotFound(e) + } + + return false +} diff --git a/errdefs/notfound_test.go b/errdefs/notfound_test.go new file mode 100644 index 000000000..52a67c458 --- /dev/null +++ b/errdefs/notfound_test.go @@ -0,0 +1,80 @@ +package errdefs + +import ( + "errors" + "fmt" + "testing" + + "gotest.tools/assert" + "gotest.tools/assert/cmp" +) + +type testingNotFoundError bool + +func (e testingNotFoundError) Error() string { + return fmt.Sprintf("%v", bool(e)) +} + +func (e testingNotFoundError) NotFound() bool { + return bool(e) +} + +func TestIsNotFound(t *testing.T) { + type testCase struct { + name string + err error + xMsg string + xNotFound bool + } + + for _, c := range []testCase{ + { + name: "NotFoundf", + err: NotFoundf("%s not found", "foo"), + xMsg: "foo not found", + xNotFound: true, + }, + { + name: "AsNotFound", + err: AsNotFound(errors.New("this is a test")), + xMsg: "this is a test", + xNotFound: true, + }, + { + name: "AsNotFoundWithNil", + err: AsNotFound(nil), + xMsg: "", + xNotFound: false, + }, + { + name: "nilError", + err: nil, + xMsg: "", + xNotFound: false, + }, + { + name: "customNotFoundFalse", + err: testingNotFoundError(false), + xMsg: "false", + xNotFound: false, + }, + { + name: "customNotFoundTrue", + err: testingNotFoundError(true), + xMsg: "true", + xNotFound: true, + }, + } { + t.Run(c.name, func(t *testing.T) { + assert.Check(t, cmp.Equal(IsNotFound(c.err), c.xNotFound)) + if c.err != nil { + assert.Check(t, cmp.Equal(c.err.Error(), c.xMsg)) + } + }) + } +} + +func TestNotFoundCause(t *testing.T) { + err := errors.New("test") + assert.Equal(t, (¬FoundError{err}).Cause(), err) +} diff --git a/errdefs/wrapped.go b/errdefs/wrapped.go new file mode 100644 index 000000000..446239311 --- /dev/null +++ b/errdefs/wrapped.go @@ -0,0 +1,10 @@ +package errdefs + +// Causal is an error interface for errors which have wrapped another error +// in a non-opaque way. +// +// This pattern is used by github.com/pkg/errors +type causal interface { + Cause() error + error +} diff --git a/providers/provider.go b/providers/provider.go index 36fcf2afc..f0976fc7a 100644 --- a/providers/provider.go +++ b/providers/provider.go @@ -11,6 +11,10 @@ import ( ) // Provider contains the methods required to implement a virtual-kubelet provider. +// +// Errors produced by these methods should implement an interface from +// github.com/virtual-kubelet/virtual-kubelet/errdefs package in order for the +// core logic to be able to understand the type of failure. type Provider interface { vkubelet.PodLifecycleHandler diff --git a/vkubelet/podcontroller.go b/vkubelet/podcontroller.go index 262f9382f..0f45a2689 100644 --- a/vkubelet/podcontroller.go +++ b/vkubelet/podcontroller.go @@ -41,6 +41,10 @@ import ( // PodLifecycleHandler defines the interface used by the PodController to react // to new and changed pods scheduled to the node that is being managed. +// +// Errors produced by these methods should implement an interface from +// github.com/virtual-kubelet/virtual-kubelet/errdefs package in order for the +// core logic to be able to understand the type of failure. type PodLifecycleHandler interface { // CreatePod takes a Kubernetes Pod and deploys it within the provider. CreatePod(ctx context.Context, pod *corev1.Pod) error