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
- Ask the cluster what it serves and what it already has. The first command is the sentence
:30-32makes: 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"
- 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
attributesmap and acapacitymap, which is the combinationresource-slice-v1.md:241says 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"}'
- 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"}'
- 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"}'
- 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-144namesplugins_registryas 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
- 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}'
- 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
- Ask for something impossible, and then for something else.
firstAvailableis 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"}'
- Two fields whose gates are in different places on the ladder.
adminAccessbelongs 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 spellingdra-api.md:417gives.extendedResourceNamebelongs 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"}'
-
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
stabletwice.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
-
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. -
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.
-
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.
-
The lab that reads a scheduler's refusal line by line, for what
FailedSchedulingsays when no node can satisfy a request — the message step 8 would have produced if the prioritized list had had only its first subrequest. -
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.mddescribes the handshake andkubelet-files.md:143-155names the directory, but no page in the tree gives the event reason, the message, or the retry interval, and the troubleshooting list inset-up-dra-cluster.md:95-120covers 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.