Skip to content

Commit dbbfc40

Browse files
committed
e2e: add first test
with all the infrastructure in place, we can now add the first real test. We start with the simplest case: a best effort pod should still get access to all the CPUs. Because we added all the infrastructure and utilities, next tests which will be much easier to add. Signed-off-by: Francesco Romani <fromani@redhat.com>
1 parent d59edff commit dbbfc40

7 files changed

Lines changed: 443 additions & 3 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ test-dracpuinfo: ## build helper to expose hardware info in the internal dracpu
130130
go build -v -o "$(OUT_DIR)/dracpuinfo" ./test/image/dracpuinfo
131131

132132
test-e2e-base: ## run e2e test base suite
133-
env DRACPUINFO_IMAGE="$(IMAGE_TESTING)" go test -v ./test/e2e/ --ginkgo.v
133+
env DRACPU_TEST_IMAGE="$(IMAGE_TESTING)" go test -v ./test/e2e/ --ginkgo.v
134134

135135
# dependencies
136136
.PHONY:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.24.0
44

55
require (
66
github.com/containerd/nri v0.9.0
7+
github.com/go-logr/logr v1.4.2
78
github.com/onsi/ginkgo/v2 v2.22.1
89
github.com/onsi/gomega v1.36.2
910
github.com/prometheus/client_golang v1.22.0
@@ -28,7 +29,6 @@ require (
2829
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2930
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
3031
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
31-
github.com/go-logr/logr v1.4.2 // indirect
3232
github.com/go-openapi/jsonpointer v0.21.1 // indirect
3333
github.com/go-openapi/jsonreference v0.21.0 // indirect
3434
github.com/go-openapi/swag v0.23.1 // indirect

test/e2e/cpu_assignment_test.go

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,116 @@ limitations under the License.
1717
package e2e
1818

1919
import (
20+
"context"
21+
"encoding/json"
22+
"os"
23+
24+
"github.com/kubernetes-sigs/dra-driver-cpu/test/pkg/discovery"
25+
"github.com/kubernetes-sigs/dra-driver-cpu/test/pkg/fixture"
26+
"github.com/kubernetes-sigs/dra-driver-cpu/test/pkg/node"
27+
e2epod "github.com/kubernetes-sigs/dra-driver-cpu/test/pkg/pod"
2028
"github.com/onsi/ginkgo/v2"
29+
"github.com/onsi/gomega"
30+
v1 "k8s.io/api/core/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/client-go/kubernetes"
33+
"k8s.io/utils/cpuset"
2134
)
2235

23-
var _ = ginkgo.Describe("CPU Assignment", func() {
36+
var _ = ginkgo.Describe("CPU Assignment", ginkgo.Ordered, ginkgo.ContinueOnFailure, func() {
37+
var rootFxt *fixture.Fixture
38+
var targetNode *v1.Node
39+
var targetNodeCPUInfo discovery.DRACPUInfo
40+
var dracpuTesterImage string
41+
42+
ginkgo.BeforeAll(func(ctx context.Context) {
43+
dracpuTesterImage = os.Getenv("DRACPU_TEST_IMAGE")
44+
gomega.Expect(dracpuTesterImage).ToNot(gomega.BeEmpty(), "missing environment variable DRACPU_TEST_IMAGE")
45+
46+
var err error
47+
rootFxt, err = fixture.Make("root", ctx)
48+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot create e2e root test fixture: %v", err)
49+
50+
workerNodes, err := node.FindWorkers(ctx, rootFxt.K8SClientset)
51+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot find worker nodes: %v", err)
52+
gomega.Expect(workerNodes).ToNot(gomega.BeEmpty(), "no worker nodes detected")
53+
54+
targetNode = workerNodes[0] // pick random one, this is the simplest random pick
55+
ginkgo.GinkgoLogr.Info("Using worker node", "nodeName", targetNode.Name)
56+
57+
infoPod, err := e2epod.RunToCompletion(ctx, rootFxt.K8SClientset, makeDiscoveryPod(rootFxt.Namespace.Name, dracpuTesterImage))
58+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot create discovery pod: %v", err)
59+
data, err := e2epod.GetLogs(rootFxt.K8SClientset, ctx, infoPod.Namespace, infoPod.Name, infoPod.Spec.Containers[0].Name)
60+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot get logs from discovery pod: %v", err)
61+
gomega.Expect(json.Unmarshal([]byte(data), &targetNodeCPUInfo)).To(gomega.Succeed())
62+
ginkgo.GinkgoLogr.Info("checking worker node", "coreCount", len(targetNodeCPUInfo.CPUs))
63+
64+
gomega.Expect(rootFxt.Teardown(ctx)).To(gomega.Succeed())
65+
})
66+
67+
ginkgo.When("not setting resource claims", func() {
68+
ginkgo.It("should grant best-effort pods access to all system CPUs", func(ctx context.Context) {
69+
fxt, err := rootFxt.MakeFrom("", ctx)
70+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot create test fixture: %v", err)
71+
ginkgo.DeferCleanup(fxt.Teardown, context.TODO()) // TODO
72+
73+
pod, err := e2epod.CreateSync(ctx, fxt.K8SClientset, makeTesterPodBestEffort(rootFxt.Namespace.Name, dracpuTesterImage))
74+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot create tester pod: %v", err)
75+
76+
cpus, err := getTesterPodCPUAllocation(fxt.K8SClientset, ctx, pod)
77+
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "cannot get the CPUs allocated to tester pod %s/%s", pod.Namespace, pod.Name)
78+
gomega.Expect(cpus.Size()).To(gomega.Equal(len(targetNodeCPUInfo.CPUs)))
79+
})
80+
})
2481
})
82+
83+
func getTesterPodCPUAllocation(cs kubernetes.Interface, ctx context.Context, pod *v1.Pod) (cpuset.CPUSet, error) {
84+
data, err := e2epod.GetLogs(cs, ctx, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)
85+
if err != nil {
86+
return cpuset.CPUSet{}, err
87+
}
88+
testerInfo := discovery.DRACPUTester{}
89+
err = json.Unmarshal([]byte(data), &testerInfo)
90+
if err != nil {
91+
return cpuset.CPUSet{}, err
92+
}
93+
return cpuset.Parse(testerInfo.Allocation.CPUs)
94+
}
95+
96+
func makeTesterPodBestEffort(ns, image string) *v1.Pod {
97+
return &v1.Pod{
98+
ObjectMeta: metav1.ObjectMeta{
99+
GenerateName: "tester-pod-",
100+
Namespace: ns,
101+
},
102+
Spec: v1.PodSpec{
103+
Containers: []v1.Container{
104+
{
105+
Name: "tester-container",
106+
Image: image,
107+
Command: []string{"/dracputester"},
108+
},
109+
},
110+
RestartPolicy: v1.RestartPolicyAlways,
111+
},
112+
}
113+
}
114+
func makeDiscoveryPod(ns, image string) *v1.Pod {
115+
return &v1.Pod{
116+
ObjectMeta: metav1.ObjectMeta{
117+
Name: "discovery-pod",
118+
Namespace: ns,
119+
},
120+
Spec: v1.PodSpec{
121+
Containers: []v1.Container{
122+
{
123+
Name: "discovery-container",
124+
Image: image,
125+
ImagePullPolicy: v1.PullIfNotPresent,
126+
Command: []string{"/dracpuinfo"},
127+
},
128+
},
129+
RestartPolicy: v1.RestartPolicyNever,
130+
},
131+
}
132+
}

test/pkg/client/k8s.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 client
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"os"
23+
24+
"k8s.io/client-go/kubernetes"
25+
"k8s.io/client-go/tools/clientcmd"
26+
)
27+
28+
func NewK8SClientset() (kubernetes.Interface, error) {
29+
kubeconfig, ok := os.LookupEnv("KUBECONFIG")
30+
if !ok {
31+
return nil, errors.New("missing environment variable KUBECONFIG")
32+
}
33+
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
34+
if err != nil {
35+
return nil, fmt.Errorf("can not create client-go configuration: %w", err)
36+
}
37+
38+
return kubernetes.NewForConfig(config)
39+
}

test/pkg/fixture/fixture.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 fixture
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
"github.com/go-logr/logr"
25+
"github.com/kubernetes-sigs/dra-driver-cpu/test/pkg/client"
26+
"github.com/onsi/ginkgo/v2"
27+
v1 "k8s.io/api/core/v1"
28+
apierrors "k8s.io/apimachinery/pkg/api/errors"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/util/wait"
31+
"k8s.io/client-go/kubernetes"
32+
)
33+
34+
type Fixture struct {
35+
Prefix string
36+
K8SClientset kubernetes.Interface
37+
Namespace *v1.Namespace
38+
Log logr.Logger
39+
}
40+
41+
func Make(prefix string, ctx context.Context) (*Fixture, error) {
42+
cs, err := client.NewK8SClientset()
43+
if err != nil {
44+
return nil, err
45+
}
46+
return makeFixture(prefix, cs, ctx)
47+
}
48+
49+
func (fxt *Fixture) MakeFrom(prefix string, ctx context.Context) (*Fixture, error) {
50+
return makeFixture(prefix, fxt.K8SClientset, ctx)
51+
}
52+
53+
func (fxt *Fixture) Setup(ctx context.Context) error {
54+
if fxt.Namespace != nil {
55+
return nil // TODO: or fail?
56+
}
57+
generateName := "dracpu-e2e-"
58+
if fxt.Prefix != "" {
59+
generateName += fxt.Prefix + "-"
60+
}
61+
ns := &v1.Namespace{
62+
ObjectMeta: metav1.ObjectMeta{
63+
GenerateName: generateName,
64+
},
65+
}
66+
nsCreated, err := fxt.K8SClientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
67+
if err != nil {
68+
return fmt.Errorf("failed to create namespace %s: %w", ns.Name, err)
69+
}
70+
fxt.Namespace = nsCreated
71+
fxt.Log.Info("Fixture setup", "namespace", fxt.Namespace.Name)
72+
return nil
73+
}
74+
75+
func (fxt *Fixture) Teardown(ctx context.Context) error {
76+
if fxt.Namespace == nil {
77+
return nil // TODO: or fail?
78+
}
79+
err := fxt.K8SClientset.CoreV1().Namespaces().Delete(ctx, fxt.Namespace.Name, metav1.DeleteOptions{})
80+
if err != nil {
81+
return fmt.Errorf("failed to delete namespace %s: %w", fxt.Namespace.Name, err)
82+
}
83+
err = waitForNamespaceToBeDeleted(ctx, fxt.K8SClientset, fxt.Namespace.Name)
84+
if err != nil {
85+
return err
86+
}
87+
fxt.Log.Info("Fixture teardown", "namespace", fxt.Namespace.Name)
88+
fxt.Namespace = nil
89+
return nil
90+
}
91+
92+
func makeFixture(prefix string, cs kubernetes.Interface, ctx context.Context) (*Fixture, error) {
93+
fxt := &Fixture{
94+
Prefix: prefix,
95+
K8SClientset: cs,
96+
Log: ginkgo.GinkgoLogr,
97+
}
98+
err := fxt.Setup(ctx)
99+
return fxt, err
100+
}
101+
102+
const (
103+
nsPollInterval = time.Second * 10
104+
nsPollTimeout = time.Minute * 2
105+
)
106+
107+
func waitForNamespaceToBeDeleted(ctx context.Context, cs kubernetes.Interface, nsName string) error {
108+
immediate := true
109+
err := wait.PollUntilContextTimeout(ctx, nsPollInterval, nsPollTimeout, immediate, func(ctx2 context.Context) (done bool, err error) {
110+
_, err = cs.CoreV1().Namespaces().Get(ctx2, nsName, metav1.GetOptions{})
111+
if err != nil {
112+
if apierrors.IsNotFound(err) {
113+
return true, nil
114+
}
115+
return false, err
116+
}
117+
return false, nil
118+
})
119+
if err != nil {
120+
return fmt.Errorf("namespace=%s was not deleted: %w", nsName, err)
121+
}
122+
return nil
123+
}

test/pkg/node/node.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 node
18+
19+
import (
20+
"context"
21+
22+
v1 "k8s.io/api/core/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/labels"
25+
"k8s.io/client-go/kubernetes"
26+
)
27+
28+
func FindWorkers(ctx context.Context, cs kubernetes.Interface) ([]*v1.Node, error) {
29+
selector := labels.Set{"node-role.kubernetes.io/worker": ""}.AsSelector()
30+
nodeList, err := cs.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: selector.String()})
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
var workerNodes []*v1.Node
36+
for _, n := range nodeList.Items {
37+
if !IsReady(&n) {
38+
continue
39+
}
40+
workerNodes = append(workerNodes, &n)
41+
}
42+
return workerNodes, nil
43+
}
44+
45+
func IsReady(node *v1.Node) bool {
46+
for _, cond := range node.Status.Conditions {
47+
if cond.Type == v1.NodeReady {
48+
return cond.Status == v1.ConditionTrue
49+
}
50+
}
51+
return false
52+
}

0 commit comments

Comments
 (0)