Repository: crossplane/crossplane-runtime
Pull request #291 (2021-09-09 07:14:01)

Add an `errors` package with a similar API to `github.com/pkg/errors`
<!--
Thank you for helping to improve Crossplane!

Please read through https://git.io/fj2m9 if this is your first time opening a
Crossplane pull request. Find us in https://slack.crossplane.io/messages/dev if
you need any help contributing.
-->

### Description of your changes

<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open Crossplane issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":

-->
Go introduced a 'native' way to wrap errors back in v1.13. At that point we were already using `github.com/pkg/errors` to 'wrap' errors with context, and we never got around to migrating. In addition to pure inertia, I've personally avoided making the switch because I prefer the `github.com/pkg/errors` API. Specifically I like that `errors.Wrap` handles the `"outer context: inner context"` error format that Go uses by convention, and that `errors.Wrap` will return `nil` when passed a `nil` error.

Given that `github.com/pkg/errors` has long been in maintenance mode, and is (per pkg/errors#245) no longer used by its original author now seems as good a time as any to migrate. This commit attempts to ease that migration for the Crossplane project - and to retain the nice API - by adding a package that acts as a small `github.com/pkg/errors` style shim layer around the stdlib `pkg/errors` (and friends, like `fmt.Errorf`).

It's worth noting that this PR would result in a behaviour change; we'd no longer include stack traces when errors were logged. In practice we only log errors at debug level most (all?) of the time, and I'm fairly confident no one will miss this behaviour. Otherwise, I'm fairly confident this subset of the `github.com/pkg/errors` API will serve as a drop-in replacement for all the parts we use.

I have:

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `make reviewable test` to ensure this PR is ready for review.

### How has this code been tested

<!--
Before reviewers can be confident in the correctness of this pull request, it
needs to tested and shown to be correct. Briefly describe the testing that has
already been done or which is planned for this change.
-->

[contribution process]: https://git.io/fj2m9

I've confirmed that this package is a drop-in replacement for all our uses within crossplane/crossplane-runtime and crossplane/crossplane.


Patch:
diff --git a/go.mod b/go.mod
index eb16e15..0d73426 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,6 @@ require (
 	github.com/google/go-cmp v0.5.6
 	github.com/hashicorp/go-getter v1.4.0
 	github.com/imdario/mergo v0.3.12
-	github.com/pkg/errors v0.9.1
 	github.com/spf13/afero v1.6.0
 	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
 	k8s.io/api v0.21.3
diff --git a/pkg/controller/engine.go b/pkg/controller/engine.go
index 4f23b4a..9967dd4 100644
--- a/pkg/controller/engine.go
+++ b/pkg/controller/engine.go
@@ -21,7 +21,6 @@ import (
 	"context"
 	"sync"
 
-	"github.com/pkg/errors"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -30,6 +29,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/source"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 // Error strings
diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go
new file mode 100644
index 0000000..24cc306
--- /dev/null
+++ b/pkg/errors/errors.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2021 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package errors is a github.com/pkg/errors compatible API for native errors.
+// It includes only the subset of the github.com/pkg/errors API that is used by
+// the Crossplane project.
+package errors
+
+import (
+	"errors"
+	"fmt"
+)
+
+// New returns an error that formats as the given text. Each call to New returns
+// a distinct error value even if the text is identical.
+func New(text string) error { return errors.New(text) }
+
+// Is reports whether any error in err's chain matches target.
+//
+// The chain consists of err itself followed by the sequence of errors obtained
+// by repeatedly calling Unwrap.
+//
+// An error is considered to match a target if it is equal to that target or if
+// it implements a method Is(error) bool such that Is(target) returns true.
+//
+// An error type might provide an Is method so it can be treated as equivalent
+// to an existing error. For example, if MyError defines
+//
+//	func (m MyError) Is(target error) bool { return target == fs.ErrExist }
+//
+// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
+// an example in the standard library.
+func Is(err, target error) bool { return errors.Is(err, target) }
+
+// As finds the first error in err's chain that matches target, and if so, sets
+// target to that error value and returns true. Otherwise, it returns false.
+//
+// The chain consists of err itself followed by the sequence of errors obtained
+// by repeatedly calling Unwrap.
+//
+// An error matches target if the error's concrete value is assignable to the
+// value pointed to by target, or if the error has a method As(interface{}) bool
+// such that As(target) returns true. In the latter case, the As method is
+// responsible for setting target.
+//
+// An error type might provide an As method so it can be treated as if it were a
+// different error type.
+//
+// As panics if target is not a non-nil pointer to either a type that implements
+// error, or to any interface type.
+func As(err error, target interface{}) bool { return errors.As(err, target) }
+
+// Unwrap returns the result of calling the Unwrap method on err, if err's type
+// contains an Unwrap method returning error. Otherwise, Unwrap returns nil.
+func Unwrap(err error) error { return errors.Unwrap(err) }
+
+// Errorf formats according to a format specifier and returns the string as a
+// value that satisfies error.
+//
+// If the format specifier includes a %w verb with an error operand, the
+// returned error will implement an Unwrap method returning the operand. It is
+// invalid to include more than one %w verb or to supply it with an operand that
+// does not implement the error interface. The %w verb is otherwise a synonym
+// for %v.
+func Errorf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) }
+
+// WithMessage annotates err with a new message. If err is nil, WithMessage
+// returns nil.
+func WithMessage(err error, message string) error {
+	if err == nil {
+		return nil
+	}
+	return fmt.Errorf("%s: %w", message, err)
+}
+
+// WithMessagef annotates err with the format specifier. If err is nil,
+// WithMessagef returns nil.
+func WithMessagef(err error, format string, args ...interface{}) error {
+	if err == nil {
+		return nil
+	}
+	return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
+}
+
+// Wrap is an alias for WithMessage.
+func Wrap(err error, message string) error {
+	return WithMessage(err, message)
+}
+
+// Wrapf is an alias for WithMessagef
+func Wrapf(err error, format string, args ...interface{}) error {
+	return WithMessagef(err, format, args...)
+}
+
+// Cause calls Unwrap on each error it finds. It returns the first error it
+// finds that does not have an Unwrap method - i.e. the first error that was not
+// the result of a Wrap call, a Wrapf call, or an Errorf call with %w wrapping.
+func Cause(err error) error {
+	type wrapped interface {
+		Unwrap() error
+	}
+
+	for err != nil {
+		// We're ignoring errorlint telling us to use errors.As because
+		// we actually do want to check the outermost error.
+		//nolint:errorlint
+		w, ok := err.(wrapped)
+		if !ok {
+			return err
+		}
+		err = w.Unwrap()
+	}
+
+	return err
+}
diff --git a/pkg/fieldpath/fieldpath.go b/pkg/fieldpath/fieldpath.go
index c9a592a..113de2f 100644
--- a/pkg/fieldpath/fieldpath.go
+++ b/pkg/fieldpath/fieldpath.go
@@ -48,7 +48,7 @@ import (
 	"strings"
 	"unicode/utf8"
 
-	"github.com/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 // A SegmentType within a field path; either a field within an object, or an
diff --git a/pkg/fieldpath/merge.go b/pkg/fieldpath/merge.go
index e8c9517..8e33ec6 100644
--- a/pkg/fieldpath/merge.go
+++ b/pkg/fieldpath/merge.go
@@ -20,9 +20,9 @@ import (
 	"reflect"
 
 	"github.com/imdario/mergo"
-	"github.com/pkg/errors"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 const (
diff --git a/pkg/fieldpath/paved.go b/pkg/fieldpath/paved.go
index 184e41d..2a2629b 100644
--- a/pkg/fieldpath/paved.go
+++ b/pkg/fieldpath/paved.go
@@ -17,9 +17,10 @@ limitations under the License.
 package fieldpath
 
 import (
-	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/json"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 type errNotFound struct {
diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go
index cdc96e5..d6e56f4 100644
--- a/pkg/meta/meta.go
+++ b/pkg/meta/meta.go
@@ -23,13 +23,13 @@ import (
 	"strings"
 	"time"
 
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 const (
diff --git a/pkg/parser/linter.go b/pkg/parser/linter.go
index ea2c27c..7fbd21c 100644
--- a/pkg/parser/linter.go
+++ b/pkg/parser/linter.go
@@ -17,8 +17,9 @@ limitations under the License.
 package parser
 
 import (
-	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 const (
diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go
index 97bbf2b..20725a9 100644
--- a/pkg/parser/parser.go
+++ b/pkg/parser/parser.go
@@ -25,13 +25,14 @@ import (
 	"strings"
 	"unicode"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/afero"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer/json"
 	"k8s.io/apimachinery/pkg/util/yaml"
 	"k8s.io/client-go/kubernetes"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 // AnnotatedReadCloser is a wrapper around io.ReadCloser that allows
diff --git a/pkg/reconciler/managed/api.go b/pkg/reconciler/managed/api.go
index 5281d2a..1669dc8 100644
--- a/pkg/reconciler/managed/api.go
+++ b/pkg/reconciler/managed/api.go
@@ -20,13 +20,13 @@ import (
 	"context"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 )
diff --git a/pkg/reconciler/managed/reconciler.go b/pkg/reconciler/managed/reconciler.go
index 28ba822..f639380 100644
--- a/pkg/reconciler/managed/reconciler.go
+++ b/pkg/reconciler/managed/reconciler.go
@@ -21,13 +21,13 @@ import (
 	"strings"
 	"time"
 
-	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
diff --git a/pkg/reconciler/providerconfig/reconciler.go b/pkg/reconciler/providerconfig/reconciler.go
index 58568e9..12e54f4 100644
--- a/pkg/reconciler/providerconfig/reconciler.go
+++ b/pkg/reconciler/providerconfig/reconciler.go
@@ -21,7 +21,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -29,6 +28,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/crossplane/crossplane-runtime/pkg/logging"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
diff --git a/pkg/reference/reference.go b/pkg/reference/reference.go
index 82dc856..06396e8 100644
--- a/pkg/reference/reference.go
+++ b/pkg/reference/reference.go
@@ -19,11 +19,11 @@ package reference
 import (
 	"context"
 
-	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 )
diff --git a/pkg/resource/api.go b/pkg/resource/api.go
index 4ad3430..7c77e2a 100644
--- a/pkg/resource/api.go
+++ b/pkg/resource/api.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"encoding/json"
 
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -28,6 +27,7 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 )
 
diff --git a/pkg/resource/providerconfig.go b/pkg/resource/providerconfig.go
index 9b6d1dc..5f15978 100644
--- a/pkg/resource/providerconfig.go
+++ b/pkg/resource/providerconfig.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"os"
 
-	"github.com/pkg/errors"
 	"github.com/spf13/afero"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,6 +28,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 )
 
diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go
index a0a8d3b..0e70660 100644
--- a/pkg/resource/resource.go
+++ b/pkg/resource/resource.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"strings"
 
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,6 +31,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 )
 


Test patch:
diff --git a/apis/common/v1/condition_test.go b/apis/common/v1/condition_test.go
index 5b9ab06..12ec046 100644
--- a/apis/common/v1/condition_test.go
+++ b/apis/common/v1/condition_test.go
@@ -20,9 +20,10 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 )
 
 func TestConditionEqual(t *testing.T) {
diff --git a/pkg/controller/engine_test.go b/pkg/controller/engine_test.go
index cc06010..1abca9f 100644
--- a/pkg/controller/engine_test.go
+++ b/pkg/controller/engine_test.go
@@ -22,7 +22,6 @@ import (
 	"time"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -31,6 +30,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go
new file mode 100644
index 0000000..be51895
--- /dev/null
+++ b/pkg/errors/errors_test.go
@@ -0,0 +1,126 @@
+/*
+Copyright 2021 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package errors
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+)
+
+func TestWrap(t *testing.T) {
+	type args struct {
+		err     error
+		message string
+	}
+	cases := map[string]struct {
+		args args
+		want error
+	}{
+		"NilError": {
+			args: args{
+				err:     nil,
+				message: "very useful context",
+			},
+			want: nil,
+		},
+		"NonNilError": {
+			args: args{
+				err:     New("boom"),
+				message: "very useful context",
+			},
+			want: Errorf("very useful context: %w", New("boom")),
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got := Wrap(tc.args.err, tc.args.message)
+			if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
+				t.Errorf("Wrap(...): -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestWrapf(t *testing.T) {
+	type args struct {
+		err     error
+		message string
+		args    []interface{}
+	}
+	cases := map[string]struct {
+		args args
+		want error
+	}{
+		"NilError": {
+			args: args{
+				err:     nil,
+				message: "very useful context",
+			},
+			want: nil,
+		},
+		"NonNilError": {
+			args: args{
+				err:     New("boom"),
+				message: "very useful context about %s",
+				args:    []interface{}{"ducks"},
+			},
+			want: Errorf("very useful context about %s: %w", "ducks", New("boom")),
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got := Wrapf(tc.args.err, tc.args.message, tc.args.args...)
+			if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
+				t.Errorf("Wrapf(...): -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestCause(t *testing.T) {
+	cases := map[string]struct {
+		err  error
+		want error
+	}{
+		"NilError": {
+			err:  nil,
+			want: nil,
+		},
+		"BareError": {
+			err:  New("boom"),
+			want: New("boom"),
+		},
+		"WrappedError": {
+			err:  Wrap(Wrap(New("boom"), "interstitial context"), "very important context"),
+			want: New("boom"),
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got := Cause(tc.err)
+			if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
+				t.Errorf("Cause(...): -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/fieldpath/fieldpath_test.go b/pkg/fieldpath/fieldpath_test.go
index ed77b22..cd910b7 100644
--- a/pkg/fieldpath/fieldpath_test.go
+++ b/pkg/fieldpath/fieldpath_test.go
@@ -22,8 +22,8 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
 
diff --git a/pkg/fieldpath/paved_test.go b/pkg/fieldpath/paved_test.go
index fbe9980..fd0e939 100644
--- a/pkg/fieldpath/paved_test.go
+++ b/pkg/fieldpath/paved_test.go
@@ -20,11 +20,11 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/json"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
 
diff --git a/pkg/meta/meta_test.go b/pkg/meta/meta_test.go
index 8f0ffad..61e784d 100644
--- a/pkg/meta/meta_test.go
+++ b/pkg/meta/meta_test.go
@@ -23,13 +23,13 @@ import (
 	"time"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
 
diff --git a/pkg/parser/linter_test.go b/pkg/parser/linter_test.go
index d929b4d..1bcd6b6 100644
--- a/pkg/parser/linter_test.go
+++ b/pkg/parser/linter_test.go
@@ -20,9 +20,9 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
 
diff --git a/pkg/reconciler/managed/api_test.go b/pkg/reconciler/managed/api_test.go
index 6502737..9cb7890 100644
--- a/pkg/reconciler/managed/api_test.go
+++ b/pkg/reconciler/managed/api_test.go
@@ -21,12 +21,12 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
diff --git a/pkg/reconciler/managed/publisher_test.go b/pkg/reconciler/managed/publisher_test.go
index cf94ec0..7d9af5b 100644
--- a/pkg/reconciler/managed/publisher_test.go
+++ b/pkg/reconciler/managed/publisher_test.go
@@ -21,8 +21,8 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
diff --git a/pkg/reconciler/managed/reconciler_test.go b/pkg/reconciler/managed/reconciler_test.go
index 5f67f66..f711254 100644
--- a/pkg/reconciler/managed/reconciler_test.go
+++ b/pkg/reconciler/managed/reconciler_test.go
@@ -27,12 +27,12 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
-	"github.com/pkg/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
diff --git a/pkg/reconciler/providerconfig/reconciler_test.go b/pkg/reconciler/providerconfig/reconciler_test.go
index 8991075..2bdc0a7 100644
--- a/pkg/reconciler/providerconfig/reconciler_test.go
+++ b/pkg/reconciler/providerconfig/reconciler_test.go
@@ -22,7 +22,6 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -32,6 +31,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
diff --git a/pkg/reference/reference_test.go b/pkg/reference/reference_test.go
index ca774e7..ec03413 100644
--- a/pkg/reference/reference_test.go
+++ b/pkg/reference/reference_test.go
@@ -22,12 +22,12 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
-	"github.com/pkg/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
diff --git a/pkg/resource/api_test.go b/pkg/resource/api_test.go
index faa0e98..f8b52e3 100644
--- a/pkg/resource/api_test.go
+++ b/pkg/resource/api_test.go
@@ -21,7 +21,6 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,6 +29,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
diff --git a/pkg/resource/predicates_test.go b/pkg/resource/predicates_test.go
index 415f3eb..3001a77 100644
--- a/pkg/resource/predicates_test.go
+++ b/pkg/resource/predicates_test.go
@@ -20,12 +20,12 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 )
diff --git a/pkg/resource/providerconfig_test.go b/pkg/resource/providerconfig_test.go
index edc1bf1..eb973bb 100644
--- a/pkg/resource/providerconfig_test.go
+++ b/pkg/resource/providerconfig_test.go
@@ -21,12 +21,12 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/pkg/errors"
 	"github.com/spf13/afero"
 	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
diff --git a/pkg/resource/resource_test.go b/pkg/resource/resource_test.go
index 42be552..2a83b5f 100644
--- a/pkg/resource/resource_test.go
+++ b/pkg/resource/resource_test.go
@@ -22,8 +22,6 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-cmp/cmp/cmpopts"
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,6 +33,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 )
@@ -822,7 +821,7 @@ func TestApplicatorWithRetry_Apply(t *testing.T) {
 				backoff:     tc.fields.backoff,
 			}
 
-			if diff := cmp.Diff(tc.wantErr, awr.Apply(tc.args.ctx, tc.args.c, tc.args.opts...), cmpopts.EquateErrors()); diff != "" {
+			if diff := cmp.Diff(tc.wantErr, awr.Apply(tc.args.ctx, tc.args.c, tc.args.opts...), test.EquateErrors()); diff != "" {
 				t.Fatalf("ApplicatorWithRetry.Apply(...): -want, +got:\n%s", diff)
 			}
 
diff --git a/pkg/test/cmp.go b/pkg/test/cmp.go
index 8717674..a7414c1 100644
--- a/pkg/test/cmp.go
+++ b/pkg/test/cmp.go
@@ -25,13 +25,14 @@ import (
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 )
 
-// TODO(negz): Replace this if a similar option is added to cmpopts per
-// https://github.com/google/go-cmp/issues/89
-
 // EquateErrors returns true if the supplied errors are of the same type and
 // produce identical strings. This mirrors the error comparison behaviour of
 // https://github.com/go-test/deep, which most Crossplane tests targeted before
 // we switched to go-cmp.
+//
+// This differs from cmpopts.EquateErrors, which does not test for error strings
+// and instead returns whether one error 'is' (in the errors.Is sense) the
+// other.
 func EquateErrors() cmp.Option {
 	return cmp.Comparer(func(a, b error) bool {
 		if a == nil || b == nil {
diff --git a/pkg/test/integration/server.go b/pkg/test/integration/server.go
index c47bcdf..4ab3b61 100644
--- a/pkg/test/integration/server.go
+++ b/pkg/test/integration/server.go
@@ -22,7 +22,6 @@ import (
 	"os"
 	"time"
 
-	"github.com/pkg/errors"
 	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/rest"
@@ -33,6 +32,7 @@ import (
 	// Allow auth to cloud providers
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
 
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 )