Skip to content

Commit 6511ba7

Browse files
authored
Merge pull request kubernetes#130319 from pohly/dra-test-integration
DRA: add dedicated integration tests
2 parents 7bd0477 + 9492a2c commit 6511ba7

File tree

8 files changed

+369
-0
lines changed

8 files changed

+369
-0
lines changed

test/integration/dra/OWNERS

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# See the OWNERS docs at https://go.k8s.io/owners
2+
3+
approvers:
4+
- johnbelamaric
5+
- klueska
6+
- pohly
7+
reviewers:
8+
- pohly
9+
- bart0sh
10+
labels:
11+
- sig/node
12+
- wg/device-management

test/integration/dra/dra_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dra
18+
19+
import (
20+
"fmt"
21+
"regexp"
22+
"sort"
23+
"strings"
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
28+
v1 "k8s.io/api/core/v1"
29+
resourcealphaapi "k8s.io/api/resource/v1alpha3"
30+
resourceapi "k8s.io/api/resource/v1beta1"
31+
apierrors "k8s.io/apimachinery/pkg/api/errors"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/runtime/schema"
34+
utilfeature "k8s.io/apiserver/pkg/util/feature"
35+
"k8s.io/component-base/featuregate"
36+
featuregatetesting "k8s.io/component-base/featuregate/testing"
37+
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
38+
"k8s.io/kubernetes/pkg/features"
39+
st "k8s.io/kubernetes/pkg/scheduler/testing"
40+
"k8s.io/kubernetes/test/integration/framework"
41+
"k8s.io/kubernetes/test/utils/ktesting"
42+
"k8s.io/utils/ptr"
43+
)
44+
45+
var (
46+
// For more test data see pkg/scheduler/framework/plugin/dynamicresources/dynamicresources_test.go.
47+
48+
podName = "my-pod"
49+
namespace = "default"
50+
resourceName = "my-resource"
51+
className = "my-resource-class"
52+
claimName = podName + "-" + resourceName
53+
podWithClaimName = st.MakePod().Name(podName).Namespace(namespace).
54+
Container("my-container").
55+
PodResourceClaims(v1.PodResourceClaim{Name: resourceName, ResourceClaimName: &claimName}).
56+
Obj()
57+
claim = st.MakeResourceClaim().
58+
Name(claimName).
59+
Namespace(namespace).
60+
Request(className).
61+
Obj()
62+
)
63+
64+
// createTestNamespace creates a namespace with a name that is derived from the
65+
// current test name:
66+
// - Non-alpha-numeric characters replaced by hyphen.
67+
// - Truncated in the middle to make it short enough for GenerateName.
68+
// - Hyphen plus random suffix added by the apiserver.
69+
func createTestNamespace(tCtx ktesting.TContext) string {
70+
tCtx.Helper()
71+
name := regexp.MustCompile(`[^[:alnum:]_-]`).ReplaceAllString(tCtx.Name(), "-")
72+
name = strings.ToLower(name)
73+
if len(name) > 63 {
74+
name = name[:30] + "--" + name[len(name)-30:]
75+
}
76+
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: name + "-"}}
77+
ns, err := tCtx.Client().CoreV1().Namespaces().Create(tCtx, ns, metav1.CreateOptions{})
78+
tCtx.ExpectNoError(err, "create test namespace")
79+
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
80+
tCtx.ExpectNoError(tCtx.Client().CoreV1().Namespaces().Delete(tCtx, ns.Name, metav1.DeleteOptions{}), "delete test namespace")
81+
})
82+
return ns.Name
83+
}
84+
85+
func TestDRA(t *testing.T) {
86+
// Each sub-test brings up the API server in a certain
87+
// configuration. These sub-tests must run sequentially because they
88+
// change the global DefaultFeatureGate. For each configuration,
89+
// multiple tests can run in parallel as long as they are careful
90+
// about what they create.
91+
for name, tc := range map[string]struct {
92+
apis map[schema.GroupVersion]bool
93+
features map[featuregate.Feature]bool
94+
f func(tCtx ktesting.TContext)
95+
}{
96+
"default": {
97+
f: func(tCtx ktesting.TContext) {
98+
tCtx.Run("Pod", func(tCtx ktesting.TContext) { testPod(tCtx, false) })
99+
tCtx.Run("APIDisabled", testAPIDisabled)
100+
},
101+
},
102+
"core": {
103+
apis: map[schema.GroupVersion]bool{
104+
resourceapi.SchemeGroupVersion: true,
105+
},
106+
features: map[featuregate.Feature]bool{features.DynamicResourceAllocation: true},
107+
f: func(tCtx ktesting.TContext) {
108+
tCtx.Run("AdminAccess", func(tCtx ktesting.TContext) { testAdminAccess(tCtx, false) })
109+
tCtx.Run("Pod", func(tCtx ktesting.TContext) { testPod(tCtx, true) })
110+
},
111+
},
112+
"all": {
113+
apis: map[schema.GroupVersion]bool{
114+
resourceapi.SchemeGroupVersion: true,
115+
resourcealphaapi.SchemeGroupVersion: true,
116+
},
117+
features: map[featuregate.Feature]bool{
118+
features.DynamicResourceAllocation: true,
119+
// Additional DRA feature gates go here,
120+
// in alphabetical order,
121+
// as needed by tests for them.
122+
features.DRAAdminAccess: true,
123+
},
124+
f: func(tCtx ktesting.TContext) {
125+
tCtx.Run("AdminAccess", func(tCtx ktesting.TContext) { testAdminAccess(tCtx, true) })
126+
tCtx.Run("Convert", testConvert)
127+
},
128+
},
129+
} {
130+
t.Run(name, func(t *testing.T) {
131+
tCtx := ktesting.Init(t)
132+
var entries []string
133+
for key, value := range tc.features {
134+
entries = append(entries, fmt.Sprintf("%s=%t", key, value))
135+
}
136+
for key, value := range tc.apis {
137+
entries = append(entries, fmt.Sprintf("%s=%t", key, value))
138+
}
139+
sort.Strings(entries)
140+
t.Logf("Config: %s", strings.Join(entries, ","))
141+
142+
for key, value := range tc.features {
143+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, key, value)
144+
}
145+
146+
etcdOptions := framework.SharedEtcd()
147+
apiServerOptions := kubeapiservertesting.NewDefaultTestServerOptions()
148+
apiServerFlags := framework.DefaultTestServerFlags()
149+
// Default kube-apiserver behavior, must be requested explicitly for test server.
150+
runtimeConfigs := []string{"api/alpha=false", "api/beta=false"}
151+
for key, value := range tc.apis {
152+
runtimeConfigs = append(runtimeConfigs, fmt.Sprintf("%s=%t", key, value))
153+
}
154+
apiServerFlags = append(apiServerFlags, "--runtime-config="+strings.Join(runtimeConfigs, ","))
155+
server := kubeapiservertesting.StartTestServerOrDie(t, apiServerOptions, apiServerFlags, etcdOptions)
156+
tCtx.Cleanup(server.TearDownFn)
157+
158+
tCtx = ktesting.WithRESTConfig(tCtx, server.ClientConfig)
159+
tc.f(tCtx)
160+
})
161+
}
162+
}
163+
164+
// testPod creates a pod with a resource claim reference and then checks
165+
// whether that field is or isn't getting dropped.
166+
func testPod(tCtx ktesting.TContext, draEnabled bool) {
167+
tCtx.Parallel()
168+
namespace := createTestNamespace(tCtx)
169+
podWithClaimName := podWithClaimName.DeepCopy()
170+
podWithClaimName.Namespace = namespace
171+
pod, err := tCtx.Client().CoreV1().Pods(namespace).Create(tCtx, podWithClaimName, metav1.CreateOptions{})
172+
tCtx.ExpectNoError(err, "create pod")
173+
if draEnabled {
174+
assert.NotEmpty(tCtx, pod.Spec.ResourceClaims, "should store resource claims in pod spec")
175+
} else {
176+
assert.Empty(tCtx, pod.Spec.ResourceClaims, "should drop resource claims from pod spec")
177+
}
178+
}
179+
180+
// testAPIDisabled checks that the resource.k8s.io API is disabled.
181+
func testAPIDisabled(tCtx ktesting.TContext) {
182+
tCtx.Parallel()
183+
_, err := tCtx.Client().ResourceV1beta1().ResourceClaims(claim.Namespace).Create(tCtx, claim, metav1.CreateOptions{})
184+
if !apierrors.IsNotFound(err) {
185+
tCtx.Fatalf("expected 'resource not found' error, got %v", err)
186+
}
187+
}
188+
189+
// testConvert creates a claim using a one API version and reads it with another.
190+
func testConvert(tCtx ktesting.TContext) {
191+
tCtx.Parallel()
192+
namespace := createTestNamespace(tCtx)
193+
claim := claim.DeepCopy()
194+
claim.Namespace = namespace
195+
claim, err := tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
196+
tCtx.ExpectNoError(err, "create claim")
197+
claimAlpha, err := tCtx.Client().ResourceV1alpha3().ResourceClaims(namespace).Get(tCtx, claim.Name, metav1.GetOptions{})
198+
tCtx.ExpectNoError(err, "get claim")
199+
// We could check more fields, but there are unit tests which cover this better.
200+
assert.Equal(tCtx, claim.Name, claimAlpha.Name, "claim name")
201+
}
202+
203+
// testAdminAccess creates a claim with AdminAccess and then checks
204+
// whether that field is or isn't getting dropped.
205+
func testAdminAccess(tCtx ktesting.TContext, adminAccessEnabled bool) {
206+
tCtx.Parallel()
207+
namespace := createTestNamespace(tCtx)
208+
claim := claim.DeepCopy()
209+
claim.Namespace = namespace
210+
claim.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
211+
claim, err := tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
212+
tCtx.ExpectNoError(err, "create claim")
213+
if adminAccessEnabled {
214+
if !ptr.Deref(claim.Spec.Devices.Requests[0].AdminAccess, false) {
215+
tCtx.Fatal("should store AdminAccess in ResourceClaim")
216+
}
217+
} else {
218+
if claim.Spec.Devices.Requests[0].AdminAccess != nil {
219+
tCtx.Fatal("should drop AdminAccess in ResourceClaim")
220+
}
221+
}
222+
}

test/integration/dra/main_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dra
18+
19+
import (
20+
"testing"
21+
22+
"k8s.io/kubernetes/test/integration/framework"
23+
)
24+
25+
func TestMain(m *testing.M) {
26+
framework.EtcdMain(m.Run)
27+
}

test/utils/ktesting/clientcontext.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func (cCtx clientContext) ExpectNoError(err error, explain ...interface{}) {
8686
expectNoError(cCtx, err, explain...)
8787
}
8888

89+
func (cCtx clientContext) Run(name string, cb func(tCtx TContext)) bool {
90+
return run(cCtx, name, cb)
91+
}
92+
8993
func (cCtx clientContext) Logger() klog.Logger {
9094
return klog.FromContext(cCtx)
9195
}

test/utils/ktesting/errorcontext.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ func (eCtx *errorContext) ExpectNoError(err error, explain ...interface{}) {
149149
expectNoError(eCtx, err, explain...)
150150
}
151151

152+
func (cCtx *errorContext) Run(name string, cb func(tCtx TContext)) bool {
153+
return run(cCtx, name, cb)
154+
}
155+
152156
func (eCtx *errorContext) Logger() klog.Logger {
153157
return klog.FromContext(eCtx)
154158
}

test/utils/ktesting/tcontext.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"flag"
2222
"fmt"
2323
"strings"
24+
"testing"
2425
"time"
2526

2627
"github.com/onsi/gomega"
@@ -75,6 +76,22 @@ type TContext interface {
7576
context.Context
7677
TB
7778

79+
// Parallel signals that this test is to be run in parallel with (and
80+
// only with) other parallel tests. In other words, it needs to be
81+
// called in each test which is meant to run in parallel.
82+
//
83+
// Only supported in Go unit tests. When such a test is run multiple
84+
// times due to use of -test.count or -test.cpu, multiple instances of
85+
// a single test never run in parallel with each other.
86+
Parallel()
87+
88+
// Run runs f as a subtest of t called name. It blocks until f returns or
89+
// calls t.Parallel to become a parallel test.
90+
//
91+
// Only supported in Go unit tests or benchmarks. It fails the current
92+
// test when called elsewhere.
93+
Run(name string, f func(tCtx TContext)) bool
94+
7895
// Cancel can be invoked to cancel the context before the test is completed.
7996
// Tests which use the context to control goroutines and then wait for
8097
// termination of those goroutines must call Cancel to avoid a deadlock.
@@ -165,6 +182,7 @@ type TContext interface {
165182
// - CleanupCtx
166183
// - Expect
167184
// - ExpectNoError
185+
// - Run
168186
// - Logger
169187
//
170188
// Usually these methods would be stand-alone functions with a TContext
@@ -328,6 +346,9 @@ func InitCtx(ctx context.Context, tb TB, _ ...InitOption) TContext {
328346
// })
329347
//
330348
// WithTB sets up cancellation for the sub-test.
349+
//
350+
// A simpler API is to use TContext.Run as replacement
351+
// for [testing.T.Run].
331352
func WithTB(parentCtx TContext, tb TB) TContext {
332353
tCtx := InitCtx(parentCtx, tb)
333354
tCtx = WithCancel(tCtx)
@@ -341,6 +362,27 @@ func WithTB(parentCtx TContext, tb TB) TContext {
341362
return tCtx
342363
}
343364

365+
// run implements the different Run methods. It's not an exported
366+
// method because tCtx.Run is more discoverable (same usage as
367+
// with normal Go).
368+
func run(tCtx TContext, name string, cb func(tCtx TContext)) bool {
369+
tCtx.Helper()
370+
switch tb := tCtx.TB().(type) {
371+
case interface {
372+
Run(string, func(t *testing.T)) bool
373+
}:
374+
return tb.Run(name, func(t *testing.T) { cb(WithTB(tCtx, t)) })
375+
case interface {
376+
Run(string, func(t *testing.B)) bool
377+
}:
378+
return tb.Run(name, func(b *testing.B) { cb(WithTB(tCtx, b)) })
379+
default:
380+
tCtx.Fatalf("Run not implemented, underlying %T does not support it", tCtx.TB())
381+
}
382+
383+
return false
384+
}
385+
344386
// WithContext constructs a new TContext with a different Context instance.
345387
// This can be used in callbacks which receive a Context, for example
346388
// from Gomega:
@@ -381,6 +423,12 @@ type testingTB struct {
381423
TB
382424
}
383425

426+
func (tCtx tContext) Parallel() {
427+
if tb, ok := tCtx.TB().(interface{ Parallel() }); ok {
428+
tb.Parallel()
429+
}
430+
}
431+
384432
func (tCtx tContext) Cancel(cause string) {
385433
if tCtx.cancel != nil {
386434
tCtx.cancel(cause)
@@ -424,6 +472,10 @@ func cleanupCtx(tCtx TContext, cb func(TContext)) {
424472
})
425473
}
426474

475+
func (cCtx tContext) Run(name string, cb func(tCtx TContext)) bool {
476+
return run(cCtx, name, cb)
477+
}
478+
427479
func (tCtx tContext) Logger() klog.Logger {
428480
return klog.FromContext(tCtx)
429481
}

0 commit comments

Comments
 (0)