Skip to content

Commit 991aba6

Browse files
committed
Add automated tests for non-CNV swap configuration
Updated testcase name and separated upgrade cases Updated testcase Addressed review comments Skip testcases on microsoft cluster Added polarion test case IDs Updated
1 parent 737f738 commit 991aba6

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed

test/extended/node/node_swap.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
g "github.com/onsi/ginkgo/v2"
9+
o "github.com/onsi/gomega"
10+
11+
v1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
"k8s.io/apimachinery/pkg/util/wait"
15+
"k8s.io/kubernetes/test/e2e/framework"
16+
17+
machineconfigv1 "github.com/openshift/api/machineconfiguration/v1"
18+
mcclient "github.com/openshift/client-go/machineconfiguration/clientset/versioned"
19+
exutil "github.com/openshift/origin/test/extended/util"
20+
)
21+
22+
var _ = g.Describe("[OTP][Jira:\"Node / Kubelet\"][sig-node] Node non-cnv swap configuration", func() {
23+
defer g.GinkgoRecover()
24+
25+
var oc = exutil.NewCLI("node-swap")
26+
27+
g.BeforeEach(func(ctx context.Context) {
28+
// Skip all tests on MicroShift clusters
29+
isMicroShift, err := exutil.IsMicroShiftCluster(oc.AdminKubeClient())
30+
o.Expect(err).NotTo(o.HaveOccurred())
31+
if isMicroShift {
32+
g.Skip("Skipping test on MicroShift cluster")
33+
}
34+
})
35+
36+
// This test validates that:
37+
// - Worker nodes have failSwapOn=false to allow kubelet to start even if swap is present at OS level
38+
// - Control plane nodes have failSwapOn=true to prevent kubelet from starting if swap is enabled
39+
// - All nodes have swapBehavior=NoSwap to ensure kubelet does not utilize swap even if available at OS level
40+
// The swapBehavior=NoSwap configuration ensures that even if swap is manually enabled on a worker node,
41+
// the kubelet will not use it for memory management, maintaining consistent behavior across the cluster.
42+
g.It("should have correct default kubelet swap settings with worker nodes failSwapOn=false, control plane nodes failSwapOn=true, and both swapBehavior=NoSwap - OCP-86394", func(ctx context.Context) {
43+
g.By("Getting worker nodes")
44+
workerNodes, err := getWorkerNodes(ctx, oc)
45+
o.Expect(err).NotTo(o.HaveOccurred())
46+
o.Expect(len(workerNodes)).Should(o.BeNumerically(">", 0), "Expected at least one worker node")
47+
48+
g.By("Validating kubelet configuration on each worker node")
49+
for _, node := range workerNodes {
50+
config, err := getKubeletConfigFromNode(ctx, oc, node.Name)
51+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet config for worker node %s", node.Name)
52+
53+
g.By(fmt.Sprintf("Checking failSwapOn=false on worker node %s", node.Name))
54+
o.Expect(config.FailSwapOn).NotTo(o.BeNil(), "failSwapOn should be set on worker node %s", node.Name)
55+
o.Expect(*config.FailSwapOn).To(o.BeFalse(), "failSwapOn should be false on worker node %s", node.Name)
56+
framework.Logf("Worker node %s: failSwapOn=%v ✓", node.Name, *config.FailSwapOn)
57+
58+
g.By(fmt.Sprintf("Checking swapDesired=NoSwap on worker node %s", node.Name))
59+
if config.SwapDesired != nil {
60+
o.Expect(*config.SwapDesired).To(o.Equal("NoSwap"), "swapDesired should be NoSwap on worker node %s", node.Name)
61+
framework.Logf("Worker node %s: swapDesired=%s ✓", node.Name, *config.SwapDesired)
62+
}
63+
// Also check memorySwap.swapBehavior if present
64+
if config.MemorySwap != nil {
65+
o.Expect(config.MemorySwap.SwapBehavior).To(o.Equal("NoSwap"), "swapBehavior should be NoSwap on worker node %s", node.Name)
66+
framework.Logf("Worker node %s: swapBehavior=%s ✓", node.Name, config.MemorySwap.SwapBehavior)
67+
}
68+
}
69+
70+
g.By("Getting control plane nodes")
71+
controlPlaneNodes, err := getControlPlaneNodes(ctx, oc)
72+
o.Expect(err).NotTo(o.HaveOccurred())
73+
o.Expect(len(controlPlaneNodes)).Should(o.BeNumerically(">", 0), "Expected at least one control plane node")
74+
75+
g.By("Validating kubelet configuration on each control plane node")
76+
for _, node := range controlPlaneNodes {
77+
config, err := getKubeletConfigFromNode(ctx, oc, node.Name)
78+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet config for control plane node %s", node.Name)
79+
80+
g.By(fmt.Sprintf("Checking failSwapOn=true on control plane node %s", node.Name))
81+
o.Expect(config.FailSwapOn).NotTo(o.BeNil(), "failSwapOn should be set on control plane node %s", node.Name)
82+
o.Expect(*config.FailSwapOn).To(o.BeTrue(), "failSwapOn should be true on control plane node %s", node.Name)
83+
framework.Logf("Control plane node %s: failSwapOn=%v ✓", node.Name, *config.FailSwapOn)
84+
85+
g.By(fmt.Sprintf("Checking swapDesired=NoSwap on control plane node %s", node.Name))
86+
if config.SwapDesired != nil {
87+
o.Expect(*config.SwapDesired).To(o.Equal("NoSwap"), "swapDesired should be NoSwap on control plane node %s", node.Name)
88+
framework.Logf("Control plane node %s: swapDesired=%s ✓", node.Name, *config.SwapDesired)
89+
}
90+
// Also check memorySwap.swapBehavior if present
91+
if config.MemorySwap != nil {
92+
o.Expect(config.MemorySwap.SwapBehavior).To(o.Equal("NoSwap"), "swapBehavior should be NoSwap on control plane node %s", node.Name)
93+
framework.Logf("Control plane node %s: swapBehavior=%s ✓", node.Name, config.MemorySwap.SwapBehavior)
94+
}
95+
}
96+
framework.Logf("Test PASSED: All nodes have correct default swap settings")
97+
})
98+
99+
g.It("should reject user override of swap settings via KubeletConfig API - OCP-86395", func(ctx context.Context) {
100+
g.By("Creating machine config client")
101+
mcClient, err := mcclient.NewForConfig(oc.KubeFramework().ClientConfig())
102+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to create machine config client")
103+
104+
g.By("Creating a KubeletConfig with swap settings")
105+
kubeletConfig := &machineconfigv1.KubeletConfig{
106+
ObjectMeta: metav1.ObjectMeta{
107+
Name: "test-swap-override",
108+
},
109+
Spec: machineconfigv1.KubeletConfigSpec{
110+
KubeletConfig: &runtime.RawExtension{
111+
Raw: []byte(`{
112+
"failSwapOn": true,
113+
"memorySwap": {
114+
"swapBehavior": "LimitedSwap"
115+
}
116+
}`),
117+
},
118+
},
119+
}
120+
121+
g.By("Attempting to apply the KubeletConfig")
122+
framework.Logf("Creating KubeletConfig with failSwapOn=true and swapBehavior=LimitedSwap")
123+
_, err = mcClient.MachineconfigurationV1().KubeletConfigs().Create(ctx, kubeletConfig, metav1.CreateOptions{})
124+
125+
// We expect this to either be rejected immediately or to be created but not applied
126+
if err != nil {
127+
g.By("Verifying the error message indicates swap is not configurable")
128+
o.Expect(err).To(o.HaveOccurred())
129+
framework.Logf("KubeletConfig creation rejected with error: %v", err)
130+
} else {
131+
framework.Logf("KubeletConfig was created, verifying it doesn't affect worker nodes")
132+
// If created, clean up and verify it doesn't affect nodes
133+
defer func() {
134+
_ = mcClient.MachineconfigurationV1().KubeletConfigs().Delete(ctx, "test-swap-override", metav1.DeleteOptions{})
135+
}()
136+
137+
// Declare variables outside poll callback so they're accessible later
138+
var workerNodes []v1.Node
139+
var config *KubeletConfiguration
140+
141+
// Poll to verify worker node configs remain unchanged
142+
err = wait.Poll(2*time.Second, 30*time.Second, func() (bool, error) {
143+
var err error
144+
workerNodes, err = getWorkerNodes(ctx, oc)
145+
if err != nil {
146+
return false, err
147+
}
148+
if len(workerNodes) == 0 {
149+
return false, fmt.Errorf("no worker nodes found")
150+
}
151+
152+
config, err = getKubeletConfigFromNode(ctx, oc, workerNodes[0].Name)
153+
if err != nil {
154+
return false, err
155+
}
156+
157+
// Check that failSwapOn is still false
158+
if config.FailSwapOn == nil || *config.FailSwapOn != false {
159+
return false, fmt.Errorf("worker node %s: failSwapOn changed from expected value false, got %v", workerNodes[0].Name, config.FailSwapOn)
160+
}
161+
162+
// Check that swapBehavior is still NoSwap
163+
if config.MemorySwap != nil && config.MemorySwap.SwapBehavior != "NoSwap" {
164+
return false, fmt.Errorf("worker node %s: swapBehavior changed from NoSwap to %s", workerNodes[0].Name, config.MemorySwap.SwapBehavior)
165+
}
166+
167+
// Continue polling to ensure config stays unchanged for the full duration
168+
return false, nil
169+
})
170+
171+
framework.Logf("Worker node %s config after poll: failSwapOn=%v, swapBehavior=%s", workerNodes[0].Name, *config.FailSwapOn, config.MemorySwap.SwapBehavior)
172+
// We expect the poll to timeout (not find a change), which means settings remained unchanged
173+
if err != nil && err == wait.ErrWaitTimeout {
174+
framework.Logf("Test PASSED: Worker node swap settings remained unchange")
175+
} else if err != nil {
176+
o.Expect(err).NotTo(o.HaveOccurred(), "Test FAILED: Unexpected error while polling worker node config")
177+
}
178+
}
179+
})
180+
})

test/extended/node/node_utils.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
v1 "k8s.io/api/core/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
11+
exutil "github.com/openshift/origin/test/extended/util"
12+
)
13+
14+
// KubeletConfiguration represents the kubelet configuration structure
15+
type KubeletConfiguration struct {
16+
FailSwapOn *bool `json:"failSwapOn,omitempty"`
17+
MemorySwap *MemorySwapConfiguration `json:"memorySwap,omitempty"`
18+
SwapDesired *string `json:"swapDesired,omitempty"`
19+
}
20+
21+
// MemorySwapConfiguration represents memory swap configuration
22+
type MemorySwapConfiguration struct {
23+
SwapBehavior string `json:"swapBehavior,omitempty"`
24+
}
25+
26+
// getWorkerNodes returns all worker nodes in the cluster
27+
func getWorkerNodes(ctx context.Context, oc *exutil.CLI) ([]v1.Node, error) {
28+
nodes, err := oc.AdminKubeClient().CoreV1().Nodes().List(ctx, metav1.ListOptions{})
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
var workerNodes []v1.Node
34+
for _, node := range nodes.Items {
35+
if _, ok := node.Labels["node-role.kubernetes.io/worker"]; ok {
36+
workerNodes = append(workerNodes, node)
37+
}
38+
}
39+
return workerNodes, nil
40+
}
41+
42+
// getControlPlaneNodes returns all control plane nodes in the cluster
43+
func getControlPlaneNodes(ctx context.Context, oc *exutil.CLI) ([]v1.Node, error) {
44+
nodes, err := oc.AdminKubeClient().CoreV1().Nodes().List(ctx, metav1.ListOptions{})
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
var controlPlaneNodes []v1.Node
50+
for _, node := range nodes.Items {
51+
if _, ok := node.Labels["node-role.kubernetes.io/master"]; ok {
52+
controlPlaneNodes = append(controlPlaneNodes, node)
53+
} else if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok {
54+
controlPlaneNodes = append(controlPlaneNodes, node)
55+
}
56+
}
57+
return controlPlaneNodes, nil
58+
}
59+
60+
// getKubeletConfigFromNode retrieves the kubelet configuration from a specific node
61+
func getKubeletConfigFromNode(ctx context.Context, oc *exutil.CLI, nodeName string) (*KubeletConfiguration, error) {
62+
// Use the node proxy API to get configz
63+
configzPath := fmt.Sprintf("/api/v1/nodes/%s/proxy/configz", nodeName)
64+
65+
data, err := oc.AdminKubeClient().CoreV1().RESTClient().Get().AbsPath(configzPath).DoRaw(ctx)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to get configz from node %s: %w", nodeName, err)
68+
}
69+
70+
// Parse the JSON response
71+
var configzResponse struct {
72+
KubeletConfig *KubeletConfiguration `json:"kubeletconfig"`
73+
}
74+
75+
if err := json.Unmarshal(data, &configzResponse); err != nil {
76+
return nil, fmt.Errorf("failed to unmarshal configz response: %w", err)
77+
}
78+
79+
if configzResponse.KubeletConfig == nil {
80+
return nil, fmt.Errorf("kubeletconfig is nil in response")
81+
}
82+
83+
return configzResponse.KubeletConfig, nil
84+
}

0 commit comments

Comments
 (0)