Fix device plugin admission failure after container restart

When a container restarts before kubelet restarts, containerMap has
multiple entries (old exited + new running). GetContainerID() may
return the exited container, causing the running check to fail. Fixed
by checking if ANY container for the pod/name is running.

Also filter terminal pods from podresources since they no longer
consume resources, and fix test error handling to avoid exiting
Eventually immediately on transient errors.

Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
This commit is contained in:
Sascha Grunert
2025-11-27 14:46:38 +01:00
committed by Xinyun Liu
parent a09ab89a6e
commit 9d8bbea593
3 changed files with 47 additions and 20 deletions

View File

@@ -64,6 +64,7 @@ func (p *v1PodResourcesServer) List(ctx context.Context, req *podresourcesv1.Lis
var pods []*v1.Pod
if p.useActivePods {
// GetActivePods already filters out terminal pods, so no need for additional filtering.
pods = p.podsProvider.GetActivePods()
} else {
pods = p.podsProvider.GetPods()

View File

@@ -1203,21 +1203,36 @@ func (m *ManagerImpl) ShouldResetExtendedResourceCapacity() bool {
}
func (m *ManagerImpl) isContainerAlreadyRunning(logger klog.Logger, podUID, cntName string) bool {
cntID, err := m.containerMap.GetContainerID(podUID, cntName)
if err != nil {
logger.Error(err, "Container not found in the initial map, assumed NOT running", "podUID", podUID, "containerName", cntName)
return false
}
// note that if container runtime is down when kubelet restarts, this set will be empty,
// so on kubelet restart containers will again fail admission, hitting https://github.com/kubernetes/kubernetes/issues/118559 again.
// Check if ANY container for this pod/container name is running.
// This handles the case where a container restarted before kubelet restart,
// so the containerMap might have multiple entries (old exited + new running).
// We need to check all of them to see if any are running.
//
// Note: if container runtime is down when kubelet restarts, containerRunningSet will be empty,
// so containers will fail admission, hitting https://github.com/kubernetes/kubernetes/issues/118559.
// This scenario should however be rare enough.
if !m.containerRunningSet.Has(cntID) {
logger.V(4).Info("Container not present in the initial running set", "podUID", podUID, "containerName", cntName, "containerID", cntID)
foundAnyContainer := false
foundRunningContainer := false
m.containerMap.Visit(func(visitPodUID, visitContainerName, visitContainerID string) {
if visitPodUID == podUID && visitContainerName == cntName {
foundAnyContainer = true
if m.containerRunningSet.Has(visitContainerID) {
foundRunningContainer = true
logger.V(4).Info("Container found in the initial running set", "podUID", podUID, "containerName", cntName, "containerID", visitContainerID)
}
}
})
if !foundAnyContainer {
logger.V(4).Info("Container not found in the initial map, assumed NOT running", "podUID", podUID, "containerName", cntName)
return false
}
if !foundRunningContainer {
logger.V(4).Info("Container found in map but not in running set", "podUID", podUID, "containerName", cntName)
return false
}
// Once we make it here we know we have a running container.
logger.V(4).Info("Container found in the initial set, assumed running", "podUID", podUID, "containerName", cntName, "containerID", cntID)
return true
}

View File

@@ -40,6 +40,7 @@ import (
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
kubeletdevicepluginv1beta1 "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
admissionapi "k8s.io/pod-security-admission/api"
"k8s.io/utils/ptr"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -940,7 +941,13 @@ func testDevicePluginNodeReboot(f *framework.Framework, pluginSockDir string) {
// exposing those devices as resource has restarted. The expected behavior is that the application pod fails at admission time.
framework.It("Does not keep device plugin assignments across node reboots if fails admission (no pod restart, no device plugin re-registration)", func(ctx context.Context) {
podRECMD := fmt.Sprintf("devs=$(ls /tmp/ | egrep '^Dev-[0-9]+$') && echo stub devices: $devs && sleep %s", sleepIntervalForever)
pod1 := e2epod.NewPodClient(f).CreateSync(ctx, makeBusyboxPod(SampleDeviceResourceName, podRECMD))
testPod := makeBusyboxPod(SampleDeviceResourceName, podRECMD)
// Set a short termination grace period to speed up the test.
// After kubelet restart, this pod will fail admission and need to be terminated.
// The default 30s grace period causes the test to timeout waiting for the pod
// to be filtered from the active pods list.
testPod.Spec.TerminationGracePeriodSeconds = ptr.To(int64(1))
pod1 := e2epod.NewPodClient(f).CreateSync(ctx, testPod)
deviceIDRE := "stub devices: (Dev-[0-9]+)"
devID1, err := parseLog(ctx, f, pod1.Name, pod1.Name, deviceIDRE)
framework.ExpectNoError(err, "getting logs for pod %q", pod1.Name)
@@ -984,19 +991,23 @@ func testDevicePluginNodeReboot(f *framework.Framework, pluginSockDir string) {
// never restarted (runs "forever" from this test timescale perspective) hence re-doing this check
// is useless.
ginkgo.By("Verifying the device assignment after kubelet restart using podresources API")
gomega.Eventually(ctx, func() error {
v1PodResources, err = getV1NodeDevices(ctx)
return err
}, 30*time.Second, framework.Poll).ShouldNot(gomega.HaveOccurred(), "cannot fetch the compute resource assignment after kubelet restart")
// if we got this far, podresources API will now report only the sample device plugin pod,
// because pods that failed admission are no longer reported in the list
// (see https://github.com/kubernetes/kubernetes/pull/132028).
// Hence, we verify that our test pod is NOT present in the podresources response.
_, found := checkPodResourcesAssignment(v1PodResources, pod1.Namespace, pod1.Name, pod1.Spec.Containers[0].Name, SampleDeviceResourceName, []string{})
framework.ExpectNoError(err, "unexpected device assignment mismatch for %s/%s", pod1.Namespace, pod1.Name)
gomega.Expect(found).To(gomega.BeFalseBecause("%s/%s/%s failed admission, so it must not appear in podresources list", pod1.Namespace, pod1.Name, pod1.Spec.Containers[0].Name))
// We need to poll because there's a delay between the pod failing admission and being
// removed from the podresources list.
gomega.Eventually(ctx, func(ctx context.Context) bool {
v1PodResources, err := getV1NodeDevices(ctx)
if err != nil {
framework.Logf("Failed to get node devices: %v, will retry", err)
return true // Return true to continue polling on error
}
_, found := checkPodResourcesAssignment(v1PodResources, pod1.Namespace, pod1.Name, pod1.Spec.Containers[0].Name, SampleDeviceResourceName, []string{})
return found
}, 30*time.Second, framework.Poll).Should(gomega.BeFalseBecause("%s/%s/%s failed admission, so it must not appear in podresources list", pod1.Namespace, pod1.Name, pod1.Spec.Containers[0].Name))
})
})
}