e2e framework: support creating TContext

This makes it possible to call helper packages which expect a TContext from E2E
tests.

The implementation uses GinkgoT as TB and supports registering cleanup
callbacks which expect a context. These callbacks then run with a context that
comes from ginkgo.DeferCleanup, just as if they had called that directly.
This commit is contained in:
Patrick Ohly 2024-01-03 11:41:26 +01:00
parent dab76ef8de
commit 47b613eded
2 changed files with 128 additions and 4 deletions

View file

@ -31,12 +31,12 @@ import (
"strings"
"time"
"k8s.io/apimachinery/pkg/runtime"
v1 "k8s.io/api/core/v1"
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
v1svc "k8s.io/client-go/applyconfigurations/core/v1"
@ -48,8 +48,10 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
scaleclient "k8s.io/client-go/scale"
"k8s.io/kubernetes/test/utils/ktesting"
admissionapi "k8s.io/pod-security-admission/api"
"github.com/go-logr/logr"
"github.com/onsi/ginkgo/v2"
)
@ -95,7 +97,16 @@ var (
// The default pod security profile is "restricted".
// Each of the labels can be overridden by using more specific NamespacePodSecurity* attributes of this
// struct.
//
// A framework instance implements ktesting.TB and thus can be passed as
// first parameter to [github.com/stretchr/testify/assert]. There are
// just two caveats:
// - Error and Errorf abort the currently running test.
// - The implementation is only usable while a test runs,
// not while defining tests.
type Framework struct {
ktesting.TB
BaseName string
// Set together with creating the ClientSet and the namespace.
@ -104,6 +115,7 @@ type Framework struct {
UniqueName string
clientConfig *rest.Config
restMapper *restmapper.DeferredDiscoveryRESTMapper
ClientSet clientset.Interface
KubemarkExternalClusterClientSet clientset.Interface
@ -139,6 +151,86 @@ type Framework struct {
DumpAllNamespaceInfo DumpAllNamespaceInfoAction
}
// CleanupCtx implements [ktesting.ContextTB.CleanupCtx]. It's identical to
// ginkgo.DeferCleanup.
func (f *Framework) CleanupCtx(cb func(context.Context)) {
ginkgo.GinkgoHelper()
ginkgo.DeferCleanup(cb)
}
// Log implements TB.Log. It overrides the implementation from Ginkgo to ensure consistent output.
func (f *Framework) Log(args ...any) {
log(1, strings.TrimSuffix(fmt.Sprintln(args...), "\n"))
}
// Logf implements TB.Logf. It overrides the implementation from Ginkgo to ensure consistent output.
func (f *Framework) Logf(format string, args ...any) {
log(1, fmt.Sprintf(format, args...))
}
var _ ktesting.ContextTB = &Framework{}
// TContext combines the framework and the context in a [ktesting.TContext].
//
// Legacy code which accepts separate [context.Context] and [clientset.Interface]
// parameters can call [ContextTODO] with those two parameters to
// create a [ktesting.TContext] which also has all clients, but this is
// an interim solution. It's cleaner to rewrite that code to accept
// a [ktesting.TContext].
func (f *Framework) TContext(ctx context.Context) ktesting.TContext {
tCtx := ktesting.InitCtx(ctx, f)
tCtx = tCtx.WithClients(f.clientConfig, f.restMapper, f.ClientSet, f.DynamicClient, apiextensions.NewForConfigOrDie(f.clientConfig))
tCtx = tCtx.WithNamespace(f.Namespace.Name)
tCtx = ensureLogger(tCtx)
return tCtx
}
// ContextTODO can be used as interim solution in functions which currently
// accept a [context.Context] and [clientset.Interface] to create
// a [ktesting.TContext] for other code which already uses that instead
// of different parameters. If the client is [Framework.ClientSet],
// the result is identical to the one from [Framework.TContext].
// In particular, dynamic client and rest config are available.
//
// Long-term code relying on ContextTODO should get rewritten.
func ContextTODO(ctx context.Context, client clientset.Interface) ktesting.TContext {
// If the client was taken from a framework instance, then
// we can cast it here to prepare a complete context.
if fc, ok := client.(fClient); ok {
return fc.f.TContext(ctx)
}
f := NewDefaultFramework("tcontext")
tCtx := ktesting.InitCtx(ctx, f)
tCtx = tCtx.WithClients(nil, nil, client, nil, nil)
tCtx = ensureLogger(tCtx)
return tCtx
}
// ensureLogger installs the Ginkgo logger in the context if there isn't one
// already.
//
// This isn't needed for code which uses klog.FromContext because that uses the
// klog logger as default, which is the Gingko logger. But there might also be
// code which uses go-logr directly: then providing the logger through the
// context is useful.
func ensureLogger(tCtx ktesting.TContext) ktesting.TContext {
_, ok := logr.FromContext(tCtx)
if ok == nil {
return tCtx
}
return tCtx.WithLogger(ginkgoLogger)
}
// fClient is stored in framework.ClientSet and used by ContextTODO.
//
// TODO: remove this and ContextTODO once it is no longer needed, i.e.
// when all code accepts a TContext.
type fClient struct {
clientset.Interface
f *Framework
}
// DumpAllNamespaceInfoAction is called after each failed test for namespaces
// created for the test.
type DumpAllNamespaceInfoAction func(ctx context.Context, f *Framework, namespace string)
@ -188,6 +280,7 @@ func NewDefaultFramework(baseName string) *Framework {
// NewFramework creates a test framework.
func NewFramework(baseName string, options Options, client clientset.Interface) *Framework {
f := &Framework{
TB: ginkgo.GinkgoT(),
BaseName: baseName,
Options: options,
ClientSet: client,
@ -230,8 +323,12 @@ func (f *Framework) BeforeEach(ctx context.Context) {
config.ContentType = TestContext.KubeAPIContentType
}
f.clientConfig = rest.CopyConfig(config)
f.ClientSet, err = clientset.NewForConfig(config)
clientSet, err := clientset.NewForConfig(config)
ExpectNoError(err)
f.ClientSet = fClient{
Interface: clientSet,
f: f,
}
f.DynamicClient, err = dynamic.NewForConfig(config)
ExpectNoError(err)
@ -250,6 +347,7 @@ func (f *Framework) BeforeEach(ctx context.Context) {
cachedDiscoClient := cacheddiscovery.NewMemCacheClient(discoClient)
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoClient)
restMapper.Reset()
f.restMapper = restMapper
resolver := scaleclient.NewDiscoveryScaleKindResolver(cachedDiscoClient)
f.ScalesGetter = scaleclient.New(restClient, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)

View file

@ -33,10 +33,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting"
"k8s.io/kubernetes/test/e2e/framework"
"k8s.io/kubernetes/test/e2e/framework/internal/output"
testapiserver "k8s.io/kubernetes/test/utils/apiserver"
"k8s.io/kubernetes/test/utils/ktesting"
)
// The line number of the following code is checked in TestFailureOutput below.
@ -102,6 +102,28 @@ var _ = ginkgo.Describe("e2e", func() {
}
ginkgo.DeferCleanup(framework.IgnoreNotFound(fail), "failure") // Without a failure the output would not be shown in JUnit.
tCtx := f.TContext(ctx)
tCtx.Log("log", "hello", "world")
tCtx.Logger().Info("info hello world")
var discardLogger klog.Logger
tCtx = tCtx.WithLogger(discardLogger)
oldCtx := tCtx
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
if tCtx.Logger() != discardLogger {
tCtx.Errorf("expected discard logger in context, got %+v", tCtx.Logger())
}
_, ok := tCtx.Value("GINKGO_SPEC_CONTEXT").(ginkgo.SpecContext)
if !ok {
tCtx.Errorf("expected Ginkgo context, got %+v", tCtx.Context)
}
if oldCtx.Err() == nil {
tCtx.Error("Ginkgo.It context should be canceled but isn't")
}
if tCtx.Err() != nil {
tCtx.Errorf("Ginkgo.DeferCleanup context should not be canceled but is: %v", tCtx.Err())
}
})
// More test cases can be added here without affeccting line numbering
// of existing tests.
})
@ -134,6 +156,8 @@ STEP: Building a namespace api object, basename test-namespace - framework.go:xx
<klog> cleanup_test.go:76] before #2
< Exit [BeforeEach] e2e - cleanup_test.go:75 <time>
> Enter [It] works - cleanup_test.go:90 <time>
<klog> cleanup_test.go:106] log hello world
<klog> cleanup_test.go:107] "info hello world"
< Exit [It] works - cleanup_test.go:90 <time>
> Enter [AfterEach] e2e - cleanup_test.go:57 <time>
<klog> cleanup_test.go:57] extension after
@ -144,6 +168,8 @@ STEP: Building a namespace api object, basename test-namespace - framework.go:xx
> Enter [AfterEach] e2e - cleanup_test.go:86 <time>
<klog> cleanup_test.go:87] after #2
< Exit [AfterEach] e2e - cleanup_test.go:86 <time>
> Enter [DeferCleanup (Each)] e2e - cleanup_test.go:111 <time>
< Exit [DeferCleanup (Each)] e2e - cleanup_test.go:111 <time>
> Enter [DeferCleanup (Each)] e2e - cleanup_test.go:103 <time>
[FAILED] DeferCleanup callback returned error: fake error for "failure"
In [DeferCleanup (Each)] at: cleanup_test.go:103 <time>