node: fix graph populator fast-path to check ExtendedResourceClaimStatus

Pods using DRAExtendedResource (e.g. nvidia.com/gpu) have no
Spec.ResourceClaims, so ResourceClaimStatuses stays nil. The updatePod
fast-path skipped AddPod when only ExtendedResourceClaimStatus changed,
leaving the synthesized claim→pod→node edge out of the authorization graph.

Add PodExtendedStatusEqual to the fast-path guard, matching what the
scheduler event handler already does in events.go.
This commit is contained in:
Davanum Srinivas 2026-05-05 11:48:38 -04:00
parent 47f9904374
commit 2490cfa4f5
No known key found for this signature in database
GPG key ID: 80D83A796103BF59
2 changed files with 102 additions and 1 deletions

View file

@ -111,7 +111,8 @@ func (g *graphPopulator) updatePod(oldObj, obj interface{}) {
hasNewEphemeralContainers := len(pod.Spec.EphemeralContainers) > len(oldPod.Spec.EphemeralContainers)
if (pod.Spec.NodeName == oldPod.Spec.NodeName) && (pod.UID == oldPod.UID) &&
!hasNewEphemeralContainers &&
resourceclaim.PodStatusEqual(oldPod.Status.ResourceClaimStatuses, pod.Status.ResourceClaimStatuses) {
resourceclaim.PodStatusEqual(oldPod.Status.ResourceClaimStatuses, pod.Status.ResourceClaimStatuses) &&
resourceclaim.PodExtendedStatusEqual(oldPod.Status.ExtendedResourceClaimStatus, pod.Status.ExtendedResourceClaimStatus) {
// Node and uid are unchanged, all object references in the pod spec are immutable respectively unmodified (claim statuses).
klog.V(5).Infof("updatePod %s/%s, node unchanged", pod.Namespace, pod.Name)
return

View file

@ -1058,6 +1058,106 @@ func TestNodeAuthorizerAddEphemeralContainers(t *testing.T) {
}
}
// TestNodeAuthorizerUpdateExtendedResourceClaim checks that a node gains
// authorization to read its pod's synthesized ResourceClaim once the scheduler
// writes ExtendedResourceClaimStatus into the pod — even when the pod carries
// no standard Spec.ResourceClaims entries.
//
// Background: the graph populator's updatePod has a fast-path that skips
// AddPod when the pod's node assignment, UID, ephemeral containers, and
// ResourceClaimStatuses are all unchanged. For pods that use the
// DRAExtendedResource path (e.g. a plain nvidia.com/gpu request),
// ResourceClaimStatuses is nil both before and after the scheduler writes
// the synthesized claim name, so the fast-path would fire prematurely and
// the claim→pod→node edge would never be added to the authorization graph.
func TestNodeAuthorizerUpdateExtendedResourceClaim(t *testing.T) {
g := NewGraph()
identifier := nodeidentifier.NewDefaultNodeIdentifier()
authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
node1 := &user.DefaultInfo{Name: "system:node:node1", Groups: []string{"system:nodes"}}
// The pod has been bound to node1, but the scheduler has not yet written
// ExtendedResourceClaimStatus. There are no standard Spec.ResourceClaims,
// so ResourceClaimStatuses stays nil throughout.
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "ns1",
UID: "pod1uid",
},
Spec: corev1.PodSpec{
NodeName: "node1",
},
}
p := &graphPopulator{}
p.graph = g
p.addPod(pod)
// Before the scheduler writes ExtendedResourceClaimStatus, the synthesized
// claim is not in the graph and node1 should have no opinion on it.
decision, _, err := authz.Authorize(context.Background(), authorizer.AttributesRecord{
User: node1,
ResourceRequest: true,
Verb: "get",
Resource: "resourceclaims",
APIGroup: "resource.k8s.io",
Namespace: "ns1",
Name: "extended-claim-1",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if decision != authorizer.DecisionNoOpinion {
t.Errorf("before ExtendedResourceClaimStatus is populated: want NoOpinion, got %v", decision)
}
// The scheduler creates the ResourceClaim and records its name in the pod
// status. updatePod must recognize that ExtendedResourceClaimStatus changed
// and rebuild the graph edge rather than taking the fast-path early exit.
updatedPod := pod.DeepCopy()
updatedPod.Status.ExtendedResourceClaimStatus = &corev1.PodExtendedResourceClaimStatus{
ResourceClaimName: "extended-claim-1",
}
p.updatePod(pod, updatedPod)
// The node that hosts the pod should now be permitted to read the claim.
decision, _, err = authz.Authorize(context.Background(), authorizer.AttributesRecord{
User: node1,
ResourceRequest: true,
Verb: "get",
Resource: "resourceclaims",
APIGroup: "resource.k8s.io",
Namespace: "ns1",
Name: "extended-claim-1",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if decision != authorizer.DecisionAllow {
t.Errorf("after ExtendedResourceClaimStatus is populated: want Allow, got %v", decision)
}
// A node that does not host the pod must not gain access to the claim.
node2 := &user.DefaultInfo{Name: "system:node:node2", Groups: []string{"system:nodes"}}
decision, _, err = authz.Authorize(context.Background(), authorizer.AttributesRecord{
User: node2,
ResourceRequest: true,
Verb: "get",
Resource: "resourceclaims",
APIGroup: "resource.k8s.io",
Namespace: "ns1",
Name: "extended-claim-1",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if decision != authorizer.DecisionNoOpinion {
t.Errorf("node2 should not access a claim belonging to node1's pod: want NoOpinion, got %v", decision)
}
}
type sampleDataOpts struct {
nodes int
namespaces int