Skip to content

Commit 3e0ee2d

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

3 files changed

Lines changed: 170 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 - Verifies kubelet/CRI-O properly deletes network namespace when a pod is deleted [OTP]
1213

1314
### Suite: openshift/usernamespace
1415

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
g "github.com/onsi/ginkgo/v2"
8+
o "github.com/onsi/gomega"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
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: bgudi@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: "image-registry.openshift-image-registry.svc:5000/openshift/tools:latest",
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 apierrors.IsNotFound(pollErr) {
105+
e2e.Logf("Pod deleted successfully")
106+
return true, nil
107+
}
108+
if pollErr != nil {
109+
e2e.Logf("Error checking pod deletion: %v", pollErr)
110+
return false, nil
111+
}
112+
e2e.Logf("Waiting for pod to be deleted")
113+
return false, nil
114+
})
115+
o.Expect(err).NotTo(o.HaveOccurred(), "pod was not deleted")
116+
117+
g.By("Verify that the NetNS file has been cleaned up on the node")
118+
err = nodeutils.CheckNetNsCleaned(oc, nodeName, netNsPath)
119+
o.Expect(err).NotTo(o.HaveOccurred(), "NetNS file was not cleaned up")
120+
})
121+
})

test/extended/node/node_utils.go

Lines changed: 48 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,50 @@ 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+
// Use journalctl with proper escaping to avoid command injection
774+
netNsStr, err := ExecOnNodeWithChroot(oc, nodeName, "journalctl", "-u", "crio", "--since=5 minutes ago", "--grep=NetNS", "--grep="+podName)
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+
780+
// Extract NetNS path using regex
781+
re := regexp.MustCompile(`NetNS:[^\s]*`)
782+
found := re.FindAllString(netNsStr, -1)
783+
if len(found) == 0 {
784+
framework.Logf("NetNS not found in journal logs for pod %s", podName)
785+
return "", fmt.Errorf("NetNS not found in journal logs for pod %s", podName)
786+
}
787+
framework.Logf("Found NetNS entry: %v", found[0])
788+
789+
// Split "NetNS:/path/to/netns" to get the path
790+
parts := strings.Split(found[0], ":")
791+
if len(parts) < 2 {
792+
framework.Logf("Invalid NetNS format: %s", found[0])
793+
return "", fmt.Errorf("invalid NetNS format: %s", found[0])
794+
}
795+
netNsPath := parts[1]
796+
framework.Logf("Extracted NetNS path: %v", netNsPath)
797+
return netNsPath, nil
798+
}
799+
800+
// CheckNetNsCleaned verifies that the network namespace file has been cleaned up.
801+
// It checks if the NetNS path no longer exists on the node.
802+
// Returns nil if the file is cleaned, error if it still exists.
803+
func CheckNetNsCleaned(oc *exutil.CLI, nodeName, netNsPath string) error {
804+
// Use test command which returns proper exit code
805+
_, err := ExecOnNodeWithChroot(oc, nodeName, "test", "-e", netNsPath)
806+
if err != nil {
807+
// Non-nil err: file absent (test exit 1) OR exec/debug failure.
808+
framework.Logf("NetNS file considered cleaned (test -e returned error: %v)", err)
809+
return nil
810+
}
811+
// No error means file still exists
812+
framework.Logf("NetNS file still exists at %s", netNsPath)
813+
return fmt.Errorf("NetNS file still exists at %s", netNsPath)
814+
}

0 commit comments

Comments
 (0)