Skip to content

Commit 38d1bd0

Browse files
committed
feat: support DRA admin access in the device allocator
1 parent 883822a commit 38d1bd0

7 files changed

Lines changed: 367 additions & 18 deletions

File tree

pkg/controllers/dynamicresources/deviceallocation/controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ func (c *Controller) reconcileClaim(ctx context.Context, nn types.NamespacedName
156156
contributions := make(map[cloudprovider.DeviceID]DeviceContribution, len(claim.Status.Allocation.Devices.Results))
157157
for i := range claim.Status.Allocation.Devices.Results {
158158
result := &claim.Status.Allocation.Devices.Results[i]
159+
// Admin-access allocations bind for privileged monitoring only; they don't consume
160+
// the device and must not mark it allocated, so other claims (and Karpenter's
161+
// scheduling simulation) still treat it as available (KEP-5018).
162+
if lo.FromPtr(result.AdminAccess) {
163+
continue
164+
}
159165
deviceID := cloudprovider.DeviceID{
160166
Driver: unique.Make(result.Driver),
161167
Pool: unique.Make(result.Pool),

pkg/controllers/dynamicresources/deviceallocation/suite_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
. "github.com/onsi/ginkgo/v2"
2727
. "github.com/onsi/gomega"
28+
corev1 "k8s.io/api/core/v1"
2829
resourcev1 "k8s.io/api/resource/v1"
2930
"k8s.io/apimachinery/pkg/api/resource"
3031
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -138,6 +139,15 @@ func deviceResult(device string) resourcev1.DeviceRequestAllocationResult {
138139
}
139140
}
140141

142+
// deviceResultAdmin builds an allocation result marked adminAccess, which binds for monitoring only
143+
// and must not be tracked as consuming the device (KEP-5018).
144+
func deviceResultAdmin(device string) resourcev1.DeviceRequestAllocationResult {
145+
r := deviceResult(device)
146+
adminAccess := true
147+
r.AdminAccess = &adminAccess
148+
return r
149+
}
150+
141151
// deviceID constructs a cloudprovider.DeviceID using interned handles, matching the controller's representation.
142152
func deviceID(device string) cloudprovider.DeviceID {
143153
return cloudprovider.DeviceID{
@@ -263,6 +273,41 @@ var _ = Describe("DeviceAllocation Controller", func() {
263273
})
264274
})
265275

276+
Describe("Admin access (KEP-5018)", func() {
277+
BeforeEach(func() {
278+
// The apiserver rejects adminAccess allocations unless the namespace carries the label.
279+
ns := &corev1.Namespace{}
280+
Expect(env.Client.Get(ctx, client.ObjectKey{Name: "default"}, ns)).To(Succeed())
281+
if ns.Labels == nil {
282+
ns.Labels = map[string]string{}
283+
}
284+
ns.Labels["resource.kubernetes.io/admin-access"] = "true"
285+
Expect(env.Client.Update(ctx, ns)).To(Succeed())
286+
triggerHydration()
287+
})
288+
It("does not track a device allocated only for admin access", func() {
289+
claim := resourceClaim("admin-claim", deviceResultAdmin("device-0"))
290+
ExpectApplied(ctx, env.Client, claim)
291+
ExpectReconcileSucceeded(ctx, controller, client.ObjectKeyFromObject(claim))
292+
293+
seq, err := controller.AllocatedDevices(ctx)
294+
Expect(err).ToNot(HaveOccurred())
295+
Expect(collectDevices(seq)).To(BeEmpty())
296+
})
297+
It("tracks non-admin devices while ignoring admin-access results in the same claim", func() {
298+
claim := resourceClaim("mixed-claim",
299+
deviceResult("device-0"),
300+
deviceResultAdmin("device-1"),
301+
)
302+
ExpectApplied(ctx, env.Client, claim)
303+
ExpectReconcileSucceeded(ctx, controller, client.ObjectKeyFromObject(claim))
304+
305+
seq, err := controller.AllocatedDevices(ctx)
306+
Expect(err).ToNot(HaveOccurred())
307+
Expect(collectDevices(seq)).To(Equal(expectedDevices(deviceID("device-0"))))
308+
})
309+
})
310+
266311
Describe("Basic allocation", func() {
267312
BeforeEach(func() {
268313
triggerHydration()

pkg/controllers/provisioning/dra_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,66 @@ var _ = Describe("Dynamic Resource Allocation", func() {
674674
})
675675
})
676676

677+
Context("Admin access against existing nodes (Y)", func() {
678+
// The apiserver rejects adminAccess claims unless the namespace carries the allowlist label.
679+
BeforeEach(func() {
680+
ns := &corev1.Namespace{}
681+
Expect(env.Client.Get(ctx, client.ObjectKey{Name: "default"}, ns)).To(Succeed())
682+
if ns.Labels == nil {
683+
ns.Labels = map[string]string{}
684+
}
685+
ns.Labels["resource.kubernetes.io/admin-access"] = "true"
686+
Expect(env.Client.Update(ctx, ns)).To(Succeed())
687+
})
688+
689+
// heldDeviceNode creates an initialized gpu-it node publishing one in-cluster device that is already held by a
690+
// live pod, so the device is tracked as allocated. Returns the node.
691+
heldDeviceNode := func() *corev1.Node {
692+
GinkgoHelper()
693+
node := existingNode("gpu-it", true, corev1.ResourceList{
694+
corev1.ResourceCPU: resource.MustParse("4"), corev1.ResourceMemory: resource.MustParse("4Gi"), corev1.ResourcePods: resource.MustParse("10"),
695+
})
696+
ExpectApplied(ctx, env.Client, nodeLocalSlice(node, gpuDriver, "incluster-gpu-0"))
697+
livePod := test.Pod(test.PodOptions{ObjectMeta: metav1.ObjectMeta{Name: "live-pod"}})
698+
ExpectApplied(ctx, env.Client, livePod)
699+
ExpectApplied(ctx, env.Client, allocatedClusterWideClaim("held-claim",
700+
test.NodeLocalPoolName(gpuDriver, node.Name), gpuDriver, "incluster-gpu-0", podConsumer(livePod)))
701+
return node
702+
}
703+
704+
It("should bind an admin-access claim to a held device on an existing node without launching a new node (Y1)", func() {
705+
cloudProvider.InstanceTypes = []*cloudprovider.InstanceType{gpuInstanceType("gpu-it", 1)}
706+
ExpectApplied(ctx, env.Client, nodePool, test.DeviceClassWithSelector("gpu", gpuDriver))
707+
node := heldDeviceNode()
708+
709+
ExpectApplied(ctx, env.Client, test.ResourceClaimForRequests("admin-claim", test.AdminExactDeviceRequest("req", "gpu", 1)))
710+
pod := draPod("gpu", "admin-claim")
711+
provisionDRA(pod)
712+
713+
// Admin access binds to the already-held in-cluster device, so the pod fits the existing node and no new
714+
// NodeClaim is provisioned — the core ask of the issue.
715+
scheduled := ExpectScheduled(ctx, env.Client, pod)
716+
Expect(scheduled.Name).To(Equal(node.Name))
717+
Expect(ExpectNodeClaims(ctx, env.Client)).To(HaveLen(0))
718+
})
719+
720+
It("should launch a new node for a normal claim when the existing node's only device is held (Y2)", func() {
721+
// Same setup as Y1 but with a normal (non-admin) claim: the held device is unavailable, so Karpenter must
722+
// provision a new node. This is the contrast that proves admin access changed the outcome.
723+
cloudProvider.InstanceTypes = []*cloudprovider.InstanceType{gpuInstanceType("gpu-it", 1)}
724+
ExpectApplied(ctx, env.Client, nodePool, test.DeviceClassWithSelector("gpu", gpuDriver))
725+
node := heldDeviceNode()
726+
727+
ExpectApplied(ctx, env.Client, test.ResourceClaimForRequests("normal-claim", test.ExactDeviceRequest("req", "gpu", 1)))
728+
pod := draPod("gpu", "normal-claim")
729+
provisionDRA(pod)
730+
731+
scheduled := ExpectScheduled(ctx, env.Client, pod)
732+
Expect(scheduled.Name).ToNot(Equal(node.Name))
733+
Expect(ExpectNodeClaims(ctx, env.Client)).To(HaveLen(1))
734+
})
735+
})
736+
677737
Context("Topology propagation from node-local devices (D)", func() {
678738
It("should tighten a new NodeClaim to the zone of a zoned in-cluster device (D1)", func() {
679739
cloudProvider.InstanceTypes = []*cloudprovider.InstanceType{gpuInstanceType("gpu-it", 1)}

pkg/scheduling/dynamicresources/allocator.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ type deviceAllocationMetadata struct {
560560
deviceWithID DeviceWithID
561561
consumedCapacity map[resourcev1.QualifiedName]resource.Quantity
562562
requestName RequestName
563+
// adminAccess marks an allocation that binds for privileged monitoring without
564+
// consuming the device (KEP-5018). Excluded from the committed device set.
565+
adminAccess bool
563566
}
564567

565568
// allocate runs a per-instance-type DFS over in-cluster and template devices.
@@ -633,10 +636,14 @@ func (a *allocator) allocate(instanceTypes []InstanceTypeID) (*AllocationResult,
633636
a.allocatingCapacity = nil
634637
a.templateAllocatingCapacity = nil
635638

636-
deviceIDsByIT[itID] = make([]DeviceID, len(a.allocatedDevicesMetadata))
639+
deviceIDsByIT[itID] = make([]DeviceID, 0, len(a.allocatedDevicesMetadata))
637640
itReqs := scheduling.NewRequirements()
638-
for di, da := range a.allocatedDevicesMetadata {
639-
deviceIDsByIT[itID][di] = da.deviceWithID.ID
641+
for _, da := range a.allocatedDevicesMetadata {
642+
// Admin-access devices are part of the solution but don't consume the device,
643+
// so they're excluded from the set the tracker commits as allocated (KEP-5018).
644+
if !da.adminAccess {
645+
deviceIDsByIT[itID] = append(deviceIDsByIT[itID], da.deviceWithID.ID)
646+
}
640647
meta := claimAllocMeta[da.claimIndex]
641648
// Update the contributed requirements for the device, each devices contributed requirements are intersected to
642649
// find the contributed requirements for the instance type.
@@ -853,15 +860,22 @@ func (a *allocator) tryDevice(
853860
deviceID := dw.ID
854861

855862
// 1. Availability check — multi-alloc devices use capacity as the gatekeeper;
856-
// exclusive devices use binary allocation tracking.
863+
// exclusive devices use binary allocation tracking. Admin-access requests bypass
864+
// both: they may bind to already-allocated devices and don't consume capacity
865+
// (KEP-5018). The in-DFS dedupe still applies so a request gets distinct devices.
857866
var consumed map[resourcev1.QualifiedName]resource.Quantity
858-
if dw.AllowMultipleAllocations {
867+
switch {
868+
case rd.AdminAccess:
869+
if a.allocatedDevices.Has(deviceID) {
870+
return false
871+
}
872+
case dw.AllowMultipleAllocations:
859873
var ok bool
860874
consumed, ok = a.checkCapacity(dw.Device, deviceID, rd)
861875
if !ok {
862876
return false
863877
}
864-
} else {
878+
default:
865879
if a.allocationTracker.IsAllocated(deviceID, a.nodeClaim, a.itID) {
866880
return false
867881
}
@@ -870,8 +884,9 @@ func (a *allocator) tryDevice(
870884
}
871885
}
872886

873-
// 2. Counter verification — check shared counter budgets.
874-
if len(dw.ConsumesCounters) > 0 {
887+
// 2. Counter verification — check shared counter budgets. Admin-access requests
888+
// ignore resource allocations, so counters are neither checked nor consumed.
889+
if !rd.AdminAccess && len(dw.ConsumesCounters) > 0 {
875890
poolKey := PoolKey{Driver: deviceID.Driver, Pool: deviceID.Pool}
876891
var remainingCounterSets map[string]map[string]resourcev1.Counter
877892
if deviceID.Template {
@@ -943,17 +958,22 @@ func (a *allocator) tryDevice(
943958
deviceWithID: dw,
944959
consumedCapacity: consumed,
945960
requestName: rd.Name,
961+
adminAccess: rd.AdminAccess,
946962
})
947-
if dw.AllowMultipleAllocations {
948-
// Ensures a multi-allocatable device has a allocating capacity map, even if it has no capacity dimensions.
949-
// This is needed so that Commit() can identify multi-alloc devices via capacityConsumptionByIT presence.
950-
allocatingCapacityMap := lo.Ternary(deviceID.Template, a.templateAllocatingCapacity, a.allocatingCapacity)
951-
if allocatingCapacityMap[deviceID] == nil {
952-
allocatingCapacityMap[deviceID] = make(map[resourcev1.QualifiedName]resource.Quantity)
963+
// Admin-access allocations don't consume the device, so skip all capacity/counter
964+
// bookkeeping (KEP-5018).
965+
if !rd.AdminAccess {
966+
if dw.AllowMultipleAllocations {
967+
// Ensures a multi-allocatable device has a allocating capacity map, even if it has no capacity dimensions.
968+
// This is needed so that Commit() can identify multi-alloc devices via capacityConsumptionByIT presence.
969+
allocatingCapacityMap := lo.Ternary(deviceID.Template, a.templateAllocatingCapacity, a.allocatingCapacity)
970+
if allocatingCapacityMap[deviceID] == nil {
971+
allocatingCapacityMap[deviceID] = make(map[resourcev1.QualifiedName]resource.Quantity)
972+
}
953973
}
974+
a.deductAllocatingCapacity(consumed, deviceID, deviceID.Template)
975+
a.deductAllocatingCounters(dw.Device, PoolKey{Driver: deviceID.Driver, Pool: deviceID.Pool}, deviceID.Template)
954976
}
955-
a.deductAllocatingCapacity(consumed, deviceID, deviceID.Template)
956-
a.deductAllocatingCounters(dw.Device, PoolKey{Driver: deviceID.Driver, Pool: deviceID.Pool}, deviceID.Template)
957977

958978
// Recurse.
959979
if a.dfs(claimIdx, reqIdx, subReqIdx, slotIdx+1) {
@@ -962,8 +982,10 @@ func (a *allocator) tryDevice(
962982

963983
// Backtrack — undo in reverse order of application: capacity, counters, allocation, then
964984
// requirements/pools, then constraints.
965-
a.restoreAllocatingCapacity(consumed, deviceID, deviceID.Template)
966-
a.restoreAllocatingCounters(dw.Device, PoolKey{Driver: deviceID.Driver, Pool: deviceID.Pool}, deviceID.Template)
985+
if !rd.AdminAccess {
986+
a.restoreAllocatingCapacity(consumed, deviceID, deviceID.Template)
987+
a.restoreAllocatingCounters(dw.Device, PoolKey{Driver: deviceID.Driver, Pool: deviceID.Pool}, deviceID.Template)
988+
}
967989
a.allocatedDevicesMetadata = a.allocatedDevicesMetadata[:len(a.allocatedDevicesMetadata)-1]
968990
a.allocatedDevices.Delete(deviceID)
969991

0 commit comments

Comments
 (0)