Skip to content

Commit 2fdee53

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 addressed comments from the pr
1 parent a522041 commit 2fdee53

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed

test/extended/node/node_swap.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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("[Jira:Node][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+
framework.Logf("KubeletConfig was created, verifying it doesn't affect worker nodes")
128+
// If created, clean up and verify it doesn't affect nodes
129+
defer func() {
130+
_ = mcClient.MachineconfigurationV1().KubeletConfigs().Delete(ctx, "test-swap-override", metav1.DeleteOptions{})
131+
}()
132+
133+
// Declare variables outside poll callback so they're accessible later
134+
var workerNodes []v1.Node
135+
var config *KubeletConfiguration
136+
137+
// Poll to verify worker node configs remain unchanged
138+
err = wait.Poll(2*time.Second, 30*time.Second, func() (bool, error) {
139+
var err error
140+
workerNodes, err = getWorkerNodes(ctx, oc)
141+
if err != nil {
142+
return false, err
143+
}
144+
if len(workerNodes) == 0 {
145+
return false, fmt.Errorf("no worker nodes found")
146+
}
147+
148+
config, err = getKubeletConfigFromNode(ctx, oc, workerNodes[0].Name)
149+
if err != nil {
150+
return false, err
151+
}
152+
153+
// Check that failSwapOn is still false
154+
if config.FailSwapOn == nil || *config.FailSwapOn != false {
155+
return false, fmt.Errorf("worker node %s: failSwapOn changed from expected value false, got %v", workerNodes[0].Name, config.FailSwapOn)
156+
}
157+
158+
// Check that swapBehavior is still NoSwap
159+
if config.MemorySwap != nil && config.MemorySwap.SwapBehavior != "NoSwap" {
160+
return false, fmt.Errorf("worker node %s: swapBehavior changed from NoSwap to %s", workerNodes[0].Name, config.MemorySwap.SwapBehavior)
161+
}
162+
163+
// Continue polling to ensure config stays unchanged for the full duration
164+
return false, nil
165+
})
166+
167+
framework.Logf("Worker node %s config after poll: failSwapOn=%v, swapBehavior=%s", workerNodes[0].Name, *config.FailSwapOn, config.MemorySwap.SwapBehavior)
168+
// We expect the poll to timeout (not find a change), which means settings remained unchanged
169+
if err != nil && err == wait.ErrWaitTimeout {
170+
framework.Logf("Test PASSED: Worker node swap settings remained unchange")
171+
} else if err != nil {
172+
o.Expect(err).NotTo(o.HaveOccurred(), "Test FAILED: Unexpected error while polling worker node config")
173+
}
174+
}
175+
})
176+
})

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)