Skip to content

The page this post links six times has moved, so the alias rescues every path and none of the five fragments, the label its one operational sentence turns on is spelled one way in the blog and another in the reference that registers it, and a GA post names no gate and no KEP

Post — Kubernetes v1.34: DRA has graduated to GA, 2025-09-01.

7,535 bytes, 109 lines, 996 words of body. The byline is The DRA team: of 2025's twelve walk posts it is the only one naming no person at all, with no GitHub handle and no employer beside it. Six ## headings and no ###, no fenced block, no Hugo shortcode, and one line carrying trailing whitespace. Eighteen markdown links across eighteen distinct targets, of which eight are GitHub profiles in the acknowledgements and six are one documentation page carrying five different fragments. Eighth of the year's twelve by publication date, and eighth of the twelve by size.

As written

The headline is one sentence long and the post repeats it three ways: the core of DRA has graduated to General Availability. :19 calls it the headline feature of the release, :26-28 says DRA is now stable, will be part of Kubernetes for the long run, and will get new features without breaking changes, so driver authors "can start adopting DRA with confidence". :30-32 draws the operational consequence: from 1.34, DRA is enabled by default, and so are the DRA features that have reached beta, because the default API version for DRA is now the stable v1 rather than the v1beta1 and v1beta2 that needed an explicit opt-in.

Then two lists. Features promoted to beta (:34-55) gives three: admin access, which now restricts the adminAccess field to users who can create ResourceClaims and ResourceClaimTemplates in a namespace carrying a particular label (:39-45); prioritized list, where a request names acceptable alternatives in order and the scheduler satisfies the best one available (:47-51); and a kubelet API that reports DRA-allocated resources to node monitoring agents (:53-55). New alpha features (:57-85) gives four: extended resource mapping, which advertises DRA devices under the familiar extended-resource request syntax (:61-65); consumable capacity, which lets unrelated claims each take a share of one physical device (:67-71); binding conditions, which delay binding a Pod to a node until an external device is confirmed ready (:75-79); and resource health status, which surfaces device health in Pod status (:81-83).

The close is short. What's next? (:87-91) says the work does not stop, that several alpha and beta features are planned for GA in the next couple of releases, and to expect "an equally ambitious set of features in DRA for the 1.35 release". Getting involved points at the WG Device Management Slack channel and meeting document, and Acknowledgments thanks eight new contributors by name and handle. There is no Learn more section, no KEP link, no feature-gate name, and no YAML: the only Kubernetes object kinds written anywhere in the post are ResourceClaim and ResourceClaimTemplate, once each, in the admin-access paragraph.

As it runs now

The page the post links six times is not at the address the post gives. Every one of the post's documentation links begins /docs/concepts/scheduling-eviction/dynamic-resource-allocation, and at the pin that directory does not exist. DRA is now a section of its own under docs/concepts/resource-management/dynamic-resource-allocation/, five files where there was one: _index.md, dra-api.md, dra-features.md, how-dra-works.md and device-taints.md. The old address still resolves, because _index.md:7-8 carries a frontmatter aliases: entry naming it. Five pages under docs carry such an entry at the pin. Four of them register a short vanity path or a former filename — /rbac/, /cve/, /security/, master-node-communication. This is the only one that records a move from one concept section to another.

The alias rescues the path and none of the fragments. An alias redirects to a page, and the page it redirects to is the section index, which defines two anchors of its own — _index.md:27 and :52 — plus whatever its ## Limitations heading generates. The post's five fragments are all somewhere else. #admin-access is dra-api.md:389, #prioritized-list is dra-api.md:120, #extended-resource is dra-api.md:598, and #consumable-capacity is generated by the heading at dra-features.md:232. The fifth, #binding-conditions, is defined nowhere in the pinned tree: the feature is written up under ## Device binding conditions at how-dra-works.md:107, whose generated anchor is device-binding-conditions. So of the post's six documentation links, the one without a fragment lands where it should and the five with one land on a page that has none of them.

The post is not the only writer who never got the memo. Forty-eight links inside docs still spell the old path at the pin. Twenty-six are bare and the alias rescues all twenty-six. Twenty-two carry a fragment, and the alias rescues none of them, because it moves the reader to a page on which not one of those fragments is defined. The fragments are twelve distinct ones: #extended-resource six times, #device-taints-and-tolerations four, #resourceclaims-templates and #deviceclass twice each, and #admin-access, #device-health-monitoring, #enabling-admin-access, #list-type-attributes, #node-allocatable-resources, #partitionable-devices, #resource-pool-status and #workload-resource-claims once each. Eleven of the twelve are defined on some sibling page the reader is not being sent to. The twelfth, #enabling-admin-access, is defined nowhere in the tree at all.

Where the pin disagrees with itself: a page links to an anchor on itself that is on a different page. how-dra-works.md:274 writes [DRA consumable capacity](#consumable-capacity), a same-page fragment with no path in front of it. how-dra-works.md has eleven headings and none of them is consumable capacity; the heading that generates that anchor is dra-features.md:232, a sibling file. One sibling file gets the same link right three times by writing the whole path: dra-observability.md:188, :283 and :299 all spell it /docs/concepts/resource-management/dynamic-resource-allocation/dra-features/#consumable-capacity.

The label the post's one operational sentence turns on is spelled two ways, and the split runs exactly along the boundary between the blog and the documentation. The post writes resource.k8s.io/admin-access: "true" at :44. The concept page writes resource.kubernetes.io/admin-access: "true" at dra-api.md:417, adding "(case-sensitive)", and the reference that registers every label in Kubernetes writes the same thing at labels-annotations-taints/_index.md:2960 and :2964. Across the whole pinned content/en tree the first spelling occurs four times and every one of them is a blog post: this post, the v1.33 DRA update, and the v1.33 and v1.34 release announcements. The second occurs three times and every one of them is documentation. Nothing in the pin says which spelling a 1.34 cluster accepted; what the pin does say is that the registry of label names and the four posts that tell you to set one do not agree, and that no page in the tree records a rename.

The registry entry for that label is the one heading in the pinned tree that opens the brace and leaves out the hash. labels-annotations-taints/_index.md:2960 reads ### resource.kubernetes.io/admin-access {resource-kubernetes-io-admin-access}. The syntax for declaring a heading anchor is {#id}, and 103 headings on that same page write it correctly; searching every markdown file in content/en returns exactly one heading that opens the brace without the hash, and it is this one. So the anchor for the admin-access label is whatever the title generates rather than what the author wrote down. Four lines further on, at :2974, the entry's one cross-reference points at /docs/concepts/scheduling-eviction/dynamic-resource-allocation/#enabling-admin-access: the moved path, and a fragment defined nowhere in the tree.

A General Availability announcement that names no feature gate, no KEP and almost no object. Search the post for the things a reader would need in order to act on it and the counts are zero: no occurrence of KEP, of kep.k8s.io, of the phrase feature gate, of DynamicResourceAllocation, of ResourceSlice, of DeviceClass, of any of the six gate names that carry the features it describes. ResourceClaim and ResourceClaimTemplate appear once each, inside the admin-access sentence. Its own release announcement does all of it: kubernetes-v1-34-release/index.md:377-408 covers the same four alpha features and gives KEP 4680, KEP 5004 and KEP 5075 for three of them, and at :381 adds the two conditions this post omits — that ResourceHealthStatus must be enabled and that the driver must implement the DRAResourceHealth gRPC service.

The gate under the headline has a shape almost nothing else in the pin has. DynamicResourceAllocation.md records four rungs, and two of them are stable: stable with defaultValue: true and locked: false from 1.34 to 1.34, then stable with locked: true from 1.35 with no toVersion. Of the 487 gate files that carry a stage table, two have more than one stable rung, and the other is VolumeAttributesClass. The post's sentence at :30 — DRA is enabled by default — was a statement about a default for exactly one release. From 1.35 it is a statement about the binary: the gate is locked, and a cluster cannot turn DRA off.

The release the post told readers to expect an equally ambitious set of features from is the thinnest DRA release the pin records. Counting gate files whose name begins DRA by the release their first rung starts in: two begin at 1.32, three at 1.33, four at 1.34, one at 1.35, five at 1.36 and four at 1.37. The single 1.35 arrival is DRADeviceTaintRules, an extension of a 1.33 feature rather than a new one. The ambition the post forecast for the next release arrived, but a release later than forecast.

Of the four DRA gates that begin at 1.34, the post names three and has no sentence for the fourth. Extended resource mapping is DRAExtendedResource, consumable capacity is DRAConsumableCapacity, binding conditions is DRADeviceBindingConditions. The fourth is DRASchedulerFilterTimeout, which aborts the scheduler's per-node filter after ten seconds by default. Searching the whole of content/en for that name returns its own gate file and nothing else: no concept page, no task, no reference, no release announcement, no blog post. It is still alpha and off at the pin, three releases later. The fourth item the post does list under New alpha features, resource health status, is carried by ResourceHealthStatus, whose alpha rung starts at 1.31 — three releases before the release this post announces.

Three years on, the two halves of the post's forecast have come apart. Everything in Features promoted to beta is stable at the pin: DRAAdminAccess and DRAPrioritizedList from 1.36, KubeletPodResourcesDynamicResources from 1.36. Three of the four in New alpha features have moved too: DRAExtendedResource is stable from 1.37, and DRAConsumableCapacity and ResourceHealthStatus are both beta on by default from 1.36. The fourth still has to be asked for. DRADeviceBindingConditions reached beta at 1.36 with defaultValue: false, which is rare: eight of the 487 gate files with a stage table end on a beta rung that is off.

The API reference calls one of those fields beta while its own gate file calls it stable. device-class-v1.md:72 documents extendedResourceName and ends the entry "This is a beta field." DRAExtendedResource.md puts the stable rung at 1.37, which is the pin's own release, and dra-api.md:600 renders the feature-state banner from that same gate file. The same entry also states its uniqueness rule about the wrong name: it says the extended resource name should be unique among all the device classes in a cluster, then explains what happens "if two device classes have the same name" — which cannot happen, because a DeviceClass name is its key.

The one way to run DRA without a node-local driver arrived three releases after the post, and it is alpha and off. dra-features.md:354-408 documents skipNodeOperations on a ResourceSlice: a driver can declare that NodePrepareResources and NodeUnprepareResources may be skipped, so the kubelet never looks for a plugin socket, and control-plane-only resources need no node-local driver at all. Its gate, DRAOptionalNodeOperations, has one rung: alpha, defaultValue: false, from 1.37. On any cluster older than the pin — including the v1.35 this exercise runs on — a claim that has been allocated still has to be prepared by a driver that is listening on the node, and there is nothing in the API that lets you say otherwise. That is why Do cannot get a container running on a hand-published slice alone, and why step 6 has to install a driver rather than declare one unnecessary.

The one setup page a reader would land on renders half of its own instruction as a code block. set-up-dra-cluster.md:52-53 lists resource.k8s.io/v1beta1 and resource.k8s.io/v1beta2 indented four spaces, which markdown reads as an indented code block; :57 lists resource.k8s.io/v1alpha3 indented three spaces, which markdown reads as a list item. Two lists five lines apart, the same purpose, and only one of them is a list. The same page at :62-94 gives the verification command as kubectl get deviceclasses and the failure output as error: the server doesn't have a resource type "deviceclasses", which is still exactly what a cluster with the API group disabled prints.

Eight links in the tutorial this exercise installs its driver from are http, and every other example link in the tree is https. install-use-dra.md applies seven manifests and names one more, and each command spells the source http://k8s.io/examples/dra/driver-install/.... Across all of docs the http spelling appears eight times and all eight are in that one file; the https spelling appears 223 times everywhere else. The site redirects, so the commands work, but they work by way of a plaintext request for a manifest that is about to be applied with cluster-admin rights. Do applies the same manifests from the local checkout instead, which is also what keeps them at the pin.

The reference for the object this exercise publishes by hand contradicts itself in its own opening sentence. resource-slice-v1.md:241 introduces the Device struct with "Device represents one individual hardware instance that can be selected based on its attributes. Besides the name, exactly one field must be set." Thirteen fields follow, and two of them — attributes and capacity — each carry the note "The maximum number of attributes and capacities combined is 32", a limit that only means something if both can be set on one device. The phrasing is borrowed from three sibling structs where it is true: DeviceAttribute at :306, DeviceSelector at device-class-v1.md:148, and DeviceConstraint at resource-claim-v1.md:312, the last carefully qualified with "besides Requests". Device is not one of them, and step 2 below disproves the sentence by publishing a device that sets both.

The one field that would let this exercise finish is documented on the concept page and absent from the reference. skipNodeOperations appears seven times in dra-features.md: the prose at :372-373, a ResourceSlice sample at :402, the copy into the allocation result at :411 and :425, and the consequences at :438 and :455. It appears nowhere else in content/en. It is not in resource-slice-v1.md, whose ResourceSliceSpec table at :66 lists every other field of that spec, and it is not in resource-claim-v1.md, where AllocationResult at :176 is what the value is copied into. A reader working from the API reference alone cannot discover the field, and a reader working from the concept page cannot tell that the reference has not caught up.

What this exercise does not cover, and where it lives. A device that exists. There is no accelerator on any topology in this repo, so nothing here measures a device doing work. What the lab can do is run the whole protocol around one: Do publishes a ResourceSlice by hand to watch the scheduler allocate from it, reads the stall that follows because no driver is listening on the node, and then installs the pin's own example driver, which simulates nine GPUs and announces each allocation by setting environment variables where a real driver would inject a device file. Everything from step 6 on is therefore true about the protocol and false about the silicon.

The API this one replaced is the 2022 alpha announcement, which owns the v1alpha1 spelling, ResourceClass and parametersRef, the split into a second gate and that gate's removal, the claim that stays unallocated because no ResourceSlice exists, and the three sample manifests examples/dra/deviceclass.yaml, examples/dra/resourceclaim.yaml and examples/dra/dra-example-job.yaml. This exercise begins where that one ends: the claim here stays unallocated for a different reason, and then stops doing so. What is taken up here and not there is the examples/dra/driver-install/ subtree and the tutorial that applies it. Neither the post nor this file re-derives the structured-parameters design; how-dra-works.md is read there, not here.

Redirection as a mechanism belongs to the Endpoints deprecation post, which owns static/_redirects.base and the 449 permanent redirects in it. The mechanism here is a different one — an aliases: key in a page's own frontmatter, which the redirect table does not know about — and the two do not overlap: there is no DRA line in _redirects.base.

Device health, consumable capacity and device taints each got their own post and each is a read row in this year's census or the next; the gates are named above and the features are not explained. Beta rungs that are off by default are counted once, here, so a later row of this year can point at the number rather than recount it. CEL as a language, and what the scheduler does with a selector that matches nothing, is the scheduling-predicates lab's FailedScheduling ground, taken from there.

The diff, and why — four cases, and the one that decides the exercise is that every claim about the feature held while every address in the prose stopped working.

Still right. The GA promise is the part that survived intact. resource.k8s.io is still v1 three releases later, still serving the same four kinds — DeviceClass, ResourceClaim, ResourceClaimTemplate and ResourceSlice — with everything since added beside them, DeviceTaintRule at v1beta2 and ResourcePoolStatusRequest at v1alpha3. Of the twenty-one gate files that carry DRA, exactly one has removed: true, and its last rung is 1.31, three releases before this post. The post's advice at :26-28 — that driver authors can start adopting DRA with confidence — is the rare forward-looking sentence in this archive that the pin simply confirms. The sentence at :30 that DRA is enabled by default is stronger now than when it was written: at 1.34 it was a default, and from 1.35 the gate is locked, so it is not a choice at all.

Broke. Six links. The alias at _index.md:7-8 keeps the one link without a fragment working and cannot help the five with one, because an alias moves a reader to a page and the fragments now live on three other pages in the same directory. A reader following the post's admin-access link today lands on a section index that does not mention admin access.

Never absorbed. The label. Four blog posts write resource.k8s.io/admin-access and three documentation pages write resource.kubernetes.io/admin-access, and the documentation includes the reference that exists precisely to register label names. The post's one operational instruction — the only sentence in it that tells a reader to change something in a cluster — is the sentence whose central string the documentation tree does not agree with. Do settles which one a cluster takes, which is a thing the pin cannot tell you.

Overtaken by stasis. What's next? asked for an equally ambitious 1.35 and got one new DRA gate, the fewest of any release from 1.32 onward, and the features it had just announced as alpha took two more releases rather than "the next couple of releases" to finish: one of the four is stable at the pin, two are beta on by default, and one is beta and still off. Meanwhile DRASchedulerFilterTimeout, shipped in the same release as the post, has been alpha for three releases and has never been written about anywhere in the tree.

The ladder

DynamicResourceAllocation.md is a four-rung ladder, parsed from its frontmatter: alpha with defaultValue: false from 1.30 to 1.31, beta with defaultValue: false from 1.32 to 1.33 — a beta rung that was off, which is why the post can present "enabled by default" as news — then stable with defaultValue: true and an explicit locked: false from 1.34 to 1.34, then stable with locked: true from 1.35 and no toVersion. The body, three lines long, still describes the feature in terms of "structured parameters", the phrase the 2022 design was renamed around.

Seven more gate files carry the features the post's two lists describe, and the split between the lists is exactly the split between what the lab can run and what it cannot. On v1.35: the three beta features are on without being asked for — DRAAdminAccess beta true from 1.34 to 1.35, DRAPrioritizedList beta true from 1.34 to 1.35, KubeletPodResourcesDynamicResources beta true from 1.34 to 1.35. The four alpha features are off — DRAExtendedResource alpha false from 1.34 to 1.35, DRAConsumableCapacity alpha false from 1.34 to 1.35, DRADeviceBindingConditions alpha false from 1.34 to 1.35, ResourceHealthStatus alpha false from 1.31 to 1.35. Step 8 below exercises the first of the three that are on and step 9 the second; step 9 also shows what the API server does with a field belonging to one that is off.

The eighth file is the one the exercise cannot use, and it is the one that explains the shape of Do. DRAOptionalNodeOperations has a single rung, alpha defaultValue: false from 1.37, and the pin is v1.37, so the feature that would let a ResourceSlice waive the kubelet handshake is exactly as old as the pin and off in it. Without it a driver is not optional: a claim can be allocated by the scheduler and still never reach a container, which is what steps 2 to 5 show and step 6 repairs. The stall in step 5 is the finding, not a failure.

Topology

solo, one node at 10.10.10.180, Kubernetes v1.35, provisioned and baselined as usual. Steps 1 to 5 and step 9 create objects in a namespace called bw-dra and change nothing on the node; step 5 reads two directories over ssh without writing to either. Steps 6 to 8 install the pin's example DRA driver as a privileged DaemonSet in the namespace the pin's own manifests name, dra-tutorial, and that driver does write on the node: it opens a socket under /var/lib/kubelet/plugins_registry and writes CDI files under /var/run/cdi. Teardown removes it. The node pulls two images it has not seen before, registry.k8s.io/dra-example-driver/dra-example-driver:v0.2.0 and ubuntu:24.04, so run step 6 once with the node online and unhurried.

You also need a checkout of kubernetes/website at the pin on the machine you run kubectl from, because steps 6 and 7 apply manifests out of it rather than over the network, and step 10 reads it. Set W to its content/en directory once and the rest of the file uses it.

Do

  1. Ask the cluster what it serves and what it already has. The first command is the sentence :30-32 makes: which version of the group a client gets when it does not ask for one. The three listings should all be empty, which is the starting state the pin's tutorial also describes.
kubectl version -o json | grep -m2 gitVersion
kubectl create namespace bw-dra
mkdir -p /tmp/bw-dra
W=/path/to/kubernetes/website/content/en
kubectl get --raw /apis/resource.k8s.io | python3 -c \
  'import json,sys; d=json.load(sys.stdin); print("preferred:", d["preferredVersion"]["version"]); print("served:   ", " ".join(v["version"] for v in d["versions"]))'
kubectl get deviceclasses
kubectl get resourceslices
kubectl get resourceclaims -A
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
echo "node: $NODE"
  1. Be the driver. A ResourceSlice is the only thing a DRA driver has to publish for the scheduler to have something to allocate, and nothing stops an administrator publishing one by hand. Two devices, both with an attributes map and a capacity map, which is the combination resource-slice-v1.md:241 says cannot exist.
kubectl apply -f - <<YAML
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
  name: bw-dra-pool-0
spec:
  driver: blogwalk.example.com
  nodeName: $NODE
  pool:
    name: bw-dra-pool
    generation: 1
    resourceSliceCount: 1
  devices:
  - name: paper-0
    attributes:
      model:
        string: "quarto"
    capacity:
      memory:
        value: 64Gi
  - name: paper-1
    attributes:
      model:
        string: "octavo"
    capacity:
      memory:
        value: 8Gi
YAML
kubectl get resourceslices -o wide
kubectl get resourceslice bw-dra-pool-0 -o jsonpath='{.spec.devices[0]}{"\n"}'
  1. Create a DeviceClass over that driver and a ResourceClaim that selects on capacity, then read the claim's status while nothing is using it. The CEL expression is the pin's own shape from dra-api.md, with the driver name changed: capacities declared unqualified in the slice are addressed under the driver's domain in the selector.
kubectl apply -f - <<'YAML'
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: bw-paper
spec:
  selectors:
  - cel:
      expression: device.driver == "blogwalk.example.com"
---
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: one-sheet
  namespace: bw-dra
spec:
  devices:
    requests:
    - name: sheet
      exactly:
        deviceClassName: bw-paper
        selectors:
        - cel:
            expression: device.capacity["blogwalk.example.com"].memory.compareTo(quantity("10Gi")) >= 0
YAML
kubectl -n bw-dra get resourceclaim one-sheet -o jsonpath='status={.status}{"\n"}'
  1. Give the claim a consumer. This is the step where the scheduler does the work the post is announcing: it picks a device that satisfies the selector, writes the allocation into the claim's status, reserves the claim for the Pod, and binds the Pod to the node.
kubectl -n bw-dra apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
  name: claimer
spec:
  containers:
  - name: c
    image: registry.k8s.io/e2e-test-images/agnhost:2.53
    command: ["sleep", "3600"]
    resources:
      claims:
      - name: sheet
  resourceClaims:
  - name: sheet
    resourceClaimName: one-sheet
YAML
sleep 20
kubectl -n bw-dra get pod claimer -o jsonpath='node={.spec.nodeName} phase={.status.phase}{"\n"}'
kubectl -n bw-dra get resourceclaim one-sheet \
  -o jsonpath='device={.status.allocation.devices.results[*].device} reservedFor={.status.reservedFor[*].name}{"\n"}'
  1. Find out why the container is not running. The claim is allocated and the Pod is bound, and the kubelet is still waiting for a driver to prepare the device. The two directory listings are the missing half: kubelet-files.md:143-144 names plugins_registry as where the kubelet looks for plugin sockets, and there is nothing in it.
kubectl -n bw-dra describe pod claimer | sed -n '/^Events/,$p'
kubectl -n bw-dra get pod claimer \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status} {end}{"\n"}'
ssh zain@10.10.10.180
sudo ls -l /var/lib/kubelet/plugins_registry/ 2>&1
sudo ls -l /var/lib/kubelet/plugins/ 2>&1
sudo journalctl -u kubelet --since '-10 min' --no-pager \
  | grep -i 'dra\|dynamicresource\|prepareresource' | tail -10
exit
  1. Supply the missing half. The pin ships an example driver that invents nine GPUs per node and needs no hardware, and a tutorial that installs it: install-use-dra.md. Apply its six manifests from the checkout rather than from the URLs the tutorial prints, then watch a second ResourceSlice appear beside the one published by hand in step 2 — this one written by a driver.
kubectl -n bw-dra delete pod claimer --now
kubectl create namespace dra-tutorial
for m in deviceclass serviceaccount clusterrole clusterrolebinding priorityclass daemonset; do
  kubectl apply --server-side -f "$W/examples/dra/driver-install/$m.yaml"
done
kubectl -n dra-tutorial rollout status ds/dra-example-driver-kubeletplugin --timeout=300s
kubectl get resourceslices -o wide
kubectl get resourceslices \
  -o jsonpath='{range .items[*]}{.spec.driver} devices={.spec.devices[*].name}{"\n"}{end}'
  1. Run a Pod that really gets one. The claim and the Pod are the tutorial's own, unchanged. The driver has no device to hand over, so it announces the allocation the only way a simulator can, by setting environment variables in the container where a real driver would inject a device file — and the container prints its environment on start, which is why the tutorial reads the allocation out of the Pod's log.
kubectl apply -f "$W/examples/dra/driver-install/example/resourceclaim.yaml"
kubectl apply -f "$W/examples/dra/driver-install/example/pod.yaml"
kubectl -n dra-tutorial wait --for=condition=Ready pod/pod0 --timeout=180s
kubectl -n dra-tutorial get resourceclaim some-gpu \
  -o jsonpath='device={.status.allocation.devices.results[*].device} pool={.status.allocation.devices.results[*].pool}{"\n"}'
kubectl -n dra-tutorial logs pod0 -c ctr0 | grep -E 'GPU_DEVICE_[0-9]+=' | head -5
kubectl -n dra-tutorial logs -l app.kubernetes.io/name=dra-example-driver --tail=5
  1. Ask for something impossible, and then for something else. firstAvailable is the prioritized list the post announces at :47-51; the scheduler takes the first subrequest it can satisfy. The first here asks for a hundred terabytes, which no simulated GPU has, so the second should win and the allocation should say which one did.
kubectl apply -f - <<'YAML'
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: second-choice
  namespace: dra-tutorial
spec:
  devices:
    requests:
    - name: req-0
      firstAvailable:
      - name: impossible
        deviceClassName: gpu.example.com
        selectors:
        - cel:
            expression: device.capacity["gpu.example.com"].memory.compareTo(quantity("100Ti")) >= 0
      - name: possible
        deviceClassName: gpu.example.com
YAML
kubectl -n dra-tutorial apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata:
  name: pod1
spec:
  containers:
  - name: ctr0
    image: registry.k8s.io/e2e-test-images/agnhost:2.53
    command: ["sleep", "3600"]
    resources:
      claims:
      - name: gpu
  resourceClaims:
  - name: gpu
    resourceClaimName: second-choice
YAML
kubectl -n dra-tutorial wait --for=condition=Ready pod/pod1 --timeout=180s
kubectl -n dra-tutorial get resourceclaim second-choice \
  -o jsonpath='request={.status.allocation.devices.results[*].request}{"\n"}'
  1. Two fields whose gates are in different places on the ladder. adminAccess belongs to a beta feature that is on, and admission accepts it only from a namespace carrying a label — the loop tries the namespace unlabelled, then with the spelling the post gives at :44, then with the spelling dra-api.md:417 gives. extendedResourceName belongs to an alpha feature that is off at v1.35, and the API server's answer to a field it does not know is not an error.
cat > /tmp/bw-dra/admin.yaml <<'YAML'
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: admin-claim
  namespace: bw-dra
spec:
  devices:
    requests:
    - name: req-0
      exactly:
        deviceClassName: gpu.example.com
        allocationMode: All
        adminAccess: true
YAML
for label in none resource.k8s.io/admin-access=true resource.kubernetes.io/admin-access=true; do
  kubectl label namespace bw-dra \
    resource.k8s.io/admin-access- resource.kubernetes.io/admin-access- >/dev/null 2>&1
  [ "$label" = none ] || kubectl label namespace bw-dra "$label" >/dev/null
  printf 'label=%s -> ' "$label"
  kubectl apply --dry-run=server -f /tmp/bw-dra/admin.yaml 2>&1 | tail -1
done
kubectl apply -f - <<'YAML'
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: bw-extended
spec:
  selectors:
  - cel:
      expression: device.driver == "gpu.example.com"
  extendedResourceName: blogwalk.example.com/paper
YAML
kubectl get deviceclass bw-extended -o jsonpath='extendedResourceName=[{.spec.extendedResourceName}]{"\n"}'
  1. Offline, in the checkout, count what the prose costs. The first two commands are the six links and the one line of frontmatter that is supposed to save them. The rest are the numbers this file asserts: how many links in the tree still spell the old path, how the label is spelled in the page that registers label names, whether the API reference knows about the field that would have made steps 2 to 5 finish, and which two gate files in the tree reach stable twice.

    cd /path/to/kubernetes/website/content/en
    grep -n 'dynamic-resource-allocation' blog/_posts/2025/dra-134-update.md
    sed -n '7,8p' docs/concepts/resource-management/dynamic-resource-allocation/_index.md
    grep -rho 'concepts/scheduling-eviction/dynamic-resource-allocation[^)"]*' docs | sort | uniq -c | sort -rn
    grep -n 'admin-access' docs/concepts/cluster-administration/labels-annotations-taints/_index.md
    grep -rn 'skipNodeOperations' docs/reference/kubernetes-api/resource/ || echo "absent from the API reference"
    grep -c 'stage: stable' docs/reference/command-line-tools-reference/feature-gates/*.md | grep -v ':[01]$'
    grep -rho 'http://k8s.io/examples[^)` ]*' docs | wc -l
    

Expect

Step 1 prints two gitVersion strings, both v1.35, against a pin that is v1.37: everything below is read at the pin and run two releases behind it. The discovery document answers the post's :30-32 directly — preferred: v1, which is the whole of the claim that the default API version is now the stable one. The served list will also contain the betas the group still keeps; it should not contain v1alpha3, because alpha groups need enabling by hand, and the page that tells you how is set-up-dra-cluster.md:40-57, whose two lists of versions are indented differently from each other. All three listings print No resources found, which is the same starting state the pin's tutorial shows.

Step 2 accepts the ResourceSlice. kubectl get resourceslices -o wide prints one row whose DRIVER is blogwalk.example.com, whose NODE is the node name, and whose POOL is bw-dra-pool, and the last command prints a JSON object with name, an attributes map and a capacity map all set on the same device. That object is the counter-example: resource-slice-v1.md:241 says "Besides the name, exactly one field must be set", the API server disagrees, and so does the pin's own sample at dra-features.md:267-279, whose one device sets three fields besides its name.

Step 3 creates both objects and the claim's status is empty — either nothing printed at all or an empty map, depending on whether the server stores a status stanza for an object that has none. What matters is the absence of two keys: there is no allocation and no reservedFor. A ResourceClaim is not allocated when it is created, even though a matching device has been published and the selector would match it. Allocation is driven by a consumer, and there is not one yet.

Step 4 supplies one, and the two jsonpaths are the point of the exercise. The Pod's spec.nodeName is set, so the scheduler bound it. The claim's status now reads device=paper-0 and reservedFor=claimer: paper-0 and not paper-1, because the selector asks for at least 10Gi and paper-1 was published with 8Gi. The Pod's phase, though, is Pending. Everything the post describes as the scheduler's job has happened, and the container has not started.

Step 5 says why. The events show the kubelet failing to prepare the claim's resources, and the directory the kubelet watches for plugin sockets is empty or absent. The pin does not print the event's reason string anywhere, so write down what your kubelet emits rather than matching it against a string quoted here; the conditions are the checkable part, and they should read PodScheduled=True with Ready=False and ContainersReady=False. The claim was allocated by a component that needed nothing on the node, and it cannot be used by a component that needs something on the node, which is the exact gap DRAOptionalNodeOperations exists to close and does not close at v1.35.

Step 6 closes it the other way, by installing a driver. rollout status should report the DaemonSet ready with one Pod, and kubectl get resourceslices should then show two rows: the one published by hand in step 2, and a second whose driver is gpu.example.com and whose device list is gpu-0 through gpu-8, nine devices, the NUM_DEVICES the DaemonSet sets. If the rollout does not complete, read the plugin's log before going on: the manifest names dra-example-driver:v0.2.0 and the pin's tutorial was written against v1.37, so a v1.35 kubelet is the one variable this exercise cannot check for you. Record what you get. Steps 7 and 8 need this step to have worked.

Step 7 gets a container running on a claim. pod0 reaches Ready, the claim's status names one of the nine devices and a pool named after the node, and the Pod's log carries a GPU_DEVICE_0= line. That line is the honest edge of the simulation: a real driver would have put a device node into the container and the environment variable is standing in for it. The driver's own log prints PrepareResourceClaims is called, which is precisely the call that had no listener in step 5.

Step 8 allocates the second subrequest. pod1 reaches Ready, and the claim's status prints request=req-0/possible — the subrequest name appended to the request name, which is how an allocation records which branch of a prioritized list was taken. Nothing rejects the impossible first subrequest; it is simply skipped. If the status instead names req-0/impossible, the selector matched something, and the pool has a device larger than a hundred terabytes, which would be worth writing down.

Step 9 prints three lines and then one. Unlabelled, the claim is refused by admission for using adminAccess from a namespace that is not permitted to. Labelled with the spelling the post gives at :44, it is refused again, with the same message: that label is not the one the API server reads. Labelled with resource.kubernetes.io/admin-access, the dry run succeeds. The post's only operational instruction is wrong, and it is wrong in a way no amount of re-reading the post can reveal. The last command then prints extendedResourceName=[]: the DeviceClass was created, the field was silently dropped on write because DRAExtendedResource is alpha and off at v1.35, and the object you get back is not the object you sent.

Step 10 is the arithmetic. Six lines of the post link the moved page, one bare and five with fragments. The frontmatter carries one aliases: entry, the old path, which is why the bare link still works and the five fragmented ones land on a page where none of those five anchors exists. The uniq -c table totals forty-eight links in docs still spelling the old path, twenty-six of them bare. The label page prints three hits, two of them writing resource.kubernetes.io and the third pointing at the moved path with a fragment defined nowhere. skipNodeOperations is absent from the API reference for the two resources it is a field of. Exactly two gate files reach stable twice. And eight example links in docs are http, all of them in the tutorial step 6 installs from.

Read on

  1. The alpha API this one replaced, where the same idea has a different group version, a resource called ResourceClass, a second feature gate that no longer exists, and a claim that stays unallocated for the older of the two reasons.

  2. The exercise that owns the pin's redirect table, for the other way a Kubernetes documentation URL survives being moved — four hundred and forty-nine permanent redirects in one file, none of them for this page, which is why the rescue here had to be written into the page's own frontmatter.

  3. The other v1.34 feature whose reference page contradicts the rest of the tree, published five weeks earlier in the same release cycle, where the disagreement is about how many values a field may take rather than how many fields an object may set.

  4. The lab that reads a scheduler's refusal line by line, for what FailedScheduling says when no node can satisfy a request — the message step 8 would have produced if the prioritized list had had only its first subrequest.

  5. Unanswerable from the pin. Nothing in the checkout records what the kubelet reports when a claim is allocated against a driver that has never registered. how-dra-works.md describes the handshake and kubelet-files.md:143-155 names the directory, but no page in the tree gives the event reason, the message, or the retry interval, and the troubleshooting list in set-up-dra-cluster.md:95-120 covers only the case where the driver is installed and failing. The one observable this exercise most wants to quote is the one the pin does not write down.

Teardown

Two namespaces and five cluster-scoped objects. The driver wrote a socket under /var/lib/kubelet/plugins_registry and CDI files under /var/run/cdi on the node; deleting the DaemonSet's namespace removes the Pod that owns them, and the last two commands check that nothing is left behind. The checkout at the pin was only read.

kubectl delete namespace bw-dra dra-tutorial
kubectl delete deviceclass bw-paper bw-extended gpu.example.com
kubectl delete resourceslice bw-dra-pool-0
kubectl delete clusterrole dra-example-driver-role
kubectl delete clusterrolebinding dra-example-driver-role-binding
kubectl delete priorityclass dra-driver-high-priority
rm -rf /tmp/bw-dra
ssh zain@10.10.10.180 'sudo ls -A /var/lib/kubelet/plugins_registry /var/run/cdi 2>&1'

The ResourceSlice published by hand in step 2 has no owner and no controller, so nothing garbage-collects it: it has to be deleted by name, and it will otherwise keep advertising two devices that were never there. If a later exercise on this cluster finds a claim allocated to a driver called blogwalk.example.com, this is why.