Skip to content

Commit 033cec1

Browse files
committed
Migrate OCP-56266: verify kubelet/crio deletes netns when pod deleted
1 parent 76ed5a8 commit 033cec1

3 files changed

Lines changed: 169 additions & 0 deletions

File tree

test/extended/node/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This directory contains OpenShift end-to-end tests for node-related features.
99
- **kubeletconfig_features.go** - Tests applying KubeletConfig to custom machine config pools, requires node reboots
1010
- **kubelet_secret_pulled_images.go** - Tests kubelet credential verification for image pulls (`KubeletEnsureSecretPulledImages` feature gate). Covers multi-tenancy isolation, credential rotation, ImagePullPolicy behavior, credential verification policy (NeverVerify/AlwaysVerify), and registry availability scenarios. Requires `TechPreviewNoUpgrade` or `CustomNoUpgrade` FeatureSet.
1111
- **node_e2e/image_registry_config.go** - Container registry config change (OCP-44820) - Verifies search registry update triggers MCO rollout and lands on nodes [Disruptive]
12+
- **node_e2e/netns_cleanup.go** - Network namespace cleanup (OCP-56266) - Verifies kubelet/CRI-O properly deletes network namespace when a pod is deleted [OTP]
1213

1314
### Suite: openshift/usernamespace
1415

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"strings"
6+
"time"
7+
8+
g "github.com/onsi/ginkgo/v2"
9+
o "github.com/onsi/gomega"
10+
11+
corev1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/util/wait"
14+
e2e "k8s.io/kubernetes/test/e2e/framework"
15+
"k8s.io/utils/ptr"
16+
17+
nodeutils "github.com/openshift/origin/test/extended/node"
18+
exutil "github.com/openshift/origin/test/extended/util"
19+
)
20+
21+
var _ = g.Describe("[sig-node] [Jira:Node/Kubelet] Network namespace cleanup", func() {
22+
var (
23+
oc = exutil.NewCLIWithoutNamespace("netns-cleanup")
24+
)
25+
26+
//author: minmli@redhat.com
27+
g.It("[OTP] kubelet/crio will delete netns when a pod is deleted [OCP-56266]", func() {
28+
ctx := context.Background()
29+
oc.SetupProject()
30+
namespace := oc.Namespace()
31+
podName := "pod-56266"
32+
33+
g.By("Create a test pod")
34+
pod := &corev1.Pod{
35+
ObjectMeta: metav1.ObjectMeta{
36+
Name: podName,
37+
Namespace: namespace,
38+
Labels: map[string]string{
39+
"name": "hello-openshift",
40+
},
41+
},
42+
Spec: corev1.PodSpec{
43+
SecurityContext: &corev1.PodSecurityContext{
44+
RunAsNonRoot: ptr.To(true),
45+
SeccompProfile: &corev1.SeccompProfile{
46+
Type: corev1.SeccompProfileTypeRuntimeDefault,
47+
},
48+
},
49+
Containers: []corev1.Container{
50+
{
51+
Name: "hello-openshift",
52+
Image: "quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83",
53+
Command: []string{"sleep", "infinity"},
54+
SecurityContext: &corev1.SecurityContext{
55+
AllowPrivilegeEscalation: ptr.To(false),
56+
Capabilities: &corev1.Capabilities{
57+
Drop: []corev1.Capability{"ALL"},
58+
},
59+
},
60+
},
61+
},
62+
},
63+
}
64+
_, err := oc.KubeClient().CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{})
65+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to create pod")
66+
67+
g.By("Wait for pod to be ready")
68+
err = wait.PollUntilContextTimeout(ctx, 3*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
69+
pod, pollErr := oc.KubeClient().CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
70+
if pollErr != nil {
71+
e2e.Logf("Error getting pod: %v", pollErr)
72+
return false, nil
73+
}
74+
for _, cond := range pod.Status.Conditions {
75+
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
76+
e2e.Logf("Pod is ready")
77+
return true, nil
78+
}
79+
}
80+
e2e.Logf("Waiting for pod to be ready")
81+
return false, nil
82+
})
83+
o.Expect(err).NotTo(o.HaveOccurred(), "pod did not become ready")
84+
85+
g.By("Get pod's node name")
86+
podObj, err := oc.KubeClient().CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
87+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get pod")
88+
nodeName := podObj.Spec.NodeName
89+
o.Expect(nodeName).NotTo(o.BeEmpty(), "pod node name is empty")
90+
e2e.Logf("Pod is running on node: %s", nodeName)
91+
92+
g.By("Get pod's network namespace path from CRI-O journal logs")
93+
netNsPath, err := nodeutils.GetPodNetNs(oc, nodeName, podName)
94+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get pod NetNS")
95+
e2e.Logf("Pod NetNS path: %s", netNsPath)
96+
97+
g.By("Delete the pod")
98+
err = oc.KubeClient().CoreV1().Pods(namespace).Delete(ctx, podName, metav1.DeleteOptions{})
99+
o.Expect(err).NotTo(o.HaveOccurred(), "failed to delete pod")
100+
101+
g.By("Wait for pod to be fully deleted")
102+
err = wait.PollUntilContextTimeout(ctx, 2*time.Second, 1*time.Minute, true, func(ctx context.Context) (bool, error) {
103+
_, pollErr := oc.KubeClient().CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
104+
if pollErr != nil && strings.Contains(pollErr.Error(), "not found") {
105+
e2e.Logf("Pod deleted successfully")
106+
return true, nil
107+
}
108+
e2e.Logf("Waiting for pod to be deleted")
109+
return false, nil
110+
})
111+
o.Expect(err).NotTo(o.HaveOccurred(), "pod was not deleted")
112+
113+
g.By("Verify that the NetNS file has been cleaned up on the node")
114+
err = nodeutils.CheckNetNsCleaned(oc, nodeName, netNsPath)
115+
o.Expect(err).NotTo(o.HaveOccurred(), "NetNS file was not cleaned up")
116+
})
117+
})

test/extended/node/node_utils.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"math/rand"
88
"os"
9+
"regexp"
910
"strings"
1011
"time"
1112

@@ -764,3 +765,53 @@ func GetFirstReadyWorkerNode(oc *exutil.CLI) string {
764765
o.Expect(false).To(o.BeTrue(), "no Ready worker node found among %v", workers)
765766
return "" // unreachable; satisfies compiler
766767
}
768+
769+
// GetPodNetNs retrieves the network namespace path for a pod from the CRI-O journal logs.
770+
// It searches the journal logs for the pod name and extracts the NetNS path.
771+
// Returns the NetNS path and an error if not found.
772+
func GetPodNetNs(oc *exutil.CLI, nodeName, podName string) (string, error) {
773+
cmd := fmt.Sprintf("journalctl -u crio --since=\"5 minutes ago\" | grep %s | grep NetNS", podName)
774+
netNsStr, err := ExecOnNodeWithChroot(oc, nodeName, "/bin/bash", "-c", cmd)
775+
if err != nil {
776+
framework.Logf("Failed to get NetNS from journal: %v", err)
777+
return "", fmt.Errorf("failed to get NetNS from journal: %w", err)
778+
}
779+
framework.Logf("NetNs journal output: %v", netNsStr)
780+
781+
// Extract NetNS path using regex
782+
re := regexp.MustCompile(`NetNS:[^\s]*`)
783+
found := re.FindAllString(netNsStr, -1)
784+
if len(found) == 0 {
785+
framework.Logf("NetNS not found in journal logs for pod %s", podName)
786+
return "", fmt.Errorf("NetNS not found in journal logs for pod %s", podName)
787+
}
788+
framework.Logf("Found NetNS: %v", found[0])
789+
790+
// Split "NetNS:/path/to/netns" to get the path
791+
parts := strings.Split(found[0], ":")
792+
if len(parts) < 2 {
793+
framework.Logf("Invalid NetNS format: %s", found[0])
794+
return "", fmt.Errorf("invalid NetNS format: %s", found[0])
795+
}
796+
netNsPath := parts[1]
797+
framework.Logf("NetNs path: %v", netNsPath)
798+
return netNsPath, nil
799+
}
800+
801+
// CheckNetNsCleaned verifies that the network namespace file has been cleaned up.
802+
// It checks if the NetNS path no longer exists on the node.
803+
// Returns nil if the file is cleaned, error if it still exists.
804+
func CheckNetNsCleaned(oc *exutil.CLI, nodeName, netNsPath string) error {
805+
result, err := ExecOnNodeWithChroot(oc, nodeName, "ls", "-l", netNsPath)
806+
if err != nil {
807+
framework.Logf("Error checking NetNS file: %v", err)
808+
}
809+
framework.Logf("NetNS check result: %v", result)
810+
811+
if strings.Contains(result, "No such file or directory") {
812+
framework.Logf("NetNS file cleaned successfully")
813+
return nil
814+
}
815+
framework.Logf("NetNS file still exists at %s", netNsPath)
816+
return fmt.Errorf("NetNS file still exists at %s", netNsPath)
817+
}

0 commit comments

Comments
 (0)