Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/extended/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This directory contains OpenShift end-to-end tests for node-related features.
- **kubeletconfig_features.go** - Tests applying KubeletConfig to custom machine config pools, requires node reboots
- **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.
- **node_e2e/image_registry_config.go** - Container registry config change (OCP-44820) - Verifies search registry update triggers MCO rollout and lands on nodes [Disruptive]
- **node_e2e/netns_cleanup.go** - Network namespace cleanup - Verifies kubelet/CRI-O properly deletes network namespace when a pod is deleted [OTP]

### Suite: openshift/usernamespace

Expand Down
130 changes: 130 additions & 0 deletions test/extended/node/node_e2e/netns_cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package node

import (
"context"
"time"

g "github.com/onsi/ginkgo/v2"
o "github.com/onsi/gomega"
ote "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
e2e "k8s.io/kubernetes/test/e2e/framework"
"k8s.io/utils/ptr"

nodeutils "github.com/openshift/origin/test/extended/node"
exutil "github.com/openshift/origin/test/extended/util"
)

var _ = g.Describe("[sig-node] [Jira:Node/Kubelet] Network namespace cleanup", func() {
var (
oc = exutil.NewCLIWithoutNamespace("netns-cleanup")
)

g.BeforeEach(func() {
isMicroShift, err := exutil.IsMicroShiftCluster(oc.AdminKubeClient())
o.Expect(err).NotTo(o.HaveOccurred())
if isMicroShift {
g.Skip("Skipping test on MicroShift cluster")
}
})

//author: bgudi@redhat.com
g.It("[OTP] kubelet/crio will delete netns when a pod is deleted [OCP-56266]", ote.Informing(), func() {
ctx := context.Background()
oc.SetupProject()
namespace := oc.Namespace()
podName := "pod-56266"

g.By("Create a test pod")
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: namespace,
Labels: map[string]string{
"name": "hello-openshift",
},
},
Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: ptr.To(true),
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
},
Containers: []corev1.Container{
{
Name: "hello-openshift",
Image: "image-registry.openshift-image-registry.svc:5000/openshift/tools:latest",
Command: []string{"sleep", "infinity"},
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: ptr.To(false),
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
},
},
},
},
}
_, err := oc.KubeClient().CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{})
o.Expect(err).NotTo(o.HaveOccurred(), "failed to create pod")

g.By("Wait for pod to be ready")
err = wait.PollUntilContextTimeout(ctx, 3*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
pod, pollErr := oc.KubeClient().CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if pollErr != nil {
e2e.Logf("Error getting pod: %v", pollErr)
return false, nil
}
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
e2e.Logf("Pod is ready")
return true, nil
}
}
e2e.Logf("Waiting for pod to be ready")
return false, nil
})
o.Expect(err).NotTo(o.HaveOccurred(), "pod did not become ready")

g.By("Get pod's node name")
podObj, err := oc.KubeClient().CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get pod")
nodeName := podObj.Spec.NodeName
o.Expect(nodeName).NotTo(o.BeEmpty(), "pod node name is empty")
e2e.Logf("Pod is running on node: %s", nodeName)

g.By("Get pod's network namespace path from CRI-O journal logs")
netNsPath, err := nodeutils.GetPodNetNs(oc, nodeName, podName)
o.Expect(err).NotTo(o.HaveOccurred(), "failed to get pod NetNS")
e2e.Logf("Pod NetNS path: %s", netNsPath)

g.By("Delete the pod")
err = oc.KubeClient().CoreV1().Pods(namespace).Delete(ctx, podName, metav1.DeleteOptions{})
o.Expect(err).NotTo(o.HaveOccurred(), "failed to delete pod")

g.By("Wait for pod to be fully deleted")
err = wait.PollUntilContextTimeout(ctx, 2*time.Second, 1*time.Minute, true, func(ctx context.Context) (bool, error) {
_, pollErr := oc.KubeClient().CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if apierrors.IsNotFound(pollErr) {
e2e.Logf("Pod deleted successfully")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return true, nil
}
if pollErr != nil {
e2e.Logf("Error checking pod deletion: %v", pollErr)
return false, nil
}
e2e.Logf("Waiting for pod to be deleted")
return false, nil
})
o.Expect(err).NotTo(o.HaveOccurred(), "pod was not deleted")

g.By("Verify that the NetNS file has been cleaned up on the node")
err = nodeutils.CheckNetNsCleaned(oc, nodeName, netNsPath)
o.Expect(err).NotTo(o.HaveOccurred(), "NetNS file was not cleaned up")
})
})
48 changes: 48 additions & 0 deletions test/extended/node/node_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"math/rand"
"os"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -764,3 +765,50 @@ func GetFirstReadyWorkerNode(oc *exutil.CLI) string {
o.Expect(false).To(o.BeTrue(), "no Ready worker node found among %v", workers)
return "" // unreachable; satisfies compiler
}

// GetPodNetNs retrieves the network namespace path for a pod from the CRI-O journal logs.
// It searches the journal logs for the pod name and extracts the NetNS path.
// Returns the NetNS path and an error if not found.
func GetPodNetNs(oc *exutil.CLI, nodeName, podName string) (string, error) {
// Use journalctl with proper escaping to avoid command injection
netNsStr, err := ExecOnNodeWithChroot(oc, nodeName, "journalctl", "-u", "crio", "--since=5 minutes ago", "--grep=NetNS", "--grep="+podName)
if err != nil {
framework.Logf("Failed to get NetNS from journal: %v", err)
return "", fmt.Errorf("failed to get NetNS from journal: %w", err)
}

// Extract NetNS path using regex
re := regexp.MustCompile(`NetNS:[^\s]*`)
found := re.FindAllString(netNsStr, -1)
if len(found) == 0 {
framework.Logf("NetNS not found in journal logs for pod %s", podName)
return "", fmt.Errorf("NetNS not found in journal logs for pod %s", podName)
}
framework.Logf("Found NetNS entry: %v", found[0])

// Split "NetNS:/path/to/netns" to get the path
parts := strings.Split(found[0], ":")
if len(parts) < 2 {
framework.Logf("Invalid NetNS format: %s", found[0])
return "", fmt.Errorf("invalid NetNS format: %s", found[0])
}
netNsPath := parts[1]
framework.Logf("Extracted NetNS path: %v", netNsPath)
return netNsPath, nil
}

// CheckNetNsCleaned verifies that the network namespace file has been cleaned up.
// It checks if the NetNS path no longer exists on the node.
// Returns nil if the file is cleaned, error if it still exists.
func CheckNetNsCleaned(oc *exutil.CLI, nodeName, netNsPath string) error {
// Use test command which returns proper exit code
_, err := ExecOnNodeWithChroot(oc, nodeName, "test", "-e", netNsPath)
if err != nil {
// Non-nil err: file absent (test exit 1) OR exec/debug failure.
framework.Logf("NetNS file considered cleaned (test -e returned error: %v)", err)
return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// No error means file still exists
framework.Logf("NetNS file still exists at %s", netNsPath)
return fmt.Errorf("NetNS file still exists at %s", netNsPath)
}