Skip to content

[proto]: bolt a DRA driver frontend on the topology-aware policy. - #536

Draft
klihub wants to merge 15 commits into
containers:mainfrom
klihub:test/build/dra-driver
Draft

[proto]: bolt a DRA driver frontend on the topology-aware policy.#536
klihub wants to merge 15 commits into
containers:mainfrom
klihub:test/build/dra-driver

Conversation

@klihub

@klihub klihub commented Jun 14, 2025

Copy link
Copy Markdown
Collaborator

This prototype patch set bolts a DRA allocation frontend on top of the existing topology aware resource policy plugin. The main intention with of this patch set is

  • provide something practical to play around with for the feasibility study of enabling DRA-based CPU allocation,
  • allow (relatively) easy experimentation with how to expose CPU as DRA devices (IOW test various CPU DRA attributes)
  • allow testing how DRA-based CPU allocation (using non-trivial CEL expressions) would scale with cluster and cluster node size

Notes:
This patched NRI plugin, especially in its current state and form, is not a proposal for a first real DRA-based CPU driver.

If you want to play around with this (for instance modify the exposed CPU abstraction), the easiest way is to

  1. fork the main NRI Reference Plugins repo
  2. enable github actions in your personal fork
  3. make any changes you want (for instance, to alter the CPU abstraction, take a look at cpu.DRA()
  4. Push your changes to ssh://git@github.com/$YOUR_FORK/nri-plugins/refs/heads/test/build/dra-driver.
  5. Wait for the image and Helm chart publishing actions to succeed
  6. Once done, you can pull the result in to your cluster with something like helm install --devel -n kube-system test oci://ghcr.io/$YOUR_GITHUB_USERID/nri-plugins/helm-charts/nri-resource-policy-topology-aware --version v0.9-dra-driver-unstable

You can then test if things work with something like

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: any-cores
spec:
  spec:
    devices:
      requests:
      - name: cpu
        exactly:
          deviceClassName: native.cpu
---
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: p-cores
spec:
  spec:
    devices:
      requests:
      - name: cpu
        exactly:
          deviceClassName: native.cpu
          selectors:
            - cel:
                expression: device.attributes["native.cpu"].coreType == "P-core"
          count: 1
---
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: e-cores
spec:
  spec:
    devices:
      requests:
      - name: cpu
        exactly:
          deviceClassName: native.cpu
          selectors:
            - cel:
                expression: device.attributes["native.cpu"].coreType == "E-core"
          count: 1
---
apiVersion: v1
kind: Pod
metadata:
  name: pcore-test
  labels:
    app: pod
spec:
  containers:
  - name: ctr0
    image: busybox
    imagePullPolicy: IfNotPresent
    args:
      - /bin/sh
      - -c
      - trap 'exit 0' TERM; sleep 3600 & wait
    resources:
      requests:
        cpu: 1
        memory: 100M
      limits:
        cpu: 1
        memory: 100M
      claims:
      - name: claim-pcores
  resourceClaims:
  - name: claim-pcores
    resourceClaimTemplateName: p-cores
  terminationGracePeriodSeconds: 1

@klihub
klihub force-pushed the test/build/dra-driver branch 3 times, most recently from d10467e to 0f3a301 Compare June 14, 2025 14:08
@klihub klihub changed the title [prototype]: bolt a test DRA driver on top of the topology-aware policy plugin. [proto]: bolt a DRA driver frontend on the topology-aware policy. Jun 14, 2025
@klihub
klihub force-pushed the test/build/dra-driver branch 3 times, most recently from 66c2519 to 8527808 Compare June 14, 2025 15:47
@klihub
klihub force-pushed the test/build/dra-driver branch from 8527808 to f96ea65 Compare June 23, 2025 06:36
@klihub
klihub force-pushed the test/build/dra-driver branch 2 times, most recently from 776684c to 7ce62a1 Compare August 4, 2025 09:39
@klihub
klihub force-pushed the test/build/dra-driver branch from 7ce62a1 to a3b4047 Compare August 11, 2025 10:12
cs.sharable = cs.sharable.Union(all.SharableCPUs().Intersection(cpus))
cs.reserved = cs.reserved.Union(all.ReservedCPUs().Intersection(cpus))
cs.claimed = cs.claimed.Difference(cpus)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a Pod with DRA claim is terminated, general workloads do not gain access to the released Unclaimed CPUs. Considering the scenario, where a DRA claimed pod access to core 1, and the other generic shared workloads has access to 2-16 cores. After terminating the claimed Pod, the internal shared pool gets updated, but the generic workloads still has access to only 2-16 cores.

During Pod termination, StopContainer is invoked first followed by NodeUnprepareResources function. This 'unclaimCPUs' function falls under 'NodeUnprepareResources' flow, it unclaims and release the claimed CPUs through updateSharedAllocation for remaining workloads. This update happens internally, but is not reflected in the end container.

Releasing the claimed CPUs back to shared/reserved pool in function 'ReleaseCPU' (part of the StopContainer flow) seems to work, the general workloads can access full shared pool of 0-16 upon a claimed pod termination. But, not sure if this would be a valid placement.

@klihub klihub Dec 16, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vigashinikesavan It was a long while ago when we rolled this early prototype, so it took me a while to pick up where we left off. I tried a few simple cases and indeed once you release a claim, if you have burstable or a best-effort container which would be eligible to run on the freed up CPUs (if they run in a shared pool where the CPU was returned to), their effective cgroup cpuset might not get correctly updated.(.. immediately.)

But based on a quick look at the code, and my few simple tests, this does not happen as/because of what you state above. Instead what happens is that we do update the remaining containers correctly, but because the update did not happen in the context of a normal resource allocation or release (CreateContainer or StopContainer) the cpuset/resource updates we did for the containers do not get propagated back to the runtime as part of an NRI response but instead they stay cached in the plugin, until the next container creation or release. You can test this easily when you are in the incorrect state, by creating for instance a new best effort container, which will then flush all the collected pending updates in the response, including the ones collected during the release/unprepare of the claim. This will then cause the containers running in the shared pools to finally get correctly updated. What we should do in this situation is to update the affected containers using an unsolicited container update, but the current code does not do it.

Anyway, this was an early prototype to see how far we could get with what was available at the time we rolled this draft PR. This needs to be updated/reworked pretty much, because a lot has happened since on the DRA front, and the approach taken in this proto is now outdated.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Thanks for the clarification.

@klihub
klihub force-pushed the test/build/dra-driver branch 2 times, most recently from f492576 to 25c5540 Compare December 16, 2025 19:11
@klihub
klihub force-pushed the test/build/dra-driver branch 4 times, most recently from 6b292c3 to 2c08fe3 Compare February 9, 2026 18:29
A dear child has many names...

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
AsYaml can be used to produced YAML-formatted log blocks with
something like this:

  logger.DebugBlock(" <my-obj> ", "%s", log.AsYaml(my-obj))

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Split out K8s client setup code from agent to make it more generally
available to any kind of plugin, not just resource management ones.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Allow setting the content types a client accepts and the
type it uses on the wire. Provide constants for JSON and
protobuf content types.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Split out K8s watch wrapper setup code from agent to make it
generally available to any kind of plugin, not just resource
management ones.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Add the necessary RBAC rules (access resource slices and claims)
and kubelet host mounts (plugin and plugin registry directories)
to the topology-aware policy Helm chart. Add a device class for
native.cpu DRA driver.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Add an interface for caching policy agnostic data, similar to
but simpler than {Get,Set}PolicyData(). Add an interface for
querying the full container environment variable list.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Sort caches by level, kind, and id to enumerate caches globally.
Add a common DRA device representation for CPUs.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Add the necessary plumbing to allow implementations act as a
CPU DRA driver. In partice, add an option for publishing CPUs
as DRA devices and interfaces to allocate and release CPUs as
claims are prepared and unprepared.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Register as a 'native.cpu' DRA resource driver/plugin. Provide
DRA CPU device publishing for policy implementations. Generate
CDI Spec for the published CPU devices. Hook the active policy
in for CPUs allocation and release for claim preparation and un-
preparation.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Flush pending changes immediately using an unsolicited update
to containers instead of waiting for the next NRI event to do
so.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Publish CPUs as DRA devices. Use a sledgehammer to bolt allocation
and release of DRA-claimed CPUs on top of the current implementation.

Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
@klihub
klihub force-pushed the test/build/dra-driver branch from 2c08fe3 to 9deb65b Compare February 10, 2026 11:55

@ozhuraki ozhuraki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Convert bare identifiers and paths across CLAUDE.md and docs/dra/*.md
to markdown links so all cross-references render as clickable on
GitHub:

- KEP-NNNN mentions link to their kubernetes/enhancements issue
- PR containers#536 mentions link to containers#536
- Cross-doc references (design.md, landscape.md, pr-536-analysis.md,
  plan.md) use markdown links
- Repo-internal directories in CLAUDE.md link to the repo tree paths
- External-repo file references (dra-driver-cpu, dra-example-driver,
  kubernetes/kubernetes) link to specific blob/tree URLs; file:line
  references use #Lnnn anchors

Also fixes two stale filename references (pct-design.md ->
design.md) missed during the earlier rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Replace the four-bullet "Takeaway for future DRA work" section with a
three-tier reuse breakdown that names concrete commits and files:

- Tier 1 — direct cherry-pick: six PR containers#536 commits that are pure
  refactors or additive utilities, orthogonal to the DRA design.
- Tier 2 — salvage-and-adapt: named files/functions where the
  mechanism is correct but the surrounding shape (package layout,
  env-var naming, driver name) changes.
- Tier 3 — learn-from-only: files whose semantics moved under the
  design changes (device schema, per-(class x punit) accounting,
  obsolete user-facing workarounds).

Motivation: the previous binary "reusable vs obsolete" framing understated
the middle tier. Future implementers now have a concrete
commit-and-file map instead of an abstract "reusable pieces" hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Both reference drivers (dra-driver-cpu, dra-example-driver) use the
upstream CDI Go library rather than hand-rolling YAML. PR containers#536 hand-
rolled its writer, but that carries no atomic-write guarantee, no spec
validation, and no version tracking. Not a dep in nri-plugins today.

- plan.md step 7: CDI writer bullet points at the library instead of
  reusing PR containers#536's shape. Names the specific API surface
  (cdiapi.Cache.WriteSpec, RemoveSpec; specs-go types;
  GenerateTransientSpecName for per-claim filenames) and points at
  dra-driver-cpu's cdi.go as the pattern source.
- plan.md step 6 Imports & deps: adds the three library packages
  (pkg/cdi, specs-go, pkg/parser) to the Must-have tier.
- pr-536-analysis.md: moves the CDI writer bullet from Tier 2
  (salvage-and-adapt) to Tier 3 (learn-from-only), with a note
  explaining what to import instead.
- landscape.md: one-line entry under Other reference code linking
  to cncf-tags/container-device-interface with a sentence on why
  drivers should import it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Detailed TDD-ordered plan for the first landable PR of the DRA
integration: lift Kubernetes client bootstrap out of pkg/agent into a
new pkg/kubernetes/client package, and move the existing
pkg/agent/watch package to pkg/kubernetes/watch verbatim (git mv,
history preserved).

The plan is derived from three commits on the pr-536-dra branch
(5dcb66d, 42ec102, 8814064) — re-derived cleanly with tests added,
since PR containers#536 shipped without tests. Deliberately does NOT adopt PR
containers#536's parallel pkg/kubernetes/watch ObjectClient redesign; that
API-change work is out of scope for a "pure refactor" step 1.

Structure: 9 tasks, each with Files: blocks and tests-first
checkboxes. Task 1-2 establish client package + options + methods.
Task 3 adds content-type options with a two-pass retry-when-config-
not-set mechanism (order-independence). Task 4 moves the watch
package and adds minimum tests it currently lacks. Tasks 5-6 reserved
for numbering stability across review rounds. Task 7 rewires
pkg/agent/agent.go to use the new client, removes httpCli field,
adds four getters (NodeName, KubeClient, KubeConfig string,
RestConfig). Tasks 8-9 are acceptance and doc updates.

Plan went through two rounds of plan-review agent auto-review;
final verdict "APPROVE with minor polish" — all 15 first-round
findings addressed, plus 8 second-round polish items applied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Second task of the DRA v1 step 1 refactor (see docs/plans/20260820-
dra-step1-kubernetes-client-watch-lift.md).

Adds the New() constructor with functional options and the Client's
method surface:

- Options: WithKubeConfig, WithInClusterConfig,
  WithKubeOrInClusterConfig, WithRestConfig (deep-copies input via
  rest.CopyConfig), WithHttpClient.
- Methods: RestConfig (returns rest.CopyConfig(c.cfg) — top-level and
  value-struct fields safe to overwrite; nested map/slice contents
  share storage per upstream convention), HttpClient, K8sClient,
  Close (idempotent, nil-safe).
- errRetryWhenConfigSet sentinel prepared for Task 3's content-type
  options.
- New() falls back to WithInClusterConfig if no option set a config.

Tests: 11 new cases covering all option paths (kubeconfig success/
missing, in-cluster fallback, RestConfig acceptance, HttpClient
injection), rest.CopyConfig semantics on both directions, and
Close idempotency. In-cluster paths guarded by KUBERNETES_SERVICE_HOST
so tests skip cleanly when the CI happens to run inside a Pod.

Plan updated in place: earlier promise of "safe to mutate at any
depth" was aspirational — rest.CopyConfig shares nested map/slice
storage (Impersonate.Extra, TLSClientConfig.CAData, etc.). Matched
to upstream contract and to what dra-driver-cpu and PR containers#536's
WithRestConfig side expect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Fourth task of the DRA v1 step 1 refactor (see docs/plans/20260820-
dra-step1-kubernetes-client-watch-lift.md). Relocates the watch
package to make it available to future consumers outside the agent
(the DRA driver in later plan steps).

Changes:

- git mv pkg/agent/watch/{watch,object,file}.go
  pkg/kubernetes/watch/ — preserves history via git rename detection.
- No API change: Object(ctx, ns, name, CreateFn) and File(path,
  UnmarshalFn) signatures unchanged; ObjectWatch.Stop(),
  FileWatch.Stop(), and the type-alias/const surface (Interface,
  Event, EventType, Added/Modified/Deleted/Bookmark/Error) unchanged.
- Logger tag changed from logger.Get("agent") to logger.Get("watch")
  so operator log-grep matches the new package location.
- Import path in pkg/agent/agent.go:36 updated from
  pkg/agent/watch to pkg/kubernetes/watch. Plan-deviation note:
  this update was scheduled for Task 7 but is included here to keep
  the tree compiling between per-task commits. Task 7 no longer needs
  to touch that import.

Tests (new, since the moved package shipped without any):

- Type-alias assertions: compile-time verification that Interface,
  EventType, Event are true aliases of k8s.io/apimachinery/pkg/watch
  types.
- Event-type constant equality: each re-exported const equals its
  upstream value.
- Object watch happy path: events pushed through the fake Interface
  from a CreateFn arrive on ResultChan in order.
- Object.Stop() idempotency: two calls do not panic.
- File watch Create-vs-Write distinction: defensive guard that Create
  fsnotify events emit Added and Write events emit Modified — PR
  containers#536's parallel implementation ships a copy-paste bug emitting
  Added for both, and this test guards against re-introducing it.
  Uses O_WRONLY|O_APPEND for the modify step so we get a pure Write
  event, not the O_CREATE|O_TRUNC that os.WriteFile does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bart0sh added a commit to bart0sh/nri-plugins that referenced this pull request Aug 20, 2026
Load-bearing task of the DRA v1 step 1 refactor (see docs/plans/
20260820-dra-step1-kubernetes-client-watch-lift.md Task 7). Replaces
the inline Kubernetes client bootstrap with the new pkg/kubernetes/
client wrapper, and exposes agent state that future consumers (the
DRA driver in later plan steps) need.

Changes:

- Agent.httpCli field removed. All uses replaced with
  a.k8sCli.HttpClient() (where a.k8sCli is now *client.Client).
- Agent.k8sCli field type changed from *k8sclient.Clientset to
  *client.Client. Existing use at a.k8sCli.CoreV1().Nodes().Watch(...)
  keeps compiling because *client.Client embeds *kubernetes.Clientset.
- setupClients: replaced ~25 lines of inline REST config resolution
  and Clientset construction with client.New(
  client.WithKubeOrInClusterConfig(a.kubeConfig)). Passes
  a.k8sCli.HttpClient() and a.k8sCli.RestConfig() to
  ConfigInterface.SetKubeClient — the callback signature is unchanged,
  so downstream consumers are unaffected.
- configure() NRT client init: uses a.k8sCli.RestConfig() and
  a.k8sCli.HttpClient() instead of a.getRESTConfig() + a.httpCli.
  Guards on a.k8sCli == nil to avoid a nil-panic if configure is
  invoked before setupClients completes.
- cleanupClients: collapsed to a.k8sCli.Close() (nil-safe from the
  client package) + nil-out of a.k8sCli and a.nrtCli.
- getRESTConfig: removed entirely. Its logic now lives inside
  client.New's option chain (WithKubeOrInClusterConfig).
- Removed unused imports "k8s.io/client-go/tools/clientcmd" and
  k8sclient "k8s.io/client-go/kubernetes".

Getters added on *Agent (public surface):

- NodeName() string — the kubernetes node name.
- KubeClient() *client.Client — the shared client wrapper; nil
  before setupClients has run successfully.
- KubeConfig() string — the kubeconfig file path (or empty for
  in-cluster). Matches PR containers#536 commit 8814064's signature.
- RestConfig() *rest.Config — shortcut for
  KubeClient().RestConfig(); nil-safe before setupClients.

Tests (new pkg/agent/agent_test.go):

- Getters return zero values on a freshly-constructed Agent (before
  setupClients).
- Getters return non-nil values once a.k8sCli is populated with a
  real client.Client built from the fixture kubeconfig
  (../kubernetes/client/testdata/kubeconfig-example.yaml).

Preexisting failure in pkg/sysfs (unrelated: panics in Ginkgo-based
cache-discovery and cluster-CPUSet tests) verified via stash-and-
retest before applying — not introduced by this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants