e2e framework: support ignoring "not found" errors during DeferCleanup

The wrapper can be used in combination with ginkgo.DeferCleanup to ignore
harmless "not found" errors during delete operations.

Original code suggested by Onsi Fakhouri.
This commit is contained in:
Patrick Ohly 2022-10-19 11:24:53 +02:00
parent 73b60ba769
commit f897c86119
2 changed files with 65 additions and 22 deletions

View File

@ -18,10 +18,36 @@ package framework
import ( import (
"path" "path"
"reflect"
"github.com/onsi/ginkgo/v2/types" "github.com/onsi/ginkgo/v2/types"
apierrors "k8s.io/apimachinery/pkg/api/errors"
) )
var errInterface = reflect.TypeOf((*error)(nil)).Elem()
// IgnoreNotFound can be used to wrap an arbitrary function in a call to
// [ginkgo.DeferCleanup]. When the wrapped function returns an error that
// `apierrors.IsNotFound` considers as "not found", the error is ignored
// instead of failing the test during cleanup. This is useful for cleanup code
// that just needs to ensure that some object does not exist anymore.
func IgnoreNotFound(in any) any {
inType := reflect.TypeOf(in)
inValue := reflect.ValueOf(in)
return reflect.MakeFunc(inType, func(args []reflect.Value) []reflect.Value {
out := inValue.Call(args)
if len(out) > 0 {
lastValue := out[len(out)-1]
last := lastValue.Interface()
if last != nil && lastValue.Type().Implements(errInterface) && apierrors.IsNotFound(last.(error)) {
out[len(out)-1] = reflect.Zero(errInterface)
}
}
return out
}).Interface()
}
// AnnotatedLocation can be used to provide more informative source code // AnnotatedLocation can be used to provide more informative source code
// locations by passing the result as additional parameter to a // locations by passing the result as additional parameter to a
// BeforeEach/AfterEach/DeferCleanup/It/etc. // BeforeEach/AfterEach/DeferCleanup/It/etc.

View File

@ -24,11 +24,13 @@ package cleanup
import ( import (
"context" "context"
"flag" "flag"
"fmt"
"regexp" "regexp"
"testing" "testing"
"github.com/onsi/ginkgo/v2" "github.com/onsi/ginkgo/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting" "k8s.io/klog/v2/ktesting"
"k8s.io/kubernetes/test/e2e/framework" "k8s.io/kubernetes/test/e2e/framework"
@ -45,10 +47,18 @@ import (
// //
// //
// //
//
//
// This must be line #50. // This must be line #50.
func init() {
framework.NewFrameworkExtensions = append(framework.NewFrameworkExtensions,
// This callback runs directly after NewDefaultFramework is done.
func(f *framework.Framework) {
ginkgo.BeforeEach(func() { framework.Logf("extension before") })
ginkgo.AfterEach(func() { framework.Logf("extension after") })
},
)
}
var _ = ginkgo.Describe("e2e", func() { var _ = ginkgo.Describe("e2e", func() {
ginkgo.BeforeEach(func() { ginkgo.BeforeEach(func() {
framework.Logf("before") framework.Logf("before")
@ -85,22 +95,21 @@ var _ = ginkgo.Describe("e2e", func() {
ginkgo.DeferCleanup(func() { ginkgo.DeferCleanup(func() {
framework.Logf("cleanup first") framework.Logf("cleanup first")
}) })
ginkgo.DeferCleanup(framework.IgnoreNotFound(f.ClientSet.CoreV1().PersistentVolumes().Delete), "simple", metav1.DeleteOptions{})
fail := func(ctx context.Context, name string) error {
return fmt.Errorf("fake error for %q", name)
}
ginkgo.DeferCleanup(framework.IgnoreNotFound(fail), "failure")
// More test cases can be added here without affeccting line numbering
// of existing tests.
}) })
}) })
func init() {
framework.NewFrameworkExtensions = append(framework.NewFrameworkExtensions,
// This callback runs directly after NewDefaultFramework is done.
func(f *framework.Framework) {
ginkgo.BeforeEach(func() { framework.Logf("extension before") })
ginkgo.AfterEach(func() { framework.Logf("extension after") })
},
)
}
const ( const (
ginkgoOutput = `[BeforeEach] e2e ginkgoOutput = `[BeforeEach] e2e
cleanup_test.go:53 cleanup_test.go:63
INFO: before INFO: before
[BeforeEach] e2e [BeforeEach] e2e
set up framework | framework.go:xxx set up framework | framework.go:xxx
@ -109,30 +118,34 @@ INFO: >>> kubeConfig: yyy/kube.config
STEP: Building a namespace api object, basename test-namespace STEP: Building a namespace api object, basename test-namespace
INFO: Skipping waiting for service account INFO: Skipping waiting for service account
[BeforeEach] e2e [BeforeEach] e2e
cleanup_test.go:95 cleanup_test.go:56
INFO: extension before INFO: extension before
[BeforeEach] e2e [BeforeEach] e2e
cleanup_test.go:61 cleanup_test.go:71
INFO: before #1 INFO: before #1
[BeforeEach] e2e [BeforeEach] e2e
cleanup_test.go:65 cleanup_test.go:75
INFO: before #2 INFO: before #2
[It] works [It] works
cleanup_test.go:80 cleanup_test.go:90
[AfterEach] e2e [AfterEach] e2e
cleanup_test.go:96 cleanup_test.go:57
INFO: extension after INFO: extension after
[AfterEach] e2e [AfterEach] e2e
cleanup_test.go:69 cleanup_test.go:79
INFO: after #1 INFO: after #1
[AfterEach] e2e [AfterEach] e2e
cleanup_test.go:76 cleanup_test.go:86
INFO: after #2 INFO: after #2
[DeferCleanup (Each)] e2e [DeferCleanup (Each)] e2e
cleanup_test.go:85 cleanup_test.go:103
[DeferCleanup (Each)] e2e
cleanup_test.go:99
[DeferCleanup (Each)] e2e
cleanup_test.go:95
INFO: cleanup first INFO: cleanup first
[DeferCleanup (Each)] e2e [DeferCleanup (Each)] e2e
cleanup_test.go:82 cleanup_test.go:92
INFO: cleanup last INFO: cleanup last
[DeferCleanup (Each)] e2e [DeferCleanup (Each)] e2e
dump namespaces | framework.go:xxx dump namespaces | framework.go:xxx
@ -187,6 +200,10 @@ func TestCleanup(t *testing.T) {
Name: "e2e works", Name: "e2e works",
NormalizeOutput: normalizeOutput, NormalizeOutput: normalizeOutput,
Output: ginkgoOutput, Output: ginkgoOutput,
// It would be nice to get the cleanup failure into the
// output, but that depends on Ginkgo enhancements:
// https://github.com/onsi/ginkgo/issues/1041#issuecomment-1274611444
Failure: `DeferCleanup callback returned error: fake error for "failure"`,
}, },
} }