From 3159f896a200d225e39b97c461e8bbb0170bd42e Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 18 Apr 2024 15:40:49 +0200 Subject: [PATCH 1/3] DRA: "Structured Parameters" as base, "classic" DRA as extension We decided to focus on structured parameters as the part of DRA which will get moved to beta first. "Classic" DRA (PodSchedulingContext, support for control plane controllers) will remain in alpha. It needs a separate feature gate. Therefore the description of DRA in the two KEPs gets shifted around such that the "structured parameters" KEP is a stand-alone, complete description of everything needed for using DRA like that. The original KEP is now an extension of that and describes PodSchedulingContext and the interaction with a DRA driver control plane controller. Immediate allocation is defined there because it does not make much sense for structured parameters. Mostly this was just a matter of copying text around and updating the wording. Changes that go beyond those editorial changes are: - title changes - new diagram for "structured parameters" - ResourceClass.StructuredParameters in 4381 became ResourceClass.ControlPlaneController in 3063 (different default, breaking change!) - new DRAControlPlaneController feature gate for everything in 3063 - removal of the kubelet v1alpha2 gRPC interface (https://github.com/kubernetes/kubernetes/pull/124316). - updated description of ephemeral ResourceClaim namimg (did not match the current implementation with generated names) - added NodeListAndWatchResources kubelet gRPC call --- .../README.md | 2571 +++-------------- .../components.png | Bin 51958 -> 51958 bytes .../components.puml | 10 +- .../3063-dynamic-resource-allocation/kep.yaml | 7 +- .../kubelet.png | Bin 31727 -> 0 bytes .../4381-dra-structured-parameters/Makefile | 35 + .../4381-dra-structured-parameters/README.md | 1832 ++++++++++-- .../components.png | Bin 0 -> 74542 bytes .../components.puml | 60 + .../4381-dra-structured-parameters/kep.yaml | 9 +- .../kubelet.png | Bin 0 -> 26102 bytes .../kubelet.puml | 0 12 files changed, 2184 insertions(+), 2340 deletions(-) delete mode 100644 keps/sig-node/3063-dynamic-resource-allocation/kubelet.png create mode 100644 keps/sig-node/4381-dra-structured-parameters/Makefile create mode 100644 keps/sig-node/4381-dra-structured-parameters/components.png create mode 100644 keps/sig-node/4381-dra-structured-parameters/components.puml create mode 100644 keps/sig-node/4381-dra-structured-parameters/kubelet.png rename keps/sig-node/{3063-dynamic-resource-allocation => 4381-dra-structured-parameters}/kubelet.puml (100%) diff --git a/keps/sig-node/3063-dynamic-resource-allocation/README.md b/keps/sig-node/3063-dynamic-resource-allocation/README.md index f49900d7616..730f6e5df9c 100644 --- a/keps/sig-node/3063-dynamic-resource-allocation/README.md +++ b/keps/sig-node/3063-dynamic-resource-allocation/README.md @@ -64,7 +64,7 @@ If none of those approvers are still appropriate, then changes to that list should be approved by the remaining approvers and/or the owning SIG (or SIG Architecture for cross-cutting KEPs). --> -# [KEP-3063](https://github.com/kubernetes/enhancements/issues/3063): Dynamic resource allocation +# [KEP-3063](https://github.com/kubernetes/enhancements/issues/3063): Dynamic Resource Allocation with Control Plane Controller @@ -75,52 +75,21 @@ SIG Architecture for cross-cutting KEPs). - [Non-Goals](#non-goals) - [Proposal](#proposal) - [User Stories](#user-stories) - - [Cluster add-on development](#cluster-add-on-development) - - [Cluster configuration](#cluster-configuration) - - [Partial GPU allocation](#partial-gpu-allocation) - [Network-attached accelerator](#network-attached-accelerator) - [Combined setup of different hardware functions](#combined-setup-of-different-hardware-functions) - [Notes/Constraints/Caveats](#notesconstraintscaveats) - - [Risks and Mitigations](#risks-and-mitigations) - - [Feature not used](#feature-not-used) - - [Compromised node](#compromised-node) - - [Compromised resource driver plugin](#compromised-resource-driver-plugin) - - [User permissions and quotas](#user-permissions-and-quotas) - - [Usability](#usability) - [Design Details](#design-details) - - [Theory of operation](#theory-of-operation) - - [Components](#components) - - [State and communication](#state-and-communication) - - [Custom parameters](#custom-parameters) - - [Allocation modes](#allocation-modes) - - [Sharing a single ResourceClaim](#sharing-a-single-resourceclaim) - - [Ephemeral vs. persistent ResourceClaims lifecycle](#ephemeral-vs-persistent-resourceclaims-lifecycle) + - [ResourceClass extension](#resourceclass-extension) + - [ResourceClaim extension](#resourceclaim-extension) + - [ResourceClaimStatus extension](#resourceclaimstatus-extension) + - [ResourceHandle extensions](#resourcehandle-extensions) + - [PodSchedulingContext](#podschedulingcontext) - [Coordinating resource allocation through the scheduler](#coordinating-resource-allocation-through-the-scheduler) - [Resource allocation and usage flow](#resource-allocation-and-usage-flow) - [Scheduled pods with unallocated or unreserved claims](#scheduled-pods-with-unallocated-or-unreserved-claims) - - [Handling non graceful node shutdowns](#handling-non-graceful-node-shutdowns) - - [API](#api) - - [resource.k8s.io](#resourcek8sio) - - [core](#core) - - [kube-controller-manager](#kube-controller-manager) - - [kube-scheduler](#kube-scheduler) - - [EventsToRegister](#eventstoregister) - - [PreEnqueue](#preenqueue) - - [Pre-filter](#pre-filter) - - [Filter](#filter) - - [Post-filter](#post-filter) - - [Pre-score](#pre-score) - - [Reserve](#reserve) - - [PreBind](#prebind) - - [Unreserve](#unreserve) - [Cluster Autoscaler](#cluster-autoscaler) - - [kubelet](#kubelet) - - [Managing resources](#managing-resources) - - [Communication between kubelet and resource kubelet plugin](#communication-between-kubelet-and-resource-kubelet-plugin) - - [NodePrepareResource](#nodeprepareresource) - - [NodeUnprepareResource](#nodeunprepareresource) - - [Implementing optional resources](#implementing-optional-resources) - - [Implementing a plugin for node resources](#implementing-a-plugin-for-node-resources) + - [Implementing a plugin for node resources](#implementing-a-plugin-for-node-resources) + - [Implementing optional resources](#implementing-optional-resources) - [Test Plan](#test-plan) - [Prerequisite testing updates](#prerequisite-testing-updates) - [Unit tests](#unit-tests) @@ -140,20 +109,6 @@ SIG Architecture for cross-cutting KEPs). - [Troubleshooting](#troubleshooting) - [Implementation History](#implementation-history) - [Drawbacks](#drawbacks) -- [Alternatives](#alternatives) - - [Semantic Parameters instead of PodSchedulingContext](#semantic-parameters-instead-of-podschedulingcontext) - - [ResourceClaimTemplate](#resourceclaimtemplate) - - [Reusing volume support as-is](#reusing-volume-support-as-is) - - [Extend volume support](#extend-volume-support) - - [Extend Device Plugins](#extend-device-plugins) - - [Webhooks instead of ResourceClaim updates](#webhooks-instead-of-resourceclaim-updates) - - [ResourceDriver](#resourcedriver) - - [Complex sharing of ResourceClaim](#complex-sharing-of-resourceclaim) - - [Improving scheduling performance](#improving-scheduling-performance) - - [Optimize for network-attached resources](#optimize-for-network-attached-resources) - - [Moving blocking API calls into goroutines](#moving-blocking-api-calls-into-goroutines) - - [RPC calls instead of PodSchedulingContext](#rpc-calls-instead-of-podschedulingcontext) -- [Infrastructure Needed](#infrastructure-needed) ## Release Signoff Checklist @@ -196,119 +151,31 @@ Items marked with (R) are required *prior to targeting to a milestone / release* ## Summary -Users are increasingly deploying Kubernetes as management solution for new -workloads (batch processing) and in new environments (edge computing). Such -workloads no longer need just RAM and CPU, but also access to specialized -hardware. With upcoming enhancements of data center interconnects, accelerators -can be installed outside of specific nodes and be connected to nodes -dynamically as needed. - -This KEP introduces a new API for describing which of these new resources -a pod needs. The API supports: - -- Network-attached resources. The existing [device plugin API](https://github.com/kubernetes/design-proposals-archive/blob/main/resource-management/device-plugin.md) - is limited to hardware on a node. -- Sharing of a resource allocation between multiple containers or pods. - The device manager API currently cannot share resources at all. It - could be extended to share resources between containers in a single pod, - but supporting sharing between pods would need a completely new - API similar to the one in this KEP. -- Using a resource that is expensive to initialize multiple times - in different pods. This is not possible at the moment. -- Custom parameters that describe resource requirements and initialization. - Parameters are not limited to a single, linear quantity that can be counted. - With the current Pod API, annotations have to be used to capture such - parameters and then hacks are needed to access them from a CSI driver or - device plugin. - -Support for new hardware will be provided by hardware vendor add-ons. It will -not be necessary anymore to modify Kubernetes itself. - -This KEP does not replace other means of requesting traditional resources -(RAM/CPU, volumes, extended resources). The scheduler will serve as coordinator -between the add-ons which own resources (CSI driver, resource driver) and the -resources owned and assigned by the scheduler (RAM/CPU, extended resources). +Originally, this KEP introduced DRA in Kubernetes 1.26 and the ["structured +parameters" KEP](../4381-dra-structured-parameters/README.md) added an +extension. Now the roles are reversed: #4381 defines the base functionality +and this KEP is an optional extension. + +With #4381, DRA drivers are limited by what the structured parameter model(s) +defined by Kubernetes support. New requirements for future hardware may depend +on changing Kubernetes first. + +With this KEP, parameters and resource availability are completely opaque +to Kubernetes. During scheduling of a pod, the kube-scheduler and any DRA +driver controller(s) handling claims for the pod communicate back-and-forth through the +apiserver by updating a `PodSchedulingContext` object, ultimately leading to the +allocation of all pending claims and the pod being scheduled onto a node. + +Beware that this approach poses a problem for the [Cluster +Autoscaler](https://github.com/kubernetes/autoscaler) (CA) or for any higher +level controller that needs to make decisions for a group of pods (e.g. a job +scheduler). It cannot simulate the effect of allocating or deallocating +claims over time. Only the third-party DRA drivers have the information +available to do this. Structured parameters from #4381 should be used +when cluster autoscaling is needed. ## Motivation -Originally, Kubernetes and its scheduler only tracked CPU and RAM as -resources for containers. Later, support for storage and discrete, -countable per-node extended resources was added. The kubelet device plugin -interface then made such local resources available to containers. But -for many newer devices, this approach and the Kubernetes API for -requesting these custom resources is too limited. This KEP addresses -limitations of the current approach for the following use cases: - -- *Device initialization*: When starting a workload that uses - an accelerator like an FPGA, I’d like to have the accelerator - reconfigured or reprogrammed for the workload before the workload - itself starts. For security reasons, workloads should not be able to - reconfigure devices directly. - - *Limitation*: Currently, it’s impossible to specify the desired - device properties that are required for reconfiguring devices. - For the FPGA example, a file containing the desired configuration - of the FPGA has to be referenced. - -- *Device cleanup*: When my workload is finished, I would like to have - a mechanism for cleanup of the device, that will ensure that device - does not contain traces/parameters/data from previous workloads and - appropriate power state/shutdown. For example, an FPGA might have - to be reset because its configuration for the workload was - confidential. - - *Limitation*: Post-stop actions are not supported. - -- *Partial allocation*: When deploying a container I’d like to be able - to use part of the shareable device inside a container and other - containers should be able to use other free resources on the same - device. - - *Limitation*: For example, newer generations of NVIDIA GPUs have a mode of - operation called MIG, that allow them to be sub-divided into a set of - mini-GPUs (called MIG devices) with varying amounts of memory and compute - resources provided by each. From a hardware-standpoint, configuring a GPU - into a set of MIG devices is highly-dynamic and creating a MIG device - tailored to the resource needs of a particular application is well - supported. However, with the current device plugin API, the only way to make - use of this feature is to pre-partition a GPU into a set of MIG devices and - advertise them to the kubelet in the same way a full / static GPU is - advertised. The user must then pick from this set of pre-partitioned MIG - devices instead of having one created for them on the fly based on their - particular resource constraints. Without the ability to create MIG devices - dynamically (i.e. at the time they are requested) the set of pre-defined MIG - devices must be carefully tuned to ensure that GPU resources do not go unused - because some of the pre-partioned devices are in low-demand. It also puts - the burden on the user to pick a particular MIG device type, rather than - declaring the resource constraints more abstractly. - -- *Optional allocation*: When deploying a workload I’d like to specify - soft(optional) device requirements. If a device exists and it’s - allocatable it will be allocated. If not - the workload will be run on - a node without a device. GPU and crypto-offload engines are - examples of this kind of device. If they’re not available, workloads - can still run by falling back to using only the CPU for the same - task. - - *Limitation*: Optional allocation is supported neither by the device - plugins nor by current Pod resource declaration. - -- *Support Over the Fabric devices*: When deploying a container, I’d - like to utilize devices available over the Fabric (network, special - links, etc). - - *Limitation*: The device plugin API is designed for node-local resources that - get discovered by a plugin running on the node. Projects like - [Akri](https://www.cncf.io/projects/akri/) have to work around that by - reporting the same network-attached resource on all nodes that it could - get attached to and then updating resource availability on all of those - nodes when resources get used. - -Several other limitations are addressed by -[CDI](https://github.com/container-orchestrated-devices/container-device-interface/), -a container runtime extension that this KEP is using to expose resources -inside a container. - ### Goals -* More flexibility: - * Arbitrary parameters for resource requests +* More flexibility beyond what is currently supported by structured parameters: + * Arbitrary parameters * Network-attached resources - * Arbitrary, resource-specific setup and cleanup actions - * Custom matching of resource requests with available resources, - including handling of optional resource requests -* User-friendly API for describing resource requests -* Allow resource management cluster add-ons that can be developed and deployed - without having to re-build or reconfigure core Kubernetes component - and that are independent of specific container runtimes. -* Rich enough semantic so that all current device plugins could - be implemented based on dynamic resource allocation + * Custom policies for matching of resource requests with available resources, + like handling of optional resource requests or application-specific + policies +* Prototyping future extensions with a control plane controller before + proposing them as Kubernetes enhancements for DRA with structured parameters ### Non-Goals -* Replace the device plugin API. For resources that fit into its model - of a single, linear quantity it is a good solution. Other resources - should use dynamic resource allocation. Both are expected to co-exist, with vendors - choosing the API that better suits their needs on a case-by-case - basis. Because the new API is going to be implemented independently of the - existing device plugin support, there's little risk of breaking stable APIs. - -* Extend the model that kube-scheduler has about - resources. Instead, it will need information from the resource driver for - each resource request to determine where a Pod using the resource - might run. The [Representing Compute Resources in Kubernetes - proposal](https://docs.google.com/document/d/1666PPUs4Lz56TqKygcy6mXkNazde-vwA7q4e5H92sUc/edit#) - had some ideas what information the scheduler might need (“supports - overcommit”, “fractional”), but ultimately any choice regarding that - will only work for certain kinds of resources. - -* Standardize how to describe available resources. Only allocated - resources are visible through the APIs defined below. How to - advertise available resources is driver specific because it depends - on the kind of resource which attributes might be relevant. Drivers - should use and document their individual approach for this (for - example, defining a CRD and publishing through that). - -* Provide an abstraction layer for resource requests, i.e., something like a - “I want some kind of GPU”. Users will need to know about specific - resource drivers and which parameters they support. Portability of - workloads could be added on top of this proposal by introducing the - selection of a resource implementation through labels and - standardizing those labels and the associated parameters. The - [Resource Class - Proposal](https://docs.google.com/document/d/1qKiIVs9AMh2Ua5thhtvWqOqW0MSle_RV3lfriO1Aj6U/edit#heading=h.jzfmfdca34kj) - included such an approach. - - +* Supporting cluster autoscaling ## Proposal -The proposal is that a resource driver handles all operations that are specific -to the resources managed by that driver. This includes operations at -the control plane level (tracking where in the cluster resources are -available, helping with pod scheduling decisions, allocating resources -when requested) as well as the node level (preparing container -startup). Such a driver can be implemented in arbitrary programming -languages as long as it supports the resource allocation protocol and -gRPC interfaces defined in this KEP. Deploying it will not depend on -reconfiguring core Kubernetes components like the scheduler. - -New API objects define parameters for a resource request ("ResourceClaim" in -the API) and track the state of such a request. The pod spec gets extended to -reference one or more resource requests. A pod only gets scheduled onto a node -when all of its requests are reserved for the pod and available on the -node. This prevents scheduling a pod that then gets stuck on a node while -waiting for resources to become available. - -### User Stories - -#### Cluster add-on development - -As a hardware vendor, I want to make my hardware available also to applications -that run in a container under Kubernetes. I want to make it easy for a cluster -administrator to configure a cluster where some nodes have this hardware. - -I develop two components, one that runs as part of the Kubernetes control plane -and one that runs on each node, and package those inside container images. YAML -files describe how to deploy my software on a Kubernetes cluster that supports -dynamic resource allocation. - -Documentation for administrators explains how the nodes need to be set -up. Documentation for users explains which parameters control the behavior of -my hardware and how to use it inside a container. - -#### Cluster configuration - -As a cluster administrator, I want to make GPUs from vendor ACME available to users -of that cluster. I prepare the nodes and deploy the vendor's components with -`kubectl create`. - -I create a ResourceClass for the hardware with parameters that only I as the -administrator am allowed to choose, like for example running a command with -root privileges that does some cluster-specific initialization for each allocation: -``` -apiVersion: gpu.example.com/v1 -kind: GPUInit -metadata: - name: acme-gpu-init -# DANGER! This option must not be accepted for -# user-supplied parameters. A real driver might -# not even allow it for admins. This is just -# an example to show the conceptual difference -# between ResourceClass and ResourceClaim -# parameters. -initCommand: -- /usr/local/bin/acme-gpu-init -- --cluster -- my-cluster ---- -apiVersion: core.k8s.io/v1alpha2 -kind: ResourceClass -metadata: - name: acme-gpu -driverName: gpu.example.com -parametersRef: - apiGroup: gpu.example.com - kind: GPUInit - name: acme-gpu-init -``` - -#### Partial GPU allocation +A resource driver handles all operations that are specific to the allocation +and deallocation of a ResourceClaim. It does that in coordination with the +scheduler (for allocation) and kube-controller-manager (for deallocation). -As a user, I want to use a GPU as accelerator, but don't need exclusive access -to that GPU. Running my workload with just 2Gb of memory is sufficient. This is -supported by the ACME GPU hardware. I know that the administrator has created -an "acme-gpu" ResourceClass. - -For a simple trial, I create a Pod directly where two containers share the same subset -of the GPU: -``` -apiVersion: gpu.example.com/v1 -kind: GPURequirements -metadata: - name: device-consumer-gpu-parameters -memory: "2Gi" ---- -apiVersion: resource.k8s.io/v1alpha2 -kind: ResourceClaimTemplate -metadata: - name: device-consumer-gpu-template -spec: - metadata: - # Additional annotations or labels for the - # ResourceClaim could be specified here. - spec: - resourceClassName: "acme-gpu" - parametersRef: - apiGroup: gpu.example.com - kind: GPURequirements - name: device-consumer-gpu-parameters ---- -apiVersion: v1 -kind: Pod -metadata: - name: device-consumer -spec: - resourceClaims: - - name: "gpu" # this name gets referenced below under "claims" - template: - resourceClaimTemplateName: device-consumer-gpu-template - containers: - - name: workload - image: my-app - command: ["/bin/program"] - resources: - requests: - memory: "64Mi" - cpu: "250m" - limits: - memory: "128Mi" - cpu: "500m" - claims: - - "gpu" - - name: monitor - image: my-app - command: ["/bin/other-program"] - resources: - requests: - memory: "32Mi" - cpu: "25m" - limits: - memory: "64Mi" - cpu: "50m" - claims: - - "gpu" -``` +![components](./components.png) -This request triggers resource allocation on a node that has a GPU device with -2Gi of memory available and then the Pod runs on that node. The remaining -capacity of the GPU may be usable for other pods, with constrains like alignment -to segment sizes ensured by the resource driver. -The lifecycle of the resource -allocation is tied to the lifecycle of the Pod. -In production, a similar PodTemplateSpec in a Deployment will be used. +### User Stories #### Network-attached accelerator @@ -552,406 +245,48 @@ The hardware that is expected to need this more flexible allocation approach is likely to be used by pods that run for extended periods of time, so this is not a major concern. -### Risks and Mitigations - - - -#### Feature not used - -In a cluster where the feature is not used (no resource driver installed, no -pods using dynamic resource allocation) the impact is minimal, both for -performance and security. The scheduler plugin and resource controller will -return quickly without doing any work for pods. - -#### Compromised node - -Kubelet is intentionally limited to read-only access for all new API types -to prevent that a -compromised kubelet interferes with scheduling of pending pods, for example -by updating information normally published by the resource driver controller. -Faking such information could be used for a denial-of-service -attack against pods using those ResourceClaims, for example by overwriting -their allocation result with a node selector that matches no node. A -denial-of-service attack against the cluster and other pods is harder, but -still possible. For example, frequently updating ResourceClaims could cause new -scheduling attempts for pending pods. - -Another potential attack goal is to get pods with sensitive workloads to run on -a compromised node. For pods that don't use special resources nothing changes -in that regard. Such an attack is possible for pods with extended resources -because kubelet is in control of which capacity it reports for those: it could -publish much higher values than the device plugin reported and thus attract -pods to the node that normally would run elsewhere. With dynamic resource -allocation, such an attack is still possible, but the attack code would have to -be different for each resource driver because all of them will use their own, -custom approach for reporting resource availability. - -The security of those custom approaches is the responsibility of the resource -driver vendor. Solutions like Akri which establish their own control plane and -then communicate with Kubernetes through the device plugin API already need to -address this. - -#### Compromised resource driver plugin - -This is the result of an attack against the resource driver, either from a -container which uses a resource exposed by the driver, a compromised kubelet -which interacts with the plugin, or through a successful attack against the -node which led to root access. - -The resource driver plugin only needs read access to objects described in this -KEP, so compromising it does not interfere with dynamic resource allocation for -other drivers. It may need write access for [CRDs that communicate or -coordinate resource -availability](#implementing-a-plugin-for-node-resources). This could be used to -attack scheduling involving the driver as outlined in the previous section. - -A resource driver may need root access on the node to manage -hardware. Attacking the driver therefore may lead to root privilege -escalation. Ideally, driver authors should try to avoid depending on root -permissions and instead use capabilities or special permissions for the kernel -APIs that they depend on. - -A resource driver may also need privileged access to remote services to manage -network-attached devices. Resource driver vendors and cluster administrators -have to consider what the effect of a compromise could be for that and how such -privileges could get revoked. - -#### User permissions and quotas - -Similar to generic ephemeral inline volumes, the [ephemeral resource use -case](#ephemeral-vs-persistent-resourceclaims-lifecycle) gets covered by -creating ResourceClaims on behalf of the user automatically through -kube-controller-manager. The implication is that RBAC rules that are meant to -prevent creating ResourceClaims for certain users can be circumvented, at least -for ephemeral resources. Administrators need to be aware of this caveat when -designing user restrictions. - -A quota system that limits how much of the underlying resources a user may consume -needs to be supported by the resource driver. When a user has exhausted their -quota, the driver then would refuse to allocate further ResourceClaims. Such a -quota system cannot be implemented in core Kubernetes because Kubernetes has no -information about how much a certain ResourceClaim would count against the quota. - -What can be limited in Kubernetes itself is the number of ResourceClaims per -namespace. For this, two new ResourceQuota resource names get added: - -- `resourceclaims` limits the number of ResourceClaim objects in a namespace - across all resource class. -- `.resourceclass.node.k8s.io/resourceclaims` limits the - number of ResourceClaim objects for the specific resource class. - - -#### Usability - -Aside from security implications, usability and usefulness of dynamic resource -allocation also may turn out to be insufficient. Some risks are: - -- Slower pod scheduling due to the interaction with resource drivers. - -- Additional complexity when describing pod requirements because - separate objects must be created for the parameters. - -- Network-attached resources may have additional constraints that are not - captured yet (like limited number of nodes that they can be attached to). - -- Cluster autoscaling will not work as expected unless the DRA driver - uses [semantic parameters](https://github.com/kubernetes/enhancements/issues/4381). - -All of these risks will have to be evaluated by gathering feedback from users -and resource driver developers. - ## Design Details -### Theory of operation - -In general, this new API works as described below (more details and exploration -of corner cases will follow): - -* A user creates a ResourceClaim. This claim may be created by the user - directly (the user owns the resource's lifetime) or indirectly through a pod - (the pod owns the resource's lifetime). +### ResourceClass extension -* A resource driver observes the claim and allocates the underlying resource if - it can. +An optional field in ResourceClass enables using the DRA driver's control +plane controller: -* A pod references the claim in its spec. - -* When a claim is meant to be allocated for a specific pod, the scheduler and - the resource driver(s) coordinate to pick a viable node before the claim gets - allocated. - -* Once allocated, a pod which consumes the resource is scheduled to a node where - the resource is available. - -* Kubelet on that node communicates with the node-level part of the resource - driver to present the resource to the pod. - -* When the pod completes, the Kubelet on that node communicates with node-level - part of the resource driver to clean up. - -* When the claim is released (deleted), the resource driver can free the - underlying resource. +```go +type ResourceClass struct { + ... -### Components + // If (and only if) allocation of claims using this class is handled + // by the DRA driver, ControlPlaneController must be set to true. + // + // This is an alpha field and requires enabling the DRAControlPlaneController + // feature gate. + ControlPlaneController bool +} +``` -![components](./components.png) +### ResourceClaim extension -Several components must be implemented or modified in Kubernetes: -- The new API must be added to kube-apiserver. The ResourceQuota admission - plugin needs to check the new quota limits when ResourceClaims get created. -- A new controller in kube-controller-manager which creates - ResourceClaims from ResourceClaimTemplates, similar to - https://github.com/kubernetes/kubernetes/tree/master/pkg/controller/volume/ephemeral. - It also removes the reservation entry for a user in `claim.status.reservedFor`, - the field that tracks who is allowed to use a claim, when that user no longer exists. -- A kube-scheduler plugin must detect Pods which reference a - ResourceClaim (directly or through a template) and ensure that the - resource is allocated before the Pod gets scheduled, similar to - https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/volume/scheduling/scheduler_binder.go -- Kubelet must be extended to retrieve information from ResourceClaims - and to call a resource kubelet plugin. That plugin returns CDI device ID(s) - which then must be passed to the container runtime. - -For a resource driver the following components are needed: -- *Resource driver controller*: a central component which handles resource allocation - by watching ResourceClaims and updating their status once it is done with - allocation. It may run inside the cluster or outside of it. The only - hard requirement is that it can connect to the API server. -- *Resource kubelet plugin*: a component which cooperates with kubelet to prepare - the usage of the resource on a node. - -A [utility library](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/dynamic-resource-allocation) for resource drivers was developed. -It does not have to be used by drivers, therefore it is not described further -in this KEP. - -### State and communication - -A ResourceClaim object defines what kind of resource is needed and what -the parameters for it are. It is owned by users and namespaced. Additional -parameters are provided by a cluster admin in ResourceClass objects. - -The ResourceClaim spec is immutable. The ResourceClaim -status is reserved for system usage and holds the current state of the -resource. The status must not get lost, which in the past was not ruled -out. For example, status could have been stored in a separate etcd instance -with lower reliability. To recover after a loss, status was meant to be recoverable. -A [recent KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-architecture/2527-clarify-status-observations-vs-rbac) -clarified that status will always be stored reliably and can be used as -proposed in this KEP. - -All relevant state of a ResourceClaim is captured inside that object -itself. For additional information that is needed only during pod scheduling, a -separate PodSchedulingContext gets created by the scheduler if needed. The -PodSchedulingContext has the same name and namespace as the pod and the pod as -its as owner. This ownership must be checked before using a PodSchedulingContext -to detect stale objects that do not match a recreated pod. Such stale -objects get deleted by the garbage collector or the scheduler, depending on who -gets to it first. - -Handling state and communication through objects has two advantages: -- Changes for a resource are atomic, which avoids race conditions. -- The only requirement for deployments is that the components can connect to - the API server. Direct communication is not needed, but in some cases - can be optionally used to improve performance. - -Using a single object is an intentional simplification compared to the -PersistentVolume/PersistentVolumeClaim model for volumes where the additional -PV object was used to capture status. That model allowed operations like -pre-provisioning volumes and then having Kubernetes bind those to claims that -get created later. For resources, the resource driver can and must handle such -pre-provisioning internally. Kubernetes wouldn't know how to match -pre-provisioned resources against claims because it has no understanding about the -parameters. - -The entire state of a resource can be determined by looking at its -ResourceClaim (see [API below](#api) for details), for example: - -- It is **allocated** if and only if `claim.status.allocation` is non-nil and - points to the `AllocationResult`, i.e. the struct where resource drivers - store information about a successful allocation. - -- It is in use if and only if `claim.status.reservedFor` contains one or - more users. It does not matter whether those users, usually pods, are - currently running because that could change at any time. - -- A resource is no longer needed when `claim.deletionTimestamp` is set. It must not - be deallocated yet when it is still in use. - -Some of the race conditions that need to be handled are: - -- A ResourceClaim gets created and deleted again while the resource driver - starts allocating it. Before it actually starts doing anything, the - driver adds a finalizer. Either adding the finalizer or removing the - ResourceClaim win. If the driver wins, it continues with allocation - and can either complete or abort the operation when it notices the non-nil - DeletionTimestamp. Otherwise, allocation gets aborted immediately. - - What this avoids is the situation where an allocation succeed without having - an object where the result can be stored. The driver can also be killed at - any time: when it restarts, the finalizer indicates that allocation may be in - progress and has to be completed or aborted. - - However, users may still force-delete a ResourceClaim, or the entire - cluster might get deleted. Driver implementations must store enough - information elsewhere to detect when some allocated resource is no - longer needed to recover from such scenarios. - -- A ResourceClaim gets deleted and recreated while the resource driver is - adding the finalizer. The driver can update the object to add the finalizer - and then will get a conflict error, which informs the driver that it must - work on a new instance of the claim. In general, patching a ResourceClaim - is only acceptable when it does not lead to race conditions. To detect - delete+recreate, the UID must be added as precondition for a patch. - To detect also potentially conflicting other changes, ResourceVersion - needs to be checked, too. - -- In a cluster with multiple scheduler instances, two pods might get - scheduled concurrently by different schedulers. When they reference - the same ResourceClaim which may only get used by one pod at a time, - only one pod can be scheduled. - - Both schedulers try to add their pod to the `claim.status.reservedFor` field, but only the - update that reaches the API server first gets stored. The other one fails - with a conflict error and the scheduler which issued it knows that it must - put the pod back into the queue, waiting for the ResourceClaim to become - usable again. - -- Two pods get created which both reference the same unallocated claim with - delayed allocation. A single scheduler could detect this special situation - and then trigger allocation only for one of the two pods. But it is simpler - to proceed with pod scheduling for both of them independently, which implies - trying to select a node and allocate for it in parallel. Depending on timing, - the resource driver will see one of the requests for allocation first and - execute it. The other pod then either can share the same resource (if - supported) or must wait until the first one is done with it and reallocate - it. - -- Scheduling a pod and allocating resources for it has been attempted, but one - claim needs to be reallocated to fit the overall resource requirements. A second - pod gets created which references the same claim that is in the process of - being deallocated. Because that is visible in the claim status, scheduling - of the second pod cannot proceed. - -### Custom parameters - -To support arbitrarily complex parameters, both ResourceClass and ResourceClaim -contain one field which references a separate object. The reference contains -API group, kind and name and thus is sufficient for generic clients to -retrieve the parameters. For ResourceClass, that object must be -cluster-scoped. For ResourceClaim, it must be in the same namespace as the -ResourceClaim and thus the Pod. Which kind of objects a resource driver accepts as parameters depends on -the driver. - -This approach was chosen because then validation of the parameters can be done -with a CRD and that validation will work regardless of where the parameters -are needed. - -A resource driver may support modification of the parameters while a resource -is in use ("online resizing"). It may update the ResourceClaim status to -reflect the modified state, for example by increasing the number of concurrent -users. The driver must not allow the state to be modified such -that a user of the resource no longer has access. - -Parameters may get deleted before the ResourceClaim or ResourceClass that -references them. In that case, a pending resource cannot be allocated until the -parameters get recreated. An allocated resource must remain usable and -deallocating it must be possible. To support this, resource drivers must copy -all relevant information: -- For usage, the `claim.status.allocation.resourceHandle` can be hold some copied information - because the ResourceClaim and thus this field must exist. -- For deallocation, drivers should use some other location to handle - cases where a user force-deletes a ResourceClaim or the entire - cluster gets removed. - -### Allocation modes - -Allocation of a resource happens either immediately when a ResourceClaim gets -created (“immediate allocation”) or when a Pod is getting scheduled which -needs the resource (“delayed allocation”), -depending on a flag in the ResourceClaim spec. +With structured parameters, allocation always happens only when a pod needs a +ResourceClaim ("delayed allocation"). With allocation through the driver, it +may also make sense to allocate a ResourceClaim as soon as it gets created +("immediate allocation"). Immediate allocation is useful when allocating a resource is expensive (for example, programming an FPGA) and the resource therefore is meant to be used by -multiple different Pods, either in parallel or one after the other. The -downside is that Pod resource requirements cannot be considered when choosing +multiple different Pods, either in parallel or one after the other. Another use +case is managing resource allocation in a third-party component which fully +understands optimal placement of everything that needs to run on a certain +cluster. + +The downside is that Pod resource requirements cannot be considered when choosing where to allocate. If a resource was allocated so that it is only available on one node and the Pod cannot run there because other resources like RAM or CPU are exhausted on that node, then the Pod cannot run elsewhere. The same applies to resources that are available on a certain subset of the nodes and those nodes are busy. -Delayed allocation solves this by integrating allocation with Pod scheduling: -an attempt to schedule a Pod triggers allocation of pending resources for nodes -that the scheduler has deemed suitable. Scheduling the pod is then put on hold -until all resources are allocated. This avoids scenarios where a Pod is -permanently assigned to a node which can't fit the pod because of the pod's -other resource requirements. - -### Sharing a single ResourceClaim - -Pods reference resource claims in a new `pod.spec.resourceClaims` list. Each -resource in that list can then be made available to one or more containers in -that Pod. Depending on the capabilities defined in the -`claim.status.allocation` by the driver, a ResourceClaim can be used exclusively -by one pod at a time or an unlimited number of pods. Support for additional -constraints (maximum number of pods, maximum number of nodes) could be -added once there are use cases for those. - -Users of a ResourceClaim don't need to be Pods. This KEP specifically supports -Pods as users and describes how kube-scheduler and kubelet will deal with Pods -that depend on a ResourceClaim, but the API and some custom resource driver -might also be useful for controllers to manage resources without using those -resources for Pods. - -### Ephemeral vs. persistent ResourceClaims lifecycle - -A persistent ResourceClaim has a lifecyle that is independent of any particular -pod. It gets created and deleted by the user. This is useful for resources -which are expensive to configure and that can be used multiple times by pods, -either at the same time or one after the other. Such persistent ResourceClaims -get referenced in the pod spec by name. When a PodTemplateSpec in an app -controller spec references a ResourceClaim by name, all pods created by that -controller also use that name and thus share the resources allocated for that -ResourceClaim. - -But often, each Pod is meant to have exclusive access to its own ResourceClaim -instance instead. To support such ephemeral resources without having to modify -all controllers that create Pods, an entry in the new PodSpec.ResourceClaims -list can also be a reference to a ResourceClaimTemplate. When a Pod gets created, such a -template will be used to create a normal ResourceClaim with the Pod as owner -with an -[OwnerReference](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#OwnerReference)), -and then the normal allocation of the resource takes place. Once the pod got -deleted, the Kubernetes garbage collector will also delete the -ResourceClaim. - -This mechanism documents ownership and serves as a fallback for scenarios where -dynamic resource allocation gets disabled in a cluster (for example, during a -downgrade). But it alone is not sufficient: for example, the job controller -does not delete pods immediately when they have completed, which would keep -their resources allocated. Therefore the resource controller watches for pods -that have completed and releases their resource allocations. - -The difference between persistent and ephemeral resources for kube-scheduler -and kubelet is that the name of the ResourceClaim needs to be determined -differently: the name of an ephemeral ResourceClaim is `-`. Ownership must be checked to detect accidental conflicts with -persistent ResourceClaims or previous incarnations of the same ephemeral -resource. This is the same approach that was chosen for [generic ephemeral -volumes](https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/1698-generic-ephemeral-volumes/README.md). -For a resource driver there is no difference. - -Different lifecycles can be combined with different allocation modes +Different lifecycles of a ResourceClaim can be combined with different allocation modes arbitrarily. Some combinations are more useful than others: ``` @@ -969,337 +304,17 @@ arbitrarily. Some combinations are more useful than others: +-----------+------------------------------------+---------------------------------+ ``` -### Coordinating resource allocation through the scheduler - -For immediate allocation, scheduling Pods is simple because the -resource is already allocated and determines the nodes on which the -Pod may run. The downside is that pod scheduling is less flexible. - -For delayed allocation, a node is selected tentatively by the scheduler -in an iterative process where the scheduler suggests some potential nodes -that fit the other resource requirements of a Pod and resource drivers -respond with information about whether they can allocate claims for those -nodes. This exchange of information happens through the `PodSchedulingContext` -for a Pod. The scheduler has to involve the drivers because it -doesn't know what claim parameters mean and where suitable resources are -currently available. - -Once the scheduler is confident that it has enough information to select -a node that will probably work for all claims, it asks the driver(s) to -allocate their resources for that node. If that -succeeds, the Pod can get scheduled. If it fails, the scheduler must -determine whether some other node fits the requirements and if so, -request allocation again. If no node fits because some resources were -already allocated for a node and are only usable there, then those -resources must be released and then get allocated elsewhere. - -This is a summary of the necessary [kube-scheduler changes](#kube-scheduler) in -pseudo-code: - -``` -while { - - if { - if { - - } - } else if { - - } else if { - - } -} -``` - -Randomly picking a node without knowing anything about the resource driver may -or may not succeed. To narrow the choice of suitable nodes for all claims using -a certain resource class, a node selector can be specified in that class. That -selector is static and typically will use labels that determine which nodes may -have resources available. - -To gather information about the current state of resource availability and to -trigger allocation of a claim, the scheduler creates one PodSchedulingContext -for each pod that uses claims. That object is owned by the pod and -will either get deleted by the scheduler when it is done with pod scheduling or -through the garbage collector. In the PodSchedulingContext, the scheduler posts -the list of all potential nodes that it was left with after considering all -other pod constraints and requirements. Resource drivers involved in the -scheduling of the pod respond by adding which of these nodes currently don't -have sufficient resources available. The next scheduling attempt is then more -likely to pick a node for which allocation succeeds. - -This scheduling information is optional and does not have to be in sync with -the current ResourceClaim state, therefore it is okay to store it -separately. - -Allowing the scheduler to trigger allocation in parallel to asking for more -information was chosen because for pods with a single resource claim, the cost -of guessing wrong is low: the driver just needs to inform the scheduler to try -again and provide the additional information. - -Additional heuristics are possible without changing the proposed API. For -example, the scheduler might ask for information and wait a while before -making a choice. This may be more suitable for pods using many different -resource claims because for those, allocation may succeed for some claims and -fail for others, which then may need to go through the recovery flow with -deallocating one or more claims. - -### Resource allocation and usage flow - -The following steps shows how resource allocation works for a resource that -gets defined in a ResourceClaimTemplate and referenced by a Pod. Several of these steps may fail without changing -the system state. They then must be retried until they succeed or something -else changes in the system, like for example deleting objects. - -* **user** creates Pod with reference to ResourceClaimTemplate -* **resource claim controller** checks ResourceClaimTemplate and ResourceClass, - then creates ResourceClaim with Pod as owner -* if *immediate allocation*: - * **resource driver** adds finalizer to claim to prevent deletion -> allocation in progress - * **resource driver** finishes allocation, sets `claim.status.allocation` -> claim ready for use by any pod -* if *pod is pending*: - * **scheduler** filters nodes based on built-in resources and the filter callback of plugins, - which includes constraints imposed by already allocated resources - * if *delayed allocation and resource not allocated yet*: - * if *at least one node fits pod*: - * **scheduler** creates or updates a `PodSchedulingContext` with `podSchedulingContext.spec.potentialNodes=` - * if *exactly one claim is pending (see below)* or *all drivers have provided information*: - * **scheduler** picks one node, sets `podSchedulingContext.spec.selectedNode=` - * if *resource is available for this selected node*: - * **resource driver** adds finalizer to claim to prevent deletion -> allocation in progress - * **resource driver** finishes allocation, sets `claim.status.allocation` and the - pod in `claim.status.reservedFor` -> claim ready for use and reserved for the pod - * else *scheduler needs to know that it must avoid this and possibly other nodes*: - * **resource driver** sets `podSchedulingContext.status.claims[name=name of claim in pod].unsuitableNodes` - * else *pod cannot be scheduled*: - * **scheduler** may trigger deallocation of some claim with delayed allocation by setting `claim.status.deallocationRequested` to true - (see [pseudo-code above](#coordinating-resource-allocation-through-the-scheduler)) or wait - * if *pod not listed in `claim.status.reservedFor` yet* (can occur for immediate allocation): - * **scheduler** adds it to `claim.status.reservedFor` - * if *resource allocated and reserved*: - * **scheduler** sets node in Pod spec -> Pod ready to run - * **scheduler** deletes `PodSchedulingContext` if one exists -* if *node is set for pod*: - * if `resource not reserved for pod` (user might have set the node field): - * **kubelet** refuses to start the pod -> permanent failure - * else `pod may run`: - * **kubelet** asks driver to prepare the resource - * if `resource is prepared`: - * **kubelet** creates container(s) which reference(s) the resource through CDI -> Pod is running -* if *pod has terminated* and *pod deleted*: - * **kubelet** asks driver to unprepare the resource - * **kubelet** allows pod deletion to complete by clearing the `GracePeriod` -* if *pod removed*: - * **garbage collector** deletes ResourceClaim -> adds `claim.deletionTimestamp` because of finalizer -* if *ResourceClaim has `claim.deletionTimestamp` and `claim.status.reservedFor` is empty*: - * **resource driver** deallocates resource - * **resource driver** clears finalizer and `claim.status.allocation` - * **API server** removes ResourceClaim - -When exactly one claim is pending, it is safe to trigger the allocation: if the -node is suitable, the allocation will succeed and the pod can get scheduled -without further delays. If the node is not suitable, allocation fails and the -next attempt can do better because it has more information. The same should not -be done when there are multiple claims because allocation might succeed for -some, but not all of them, which would force the scheduler to recover by asking -for deallocation. It's better to wait for information in this case. - -The flow is similar for a ResourceClaim that gets created as a stand-alone -object by the user. In that case, the Pod reference that ResourceClaim by -name. The ResourceClaim does not get deleted at the end and can be reused by -another Pod and/or used by multiple different Pods at the same time (if -supported by the driver). The resource remains allocated as long as the -ResourceClaim doesn't get deleted by the user. - -If a Pod references multiple claims managed by the same driver, then the driver -can combine updating `podSchedulingContext.claims[*].unsuitableNodes` for all -of them, after considering all claims. - -### Scheduled pods with unallocated or unreserved claims - -There are several scenarios where a Pod might be scheduled (= `pod.spec.nodeName` -set) while the claims that it depends on are not allocated or not reserved for -it: - -* A user might manually create a pod with `pod.spec.nodeName` already set. -* Some special cluster might use its own scheduler and schedule pods without - using kube-scheduler. -* The feature might have been disabled in kube-scheduler while scheduling - a pod with claims. - -The kubelet is refusing to run such pods and reports the situation through -an event (see below). It's an error scenario that should better be avoided. - -Users should avoid this situation by not scheduling pods manually. If they need -it for some reason, they can use a node selector which matches only the desired -node and then let kube-scheduler do the normal scheduling. - -Custom schedulers should emulate the behavior of kube-scheduler and ensure that -claims are allocated and reserved before setting `pod.spec.nodeName`. - -The last scenario might occur during a downgrade or because of an -administrator's mistake. Administrators can fix this by deleting such pods or -ensuring that claims are usable by them. The latter is work that can be -automated in kube-controller-manager: - -- If `pod.spec.nodeName` is set, kube-controller-manager can be sure that - kube-scheduler is not doing anything for the pod. -- If such a pod has unallocated claims, kube-controller-manager can - create a `PodSchedulingContext` with only the `spec.selectedNode` field set - to the name of the node chosen for the pod. There is no need to list - suitable nodes because that choice is permanent, so resource drivers don't - need check for unsuitable nodes. All that they can do is to (re)try allocating - the claim until that succeeds. -- If such a pod has allocated claims that are not reserved for it yet, - then kube-controller-manager can (re)try to reserve the claim until - that succeeds. - -Once all of those steps are complete, kubelet will notice that the claims are -ready and run the pod. Until then it will keep checking periodically, just as -it does for other reasons that prevent a pod from running. - -### Handling non graceful node shutdowns - -When a node is shut down unexpectedly and is tainted with an `out-of-service` -taint with NoExecute effect as explained in the [Non graceful node shutdown KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/2268-non-graceful-shutdown), -all running pods on the node will be deleted by the GC controller and the -resources used by the pods will be deallocated. However, they will not be -un-prepared as the node is down and Kubelet is not running on it. - -Resource drivers should be able to handle this situation correctly and -should not expect `UnprepareNodeResources` to be always called. -If resources are unprepared when `Deallocate` is called, `Deallocate` -might need to perform additional actions to correctly deallocate -resources. - -### API - -The PodSpec gets extended. To minimize the changes in core/v1, all new types -get defined in a new resource group. This makes it possible to revise those -more experimental parts of the API in the future. The new fields in the -PodSpec are gated by the DynamicResourceAllocation feature gate and can only be -set when it is enabled. Initially, they are declared as alpha. Even though they -are alpha, changes to their schema are discouraged and would have to be done by -using new field names. - -ResourceClaim, ResourceClass and ResourceClaimTemplate are new built-in types -in `resource.k8s.io/v1alpha2`. This alpha group must be explicitly enabled in -the apiserver's runtime configuration. Using builtin types was chosen instead -of using CRDs because core Kubernetes components must interact with the new -objects and installation of CRDs as part of cluster creation is an unsolved -problem. - -Secrets are not part of this API: if a resource driver needs secrets, for -example to access its own backplane, then it can define custom parameters for -those secrets and retrieve them directly from the apiserver. This works because -drivers are expected to be written for Kubernetes. - -#### resource.k8s.io - -``` -// ResourceClass is used by administrators to influence how resources -// are allocated. -// -// This is an alpha type and requires enabling the DynamicResourceAllocation -// feature gate. -type ResourceClass struct { - metav1.TypeMeta - // Standard object metadata - // +optional - metav1.ObjectMeta - - // DriverName defines the name of the dynamic resource driver that is - // used for allocation of a ResourceClaim that uses this class. - // - // Resource drivers have a unique name in forward domain order - // (acme.example.com). - DriverName string - - // ParametersRef references an arbitrary separate object that may hold - // parameters that will be used by the driver when allocating a - // resource that uses this class. A dynamic resource driver can - // distinguish between parameters stored here and and those stored in - // ResourceClaimSpec. - // +optional - ParametersRef *ResourceClassParametersReference - - // Only nodes matching the selector will be considered by the scheduler - // when trying to find a Node that fits a Pod when that Pod uses - // a ResourceClaim that has not been allocated yet. - // - // Setting this field is optional. If null, all nodes are candidates. - // +optional - SuitableNodes *core.NodeSelector -} -``` - -A copy of the driver name is necessary to enable usage -of the claim by the kubelet in case the ResourceClass gets -removed in the meantime. It also helps the resource driver -to determine whether it needs to handle a claim that got -marked for deletion. - -``` -// ResourceClaim describes which resources are needed by a resource consumer. -// Its status tracks whether the resource has been allocated and what the -// resulting attributes are. -// -// This is an alpha type and requires enabling the DynamicResourceAllocation -// feature gate. -type ResourceClaim struct { - metav1.TypeMeta - // Standard object metadata - // +optional - metav1.ObjectMeta - - // Spec describes the desired attributes of a resource that then needs - // to be allocated. It can only be set once when creating the - // ResourceClaim. - Spec ResourceClaimSpec - - // Status describes whether the resource is available and with which - // attributes. - // +optional - Status ResourceClaimStatus -} ``` - -The driver must set a finalizer in a ResourceClaim before it attempts to allocate -the resource. It removes the finalizer when a) the allocation -attempt has definitely failed or b) the allocated resource was -deallocated. This helps to ensure that resources are not leaked -during normal operation of the cluster. - -It cannot prevent force-deleting a ResourceClaim by clearing its -finalizers (something that users should never do without being aware -of the consequences) or help when the entire cluster gets deleted. - -``` -// ResourceClaimSpec defines how a resource is to be allocated. type ResourceClaimSpec struct { - // ResourceClassName references the driver and additional parameters - // via the name of a ResourceClass that was created as part of the - // driver deployment. - ResourceClassName string - - // ParametersRef references a separate object with arbitrary parameters - // that will be used by the driver when allocating a resource for the - // claim. - // - // The object must be in the same namespace as the ResourceClaim. - // +optional - ParametersRef *ResourceClaimParametersReference - - // Allocation can start immediately or when a Pod wants to use the - // resource. "WaitForFirstConsumer" is the default. - // +optional - AllocationMode AllocationMode + ... + + // Allocation can start immediately or when a Pod wants to use the + // resource. "WaitForFirstConsumer" is the default. + // +optional + // + // This is an alpha field and requires enabling the DRAControlPlaneController + // feature gate. + AllocationMode AllocationMode } // AllocationMode describes whether a ResourceClaim gets allocated immediately @@ -1310,57 +325,49 @@ type ResourceClaimSpec struct { type AllocationMode string const ( - // When a ResourceClaim has AllocationModeWaitForFirstConsumer, allocation is - // delayed until a Pod gets scheduled that needs the ResourceClaim. The - // scheduler will consider all resource requirements of that Pod and - // trigger allocation for a node that fits the Pod. - AllocationModeWaitForFirstConsumer AllocationMode = "WaitForFirstConsumer" - - // When a ResourceClaim has AllocationModeImmediate, allocation starts - // as soon as the ResourceClaim gets created. This is done without - // considering the needs of Pods that will use the ResourceClaim - // because those Pods are not known yet. - AllocationModeImmediate AllocationMode = "Immediate" + // When a ResourceClaim has AllocationModeWaitForFirstConsumer, allocation is + // delayed until a Pod gets scheduled that needs the ResourceClaim. The + // scheduler will consider all resource requirements of that Pod and + // trigger allocation for a node that fits the Pod. + // + // The ResourceClaim gets deallocated as soon as it is not in use anymore. + AllocationModeWaitForFirstConsumer AllocationMode = "WaitForFirstConsumer" + + // When a ResourceClaim has AllocationModeImmediate and the ResourceClass + // uses a control plane controller, allocation starts + // as soon as the ResourceClaim gets created. This is done without + // considering the needs of Pods that will use the ResourceClaim + // because those Pods are not known yet. + // + // When structured parameters are used, nothing special is done for + // allocation and thus allocation happens when the scheduler handles + // first Pod which needs the ResourceClaim, as with "WaitForFirstConsumer". + // + // In both cases, claims remain allocated even when not in use. + AllocationModeImmediate AllocationMode = "Immediate" ) +``` + +### ResourceClaimStatus extension -// ResourceClaimStatus tracks whether the resource has been allocated and what -// the resulting attributes are. +``` type ResourceClaimStatus struct { - // DriverName is a copy of the driver name from the ResourceClass at - // the time when allocation started. - // +optional - DriverName string - - // Allocation is set by the resource driver once a resource or set of - // resources has been allocated successfully. If this is not specified, the - // resources have not been allocated yet. - // +optional - Allocation *AllocationResult - - // ReservedFor indicates which entities are currently allowed to use - // the claim. A Pod which references a ResourceClaim which is not - // reserved for that Pod will not be started. - // - // There can be at most 32 such reservations. This may get increased in - // the future, but not reduced. - // +optional - ReservedFor []ResourceClaimConsumerReference - - // DeallocationRequested indicates that a ResourceClaim is to be - // deallocated. - // - // The driver then must deallocate this claim and reset the field - // together with clearing the Allocation field. - // - // While DeallocationRequested is set, no new consumers may be added to - // ReservedFor. - // +optional - DeallocationRequested bool -} + ... -// ReservedForMaxSize is the maximum number of entries in -// claim.status.reservedFor. -const ResourceClaimReservedForMaxSize = 32 + // DeallocationRequested indicates that a ResourceClaim is to be + // deallocated. + // + // The driver then must deallocate this claim and reset the field + // together with clearing the Allocation field. + // + // While DeallocationRequested is set, no new consumers may be added to + // ReservedFor. + // + // This is an alpha field and requires enabling the DRAControlPlaneController + // feature gate. + // + // +optional + DeallocationRequested bool ``` DeallocationRequested gets set by the scheduler when it detects @@ -1368,75 +375,49 @@ that pod scheduling cannot proceed because some claim was allocated for a node for which some other pending claims cannot be allocated because that node ran out of resources for those. -``` -// AllocationResult contains attributes of an allocated resource. -type AllocationResult struct { - // ResourceHandles contain the state associated with an allocation that - // should be maintained throughout the lifetime of a claim. Each - // ResourceHandle contains data that should be passed to a specific kubelet - // plugin once it lands on a node. This data is returned by the driver - // after a successful allocation and is opaque to Kubernetes. Driver - // documentation may explain to users how to interpret this data if needed. - // - // Setting this field is optional. It has a maximum size of 32 entries. - // If null (or empty), it is assumed this allocation will be processed by a - // single kubelet plugin with no ResourceHandle data attached. The name of - // the kubelet plugin invoked will match the DriverName set in the - // ResourceClaimStatus this AllocationResult is embedded in. - // - // +listType=atomic - ResourceHandles []ResourceHandle - - // This field will get set by the resource driver after it has allocated - // the resource to inform the scheduler where it can schedule Pods using - // the ResourceClaim. - // - // Setting this field is optional. If null, the resource is available - // everywhere. - // +optional - AvailableOnNodes *core.NodeSelector - - // Shareable determines whether the resource supports more - // than one consumer at a time. - // +optional - Shareable bool -} +It also gets set by kube-controller-manager when it detects that +a claim is no longer in use. + +### ResourceHandle extensions -// AllocationResultResourceHandlesMaxSize represents the maximum number of -// entries in allocation.resourceHandles. -const AllocationResultResourceHandlesMaxSize = 32 +Resource drivers can use each `ResourceHandle` to store data directly or +cross-reference some other place where information is stored. +This data is guaranteed to be available when a Pod is about +to run on a node, in contrast to the ResourceClass which +may have been deleted in the meantime. It's also protected from +modification by a user, in contrast to an annotation. +``` // ResourceHandle holds opaque resource data for processing by a specific kubelet plugin. type ResourceHandle struct { - // DriverName specifies the name of the resource driver whose kubelet - // plugin should be invoked to process this ResourceHandle's data once it - // lands on a node. This may differ from the DriverName set in - // ResourceClaimStatus this ResourceHandle is embedded in. - DriverName string - - // Data contains the opaque data associated with this ResourceHandle. It is - // set by the controller component of the resource driver whose name - // matches the DriverName set in the ResourceClaimStatus this - // ResourceHandle is embedded in. It is set at allocation time and is - // intended for processing by the kubelet plugin whose name matches - // the DriverName set in this ResourceHandle. - // - // The maximum size of this field is 16KiB. This may get increased in the - // future, but not reduced. - // +optional - Data string + ... + + // Data contains the opaque data associated with this ResourceHandle. It is + // set by the controller component of the resource driver whose name + // matches the DriverName set in the ResourceClaimStatus this + // ResourceHandle is embedded in. It is set at allocation time and is + // intended for processing by the kubelet plugin whose name matches + // the DriverName set in this ResourceHandle. + // + // The maximum size of this field is 16KiB. This may get increased in the + // future, but not reduced. + // + // This is an alpha field and requires enabling the DRAControlPlaneController feature gate. + // + // +optional + Data string } // ResourceHandleDataMaxSize represents the maximum size of resourceHandle.data. const ResourceHandleDataMaxSize = 16 * 1024 ``` -Resource drivers can use each `ResourceHandle` to store data directly or -cross-reference some other place where information is stored. -This data is guaranteed to be available when a Pod is about -to run on a node, in contrast to the ResourceClass which -may have been deleted in the meantime. It's also protected from -modification by a user, in contrast to an annotation. + +### PodSchedulingContext + +PodSchedulingContexts get created by a scheduler when it processes a pod which +uses one or more unallocated ResourceClaims with delayed allocation and +allocation of those ResourceClaims is handled by control plane controllers. ``` // PodSchedulingContext holds information that is needed to schedule @@ -1444,25 +425,21 @@ modification by a user, in contrast to an annotation. // mode. // // This is an alpha type and requires enabling the DynamicResourceAllocation -// feature gate. +// and DRAControlPlaneController feature gates. type PodSchedulingContext struct { - metav1.TypeMeta - // Standard object metadata - // +optional - metav1.ObjectMeta + metav1.TypeMeta + // Standard object metadata + // +optional + metav1.ObjectMeta - // Spec describes where resources for the Pod are needed. - Spec PodSchedulingContextSpec + // Spec describes where resources for the Pod are needed. + Spec PodSchedulingContextSpec - // Status describes where resources for the Pod can be allocated. - Status PodSchedulingContextStatus + // Status describes where resources for the Pod can be allocated. + Status PodSchedulingContextStatus } ``` -PodSchedulingContexts get created by a scheduler when it processes -a pod which uses one or more unallocated ResourceClaims with delayed -allocation. - The name of a PodSchedulingContext must be the same as the corresponding Pod. That Pod must be listed as an owner in OwnerReferences to ensure that the PodSchedulingContext gets deleted when no longer needed. Normally the scheduler @@ -1475,19 +452,19 @@ and will be removed soon. ``` // PodSchedulingContextSpec describes where resources for the Pod are needed. type PodSchedulingContextSpec struct { - // SelectedNode is the node for which allocation of ResourceClaims that - // are referenced by the Pod and that use "WaitForFirstConsumer" - // allocation is to be attempted. - SelectedNode string - - // PotentialNodes lists nodes where the Pod might be able to run. - // - // The size of this field is limited to 128. This is large enough for - // many clusters. Larger clusters may need more attempts to find a node - // that suits all pending resources. This may get increased in the - // future, but not reduced. - // +optional - PotentialNodes []string + // SelectedNode is the node for which allocation of ResourceClaims that + // are referenced by the Pod and that use "WaitForFirstConsumer" + // allocation is to be attempted. + SelectedNode string + + // PotentialNodes lists nodes where the Pod might be able to run. + // + // The size of this field is limited to 128. This is large enough for + // many clusters. Larger clusters may need more attempts to find a node + // that suits all pending resources. This may get increased in the + // future, but not reduced. + // +optional + PotentialNodes []string } ``` @@ -1522,812 +499,270 @@ needed through the ResourceClaimStatus.DeallocationRequested field. The ResourceClass.SuiteableNodes node selector can be -used to filter out nodes based on labels. This prevents -adding nodes here that the driver then would need to -reject through UnsuitableNodes. - -``` -// PodSchedulingContextStatus describes where resources for the Pod can be allocated. -type PodSchedulingContextStatus struct { - // ResourceClaims describes resource availability for each - // pod.spec.resourceClaim entry where the corresponding ResourceClaim - // uses "WaitForFirstConsumer" allocation mode. - // +optional - ResourceClaims []ResourceClaimSchedulingStatus - - // If there ever is a need to support other kinds of resources - // than ResourceClaim, then new fields could get added here - // for those other resources. -} -``` - -Each resource driver is responsible for providing information about -those resources in the Pod that the driver manages. It can skip -adding this information once it has allocated the resource. - -A driver must add entries here for all its pending claims, even if -the ResourceSchedulingStatus.UnsuitabeNodes field is empty, -because the scheduler may decide to wait with selecting -a node until it has information from all drivers. - -``` -// ResourceClaimSchedulingStatus contains information about one particular -// ResourceClaim with "WaitForFirstConsumer" allocation mode. -type ResourceClaimSchedulingStatus struct { - // Name matches the pod.spec.resourceClaims[*].Name field. - Name string - - // UnsuitableNodes lists nodes that the ResourceClaim cannot be - // allocated for. - // - // The size of this field is limited to 128, the same as for - // PodSchedulingContextSpec.PotentialNodes. This may get increased in the - // future, but not reduced. - // +optional - UnsuitableNodes []string -} - -// PodSchedulingContextNodeListMaxSize defines the maximum number of entries in -// the node lists that are stored in PodSchedulingContexts. This limit is part -// of the API. -const PodSchedulingContextNodeListMaxSize = 256 -``` - -UnsuitableNodes lists nodes that the claim cannot be allocated for. -Nodes listed here will be ignored by the scheduler when selecting a -node for a Pod. All other nodes are potential candidates, either -because no information is available yet or because allocation might -succeed. - -A change to the PodSchedulingContextSpec.PotentialNodes field and/or a failed -allocation attempt triggers an update of this field: the driver -then checks all nodes listed in PotentialNodes and UnsuitableNodes -and updates UnsuitableNodes. - -It must include the prior UnsuitableNodes in this check because the -scheduler will not list those again in PotentialNodes but they might -still be unsuitable. - -This can change, so the driver must also refresh this information -periodically and/or after changing resource allocation for some -other ResourceClaim until a node gets selected by the scheduler. - -``` -// ResourceClaimTemplate is used to produce ResourceClaim objects. -type ResourceClaimTemplate struct { - metav1.TypeMeta - // Standard object metadata - // +optional - metav1.ObjectMeta - - // Describes the ResourceClaim that is to be generated. - // - // This field is immutable. A ResourceClaim will get created by the - // control plane for a Pod when needed and then not get updated - // anymore. - Spec ResourceClaimTemplateSpec -} - -// ResourceClaimTemplateSpec contains the metadata and fields for a ResourceClaim. -type ResourceClaimTemplateSpec struct { - // ObjectMeta may contain labels and annotations that will be copied into the PVC - // when creating it. No other fields are allowed and will be rejected during - // validation. - // +optional - metav1.ObjectMeta - - // Spec for the ResourceClaim. The entire content is copied unchanged - // into the ResourceClaim that gets created from this template. The - // same fields as in a ResourceClaim are also valid here. - Spec ResourceClaimSpec -} - -// ResourceClassParametersReference contains enough information to let you -// locate the parameters for a ResourceClass. -type ResourceClassParametersReference struct { - // APIGroup is the group for the resource being referenced. It is - // empty for the core API. This matches the group in the APIVersion - // that is used when creating the resources. - // +optional - APIGroup string - // Kind is the type of resource being referenced. This is the same - // value as in the parameter object's metadata. - Kind string - // Name is the name of resource being referenced. - Name string - // Namespace that contains the referenced resource. Must be empty - // for cluster-scoped resources and non-empty for namespaced - // resources. - // +optional - Namespace string -} - -// ResourceClaimParametersReference contains enough information to let you -// locate the parameters for a ResourceClaim. The object must be in the same -// namespace as the ResourceClaim. -type ResourceClaimParametersReference struct { - // APIGroup is the group for the resource being referenced. It is - // empty for the core API. This matches the group in the APIVersion - // that is used when creating the resources. - // +optional - APIGroup string - // Kind is the type of resource being referenced. This is the same - // value as in the parameter object's metadata, for example "ConfigMap". - Kind string - // Name is the name of resource being referenced. - Name string -} - -// ResourceClaimConsumerReference contains enough information to let you -// locate the consumer of a ResourceClaim. The user must be a resource in the same -// namespace as the ResourceClaim. -type ResourceClaimConsumerReference struct { - // APIGroup is the group for the resource being referenced. It is - // empty for the core API. This matches the group in the APIVersion - // that is used when creating the resources. - // +optional - APIGroup string - // Resource is the type of resource being referenced, for example "pods". - Resource string - // Name is the name of resource being referenced. - Name string - // UID identifies exactly one incarnation of the resource. - UID types.UID -} -``` - -`ResourceClassParametersReference` and `ResourceClaimParametersReference` use -the more user-friendly "kind" to identify the object type because those -references are provided by users. `ResourceClaimConsumerReference` is typically -set by the control plane and therefore uses the more technically correct -"resource" name. - -#### core - -``` -type PodSpec { - ... - // ResourceClaims defines which ResourceClaims must be allocated - // and reserved before the Pod is allowed to start. The resources - // will be made available to those containers which consume them - // by name. - // - // This is an alpha field and requires enabling the - // DynamicResourceAllocation feature gate. - // - // This field is immutable. - // - // +featureGate=DynamicResourceAllocation - // +optional - ResourceClaims []PodResourceClaim - ... -} - -type ResourceRequirements { - Limits ResourceList - Requests ResourceList - ... - // Claims lists the names of resources, defined in spec.resourceClaims, - // that are used by this container. - // - // This is an alpha field and requires enabling the - // DynamicResourceAllocation feature gate. - // - // This field is immutable. - // - // +featureGate=DynamicResourceAllocation - // +optional - Claims []ResourceClaim -} - -// ResourceClaim references one entry in PodSpec.ResourceClaims. -type ResourceClaim struct { - // Name must match the name of one entry in pod.spec.resourceClaims of - // the Pod where this field is used. It makes that resource available - // inside a container. - Name string -} -``` - -`Claims` is a list of structs with a single `Name` element because that struct -can be extended later, for example to add parameters that influence how the -resource is made available to a container. This wouldn't be possible if -it was a list of strings. - -``` -// PodResourceClaim references exactly one ResourceClaim through a ClaimSource. -// It adds a name to it that uniquely identifies the ResourceClaim inside the Pod. -// Containers that need access to the ResourceClaim reference it with this name. -type PodResourceClaim struct { - // Name uniquely identifies this resource claim inside the pod. - // This must be a DNS_LABEL. - Name string - - // Source describes where to find the ResourceClaim. - Source ClaimSource -} - -// ClaimSource describes a reference to a ResourceClaim. -// -// Exactly one of these fields should be set. Consumers of this type must -// treat an empty object as if it has an unknown value. -type ClaimSource struct { - // ResourceClaimName is the name of a ResourceClaim object in the same - // namespace as this pod. - ResourceClaimName *string - - // ResourceClaimTemplateName is the name of a ResourceClaimTemplate - // object in the same namespace as this pod. - // - // The template will be used to create a new ResourceClaim, which will - // be bound to this pod. When this pod is deleted, the ResourceClaim - // will also be deleted. The name of the ResourceClaim will be -, where is the - // PodResourceClaim.Name. Pod validation will reject the pod if the - // concatenated name is not valid for a ResourceClaim (e.g. too long). - // - // An existing ResourceClaim with that name that is not owned by the - // pod will not be used for the pod to avoid using an unrelated - // resource by mistake. Scheduling and pod startup are then blocked - // until the unrelated ResourceClaim is removed. - // - // This field is immutable and no changes will be made to the - // corresponding ResourceClaim by the control plane after creating the - // ResourceClaim. - ResourceClaimTemplateName *string -} -``` - -### kube-controller-manager - -The code that creates a ResourceClaim from a ResourceClaimTemplate will -be an almost verbatim copy of the [generic ephemeral volume -code](https://github.com/kubernetes/kubernetes/tree/master/pkg/controller/volume/ephemeral), -just with different types. - -kube-controller-manager will need new [RBAC -permissions](https://github.com/kubernetes/kubernetes/commit/ff3e5e06a79bc69ad3d7ccedd277542b6712514b#diff-2ad93af2302076e0bdb5c7a4ebe68dd3188eee8959c72832181a7597417cd196) that allow creating and updating ResourceClaims. - -kube-controller-manager also removes `claim.status.reservedFor` entries that reference -deleted objects or pods that have completed ("Phase" is "done"). -This is required for pods because kubelet does not have write -permission for ResourceClaimStatus. Pods as user is the common case, so special -code based on a shared pod informer will handle it. All other user types can -be handled through a generic informer or simply polling. - -In addition to updating `claim.status.reservedFor`, kube-controller-manager also deletes -ResourceClaims that are owned by a completed pod to ensure that they -get deallocated as soon as possible once they are not needed anymore. - -Finally, kube-controller-manager tries to make pods runnable that were -[scheduled to a node -prematurely](#scheduled-pods-with-unallocated-or-unreserved-claims) by -triggering allocation and reserving claims when it is certain that -kube-scheduler is not going to handle that. - -### kube-scheduler - -The scheduler plugin for ResourceClaims ("claim plugin" in this section) -needs to implement several extension points. It handles -communication with a resource driver through the apiserver. The [volume -binder -plugin](https://github.com/kubernetes/kubernetes/tree/master/pkg/scheduler/framework/plugins/volumebinding) -can serve as a reference. - -Scheduling of a pod using a ResourceClaim may have to wait for a resource -driver to do something, typically allocating the resource. When the scheduler -notices this, the current scheduling attempt for the pod must stop and the pod -needs to be put back into the work queue. It then gets retried whenever a -ResourceClaim gets added or modified. - -The following extension points are implemented in the new claim plugin. Except -for some unlikely edge cases (see below) there are no API calls during the main -scheduling cycle. Instead, the plugin collects information and updates the -cluster in the separate goroutine which invokes PreBind. - - -#### EventsToRegister - -This registers all cluster events that might make an unschedulable pod -schedulable, like creating a claim that the pod needs or finishing the -allocation of a claim. - -[Queuing hints](https://github.com/kubernetes/enhancements/issues/4247) are -supported. These are callbacks that can limit the effect of a cluster event to -specific pods. For example, allocating a claim only makes those pods -scheduleable which reference the claim. There is no need to try scheduling a pod -which waits for some other claim. Hints are also used to trigger the next -scheduling cycle for a pod immediately when some expected and require event -like "drivers have provided information" occurs, instead of forcing the pod to -go through the backoff queue and the usually 5 second long delay associated -with that. - -Queuing hints are an optional feature of the scheduler, with (as of Kubernetes -1.29) their own `SchedulerQueueingHints` feature gate that defaults to -off. When turned off, performance of scheduling pods with resource claims is -slower compared to a cluster configuration where they are turned on. - -#### PreEnqueue - -This checks whether all claims referenced by a pod exist. If they don't, -scheduling the pod has to wait until the kube-controller-manager or user create -the claims. - -#### Pre-filter - -This checks whether a Pod uses any ResourceClaims. If there are ResourceClaims -with immediate binding that are not allocated yet, then the Pod will be marked -as unschedulable at the moment. - -#### Filter - -This checks whether the given node has access to those ResourceClaims which -were already allocated. - -For unallocated ResourceClaims with delayed allocation, only those nodes are -filtered out that are explicitly listed in their UnsuitableNodes field of their -PodSchedulingContext.Claims entry (if such an entry already exists) or that -don't match the optional ResourceClass.SuitableNodes node selector. - -There are several -reasons why such a deny list is more suitable than an allow list: -- Nodes for which no information is available must pass the filter phase to be - included in the list that will be passed to pre-score and to get copied - into the PodSchedulingContext.PotentialNodes field there. -- A node can already be chosen while there is no information yet and, if - allocation for that node actually works, the Pod can get scheduled sooner. -- Some resource drivers might not have any unsuitable nodes, for example - because they modify containers and that works on all nodes at all - times. Forcing such drivers to set an allow list would cause unnecessary - work. - -In its state for the Pod the claim plugin must remember when it rejected a -node because of UnsuitableNodes. That information will be used in Post-filter -to deallocate resources. - -#### Post-filter - -This is called when no suitable node could be found. If the Pod depends on ResourceClaims with delayed -allocation, then deallocating one or more of these ResourceClaims may make the -Pod schedulable after allocating the resource elsewhere. Therefore each -ResourceClaim with delayed allocation is checked whether all of the following -conditions apply: -- allocated -- not currently in use -- it was the reason why some node could not fit the Pod, as recorded earlier in - Filter - -One of the ResourceClaims satisfying these criteria is picked randomly and deallocation -is requested by setting the ResourceClaimStatus.DeallocationRequested field. The scheduler then needs to wait -for the resource driver to react to that change and deallocate the resource. - -This may make it possible to run the Pod -elsewhere. If it still doesn't help, deallocation may continue with another -ResourceClaim, if there is one. To prevent deallocating all resources merely -because the scheduler happens to check quickly, the next deallocation will only -requested when there is none currently pending. - -At the moment, the claim plugin has no information that might enable it to -prioritize which resource to deallocate first. Future extensions of this KEP -might attempt to improve this. - -This is currently using blocking API calls. It's quite rare because this -situation can only arise when there are multiple claims per pod and allocation -for one of them fails despite all drivers agreeing that a node should be -suitable, or when reusing a claim for multiple pods (not a common use case) and -the original node became unusable for the next pod. - -#### Pre-score - -This is passed a list of nodes that have passed filtering by the claim -plugin and the other plugins. That list is stored by the claim plugin and will -be copied to PodSchedulingContextSpec.PotentialNodes when the claim plugin -creates or updates the object in Reserve. - -Pre-score is not called when there is only a single potential node. In that -case Reserve will store the selected node in PodSchedulingContextSpec.PotentialNodes. - -#### Reserve - -A node has been chosen for the Pod. - -If using delayed allocation and one or more claims have not been allocated yet, -the claim plugin now needs to decide whether it wants to trigger allocation by -setting the PodSchedulingContextSpec.SelectedNode field. For a single unallocated -claim that is safe even if no information about unsuitable nodes is available -because the allocation will either succeed or fail. For multiple such claims -allocation only gets triggered when that information is available, to minimize -the risk of getting only some but not all claims allocated. In both cases the -PodSchedulingContext gets created or updated as needed. This is also where the -PodSchedulingContextSpec.PotentialNodes field gets set. - -If all resources have been allocated already, -the claim plugin ensures that the Pod is listed in the `claim.status.reservedFor` field -of its ResourceClaims. The driver can and should already have added -the Pod when specifically allocating the claim for it, so it may -be possible to skip this update. - -All the PodSchedulingContext and ResourceClaim updates are recorded in the -plugin state. They will be written to the cluster during PreBind. - -If some resources are not allocated yet or reserving an allocated resource -fails, the scheduling attempt needs to be aborted and retried at a later time -or when the statuses change. The Reserve call itself never fails. If resources -are not currently available, that information is recorded in the plugin state -and will cause the PreBind call to fail instead. - -#### PreBind - -This is called in a separate goroutine. The plugin now checks all the -information gathered earlier and updates the cluster accordingly. If some -claims are not allocated or not reserved, PreBind fails and the pod must be -retried. - -#### Unreserve - -The claim plugin removes the Pod from the `claim.status.reservedFor` field -because it cannot be scheduled after all. - -This is necessary to prevent a deadlock: suppose there are two stand-alone -claims that only can be used by one pod at a time and two pods which both -reference them. Both pods will get scheduled independently, perhaps even by -different schedulers. When each pod manages to allocate and reserve one claim, -then neither of them can get scheduled because they cannot reserve the other -claim. - -Giving up the reservations in Unreserve means that the next pod scheduling -attempts have a chance to succeed. It's non-deterministic which pod will win, -but eventually one of them will. Not giving up the reservations would lead to a -permanent deadlock that somehow would have to be detected and resolved to make -progress. - -Unreserve is called in two scenarios: -- In the main goroutine when scheduling a pod has failed: in that case the plugin's - Reserve call hasn't actually changed the claim status yet, so there is nothing - that needs to be rolled back. -- After binding has failed: this runs in a goroutine, so reverting the - `claim.status.reservedFor` with a blocking call is acceptable. - -### Cluster Autoscaler - -When [Cluster -Autoscaler](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#cluster-autoscaler) -encounters a pod that uses a resource claim for node-local resources, it needs -to understand the parameters for the claim and available capacity in order -to simulate the effect of allocating claims as part of scheduling and of -creating or removing nodes. - -This is not possible with opaque parameters as described in this KEP. If a DRA -driver developer wants to support Cluster Autoscaler, they have to use semantic -parameters. Semantic parameters are an extension of this KEP that is defined in -[KEP #4381](https://github.com/kubernetes/enhancements/issues/4381). - -Semantic parameters are not necessary for network-attached resources because -adding or removing nodes doesn't change their availability and thus Cluster -Autoscaler does not need to understand their parameters. - -### kubelet - -#### Managing resources - -kubelet must ensure that resources are ready for use on the node before running -the first Pod that uses a specific resource instance and make the resource -available elsewhere again when the last Pod has terminated that uses it. For -both operations, kubelet calls a resource kubelet plugin as explained in the next -section. - -Pods that are not listed in ReservedFor or where the ResourceClaim doesn't -exist at all must not be allowed to run. Instead, a suitable event must be -emitted which explains the problem. Such a situation can occur as part of -downgrade scenarios. - -If this was the last Pod on the node that uses the specific -resource instance, then NodeUnprepareResource (see below) must have been called -successfully before allowing the pod to be deleted. This ensures that network-attached resource are available again -for other Pods, including those that might get scheduled to other nodes. It -also signals that it is safe to deallocate and delete the ResourceClaim. - - -![kubelet](./kubelet.png) - -#### Communication between kubelet and resource kubelet plugin - -Resource kubelet plugins are discovered through the [kubelet plugin registration -mechanism](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/#device-plugin-registration). A -new "ResourcePlugin" type will be used in the Type field of the -[PluginInfo](https://pkg.go.dev/k8s.io/kubelet/pkg/apis/pluginregistration/v1#PluginInfo) -response to distinguish the plugin from device and CSI plugins. - -Under the advertised Unix Domain socket the kubelet plugin provides one of the -following supported gRPC interface versions. It was inspired by -[CSI](https://github.com/container-storage-interface/spec/blob/master/spec.md), -with “volume” replaced by “resource” and volume specific parts removed. - -Key difference between interface versions: - -- [v1alpha2](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubelet/pkg/apis/dra/v1alpha2/api.proto) -interface provides resource claim information to a kubelet plugin one at a -time. **Note: v1alpha2 will be deprecared, switch to v1alpha3** -- [v1alpha3](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubelet/pkg/apis/dra/v1alpha3/api.proto) -interface provides information about all resource claims of a pod that belong -to a particular driver in a single call. This way the kubelet plugin of this driver can consider all -resources that need to be prepared or unprepared for the pod simultaneously. +used to filter out nodes based on labels. This prevents +adding nodes here that the driver then would need to +reject through UnsuitableNodes. +``` +// PodSchedulingContextStatus describes where resources for the Pod can be allocated. +type PodSchedulingContextStatus struct { + // ResourceClaims describes resource availability for each + // pod.spec.resourceClaim entry where the corresponding ResourceClaim + // uses "WaitForFirstConsumer" allocation mode. + // +optional + ResourceClaims []ResourceClaimSchedulingStatus + + // If there ever is a need to support other kinds of resources + // than ResourceClaim, then new fields could get added here + // for those other resources. +} +``` -##### `NodePrepareResource` +Each resource driver is responsible for providing information about +those resources in the Pod that the driver manages. It can skip +adding this information once it has allocated the resource. -This RPC is called by the kubelet when a Pod that wants to use the specified -resource is scheduled on a node. The Plugin SHALL assume that this RPC will be -executed on the node where the resource will be used. +A driver must add entries here for all its pending claims, even if +the ResourceSchedulingStatus.UnsuitabeNodes field is empty, +because the scheduler may decide to wait with selecting +a node until it has information from all drivers. -ResourceClaim.meta.Namespace, ResourceClaim.meta.UID, ResourceClaim.Name and -one of the ResourceHandles from the ResourceClaimStatus.AllocationResult with -a matching DriverName should be passed to the Plugin as parameters to identify -the claim and perform resource preparation. +``` +// ResourceClaimSchedulingStatus contains information about one particular +// ResourceClaim with "WaitForFirstConsumer" allocation mode. +type ResourceClaimSchedulingStatus struct { + // Name matches the pod.spec.resourceClaims[*].Name field. + Name string -ResourceClaim parameters (namespace, UUID, name) are useful for debugging. -They enable the Plugin to retrieve the full ResourceClaim object, should it -ever be needed (normally it shouldn't). + // UnsuitableNodes lists nodes that the ResourceClaim cannot be + // allocated for. + // + // The size of this field is limited to 128, the same as for + // PodSchedulingContextSpec.PotentialNodes. This may get increased in the + // future, but not reduced. + // +optional + UnsuitableNodes []string +} -The Plugin SHALL return fully qualified device name[s]. +// PodSchedulingContextNodeListMaxSize defines the maximum number of entries in +// the node lists that are stored in PodSchedulingContexts. This limit is part +// of the API. +const PodSchedulingContextNodeListMaxSize = 256 +``` -The Plugin SHALL ensure that there are json file[s] in CDI format -for the allocated resource. These files SHALL be used by runtime to -update runtime configuration before creating containers that use the -resource. +UnsuitableNodes lists nodes that the claim cannot be allocated for. +Nodes listed here will be ignored by the scheduler when selecting a +node for a Pod. All other nodes are potential candidates, either +because no information is available yet or because allocation might +succeed. -This operation SHALL do as little work as possible as it’s called -after a pod is scheduled to a node. All potentially failing operations -SHALL be done during allocation phase. +A change to the PodSchedulingContextSpec.PotentialNodes field and/or a failed +allocation attempt triggers an update of this field: the driver +then checks all nodes listed in PotentialNodes and UnsuitableNodes +and updates UnsuitableNodes. -This operation MUST be idempotent. If the resource corresponding to -the `resource_id` has already been prepared, the Plugin MUST reply `0 -OK`. +It must include the prior UnsuitableNodes in this check because the +scheduler will not list those again in PotentialNodes but they might +still be unsuitable. -If this RPC failed, or kubelet does not know if it failed or not, it -MAY choose to call `NodePrepareResource` again, or choose to call -`NodeUnprepareResource`. +This can change, so the driver must also refresh this information +periodically and/or after changing resource allocation for some +other ResourceClaim until a node gets selected by the scheduler. -On a successful call this RPC should return set of fully qualified -CDI device names, which kubelet MUST pass to the runtime through the CRI -protocol. For version v1alpha3, the RPC should return multiple sets of -fully qualified CDI device names, one per claim that was sent in the input parameters. +### Coordinating resource allocation through the scheduler -###### v1alpha2 +For immediate allocation, scheduling Pods is simple because the +resource is already allocated and determines the nodes on which the +Pod may run. The downside is that pod scheduling is less flexible. -> [!WARNING] -> v1alpha2 will be deprecated, switch to v1alpha3. +For delayed allocation, a node is selected tentatively by the scheduler +in an iterative process where the scheduler suggests some potential nodes +that fit the other resource requirements of a Pod and resource drivers +respond with information about whether they can allocate claims for those +nodes. This exchange of information happens through the `PodSchedulingContext` +for a Pod. The scheduler has to involve the drivers because it +doesn't know what claim parameters mean and where suitable resources are +currently available. -
-v1alpha2 +Once the scheduler is confident that it has enough information to select +a node that will probably work for all claims, it asks the driver(s) to +allocate their resources for that node. If that +succeeds, the Pod can get scheduled. If it fails, the scheduler must +determine whether some other node fits the requirements and if so, +request allocation again. If no node fits because some resources were +already allocated for a node and are only usable there, then those +resources must be released and then get allocated elsewhere. -```protobuf -message NodePrepareResourceRequest { - // The ResourceClaim namespace (ResourceClaim.meta.Namespace). - // This field is REQUIRED. - string namespace = 1; - // The UID of the Resource claim (ResourceClaim.meta.UUID). - // This field is REQUIRED. - string claim_uid = 2; - // The name of the Resource claim (ResourceClaim.meta.Name) - // This field is REQUIRED. - string claim_name = 3; - // Resource handle (AllocationResult.ResourceHandles[*].Data) - // This field is REQUIRED. - string resource_handle = 4; -} +This is a summary of the necessary [kube-scheduler changes](#kube-scheduler) in +pseudo-code: -message NodePrepareResourceResponse { - // These are the additional devices that kubelet must - // make available via the container runtime. A resource - // may have zero or more devices. - repeated string cdi_device = 1; +``` +while { + + if { + if { + + } + } else if { + + } else if { + + } } ``` -
- -###### v1alpha3 - -```protobuf -message NodePrepareResourcesRequest { - // The list of ResourceClaims that are to be prepared. - repeated Claim claims = 1; -} +Randomly picking a node without knowing anything about the resource driver may +or may not succeed. To narrow the choice of suitable nodes for all claims using +a certain resource class, a node selector can be specified in that class. That +selector is static and typically will use labels that determine which nodes may +have resources available. -message Claim { - // The ResourceClaim namespace (ResourceClaim.meta.Namespace). - // This field is REQUIRED. - string namespace = 1; - // The UID of the Resource claim (ResourceClaim.meta.UUID). - // This field is REQUIRED. - string uid = 2; - // The name of the Resource claim (ResourceClaim.meta.Name) - // This field is REQUIRED. - string name = 3; - // Resource handle (AllocationResult.ResourceHandles[*].Data) - // This field is REQUIRED. - string resource_handle = 4; -} +To gather information about the current state of resource availability and to +trigger allocation of a claim, the scheduler creates one PodSchedulingContext +for each pod that uses claims. That object is owned by the pod and +will either get deleted by the scheduler when it is done with pod scheduling or +through the garbage collector. In the PodSchedulingContext, the scheduler posts +the list of all potential nodes that it was left with after considering all +other pod constraints and requirements. Resource drivers involved in the +scheduling of the pod respond by adding which of these nodes currently don't +have sufficient resources available. The next scheduling attempt is then more +likely to pick a node for which allocation succeeds. -message NodePrepareResourcesResponse { - // The ResourceClaims for which preparation was done - // or attempted, with claim_uid as key. - // - // It is an error if some claim listed in NodePrepareResourcesRequest - // does not get prepared. NodePrepareResources - // will be called again for those that are missing. - map claims = 1; -} +This scheduling information is optional and does not have to be in sync with +the current ResourceClaim state, therefore it is okay to store it +separately. -message NodePrepareResourceResponse { - // These are the additional devices that kubelet must - // make available via the container runtime. A resource - // may have zero or more devices. - repeated string cdi_devices = 1 [(gogoproto.customname) = "CDIDevices"]; - // If non-empty, preparing the ResourceClaim failed. - // cdi_devices is ignored in that case. - string error = 2; -} -``` +Allowing the scheduler to trigger allocation in parallel to asking for more +information was chosen because for pods with a single resource claim, the cost +of guessing wrong is low: the driver just needs to inform the scheduler to try +again and provide the additional information. -CRI protocol MUST be extended for this purpose: - - * CDIDevice structure should be added to the CRI specification -```protobuf -// CDIDevice specifies a CDI device information. -message CDIDevice { - // Fully qualified CDI device name - // for example: vendor.com/gpu=gpudevice1 - // see more details in the CDI specification: - // https://github.com/container-orchestrated-devices/container-device-interface/blob/main/SPEC.md - string name = 1; -} -``` - * CDI devices should be added to the ContainerConfig structure: -```protobuf -// ContainerConfig holds all the required and optional fields for creating a -// container. -message ContainerConfig { - // Metadata of the container. This information will uniquely identify the - // container, and the runtime should leverage this to ensure correct - // operation. The runtime may also use this information to improve UX, such - // as by constructing a readable name. - ContainerMetadata metadata = 1 ; - // Image to use. - ImageSpec image = 2; - // Command to execute (i.e., entrypoint for docker) - repeated string command = 3; -... - // CDI devices for the container. - repeated CDIDevice cdi_devices = 17; -} -``` +Additional heuristics are possible without changing the proposed API. For +example, the scheduler might ask for information and wait a while before +making a choice. This may be more suitable for pods using many different +resource claims because for those, allocation may succeed for some claims and +fail for others, which then may need to go through the recovery flow with +deallocating one or more claims. -###### NodePrepareResource Errors - -If the plugin is unable to complete the NodePrepareResource call -successfully, it MUST return a non-ok gRPC code in the gRPC status. -If the conditions defined below are encountered, the plugin MUST -return the specified gRPC error code. Kubelet MUST implement the -specified error recovery behavior when it encounters the gRPC error -code. - -| Condition | gRPC Code | Description | Recovery Behavior | -|-----------|-----------|-------------|-------------------| -| Resource does not exist | 5 NOT_FOUND | Indicates that a resource corresponding to the specified `resource_id` does not exist. | Caller MUST verify that the `resource_id` is correct and that the resource is accessible and has not been deleted before retrying with exponential back off. | - - -##### `NodeUnprepareResource` - -A Kubelet Plugin MUST implement this RPC call. This RPC is a reverse -operation of `NodePrepareResource`. This RPC MUST undo the work by -the corresponding `NodePrepareResource`. This RPC SHALL be called by -kubelet at least once for each successful `NodePrepareResource`. The -Plugin SHALL assume that this RPC will be executed on the node where -the resource is being used. - -This RPC is called by the kubelet when the last Pod using the resource is being -deleted or has reached a final state ("Phase" is "done"). - -This operation MUST be idempotent. If this RPC failed, or kubelet does -not know if it failed or not, it can choose to call -`NodeUnprepareResource` again. - -###### v1alpha2 - -> [!WARNING] -> v1alpha2 will be deprecated, switch to v1alpha3. - -
-v1alpha2 - -```protobuf -message NodeUnprepareResourceRequest { - // The ResourceClaim namespace (ResourceClaim.meta.Namespace). - // This field is REQUIRED. - string namespace = 1; - // The UID of the Resource claim (ResourceClaim.meta.UUID). - // This field is REQUIRED. - string claim_uid = 2; - // The name of the Resource claim (ResourceClaim.meta.Name) - // This field is REQUIRED. - string claim_name = 3; - // Resource handle (AllocationResult.ResourceHandles[*].Data) - // This field is REQUIRED. - string resource_handle = 4; -} +### Resource allocation and usage flow -message NodeUnprepareResourceResponse { - // Intentionally empty. -} -``` +The following steps shows how resource allocation works for a resource that +gets defined in a ResourceClaimTemplate and referenced by a Pod. Several of these steps may fail without changing +the system state. They then must be retried until they succeed or something +else changes in the system, like for example deleting objects. -
+* **user** creates Pod with reference to ResourceClaimTemplate +* **resource claim controller** checks ResourceClaimTemplate and ResourceClass, + then creates ResourceClaim with Pod as owner +* if *immediate allocation*: + * **resource driver** adds finalizer to claim to prevent deletion -> allocation in progress + * **resource driver** finishes allocation, sets `claim.status.allocation` -> claim ready for use by any pod +* if *pod is pending*: + * **scheduler** filters nodes based on built-in resources and the filter callback of plugins, + which includes constraints imposed by already allocated resources + * if *delayed allocation and resource not allocated yet*: + * if *at least one node fits pod*: + * **scheduler** creates or updates a `PodSchedulingContext` with `podSchedulingContext.spec.potentialNodes=` + * if *exactly one claim is pending (see below)* or *all drivers have provided information*: + * **scheduler** picks one node, sets `podSchedulingContext.spec.selectedNode=` + * if *resource is available for this selected node*: + * **resource driver** adds finalizer to claim to prevent deletion -> allocation in progress + * **resource driver** finishes allocation, sets `claim.status.allocation` and the + pod in `claim.status.reservedFor` -> claim ready for use and reserved for the pod + * else *scheduler needs to know that it must avoid this and possibly other nodes*: + * **resource driver** sets `podSchedulingContext.status.claims[name=name of claim in pod].unsuitableNodes` + * else *pod cannot be scheduled*: + * **scheduler** may trigger deallocation of some claim with delayed allocation by setting `claim.status.deallocationRequested` to true + (see [pseudo-code above](#coordinating-resource-allocation-through-the-scheduler)) or wait + * if *pod not listed in `claim.status.reservedFor` yet* (can occur for immediate allocation): + * **scheduler** adds it to `claim.status.reservedFor` + * if *resource allocated and reserved*: + * **scheduler** sets node in Pod spec -> Pod ready to run + * **scheduler** deletes `PodSchedulingContext` if one exists +* if *node is set for pod*: + * if `resource not reserved for pod` (user might have set the node field): + * **kubelet** refuses to start the pod -> permanent failure + * else `pod may run`: + * **kubelet** asks driver to prepare the resource + * if `resource is prepared`: + * **kubelet** creates container(s) which reference(s) the resource through CDI -> Pod is running +* if *pod has terminated* and *pod deleted*: + * **kubelet** asks driver to unprepare the resource + * **kubelet** allows pod deletion to complete by clearing the `GracePeriod` +* if *pod removed*: + * **garbage collector** deletes ResourceClaim -> adds `claim.deletionTimestamp` because of finalizer +* if *ResourceClaim has `claim.deletionTimestamp` and `claim.status.reservedFor` is empty*: + * **resource driver** deallocates resource + * **resource driver** clears finalizer and `claim.status.allocation` + * **API server** removes ResourceClaim -###### v1alpha3 +When exactly one claim is pending, it is safe to trigger the allocation: if the +node is suitable, the allocation will succeed and the pod can get scheduled +without further delays. If the node is not suitable, allocation fails and the +next attempt can do better because it has more information. The same should not +be done when there are multiple claims because allocation might succeed for +some, but not all of them, which would force the scheduler to recover by asking +for deallocation. It's better to wait for information in this case. -```protobuf -message NodeUnprepareResourcesRequest { - // The list of ResourceClaims that are to be unprepared. - repeated Claim claims = 1; -} +The flow is similar for a ResourceClaim that gets created as a stand-alone +object by the user. In that case, the Pod reference that ResourceClaim by +name. The ResourceClaim does not get deleted at the end and can be reused by +another Pod and/or used by multiple different Pods at the same time (if +supported by the driver). The resource remains allocated as long as the +ResourceClaim doesn't get deleted by the user. -message NodeUnprepareResourcesResponse { - // The ResourceClaims for which preparation was reverted. - // The same rules as for NodePrepareResourcesResponse.claims - // apply. - map claims = 1; -} +If a Pod references multiple claims managed by the same driver, then the driver +can combine updating `podSchedulingContext.claims[*].unsuitableNodes` for all +of them, after considering all claims. -message NodeUnprepareResourceResponse { - // If non-empty, unpreparing the ResourceClaim failed. - string error = 1; -} +### Scheduled pods with unallocated or unreserved claims -message Claim { - // The ResourceClaim namespace (ResourceClaim.meta.Namespace). - // This field is REQUIRED. - string namespace = 1; - // The UID of the Resource claim (ResourceClaim.meta.UUID). - // This field is REQUIRED. - string uid = 2; - // The name of the Resource claim (ResourceClaim.meta.Name) - // This field is REQUIRED. - string name = 3; - // Resource handle (AllocationResult.ResourceHandles[*].Data) - // This field is REQUIRED. - string resource_handle = 4; -} -``` +As with structured parameters, there are several scenarios where a Pod might be +scheduled (= `pod.spec.nodeName` set) while the claims that it depends on are +not allocated or not reserved for it. The kubelet is refusing to run such pods. + +In addition to the solutions described for structured parameters, using a control +plane controller provides one additional solution: +- When kube-controller-manager observes that allocation is missing, it creates + a `PodSchedulingContext` with only the `spec.selectedNode` field set to the + name of the node chosen for the pod. There is no need to list suitable nodes + because that choice is permanent, so resource drivers don't need check for + unsuitable nodes. All that they can do is to (re)try allocating the claim + until that succeeds. +- If such a pod has allocated claims that are not reserved for it yet, + then kube-controller-manager can (re)try to reserve the claim until + that succeeds. -###### NodeUnprepareResource Errors +Once all of those steps are complete, kubelet will notice that the claims are +ready and run the pod. Until then it will keep checking periodically, just as +it does for other reasons that prevent a pod from running. -If the plugin is unable to complete the NodeUprepareResource call -successfully, it MUST return a non-ok gRPC code in the gRPC status. -If the conditions defined below are encountered, the plugin MUST -return the specified gRPC error code. Kubelet MUST implement the -specified error recovery behavior when it encounters the gRPC error -code. +### Cluster Autoscaler -| Condition | gRPC Code | Description | Recovery Behavior | -|-----------|-----------|-------------|-------------------| -| Resource does not exist | 5 NOT_FOUND | Indicates that a resource corresponding to the specified `resource_id` does not exist. | Caller MUST verify that the `resource_id` is correct and that the resource is accessible and has not been deleted before retrying with exponential back off. | +When [Cluster +Autoscaler](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler#cluster-autoscaler) +encounters a pod that uses a resource claim for node-local resources, it needs +to understand the parameters for the claim and available capacity in order +to simulate the effect of allocating claims as part of scheduling and of +creating or removing nodes. -#### Implementing optional resources +This is not possible with opaque parameters as described in this KEP. If a DRA +driver developer wants to support Cluster Autoscaler, they have to use semantic +parameters. Semantic parameters are an extension of this KEP that is defined in +[KEP #4381](https://github.com/kubernetes/enhancements/issues/4381). -This can be handled entirely by a resource driver: its parameters can support a -range starting at zero or a boolean flag that indicates that something is not a -hard requirement. When asked to filter nodes for delayed allocation, the driver -reports nodes where the resource is available and only falls back to those -without it when resources are exhausted. When asked to allocate, it reserves -actual resources if possible, but also proceeds with marking the ResourceClaim -as allocated if that is not possible. Kubernetes then can schedule a pod using -the ResourceClaim. The pod needs to determine through information passed in by -the resource driver which resources are actually available to it. +Semantic parameters are not necessary for network-attached resources because +adding or removing nodes doesn't change their availability and thus Cluster +Autoscaler does not need to understand their parameters. -#### Implementing a plugin for node resources +### Implementing a plugin for node resources The proposal depends on a central resource driver controller. Implementing that part poses an additional challenge for drivers that manage resources @@ -2361,6 +796,19 @@ module. can unset the selected node field to trigger another allocation attempt elsewhere. +### Implementing optional resources + +This can be handled entirely by a resource driver: its parameters can support a +range starting at zero or a boolean flag that indicates that something is not a +hard requirement. When asked to filter nodes for delayed allocation, the driver +reports nodes where the resource is available and only falls back to those +without it when resources are exhausted. When asked to allocate, it reserves +actual resources if possible, but also proceeds with marking the ResourceClaim +as allocated if that is not possible. Kubernetes then can schedule a pod using +the ResourceClaim. The pod needs to determine through information passed in by +the resource driver which resources are actually available to it. + + ### Test Plan [X] I/we understand the owners of the involved components may require updates to @@ -2819,6 +1267,7 @@ instructions. - Kubernetes 1.28: API break (ResourceClaim names for claims created from a template are generated instead of deterministic), scheduler performance enhancements (no more backoff delays). +- Kubernetes 1.29, 1.30: most blocking API calls moved into Pod binding goroutine ## Drawbacks @@ -2826,197 +1275,5 @@ instructions. Why should this KEP _not_ be implemented? --> -## Alternatives - -### Semantic Parameters instead of PodSchedulingContext - -When a DRA driver uses semantic parameters, there is no need for a DRA driver controller -which allocates the claim -and no need for communication between scheduler and such a controller. The -PodSchedulingContext object and the associated support in the scheduler then -aren't needed. Once semantic parameters are mature enough and confirmed to be -sufficient for DRA drivers, it might become possible to remove the -PodSchedulingContext API from this KEP. - -It might still be needed for other drivers and use cases, which then can be -discussed in a new KEP which focuses specifically on those use cases. - -### ResourceClaimTemplate - -Instead of creating a ResourceClaim from a template, the -PodStatus could be extended to hold the same information as a -ResourceClaimStatus. Every component which works with that information -then needs permission and extra code to work with PodStatus. Creating -an extra object seems simpler. - -### Reusing volume support as-is - -ResourceClaims are similar to PersistentVolumeClaims and also a lot of -the associated logic is similar. An [early -prototype](https://github.com/intel/proof-of-concept-cdi) used a -custom CSI driver to manage resources. - -The user experience with that approach is poor because per-resource -parameters must be stored in annotations of a PVC due to the lack of -custom per-PVC parameters. Passing annotations as additional parameters was [proposed -before](https://github.com/kubernetes-csi/external-provisioner/issues/86) -but were essentially [rejected by -SIG-Storage](https://github.com/kubernetes-csi/external-provisioner/issues/86#issuecomment-465836185) -because allowing apps to set custom parameters would make apps -non-portable. - -The current volume support also has open issues that affect the -“volume as resource” approach: Multiple different Pods on a node are -allowed to use the same -volume. https://github.com/kubernetes/enhancements/pull/2489 will -address that, but is still work in progress. Recovery from a bad node -selection during delayed binding may get stuck when a Pod has multiple -volumes because volumes are not getting deleted after a partial -provisioning. A proposal to fix that needs further work -(https://github.com/kubernetes/enhancements/pull/1703). Each “fake” -CSI driver would have to implement and install a scheduler extender -because storage capacity tracking only considers volume size as -criteria for selecting nodes, which is not applicable for custom -resources. - -### Extend volume support - -The StorageClass and PersistentVolumeClaim structs could be extended -to allow custom parameters. Together with an extension of the CSI -standard that would address the main objection against the previous -alternative. - -However, SIG-Storage and the CSI community would have to agree to this -kind of reuse and accept that some of the code maintained by them -becomes more complex because of these new use cases. - -### Extend Device Plugins - -The device plugins API could be extended to implement some of the -requirements mentioned in the “Motivation” section of this -document. There were certain attempts to do it, for example an attempt -to [add ‘Deallocate’ API call](https://github.com/kubernetes/enhancements/pull/1949) and [pass pod annotations to 'Allocate' API call](https://github.com/kubernetes/kubernetes/pull/61775) - -However, most of the requirements couldn’t be satisfied using this -approach as they would require major incompatible changes in the -Device Plugins API. For example: partial and optional resource -allocation couldn’t be done without changing the way resources are -currently declared on the Pod and Device Plugin level. - -Extending the device plugins API to use [Container Device Interface](https://github.com/container-orchestrated-devices/container-device-interface) -would help address some of the requirements, but not all of them. - -NodePrepareResource and NodeUnprepareResource could be added to the Device Plugins API and only get called for -resource claims. - -However, this would mean that -developers of the device plugins would have to implement mandatory -API calls (ListAndWatch, Allocate), which could create confusion -as those calls are meaningless for the Dynamic Resource Allocation -purposes. - -Even worse, existing device plugins would have to implement the new -calls with stubs that return errors because the generated Go interface -will require them. - -It should be also taken into account that device plugins API is -beta. Introducing incompatible changes to it may not be accepted by -the Kubernetes community. - -### Webhooks instead of ResourceClaim updates - -In the current design, scheduler and the third-party resource driver communicate by -updating fields in a ResourceClaim. This has several advantages compared to an -approach were kube-scheduler retrieves information from the resource driver -via HTTP: -* No need for a new webhook API. -* Simpler deployment of a resource driver because all it needs are - credentials to communicate with the apiserver. -* Current status can be checked by querying the ResourceClaim. - -The downside is higher load on the apiserver and an increase of the size of -ResourceClaim objects. - -### ResourceDriver - -Similar to CSIDriver for storage, a separate object describing a resource -driver might be useful at some point. At the moment it is not needed yet and -therefore not part of the v1alpha2 API. If it becomes necessary to describe -optional features of a resource driver, such a ResourceDriver type might look -like this: - -``` -type ResourceDriver struct { - // The name of the object is the unique driver name. - ObjectMeta - - // Features contains a list of features supported by the driver. - // New features may be added over time and must be ignored - // by code that does not know about them. - Features []ResourceDriverFeature -} - -type ResourceDriverFeature struct { - // Name is one of the pre-defined names for a feature. - Name ResourceDriverFeatureName - // Parameters might provide additional information about how - // the driver supports the feature. Boolean features have - // no parameters, merely listing them indicates support. - Parameters runtime.RawExtension -} -``` - -### Complex sharing of ResourceClaim - -At the moment, the allocation result marks as a claim as either "shareable" by -an unlimited number of consumers or "not shareable". More complex scenarios -might be useful like "may be shared by a certain number of consumers", but so -far such use cases have not come up yet. If they do, the `AllocationResult` can -be extended with new fields as defined by a follow-up KEP. - -### Improving scheduling performance - -Some enhancements are possible which haven't been implemented yet because it is -unclear how important they would be in practice. All of the following ideas -could still be added later as they don't conflict with the underlying design, -either as part of this KEP or in follow-up KEPs. - -#### Optimize for network-attached resources - -When a network-attached resource is available on all nodes in a cluster, the -driver will never mark any nodes as unsuitable. If all claims for a pod fall -into that category, the scheduler a) does not need to wait for information and -b) does not need to publish "potential nodes". - -The `ResourceClass` could be extended with a `AvailableForNodes -*core.NodeSelector`. This can be a selector that matches all nodes or a -subset. Either way, if a potential node matches this selector, the scheduler -knows that claims using this class can be allocated and can do the optimization -outlined above. - -#### Moving blocking API calls into goroutines - -This [is being -discussed](https://github.com/kubernetes/kubernetes/issues/120502) and has been -[partially -implemented](https://github.com/kubernetes/kubernetes/pull/120963). That -implementation made the scheduler framework more complex, so [the -conclusion](https://kubernetes.slack.com/archives/C09TP78DV/p1696307377064469?thread_ts=1696246271.825109&cid=C09TP78DV) -was that using blocking calls is the lesser evil until user feedback indicates -that improvements are really needed. - -#### RPC calls instead of `PodSchedulingContext` - -The current design is not making it a hard requirement that admins change the -scheduler configuration to enable communication between scheduler and DRA -drivers. For scenarios where admins and vendors are willing to invest more -effort and doing so would provide performance benefits, a communication path -similar to scheduler extenders could be added. - -## Infrastructure Needed - -Initially, all development will happen inside the main Kubernetes -repository. The mock driver can be developed inside test/e2e/dra. For the -generic part of that driver, i.e. the code that other drivers can reuse, and -other common code a new staging repo `k8s.io/dynamic-resource-allocation` is -needed. +The flow of information between the scheduler and DRA drivers through the +PodSchedulingContext is complex. diff --git a/keps/sig-node/3063-dynamic-resource-allocation/components.png b/keps/sig-node/3063-dynamic-resource-allocation/components.png index 261133200f324d935aa01b94daed31f9273b51be..f64525d86bdce0c102fb6ed991997057b38ac529 100644 GIT binary patch delta 43275 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 delta 43275 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 kubelet drivercontroller -[hidden]> driverplugin Pod <. ResourceClaim: owned by\n(if created from template) -Pod <. PodScheduling: owned by +Pod <. PodSchedulingContext: owned by Pod -u-> k8sresourceclaimcontroller: read claim template\nfrom Pod spec -ResourceClaim <-u- k8sresourceclaimcontroller: create claim,\nclean up users +ResourceClaim <-u- k8sresourceclaimcontroller: create claim,\nclean up users,\ntrigger deallocation\n ResourceClaim <-u-> kubelet -k8sresourceplugin <-u-> PodScheduling +k8sresourceplugin <-u-> PodSchedulingContext Pod <--> scheduler ResourceClaim <--> k8sresourceplugin @@ -43,5 +43,5 @@ ResourceClaim <--> k8sresourceplugin ResourceClaim <-> drivercontroller pluginmanager <-> driverplugin resourcemanager <-> driverplugin -PodScheduling <-> drivercontroller +PodSchedulingContext <-> drivercontroller @enduml diff --git a/keps/sig-node/3063-dynamic-resource-allocation/kep.yaml b/keps/sig-node/3063-dynamic-resource-allocation/kep.yaml index a7d62e682fb..0e557bd11a8 100644 --- a/keps/sig-node/3063-dynamic-resource-allocation/kep.yaml +++ b/keps/sig-node/3063-dynamic-resource-allocation/kep.yaml @@ -24,7 +24,7 @@ stage: alpha # The most recent milestone for which work toward delivery of this KEP has been # done. This can be the current (upcoming) milestone, if it is being actively # worked on. -latest-milestone: "v1.30" +latest-milestone: "v1.31" # The milestone at which this feature was, or is targeted to be, at each stage. milestone: @@ -37,6 +37,11 @@ feature-gates: - kube-controller-manager - kube-scheduler - kubelet + - name: DRAControlPlaneController + components: + - kube-apiserver + - kube-controller-manager + - kube-scheduler disable-supported: true metrics: diff --git a/keps/sig-node/3063-dynamic-resource-allocation/kubelet.png b/keps/sig-node/3063-dynamic-resource-allocation/kubelet.png deleted file mode 100644 index a497a54333637f25d5326edb49d8d56ae28c8b2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31727 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 diff --git a/keps/sig-node/4381-dra-structured-parameters/Makefile b/keps/sig-node/4381-dra-structured-parameters/Makefile new file mode 100644 index 00000000000..b489d179ddb --- /dev/null +++ b/keps/sig-node/4381-dra-structured-parameters/Makefile @@ -0,0 +1,35 @@ +# Copyright 2022 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +IMAGES += components.png kubelet.png + +all: $(IMAGES) +clean: + rm -f $(IMAGES) + +# We use the http://plantuml.com/plantuml server to generate +# images. That way nothing needs to be installed besides Go. +DOC_PLANTUML_GO = $(shell go env GOPATH)/bin/plantuml-go + +%.png: %.puml $(DOC_PLANTUML_GO) + $(DOC_PLANTUML_GO) -format png $< + +%.svg: %.puml $(DOC_PLANTUML_GO) + $(DOC_PLANTUML_GO) -format svg $< + +# Builds the binary in GOPATH/bin. Changing into / first avoids +# modifying the project's go.mod file. +$(DOC_PLANTUML_GO): + cd / && go get github.com/acarlson99/plantuml-go diff --git a/keps/sig-node/4381-dra-structured-parameters/README.md b/keps/sig-node/4381-dra-structured-parameters/README.md index 22761292342..5a91ddea8bb 100644 --- a/keps/sig-node/4381-dra-structured-parameters/README.md +++ b/keps/sig-node/4381-dra-structured-parameters/README.md @@ -59,7 +59,7 @@ should be approved by the remaining approvers and/or the owning SIG (or SIG Architecture for cross-cutting KEPs). --> -# [KEP-4381](https://github.com/kubernetes/enhancements/issues/4381): Structured Parameters for Dynamic Resource Allocation +# [KEP-4381](https://github.com/kubernetes/enhancements/issues/4381): Dynamic Resource Allocation with Structured Parameters - [Release Signoff Checklist](#release-signoff-checklist) @@ -68,19 +68,53 @@ SIG Architecture for cross-cutting KEPs). - [Goals](#goals) - [Non-Goals](#non-goals) - [Proposal](#proposal) + - [User Stories](#user-stories) + - [Cluster add-on development](#cluster-add-on-development) + - [Cluster configuration](#cluster-configuration) + - [Partial GPU allocation](#partial-gpu-allocation) - [Publishing node resources](#publishing-node-resources) - [Using structured parameters](#using-structured-parameters) - [Communicating allocation to the DRA driver](#communicating-allocation-to-the-dra-driver) + - [Risks and Mitigations](#risks-and-mitigations) + - [Feature not used](#feature-not-used) + - [Compromised node](#compromised-node) + - [Compromised resource driver plugin](#compromised-resource-driver-plugin) + - [User permissions and quotas](#user-permissions-and-quotas) + - [Usability](#usability) - [Design Details](#design-details) - - [ResourceClass extension](#resourceclass-extension) - - [ResourceSlice](#resourceslice) - - [ResourceClaimParameters](#resourceclaimparameters) - - [ResourceClassParameters](#resourceclassparameters) - - [ResourceHandle extension](#resourcehandle-extension) - - [Implementation of structured models](#implementation-of-structured-models) - - [Scheduling + Allocation](#scheduling--allocation) - - [Deallocation](#deallocation) - - [Immediate allocation](#immediate-allocation) + - [Components](#components) + - [State and communication](#state-and-communication) + - [Custom parameters](#custom-parameters) + - [Sharing a single ResourceClaim](#sharing-a-single-resourceclaim) + - [Ephemeral vs. persistent ResourceClaims lifecycle](#ephemeral-vs-persistent-resourceclaims-lifecycle) + - [Scheduled pods with unallocated or unreserved claims](#scheduled-pods-with-unallocated-or-unreserved-claims) + - [Handling non graceful node shutdowns](#handling-non-graceful-node-shutdowns) + - [API](#api) + - [resource.k8s.io](#resourcek8sio) + - [ResourceSlice](#resourceslice) + - [ResourceClass](#resourceclass) + - [ResourceClassParameters](#resourceclassparameters) + - [ResourceClaimParameters](#resourceclaimparameters) + - [Allocation result](#allocation-result) + - [ResourceClaimTemplate](#resourceclaimtemplate) + - [Object references](#object-references) + - [core](#core) + - [kube-controller-manager](#kube-controller-manager) + - [kube-scheduler](#kube-scheduler) + - [EventsToRegister](#eventstoregister) + - [PreEnqueue](#preenqueue) + - [Pre-filter](#pre-filter) + - [Filter](#filter) + - [Post-filter](#post-filter) + - [Reserve](#reserve) + - [PreBind](#prebind) + - [Unreserve](#unreserve) + - [kubelet](#kubelet) + - [Managing resources](#managing-resources) + - [Communication between kubelet and resource kubelet plugin](#communication-between-kubelet-and-resource-kubelet-plugin) + - [NodeListAndWatchResources](#nodelistandwatchresources) + - [NodePrepareResource](#nodeprepareresource) + - [NodeUnprepareResources](#nodeunprepareresources) - [Simulation with CA](#simulation-with-ca) - [Test Plan](#test-plan) - [Prerequisite testing updates](#prerequisite-testing-updates) @@ -105,6 +139,14 @@ SIG Architecture for cross-cutting KEPs). - [Alternatives](#alternatives) - [Publishing resource information in node status](#publishing-resource-information-in-node-status) - [Injecting vendor logic into CA](#injecting-vendor-logic-into-ca) + - [ResourceClaimTemplate](#resourceclaimtemplate-1) + - [Reusing volume support as-is](#reusing-volume-support-as-is) + - [Extend volume support](#extend-volume-support) + - [Extend Device Plugins](#extend-device-plugins) + - [Webhooks instead of ResourceClaim updates](#webhooks-instead-of-resourceclaim-updates) + - [ResourceDriver](#resourcedriver) + - [Complex sharing of ResourceClaim](#complex-sharing-of-resourceclaim) +- [Infrastructure Needed](#infrastructure-needed) ## Release Signoff Checklist @@ -151,35 +193,48 @@ Items marked with (R) are required *prior to targeting to a milestone / release* ## Summary -Dynamic Resource Allocation (DRA) was added to Kubernetes as an alpha feature in -v1.26. It defines an alternative to the traditional device-plugin API for -requesting access to third-party resources. - -By design, DRA uses parameters for resources that are completely -opaque to core Kubernetes. They get interpreted by a DRA driver's controller -(for allocating claims) and a DRA driver's kubelet plugin (for configuring -resources on a node). During scheduling of a pod, the kube-scheduler and any DRA -driver controller(s) handling claims for the pod communicate back-and-forth through the -apiserver by updating a `PodSchedulingContext` object, ultimately leading to the -allocation of all pending claims and the pod being scheduled onto a node. - -This approach poses a problem for the [Cluster -Autoscaler](https://github.com/kubernetes/autoscaler) (CA) or for any higher -level controller that needs to make decisions for a group of pods (e.g. a job -scheduler). It cannot simulate the effect of allocating or deallocating -claims over time. Only the third-party DRA drivers have the information -available to do this. - -"Structured parameters" is an extension to DRA that addresses this problem by -making claim parameters less opaque. Instead of handling the semantics of all -claim parameters themselves, drivers now manage resources and describe them -using a specific "structured model" pre-defined by Kubernetes. This allows -components aware of this "structured model" to make decisions about these -resources without outsourcing them to some third-party controller. For example, -the scheduler is now able to allocate claims rapidly, without back-and-forth -communication with DRA drivers. - -At a high-level, this extension takes the following form: +This KEP originally defined an extension of the ["classic" DRA #3063 +KEP](../3063-dynamic-resource-allocation/README.md). Now the roles are +reversed: this KEP defines the base functionality and #3063 adds an optional +extension. + +Users are increasingly deploying Kubernetes as management solution for new +workloads (batch processing) and in new environments (edge computing). Such +workloads no longer need just RAM and CPU, but also access to specialized +hardware. With upcoming enhancements of data center interconnects, accelerators +can be installed outside of specific nodes and be connected to nodes +dynamically as needed. + +This KEP introduces a new API for describing which of these new resources +a pod needs. The API supports: + +- Network-attached resources. The existing [device plugin API](https://github.com/kubernetes/design-proposals-archive/blob/main/resource-management/device-plugin.md) + is limited to hardware on a node. However, further work is still + needed to actually use the new API with those. +- Sharing of a resource allocation between multiple containers or pods. + The device manager API currently cannot share resources at all. It + could be extended to share resources between containers in a single pod, + but supporting sharing between pods would need a completely new + API similar to the one in this KEP. +- Using a resource that is expensive to initialize multiple times + in different pods. This is not possible at the moment. +- Custom parameters that describe resource requirements and initialization. + Parameters are not limited to a single, linear quantity that can be counted. + With the current Pod API, annotations have to be used to capture such + parameters and then hacks are needed to access them from a CSI driver or + device plugin. + +Support for new hardware will be provided by hardware vendor add-ons. Those add-ons +are responsible for reporting available resources in a format defined and +understood by Kubernetes and for configuring hardware before it is used. Kubernetes +handles the allocation of those resources as part of pod scheduling. + +This KEP does not replace other means of requesting traditional resources +(RAM/CPU, volumes, extended resources). The scheduler will serve as coordinator +between the add-ons which own resources (CSI driver, resource driver) and the +resources owned and assigned by the scheduler (RAM/CPU, extended resources). + +At a high-level, DRA with structured parameters takes the following form: * DRA drivers publish their available resources in the form of a `ResourceSlice` object on a node-by-node basis according to one or more of the @@ -232,27 +287,249 @@ demonstrate the interest in a KEP within the wider Kubernetes community. [experience reports]: https://github.com/golang/go/wiki/ExperienceReports --> +Originally, Kubernetes and its scheduler only tracked CPU and RAM as +resources for containers. Later, support for storage and discrete, +countable per-node extended resources was added. The kubelet device plugin +interface then made such local resources available to containers. But +for many newer devices, this approach and the Kubernetes API for +requesting these custom resources is too limited. This KEP may eventually +address limitations of the current approach for the following use cases: + +- *Device initialization*: When starting a workload that uses + an accelerator like an FPGA, I’d like to have the accelerator + reconfigured or reprogrammed for the workload before the workload + itself starts. For security reasons, workloads should not be able to + reconfigure devices directly. + + *Limitation*: Currently, it’s impossible to specify the desired + device properties that are required for reconfiguring devices. + For the FPGA example, a file containing the desired configuration + of the FPGA has to be referenced. + +- *Device cleanup*: When my workload is finished, I would like to have + a mechanism for cleanup of the device, that will ensure that device + does not contain traces/parameters/data from previous workloads and + appropriate power state/shutdown. For example, an FPGA might have + to be reset because its configuration for the workload was + confidential. + + *Limitation*: Post-stop actions are not supported. + +- *Partial allocation*: When deploying a container I’d like to be able + to use part of the shareable device inside a container and other + containers should be able to use other free resources on the same + device. + + *Limitation*: For example, newer generations of NVIDIA GPUs have a mode of + operation called MIG, that allow them to be sub-divided into a set of + mini-GPUs (called MIG devices) with varying amounts of memory and compute + resources provided by each. From a hardware-standpoint, configuring a GPU + into a set of MIG devices is highly-dynamic and creating a MIG device + tailored to the resource needs of a particular application is well + supported. However, with the current device plugin API, the only way to make + use of this feature is to pre-partition a GPU into a set of MIG devices and + advertise them to the kubelet in the same way a full / static GPU is + advertised. The user must then pick from this set of pre-partitioned MIG + devices instead of having one created for them on the fly based on their + particular resource constraints. Without the ability to create MIG devices + dynamically (i.e. at the time they are requested) the set of pre-defined MIG + devices must be carefully tuned to ensure that GPU resources do not go unused + because some of the pre-partioned devices are in low-demand. It also puts + the burden on the user to pick a particular MIG device type, rather than + declaring the resource constraints more abstractly. + +- *Optional allocation*: When deploying a workload I’d like to specify + soft(optional) device requirements. If a device exists and it’s + allocatable it will be allocated. If not - the workload will be run on + a node without a device. GPU and crypto-offload engines are + examples of this kind of device. If they’re not available, workloads + can still run by falling back to using only the CPU for the same + task. + + *Limitation*: Optional allocation is supported neither by the device + plugins nor by current Pod resource declaration. + +- *Support Over the Fabric devices*: When deploying a container, I’d + like to utilize devices available over the Fabric (network, special + links, etc). + + *Limitation*: The device plugin API is designed for node-local resources that + get discovered by a plugin running on the node. Projects like + [Akri](https://www.cncf.io/projects/akri/) have to work around that by + reporting the same network-attached resource on all nodes that it could + get attached to and then updating resource availability on all of those + nodes when resources get used. + +Several other limitations are addressed by +[CDI](https://github.com/container-orchestrated-devices/container-device-interface/), +a container runtime extension that this KEP is using to expose resources +inside a container. + ### Goals - Enable cluster autoscaling when pods use resource claims, with correct decisions and changing the cluster size by more than one node at a time. -- Support node-local resources. Adding or removing nodes has no effect - on network-attached resources and therefore CA does not need to (and cannot) - simulate them. +- Support node-local resources -- Allow DRA driver developers to provide a user experience that is similar to - the one possible without structured parameters. Ideally, users should not notice +- Support claim parameters that are specified in a vendor CRD as + an alternative to letting users directly specify parameters with + the in-tree type. This provides a user experience that is similar to + what has been possible since Kubernetes 1.26. Ideally, users should not notice at all that a driver is using structured parameters under the hood. ### Non-Goals -- Scheduling performance is expected to become better compared to using the - PodSchedulingContext. However, this is not the reason for this KEP. - +* Replace the device plugin API. For resources that fit into its model + of a single, linear quantity it is a good solution. Other resources + should use dynamic resource allocation. Both are expected to co-exist, with vendors + choosing the API that better suits their needs on a case-by-case + basis. Because the new API is going to be implemented independently of the + existing device plugin support, there's little risk of breaking stable APIs. + +* Provide an abstraction layer for resource requests, i.e., something like a + “I want some kind of GPU”. Users will need to know about specific + resource drivers and which parameters they support. Portability of + workloads could be added on top of this proposal by introducing the + selection of a resource implementation through labels and + standardizing those labels and the associated parameters. The + [Resource Class + Proposal](https://docs.google.com/document/d/1qKiIVs9AMh2Ua5thhtvWqOqW0MSle_RV3lfriO1Aj6U/edit#heading=h.jzfmfdca34kj) + included such an approach. + +* Support network-attached resources ## Proposal +### User Stories + +#### Cluster add-on development + +As a hardware vendor, I want to make my hardware available also to applications +that run in a container under Kubernetes. I want to make it easy for a cluster +administrator to configure a cluster where some nodes have this hardware. + +I develop two components, one that runs as part of the Kubernetes control plane +and one that runs on each node, and package those inside container images. YAML +files describe how to deploy my software on a Kubernetes cluster that supports +dynamic resource allocation. + +Documentation for administrators explains how the nodes need to be set +up. Documentation for users explains which parameters control the behavior of +my hardware and how to use it inside a container. + +#### Cluster configuration + +As a cluster administrator, I want to make GPUs from vendor ACME available to users +of that cluster. I prepare the nodes and deploy the vendor's components with +`kubectl create`. + +I create a ResourceClass for the hardware with parameters that only I as the +administrator am allowed to choose, like for example running a command with +root privileges that does some cluster-specific initialization for each allocation: +``` +apiVersion: gpu.example.com/v1 +kind: GPUInit +metadata: + name: acme-gpu-init +# DANGER! This option must not be accepted for +# user-supplied parameters. A real driver might +# not even allow it for admins. This is just +# an example to show the conceptual difference +# between ResourceClass and ResourceClaim +# parameters. +initCommand: +- /usr/local/bin/acme-gpu-init +- --cluster +- my-cluster +--- +apiVersion: core.k8s.io/v1alpha2 +kind: ResourceClass +metadata: + name: acme-gpu +driverName: gpu.example.com +parametersRef: + apiGroup: gpu.example.com + kind: GPUInit + name: acme-gpu-init +``` + +#### Partial GPU allocation + +As a user, I want to use a GPU as accelerator, but don't need exclusive access +to that GPU. Running my workload with just 2Gb of memory is sufficient. This is +supported by the ACME GPU hardware. I know that the administrator has created +an "acme-gpu" ResourceClass. + +For a simple trial, I create a Pod directly where two containers share the same subset +of the GPU: +``` +apiVersion: gpu.example.com/v1 +kind: GPURequirements +metadata: + name: device-consumer-gpu-parameters +memory: "2Gi" +--- +apiVersion: resource.k8s.io/v1alpha2 +kind: ResourceClaimTemplate +metadata: + name: device-consumer-gpu-template +spec: + metadata: + # Additional annotations or labels for the + # ResourceClaim could be specified here. + spec: + resourceClassName: "acme-gpu" + parametersRef: + apiGroup: gpu.example.com + kind: GPURequirements + name: device-consumer-gpu-parameters +--- +apiVersion: v1 +kind: Pod +metadata: + name: device-consumer +spec: + resourceClaims: + - name: "gpu" # this name gets referenced below under "claims" + template: + resourceClaimTemplateName: device-consumer-gpu-template + containers: + - name: workload + image: my-app + command: ["/bin/program"] + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" + claims: + - "gpu" + - name: monitor + image: my-app + command: ["/bin/other-program"] + resources: + requests: + memory: "32Mi" + cpu: "25m" + limits: + memory: "64Mi" + cpu: "50m" + claims: + - "gpu" +``` + +This request triggers resource allocation on a node that has a GPU device with +2Gi of memory available and then the Pod runs on that node. The remaining +capacity of the GPU may be usable for other pods, with constrains like alignment +to segment sizes ensured by the resource driver. +The lifecycle of the resource +allocation is tied to the lifecycle of the Pod. + +In production, a similar PodTemplateSpec in a Deployment will be used. + ### Publishing node resources The resources available on a node need to be published to the API server. In @@ -549,24 +826,365 @@ namedResources: - gpu-1 ``` -## Design Details +### Risks and Mitigations -### ResourceClass extension + -### ResourceSlice +#### Feature not used + +In a cluster where the feature is not used (no resource driver installed, no +pods using dynamic resource allocation) the impact is minimal, both for +performance and security. The scheduler plugin will +return quickly without doing any work for pods. + +#### Compromised node + +Kubelet is intentionally limited to read-only access for ResourceClass and ResourceClaim +to prevent that a +compromised kubelet interferes with scheduling of pending pods, for example +by updating status information normally set by the scheduler. +Faking such information could be used for a denial-of-service +attack against pods using those ResourceClaims, for example by overwriting +their allocation result with a node selector that matches no node. A +denial-of-service attack against the cluster and other pods is harder, but +still possible. For example, frequently updating ResourceSlice objects could +cause new scheduling attempts for pending pods. + +Another potential attack goal is to get pods with sensitive workloads to run on +a compromised node. For pods that don't use special resources nothing changes +in that regard. Such an attack is possible for pods with extended resources +because kubelet is in control of which capacity it reports for those: it could +publish much higher values than the device plugin reported and thus attract +pods to the node that normally would run elsewhere. With dynamic resource +allocation, such an attack is still possible, but the attack code would have to +be different for each resource driver because all of them will use structured +parameters differently for reporting resource availability. + +#### Compromised resource driver plugin + +This is the result of an attack against the resource driver, either from a +container which uses a resource exposed by the driver, a compromised kubelet +which interacts with the plugin, or through a successful attack against the +node which led to root access. + +The resource driver plugin only needs read access to objects described in this +KEP, so compromising it does not interfere with dynamic resource allocation for +other drivers. + +A resource driver may need root access on the node to manage +hardware. Attacking the driver therefore may lead to root privilege +escalation. Ideally, driver authors should try to avoid depending on root +permissions and instead use capabilities or special permissions for the kernel +APIs that they depend on. + +A resource driver may also need privileged access to remote services to manage +network-attached devices. Resource driver vendors and cluster administrators +have to consider what the effect of a compromise could be for that and how such +privileges could get revoked. + +#### User permissions and quotas + +Similar to generic ephemeral inline volumes, the [ephemeral resource use +case](#ephemeral-vs-persistent-resourceclaims-lifecycle) gets covered by +creating ResourceClaims on behalf of the user automatically through +kube-controller-manager. The implication is that RBAC rules that are meant to +prevent creating ResourceClaims for certain users can be circumvented, at least +for ephemeral resources. Administrators need to be aware of this caveat when +designing user restrictions. + +A quota system that is based on the information in the structured parameter model +could be implemented in Kubernetes. When a user has exhausted their +quota, the scheduler then refuses to allocate further ResourceClaims. + +#### Usability + +Aside from security implications, usability and usefulness of dynamic resource +allocation also may turn out to be insufficient. Some risks are: + +- Slower pod scheduling due to more complex decision making. + +- Additional complexity when describing pod requirements because + separate objects must be created for the parameters. + +All of these risks will have to be evaluated by gathering feedback from users +and resource driver developers. + +## Design Details + +### Components + +![components](./components.png) + +Several components must be implemented or modified in Kubernetes: +- The new API must be added to kube-apiserver. +- A new controller in kube-controller-manager which creates + ResourceClaims from ResourceClaimTemplates, similar to + https://github.com/kubernetes/kubernetes/tree/master/pkg/controller/volume/ephemeral. + It also removes the reservation entry for a consumer in `claim.status.reservedFor`, + the field that tracks who is allowed to use a claim, when that user no longer exists. + It clears the allocation and thus makes the underlying resources available again + when a ResourceClaim is no longer reserved. +- A kube-scheduler plugin must detect Pods which reference a + ResourceClaim (directly or through a template) and ensure that the + resource is allocated before the Pod gets scheduled, similar to + https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/volume/scheduling/scheduler_binder.go +- Kubelet must be extended to retrieve information from ResourceClaims + and to call a resource kubelet plugin. That plugin returns CDI device ID(s) + which then must be passed to the container runtime. + +A resource driver can have the following components: +- *CRD controller* (optional): a central component which translates parameters + defined with a vendor CRD into in-tree parameter types. +- *kubelet plugin* (required): a component which cooperates with kubelet to + publish resource information and to prepare the usage of the resource on a node. + +When a resource driver doesn't use its own CRD for parameters, the CRD controller +is not needed and a ResourceClaim references ResourceClaimParameters directly. + +A [utility library](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/dynamic-resource-allocation) for resource drivers was developed. +It does not have to be used by drivers, therefore it is not described further +in this KEP. + +### State and communication + +A ResourceClaim object defines what kind of resource is needed and what +the parameters for it are. It is owned by users and namespaced. Additional +parameters are provided by a cluster admin in ResourceClass objects. + +The ResourceClaim spec is immutable. The ResourceClaim +status is reserved for system usage and holds the current state of the +resource. The status must not get lost, which in the past was not ruled +out. For example, status could have been stored in a separate etcd instance +with lower reliability. To recover after a loss, status was meant to be recoverable. +A [recent KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-architecture/2527-clarify-status-observations-vs-rbac) +clarified that status will always be stored reliably and can be used as +proposed in this KEP. + +Handling state and communication through objects has two advantages: +- Changes for a resource are (almost) atomic, which avoids race conditions. + One small exception is that changing finalizers and the status have to + be done in separate operations. +- The only requirement for deployments is that the components can connect to + the API server. + +The entire state of a resource can be determined by looking at its +ResourceClaim (see [API below](#api) for details), for example: + +- It is **allocated** if and only if `claim.status.allocation` is non-nil and + points to the `AllocationResult`, i.e. the struct where information about + a successful allocation is stored. + +- It is in use if and only if `claim.status.reservedFor` contains one or + more consumers. It does not matter whether those users, usually pods, are + currently running because that could change at any time. + +- A resource is no longer needed when `claim.deletionTimestamp` is set. It must not + be deallocated yet when it is still in use. + +Some of the race conditions that need to be handled are: + +- A ResourceClaim gets created and deleted again while the scheduler + is allocating it. Before it actually starts doing anything, the + scheduler adds a finalizer. Either adding the finalizer or removing the + ResourceClaim win. If the scheduler wins, it continues with allocation + and can either complete or abort the operation when it notices the non-nil + DeletionTimestamp. Otherwise, allocation gets aborted immediately. + + What this avoids is the situation where an allocation succeed without having + an object where the result can be stored. The driver can also be killed at + any time: when it restarts, the finalizer indicates that allocation may be in + progress and has to be completed or aborted. + + However, users may still force-delete a ResourceClaim, or the entire + cluster might get deleted. Driver implementations must store enough + information elsewhere to detect when some allocated resource is no + longer needed to recover from such scenarios. + +- A ResourceClaim gets deleted and recreated while the resource driver is + adding the finalizer. The driver can update the object to add the finalizer + and then will get a conflict error, which informs the driver that it must + work on a new instance of the claim. In general, patching a ResourceClaim + is only acceptable when it does not lead to race conditions. To detect + delete+recreate, the UID must be added as precondition for a patch. + To detect also potentially conflicting other changes, ResourceVersion + needs to be checked, too. + +- In a cluster with multiple scheduler instances, two pods might get + scheduled concurrently by different schedulers. When they reference + the same ResourceClaim which may only get used by one pod at a time, + only one pod can be scheduled. + + Both schedulers try to add their pod to the `claim.status.reservedFor` field, but only the + update that reaches the API server first gets stored. The other one fails + with a conflict error and the scheduler which issued it knows that it must + put the pod back into the queue, waiting for the ResourceClaim to become + usable again. + +- Two pods get created which both reference the same unallocated claim with + delayed allocation. A single scheduler can detect this special situation + and then do allocation only for one of the two pods. When the pods + are handled by different schedulers, only one will succeed with writing + back the `claim.status.allocation`. + +- Scheduling a pod and allocating resources for it has been attempted, but one + claim needs to be reallocated to fit the overall resource requirements. A second + pod gets created which references the same claim that is in the process of + being deallocated. Because that is visible in the claim status, scheduling + of the second pod cannot proceed. + +### Custom parameters + +To support arbitrarily complex parameters, both ResourceClass and ResourceClaim +contain one field which references a separate object. The reference contains +API group, kind and name and thus is sufficient for generic clients to +retrieve the parameters. For ResourceClass, that object must be +cluster-scoped. For ResourceClaim, it must be in the same namespace as the +ResourceClaim and thus the Pod. Which kind of objects a resource driver accepts as parameters depends on +the driver. + +This approach was chosen because then validation of the parameters can be done +with a CRD and that validation will work regardless of where the parameters +are needed. + +It is the responsibility of the resource driver to convert these CRD parameters +into in-tree ResourceClaimParameters and ResourceClassParameters. Kubernetes +finds those generated parameters based on their `generatedFrom` back-reference. + +Parameters may get deleted before the ResourceClaim or ResourceClass that +references them. In that case, a pending resource cannot be allocated until the +parameters get recreated. An allocated resource must remain usable and +deallocating it must be possible. To support this, resource drivers must copy +all relevant information: +- For usage, the `claim.status.allocation.resourceHandle` can be hold some copied information + because the ResourceClaim and thus this field must exist. +- For deallocation, drivers should use some other location to handle + cases where a user force-deletes a ResourceClaim or the entire + cluster gets removed. + +### Sharing a single ResourceClaim + +Pods reference resource claims in a new `pod.spec.resourceClaims` list. Each +resource in that list can then be made available to one or more containers in +that Pod. Depending on the capabilities defined in the +`claim.status.allocation` by the driver, a ResourceClaim can be used exclusively +by one pod at a time or an unlimited number of pods. Support for additional +constraints (maximum number of pods, maximum number of nodes) could be +added once there are use cases for those. + +Consumers of a ResourceClaim are listed in `claim.status.reservedFor`. They +don't need to be Pods. At the moment, Kubernetes itself only handles Pods and +allocation for Pods. + +### Ephemeral vs. persistent ResourceClaims lifecycle + +A persistent ResourceClaim has a lifecyle that is independent of any particular +pod. It gets created and deleted by the user. This is useful for resources +which are expensive to configure and that can be used multiple times by pods, +either at the same time or one after the other. Such persistent ResourceClaims +get referenced in the pod spec by name. When a PodTemplateSpec in an app +controller spec references a ResourceClaim by name, all pods created by that +controller also use that name and thus share the resources allocated for that +ResourceClaim. + +But often, each Pod is meant to have exclusive access to its own ResourceClaim +instance instead. To support such ephemeral resources without having to modify +all controllers that create Pods, an entry in the new PodSpec.ResourceClaims +list can also be a reference to a ResourceClaimTemplate. When a Pod gets created, such a +template will be used to create a normal ResourceClaim with the Pod as owner +with an +[OwnerReference](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#OwnerReference)), +and then the normal allocation of the resource takes place. Once the pod got +deleted, the Kubernetes garbage collector will also delete the +ResourceClaim. + +This mechanism documents ownership and serves as a fallback for scenarios where +dynamic resource allocation gets disabled in a cluster (for example, during a +downgrade). But it alone is not sufficient: for example, the job controller +does not delete pods immediately when they have completed, which would keep +their resources allocated. Therefore the resource controller watches for pods +that have completed and releases their resource allocations. + +The difference between persistent and ephemeral resources for kube-scheduler +and kubelet is that the name of the ResourceClaim needs to be determined +differently: the name of an ephemeral ResourceClaim is recorded in the Pod status. +Ownership must be checked to detect accidental conflicts with +persistent ResourceClaims or previous incarnations of the same ephemeral +resource. + +### Scheduled pods with unallocated or unreserved claims + +There are several scenarios where a Pod might be scheduled (= `pod.spec.nodeName` +set) while the claims that it depends on are not allocated or not reserved for +it: + +* A user might manually create a pod with `pod.spec.nodeName` already set. +* Some special cluster might use its own scheduler and schedule pods without + using kube-scheduler. +* The feature might have been disabled in kube-scheduler while scheduling + a pod with claims. + +The kubelet is refusing to run such pods and reports the situation through +an event (see below). It's an error scenario that should better be avoided. + +Users should avoid this situation by not scheduling pods manually. If they need +it for some reason, they can use a node selector which matches only the desired +node and then let kube-scheduler do the normal scheduling. + +Custom schedulers should emulate the behavior of kube-scheduler and ensure that +claims are allocated and reserved before setting `pod.spec.nodeName`. + +The last scenario might occur during a downgrade or because of an +administrator's mistake. Administrators can fix this by deleting such pods. + +### Handling non graceful node shutdowns + +When a node is shut down unexpectedly and is tainted with an `out-of-service` +taint with NoExecute effect as explained in the [Non graceful node shutdown KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/2268-non-graceful-shutdown), +all running pods on the node will be deleted by the GC controller and the +resources used by the pods will be deallocated. However, they will not be +un-prepared as the node is down and Kubelet is not running on it. + +Resource drivers should be able to handle this situation correctly and +should not expect `UnprepareNodeResources` to be always called. +If resources are unprepared when `Deallocate` is called, `Deallocate` +might need to perform additional actions to correctly deallocate +resources. + +### API + +The PodSpec gets extended. To minimize the changes in core/v1, all new types +get defined in a new resource group. This makes it possible to revise those +more experimental parts of the API in the future. The new fields in the +PodSpec are gated by the DynamicResourceAllocation feature gate and can only be +set when it is enabled. Initially, they are declared as alpha. Even though they +are alpha, changes to their schema are discouraged and would have to be done by +using new field names. + +ResourceClaim, ResourceClass and ResourceClaimTemplate are new built-in types +in `resource.k8s.io/v1alpha2`. This alpha group must be explicitly enabled in +the apiserver's runtime configuration. Using builtin types was chosen instead +of using CRDs because core Kubernetes components must interact with the new +objects and installation of CRDs as part of cluster creation is an unsolved +problem. + +Secrets are not part of this API: if a resource driver needs secrets, for +example to access its own backplane, then it can define custom parameters for +those secrets and retrieve them directly from the apiserver. This works because +drivers are expected to be written for Kubernetes. + +#### resource.k8s.io + +##### ResourceSlice For each node, one or more ResourceSlice objects get created. The kubelet publishes them with the node as the owner, so they get deleted when a node goes @@ -693,8 +1311,200 @@ type NamedResourcesStringSlice struct { All names must be DNS sub-domains. This excludes the "/" character, therefore combining different names with that separator to form an ID is valid. +In the Go types above, all structs starting with `NamedResources` are part of +that structured model. Code generators (more specifically, the applyconfig +generator) assume that all Go types of an API are defined in the same Go +package. If it wasn't for that, defining those structs in their own package +without the `NamedResources` prefix would be possible and make the Go code +cleaner without affecting the Kubernetes API. + +##### ResourceClass + +```go +// ResourceClass is used by administrators to influence how resources +// are allocated. +// +// This is an alpha type and requires enabling the DynamicResourceAllocation +// feature gate. +type ResourceClass struct { + metav1.TypeMeta + // Standard object metadata + // +optional + metav1.ObjectMeta + + // DriverName defines the name of the dynamic resource driver that is + // used for allocation of a ResourceClaim that uses this class. + // + // Resource drivers have a unique name in forward domain order + // (acme.example.com). + DriverName string + + // ParametersRef references an arbitrary separate object that may hold + // parameters that will be used by the driver when allocating a + // resource that uses this class. A dynamic resource driver can + // distinguish between parameters stored here and and those stored in + // ResourceClaimSpec. + // +optional + ParametersRef *ResourceClassParametersReference + + // Only nodes matching the selector will be considered by the scheduler + // when trying to find a Node that fits a Pod when that Pod uses + // a ResourceClaim that has not been allocated yet. + // + // Setting this field is optional. If null, all nodes are candidates. + // +optional + SuitableNodes *core.NodeSelector +} +``` + +##### ResourceClassParameters + +```go +// ResourceClassParameters defines resource requests for a ResourceClass in an +// in-tree format understood by Kubernetes. +type ResourceClassParameters struct { + metav1.TypeMeta + // Standard object metadata + metav1.ObjectMeta + + // If this object was created from some other resource, then this links + // back to that resource. This field is used to find the in-tree representation + // of the class parameters when the parameter reference of the class refers + // to some unknown type. + GeneratedFrom *ResourceClassParametersReference + + // VendorParameters are arbitrary setup parameters for all claims using + // this class. They are ignored while allocating the claim. There must + // not be more than one entry per driver. + VendorParameters []VendorParameters + + // Filters describes additional contraints that must be met when using the class. + Filters []ResourceFilter +} + +// ResourceFilter is a filter for resources from one particular driver. +type ResourceFilter struct { + // DriverName is the name used by the DRA driver kubelet plugin. + DriverName string + + ResourceFilterModel +} + +// ResourceFilterModel must have one and only one field set. +type ResourceFilterModel struct { + // NamedResources describes a resource filter using the named resources model. + NamedResources *NamedResourcesFilter +} + +// NamedResourcesFilter is used in ResourceFilterModel. +type NamedResourcesFilter struct { + // Selector is a CEL expression which must evaluate to true if a + // resource instance is suitable. The language is as defined in + // https://kubernetes.io/docs/reference/using-api/cel/ + // + // In addition, for each type NamedResourcesin AttributeValue there is a map that + // resolves to the corresponding value of the instance under evaluation. + // For example: + // + // attributes.quantity["a"].isGreaterThan(quantity("0")) && + // attributes.stringslice["b"].isSorted() + Selector string +} +``` + +###### ResourceClaim + + +```go +// ResourceClaim describes which resources are needed by a resource consumer. +// Its status tracks whether the resource has been allocated and what the +// resulting attributes are. +// +// This is an alpha type and requires enabling the DynamicResourceAllocation +// feature gate. +type ResourceClaim struct { + metav1.TypeMeta + // Standard object metadata + // +optional + metav1.ObjectMeta + + // Spec describes the desired attributes of a resource that then needs + // to be allocated. It can only be set once when creating the + // ResourceClaim. + Spec ResourceClaimSpec + + // Status describes whether the resource is available and with which + // attributes. + // +optional + Status ResourceClaimStatus +} + +// Finalizer is the finalizer that gets set for claims +// which were allocated through a builtin controller. +const Finalizer = "dra.k8s.io/delete-protection" +``` + +The scheduler must set a finalizer in a ResourceClaim before it adds +an allocation. This ensures that an allocated, reserved claim cannot +be removed accidentally by a user. + +If storing the status fails, the scheduler will retry on the next +scheduling attempt. If the ResourceClaim gets deleted in the meantime, +the scheduler will not try to schedule again. This situation is handled +by the kube-controller-manager by removing the finalizer. + +Force-deleting a ResourceClaim by clearing its finalizers (something that users +should never do without being aware of the consequences) cannot be +prevented. Deleting the entire cluster also leaves resources allocated outside +of the cluster in an allocated state. + +```go +// ResourceClaimSpec defines how a resource is to be allocated. +type ResourceClaimSpec struct { + // ResourceClassName references the driver and additional parameters + // via the name of a ResourceClass that was created as part of the + // driver deployment. + ResourceClassName string + + // ParametersRef references a separate object with arbitrary parameters + // that will be used by the driver when allocating a resource for the + // claim. + // + // The object must be in the same namespace as the ResourceClaim. + // +optional + ParametersRef *ResourceClaimParametersReference +} + +// ResourceClaimStatus tracks whether the resource has been allocated and what +// the resulting attributes are. +type ResourceClaimStatus struct { + // DriverName is a copy of the driver name from the ResourceClass at + // the time when allocation started. + // +optional + DriverName string + + // Allocation is set by the resource driver once a resource or set of + // resources has been allocated successfully. If this is not specified, the + // resources have not been allocated yet. + // +optional + Allocation *AllocationResult + + // ReservedFor indicates which entities are currently allowed to use + // the claim. A Pod which references a ResourceClaim which is not + // reserved for that Pod will not be started. + // + // There can be at most 32 such reservations. This may get increased in + // the future, but not reduced. + // +optional + ReservedFor []ResourceClaimConsumerReference +} + +// ReservedForMaxSize is the maximum number of entries in +// claim.status.reservedFor. +const ResourceClaimReservedForMaxSize = 32 +``` -### ResourceClaimParameters +##### ResourceClaimParameters ```go // ResourceClaimParameters defines resource requests for a ResourceClaim in an @@ -767,71 +1577,49 @@ type NamedResourcesRequest struct { } ``` -### ResourceClassParameters - -```go -// ResourceClassParameters defines resource requests for a ResourceClass in an -// in-tree format understood by Kubernetes. -type ResourceClassParameters struct { - metav1.TypeMeta - // Standard object metadata - metav1.ObjectMeta - - // If this object was created from some other resource, then this links - // back to that resource. This field is used to find the in-tree representation - // of the class parameters when the parameter reference of the class refers - // to some unknown type. - GeneratedFrom *ResourceClassParametersReference - - // VendorParameters are arbitrary setup parameters for all claims using - // this class. They are ignored while allocating the claim. There must - // not be more than one entry per driver. - VendorParameters []VendorParameters - - // Filters describes additional contraints that must be met when using the class. - Filters []ResourceFilter -} - -// ResourceFilter is a filter for resources from one particular driver. -type ResourceFilter struct { - // DriverName is the name used by the DRA driver kubelet plugin. - DriverName string +NamedResourcesFilter and NamedResourcesRequest currently have the same +content. Despite that, they are defined as separate structs because that might +change in the future. - ResourceFilterModel -} -// ResourceFilterModel must have one and only one field set. -type ResourceFilterModel struct { - // NamedResources describes a resource filter using the named resources model. - NamedResources *NamedResourcesFilter -} +##### Allocation result -// NamedResourcesFilter is used in ResourceFilterModel. -type NamedResourcesFilter struct { - // Selector is a CEL expression which must evaluate to true if a - // resource instance is suitable. The language is as defined in - // https://kubernetes.io/docs/reference/using-api/cel/ +```go +// AllocationResult contains attributes of an allocated resource. +type AllocationResult struct { + // ResourceHandles contain the state associated with an allocation that + // should be maintained throughout the lifetime of a claim. Each + // ResourceHandle contains data that should be passed to a specific kubelet + // plugin once it lands on a node. // - // In addition, for each type NamedResourcesin AttributeValue there is a map that - // resolves to the corresponding value of the instance under evaluation. - // For example: + // Setting this field is optional. It has a maximum size of 32 entries. + // If null (or empty), it is assumed this allocation will be processed by a + // single kubelet plugin with no ResourceHandle data attached. The name of + // the kubelet plugin invoked will match the DriverName set in the + // ResourceClaimStatus this AllocationResult is embedded in. // - // attributes.quantity["a"].isGreaterThan(quantity("0")) && - // attributes.stringslice["b"].isSorted() - Selector string -} -``` + // +listType=atomic + ResourceHandles []ResourceHandle -NamedResourcesFilter and NamedResourcesRequest currently have the same -content. Despite that, they are defined as separate structs because that might -change in the future. + // This field will get set by the resource driver after it has allocated + // the resource to inform the scheduler where it can schedule Pods using + // the ResourceClaim. + // + // Setting this field is optional. If null, the resource is available + // everywhere. + // +optional + AvailableOnNodes *core.NodeSelector -### ResourceHandle extension + // Shareable determines whether the resource supports more + // than one consumer at a time. + // +optional + Shareable bool +} -The ResourceHandle is embedded inside the claim status. When using structured parameters, -a new field must get populated instead of the opaque driver data. +// AllocationResultResourceHandlesMaxSize represents the maximum number of +// entries in allocation.resourceHandles. +const AllocationResultResourceHandlesMaxSize = 32 -```go // ResourceHandle holds opaque resource data for processing by a specific kubelet plugin. type ResourceHandle struct { // DriverName specifies the name of the resource driver whose kubelet @@ -840,19 +1628,8 @@ type ResourceHandle struct { // ResourceClaimStatus this ResourceHandle is embedded in. DriverName string - // Data contains the opaque data associated with this ResourceHandle. It is - // set by the controller component of the resource driver whose name - // matches the DriverName set in the ResourceClaimStatus this - // ResourceHandle is embedded in. It is set at allocation time and is - // intended for processing by the kubelet plugin whose name matches - // the DriverName set in this ResourceHandle. - // - // The maximum size of this field is 16KiB. This may get increased in the - // future, but not reduced. - // +optional - Data string - - // If StructuredData is set, then it needs to be used instead of Data. + // StructuredData captures the result of the allocation for this + // particular driver. StructuredData *StructuredResourceHandle } @@ -890,58 +1667,601 @@ type AllocationResultModel struct { // NamedResources describes the allocation result when using the named resources model. NamedResources *NamedResourcesAllocationResult } +``` + +##### ResourceClaimTemplate + +```go +// ResourceClaimTemplate is used to produce ResourceClaim objects. +type ResourceClaimTemplate struct { + metav1.TypeMeta + // Standard object metadata + // +optional + metav1.ObjectMeta + + // Describes the ResourceClaim that is to be generated. + // + // This field is immutable. A ResourceClaim will get created by the + // control plane for a Pod when needed and then not get updated + // anymore. + Spec ResourceClaimTemplateSpec +} + +// ResourceClaimTemplateSpec contains the metadata and fields for a ResourceClaim. +type ResourceClaimTemplateSpec struct { + // ObjectMeta may contain labels and annotations that will be copied into the PVC + // when creating it. No other fields are allowed and will be rejected during + // validation. + // +optional + metav1.ObjectMeta + + // Spec for the ResourceClaim. The entire content is copied unchanged + // into the ResourceClaim that gets created from this template. The + // same fields as in a ResourceClaim are also valid here. + Spec ResourceClaimSpec +} +``` -// NamedResourcesAllocationResult is used in AllocationResultModel. -type NamedResourcesAllocationResult struct { - // Name is the name of the selected resource instance. +##### Object references + +```go +// ResourceClassParametersReference contains enough information to let you +// locate the parameters for a ResourceClass. +type ResourceClassParametersReference struct { + // APIGroup is the group for the resource being referenced. It is + // empty for the core API. This matches the group in the APIVersion + // that is used when creating the resources. + // +optional + APIGroup string + // Kind is the type of resource being referenced. This is the same + // value as in the parameter object's metadata. + Kind string + // Name is the name of resource being referenced. Name string + // Namespace that contains the referenced resource. Must be empty + // for cluster-scoped resources and non-empty for namespaced + // resources. + // +optional + Namespace string +} + +// ResourceClaimParametersReference contains enough information to let you +// locate the parameters for a ResourceClaim. The object must be in the same +// namespace as the ResourceClaim. +type ResourceClaimParametersReference struct { + // APIGroup is the group for the resource being referenced. It is + // empty for the core API. This matches the group in the APIVersion + // that is used when creating the resources. + // +optional + APIGroup string + // Kind is the type of resource being referenced. This is the same + // value as in the parameter object's metadata, for example "ConfigMap". + Kind string + // Name is the name of resource being referenced. + Name string +} + +// ResourceClaimConsumerReference contains enough information to let you +// locate the consumer of a ResourceClaim. The user must be a resource in the same +// namespace as the ResourceClaim. +type ResourceClaimConsumerReference struct { + // APIGroup is the group for the resource being referenced. It is + // empty for the core API. This matches the group in the APIVersion + // that is used when creating the resources. + // +optional + APIGroup string + // Resource is the type of resource being referenced, for example "pods". + Resource string + // Name is the name of resource being referenced. + Name string + // UID identifies exactly one incarnation of the resource. + UID types.UID } ``` -### Implementation of structured models +`ResourceClassParametersReference` and `ResourceClaimParametersReference` use +the more user-friendly "kind" to identify the object type because those +references are provided by users. `ResourceClaimConsumerReference` is typically +set by the control plane and therefore uses the more technically correct +"resource" name. -In the Go types above, all structs starting with `NamedResources` are part of -that structured model. Code generators (more specifically, the applyconfig -generator) assume that all Go types of an API are defined in the same Go -package. If it wasn't for that, defining those structs in their own package -without the `NamedResources` prefix would be possible and make the Go code -cleaner without affecting the Kubernetes API. +#### core -### Scheduling + Allocation +```go +type PodSpec { + ... + // ResourceClaims defines which ResourceClaims must be allocated + // and reserved before the Pod is allowed to start. The resources + // will be made available to those containers which consume them + // by name. + // + // This is an alpha field and requires enabling the + // DynamicResourceAllocation feature gate. + // + // This field is immutable. + // + // +featureGate=DynamicResourceAllocation + // +optional + ResourceClaims []PodResourceClaim + ... +} + +type ResourceRequirements { + Limits ResourceList + Requests ResourceList + ... + // Claims lists the names of resources, defined in spec.resourceClaims, + // that are used by this container. + // + // This is an alpha field and requires enabling the + // DynamicResourceAllocation feature gate. + // + // This field is immutable. + // + // +featureGate=DynamicResourceAllocation + // +optional + Claims []ResourceClaim +} + +// ResourceClaim references one entry in PodSpec.ResourceClaims. +type ResourceClaim struct { + // Name must match the name of one entry in pod.spec.resourceClaims of + // the Pod where this field is used. It makes that resource available + // inside a container. + Name string +} +``` + +`Claims` is a list of structs with a single `Name` element because that struct +can be extended later, for example to add parameters that influence how the +resource is made available to a container. This wouldn't be possible if +it was a list of strings. + +```go +// PodResourceClaim references exactly one ResourceClaim through a ClaimSource. +// It adds a name to it that uniquely identifies the ResourceClaim inside the Pod. +// Containers that need access to the ResourceClaim reference it with this name. +type PodResourceClaim struct { + // Name uniquely identifies this resource claim inside the pod. + // This must be a DNS_LABEL. + Name string + + // Source describes where to find the ResourceClaim. + Source ClaimSource +} + +// ClaimSource describes a reference to a ResourceClaim. +// +// Exactly one of these fields should be set. Consumers of this type must +// treat an empty object as if it has an unknown value. +type ClaimSource struct { + // ResourceClaimName is the name of a ResourceClaim object in the same + // namespace as this pod. + ResourceClaimName *string + + // ResourceClaimTemplateName is the name of a ResourceClaimTemplate + // object in the same namespace as this pod. + // + // The template will be used to create a new ResourceClaim, which will + // be bound to this pod. When this pod is deleted, the ResourceClaim + // will also be deleted. The pod name and resource name, along with a + // generated component, will be used to form a unique name for the + // ResourceClaim, which will be recorded in pod.status.resourceClaimStatuses. + // + // This field is immutable and no changes will be made to the + // corresponding ResourceClaim by the control plane after creating the + // ResourceClaim. + ResourceClaimTemplateName *string +} -The dynamic resource scheduler plugin handles the common fields of -ResourceSlice, ResourceClaimParameters and StructuredResourceHandle. For the -structured model fields it calls out to code that is associated with the -corresponding model. +struct PodStatus { + ... + // Status of resource claims. + // +featureGate=DynamicResourceAllocation + // +optional + ResourceClaimStatuses []PodResourceClaimStatus +} -During filtering it is decided which nodes have the necessary resources. If a -node is found, the scheduler plugin updates the resource claim status as part -of goroutine which handles pod binding. +// PodResourceClaimStatus is stored in the PodStatus for each PodResourceClaim +// which references a ResourceClaimTemplate. It stores the generated name for +// the corresponding ResourceClaim. +type PodResourceClaimStatus struct { + // Name uniquely identifies this resource claim inside the pod. + // This must match the name of an entry in pod.spec.resourceClaims, + // which implies that the string must be a DNS_LABEL. + Name string -Like a normal DRA driver controller, the scheduler also sets a finalizer to -ensure that users cannot accidentally delete the allocated claim while a pod -is about to start which depends on it. That finalizer is -"structured.dra.k8s.io/delete-protection". + // ResourceClaimName is the name of the ResourceClaim that was + // generated for the Pod in the namespace of the Pod. If this is + // unset, then generating a ResourceClaim was not necessary. The + // pod.spec.resourceClaims entry can be ignored in this case. + ResourceClaimName *string +} +``` -### Deallocation +### kube-controller-manager -Deallocation is handled by kube-controller-manager when its claim controller -observes that a claim is no longer in use *and* the claim has the special -"structured.dra.k8s.io/delete-protection" finalizer. This finalizer tells the -controller that it may clear the allocation result directly instead of setting -the `DeletionRequested` field, which is what it normally would do. +The code that creates a ResourceClaim from a ResourceClaimTemplate started +as an almost verbatim copy of the [generic ephemeral volume +code](https://github.com/kubernetes/kubernetes/tree/master/pkg/controller/volume/ephemeral), +just with different types. Later, generating the name of the ephemeral ResourceClaim +was added. +kube-controller-manager needs [RBAC +permissions](https://github.com/kubernetes/kubernetes/commit/ff3e5e06a79bc69ad3d7ccedd277542b6712514b#diff-2ad93af2302076e0bdb5c7a4ebe68dd3188eee8959c72832181a7597417cd196) that allow creating and updating ResourceClaims. + +kube-controller-manager also removes `claim.status.reservedFor` entries that reference +removed pods or pods that have completed ("Phase" is "done" or will never start). +This is required for pods because kubelet does not have write +permission for ResourceClaimStatus. Pods as user is the common case, so special +code based on a shared pod informer will handle it. Other consumers +need to be handled by whatever controller added them. + +In addition to updating `claim.status.reservedFor`, kube-controller-manager also +removes the allocation from ResourceClaims that are no longer in use. Updating the claim during deallocation will be observed by kube-scheduler and tells it that it can use the capacity set aside for the claim again. kube-controller-manager itself doesn't need to support specific structured models. -### Immediate allocation +### kube-scheduler + +The scheduler plugin for ResourceClaims ("claim plugin" in this section) +needs to implement several extension points. It is responsible for +ensuring that a ResourceClaim is allocated and reserved for a Pod before +the final binding of a Pod to a node. + +The following extension points are implemented in the new claim plugin. Except +for some unlikely edge cases (see below) there are no API calls during the main +scheduling cycle. Instead, the plugin collects information and updates the +cluster in the separate goroutine which invokes PreBind. + + +#### EventsToRegister + +This registers all cluster events that might make an unschedulable pod +schedulable, like creating a claim that the pod needs or finishing the +allocation of a claim. + +[Queuing hints](https://github.com/kubernetes/enhancements/issues/4247) are +supported. These are callbacks that can limit the effect of a cluster event to +specific pods. For example, allocating a claim only makes those pods +scheduleable which reference the claim. There is no need to try scheduling a pod +which waits for some other claim. Hints are also used to trigger the next +scheduling cycle for a pod immediately when some expected and require event +like "drivers have provided information" occurs, instead of forcing the pod to +go through the backoff queue and the usually 5 second long delay associated +with that. + +Queuing hints are an optional feature of the scheduler, with (as of Kubernetes +1.29) their own `SchedulerQueueingHints` feature gate that defaults to +off. When turned off, performance of scheduling pods with resource claims is +slower compared to a cluster configuration where they are turned on. + +#### PreEnqueue + +This checks whether all claims referenced by a pod exist. If they don't, +scheduling the pod has to wait until the kube-controller-manager or user create +the claims. PreEnqueue tries to finish quickly because it is called from +event handlers, so not everything is checked. + +#### Pre-filter + +This is a more thorough version of the checks done by PreEnqueue. It ensures +that all information that is needed (ResourceClaim, ResourceClass, parameters) +is available. + +Another reason why a Pod might not be schedulable is when it depends on claims +which are in the process of being allocated. That process starts in Reserve and +ends in PreBind or Unreserve (see below). + +It then prepares for filtering by converting information stored in various +places (node filter in ResourceClass, available resources in ResourceSlices, +allocated resources in ResourceClaim statuses, in-flight allocations) into a +format that can be used efficiently by Filter. + +#### Filter + +This checks whether the given node has access to those ResourceClaims which +were already allocated. For ResourceClaims that were not, it checks that the +allocation can succeed for a node. + +#### Post-filter + +This is called when no suitable node could be found. If the Pod depends on ResourceClaims with delayed +allocation, then deallocating one or more of these ResourceClaims may make the +Pod schedulable after allocating the resource elsewhere. Therefore each +ResourceClaim with delayed allocation is checked whether all of the following +conditions apply: +- allocated +- not currently in use +- it was the reason why some node could not fit the Pod, as recorded earlier in + Filter + +One of the ResourceClaims satisfying these criteria is picked randomly and gets +deallocated by clearing the allocation in its status. This may make it possible to run the Pod +elsewhere. If it still doesn't help, deallocation may continue with another +ResourceClaim, if there is one. + +This is currently using blocking API calls. It's quite rare because this +situation can only arise when there are multiple claims per pod and writing +the status of one of them fails, thus leaving the other claims in the +allocated state. + +#### Reserve + +A node has been chosen for the Pod. + +For each unallocated claim, the actual allocation result is determined now. To +avoid blocking API calls, that result is not written to the status yet. Instead, +it gets stored in a map of in-flight claims. + +#### PreBind + +This is called in a separate goroutine. The plugin now checks all the +information gathered earlier and updates the cluster accordingly. If some +some API request fails now, PreBind fails and the pod must be +retried. + +Claims whose status got written back get removed from the in-flight claim map. + +#### Unreserve + +The claim plugin removes the Pod from the `claim.status.reservedFor` field if +set there because it cannot be scheduled after all. + +This is necessary to prevent a deadlock: suppose there are two stand-alone +claims that only can be used by one pod at a time and two pods which both +reference them. Both pods will get scheduled independently, perhaps even by +different schedulers. When each pod manages to allocate and reserve one claim, +then neither of them can get scheduled because they cannot reserve the other +claim. + +Giving up the reservations in Unreserve means that the next pod scheduling +attempts have a chance to succeed. It's non-deterministic which pod will win, +but eventually one of them will. Not giving up the reservations would lead to a +permanent deadlock that somehow would have to be detected and resolved to make +progress. + +All claims get removed from the in-flight claim map. + +Unreserve is called in two scenarios: +- In the main goroutine when scheduling a pod has failed: in that case the plugin's + Reserve call hasn't actually changed the claim status yet, so there is nothing + that needs to be rolled back. +- After binding has failed: this runs in a goroutine, so reverting the + `claim.status.reservedFor` with a blocking call is acceptable. + +### kubelet + +#### Managing resources + +kubelet must ensure that resources are ready for use on the node before running +the first Pod that uses a specific resource instance and make the resource +available elsewhere again when the last Pod has terminated that uses it. For +both operations, kubelet calls a resource kubelet plugin as explained in the next +section. + +Pods that are not listed in ReservedFor or where the ResourceClaim doesn't +exist at all must not be allowed to run. Instead, a suitable event must be +emitted which explains the problem. Such a situation can occur as part of +downgrade scenarios. + +If this was the last Pod on the node that uses the specific +resource instance, then NodeUnprepareResource (see below) must have been called +successfully before allowing the pod to be deleted. This ensures that network-attached resource are available again +for other Pods, including those that might get scheduled to other nodes. It +also signals that it is safe to deallocate and delete the ResourceClaim. + + +![kubelet](./kubelet.png) + +#### Communication between kubelet and resource kubelet plugin + +Resource kubelet plugins are discovered through the [kubelet plugin registration +mechanism](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/#device-plugin-registration). A +new "ResourcePlugin" type will be used in the Type field of the +[PluginInfo](https://pkg.go.dev/k8s.io/kubelet/pkg/apis/pluginregistration/v1#PluginInfo) +response to distinguish the plugin from device and CSI plugins. + +Under the advertised Unix Domain socket the kubelet plugin provides the +k8s.io/kubelet/pkg/apis/dra gRPC interface. It was inspired by +[CSI](https://github.com/container-storage-interface/spec/blob/master/spec.md), +with “volume” replaced by “resource” and volume specific parts removed. + +##### NodeListAndWatchResources + +NodeListAndWatchResources returns a stream of NodeResourcesResponse objects. +At the start and whenever resource availability changes, the +plugin must send one such object with all information to the kubelet. The +kubelet then syncs that information with ResourceSlice objects. + +``` +message NodeListAndWatchResourcesRequest { +} + +message NodeListAndWatchResourcesResponse { + repeated k8s.io.api.resource.v1alpha2.ResourceModel resources = 1; +} +``` + +##### NodePrepareResource + +This RPC is called by the kubelet when a Pod that wants to use the specified +resource is scheduled on a node. The Plugin SHALL assume that this RPC will be +executed on the node where the resource will be used. + +ResourceClaim.meta.Namespace, ResourceClaim.meta.UID, ResourceClaim.Name and +one of the ResourceHandles from the ResourceClaimStatus.AllocationResult with +a matching DriverName should be passed to the Plugin as parameters to identify +the claim and perform resource preparation. + +ResourceClaim parameters (namespace, UUID, name) are useful for debugging. +They enable the Plugin to retrieve the full ResourceClaim object, should it +ever be needed (normally it shouldn't). + +The Plugin SHALL return fully qualified device name[s]. + +The Plugin SHALL ensure that there are json file[s] in CDI format +for the allocated resource. These files SHALL be used by runtime to +update runtime configuration before creating containers that use the +resource. + +This operation SHALL do as little work as possible as it’s called +after a pod is scheduled to a node. All potentially failing operations +SHALL be done during allocation phase. + +This operation MUST be idempotent. If the resource corresponding to +the `resource_id` has already been prepared, the Plugin MUST reply `0 +OK`. + +If this RPC failed, or kubelet does not know if it failed or not, it +MAY choose to call `NodePrepareResource` again, or choose to call +`NodeUnprepareResource`. + +On a successful call this RPC should return set of fully qualified +CDI device names, which kubelet MUST pass to the runtime through the CRI +protocol. For version v1alpha3, the RPC should return multiple sets of +fully qualified CDI device names, one per claim that was sent in the input parameters. + +```protobuf +message NodePrepareResourcesRequest { + // The list of ResourceClaims that are to be prepared. + repeated Claim claims = 1; +} + +message Claim { + // The ResourceClaim namespace (ResourceClaim.meta.Namespace). + // This field is REQUIRED. + string namespace = 1; + // The UID of the Resource claim (ResourceClaim.meta.UUID). + // This field is REQUIRED. + string uid = 2; + // The name of the Resource claim (ResourceClaim.meta.Name) + // This field is REQUIRED. + string name = 3; + // Resource handle (AllocationResult.ResourceHandles[*].Data) + // This field is OPTIONAL. + string resource_handle = 4; + // Structured parameter resource handle (AllocationResult.ResourceHandles[*].StructuredData). + // This field is OPTIONAL. If present, it needs to be used + // instead of resource_handle. It will only have a single entry. + // + // Using "repeated" instead of "optional" is a workaround for https://github.com/gogo/protobuf/issues/713. + repeated k8s.io.api.resource.v1alpha2.StructuredResourceHandle structured_resource_handle = 5; +} +``` + +`resource_handle` and `structured_resource_handle` will be set depending on how +the claim was allocated. See also KEP #3063. + +``` +message NodePrepareResourcesResponse { + // The ResourceClaims for which preparation was done + // or attempted, with claim_uid as key. + // + // It is an error if some claim listed in NodePrepareResourcesRequest + // does not get prepared. NodePrepareResources + // will be called again for those that are missing. + map claims = 1; +} +``` + +CRI protocol MUST be extended for this purpose: + + * CDIDevice structure should be added to the CRI specification +```protobuf +// CDIDevice specifies a CDI device information. +message CDIDevice { + // Fully qualified CDI device name + // for example: vendor.com/gpu=gpudevice1 + // see more details in the CDI specification: + // https://github.com/container-orchestrated-devices/container-device-interface/blob/main/SPEC.md + string name = 1; +} +``` + * CDI devices should be added to the ContainerConfig structure: +```protobuf +// ContainerConfig holds all the required and optional fields for creating a +// container. +message ContainerConfig { + // Metadata of the container. This information will uniquely identify the + // container, and the runtime should leverage this to ensure correct + // operation. The runtime may also use this information to improve UX, such + // as by constructing a readable name. + ContainerMetadata metadata = 1 ; + // Image to use. + ImageSpec image = 2; + // Command to execute (i.e., entrypoint for docker) + repeated string command = 3; +... + // CDI devices for the container. + repeated CDIDevice cdi_devices = 17; +} +``` + +###### NodePrepareResource Errors + +If the plugin is unable to complete the NodePrepareResource call +successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST +return the specified gRPC error code. Kubelet MUST implement the +specified error recovery behavior when it encounters the gRPC error +code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Resource does not exist | 5 NOT_FOUND | Indicates that a resource corresponding to the specified `resource_id` does not exist. | Caller MUST verify that the `resource_id` is correct and that the resource is accessible and has not been deleted before retrying with exponential back off. | + + +##### NodeUnprepareResources + +A Kubelet Plugin MUST implement this RPC call. This RPC is a reverse +operation of `NodePrepareResource`. This RPC MUST undo the work by +the corresponding `NodePrepareResource`. This RPC SHALL be called by +kubelet at least once for each successful `NodePrepareResource`. The +Plugin SHALL assume that this RPC will be executed on the node where +the resource is being used. + +This RPC is called by the kubelet when the last Pod using the resource is being +deleted or has reached a final state ("Phase" is "done"). + +This operation MUST be idempotent. If this RPC failed, or kubelet does +not know if it failed or not, it can choose to call +`NodeUnprepareResource` again. + +```protobuf +message NodeUnprepareResourcesRequest { + // The list of ResourceClaims that are to be unprepared. + repeated Claim claims = 1; +} + +message NodeUnprepareResourcesResponse { + // The ResourceClaims for which preparation was reverted. + // The same rules as for NodePrepareResourcesResponse.claims + // apply. + map claims = 1; +} + +message NodeUnprepareResourceResponse { + // If non-empty, unpreparing the ResourceClaim failed. + string error = 1; +} +``` + +###### NodeUnprepareResource Errors + +If the plugin is unable to complete the NodeUprepareResource call +successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST +return the specified gRPC error code. Kubelet MUST implement the +specified error recovery behavior when it encounters the gRPC error +code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Resource does not exist | 5 NOT_FOUND | Indicates that a resource corresponding to the specified `resource_id` does not exist. | Caller MUST verify that the `resource_id` is correct and that the resource is accessible and has not been deleted before retrying with exponential back off. | -Because there is no separate controller anymore, claims with immediate -allocation will only get allocated once there is a pod which needs them. The -remaining structured difference compared to delayed allocation is that claims -with immediate allocation remain allocated when no longer in use. ### Simulation with CA @@ -1014,17 +2334,13 @@ This can inform certain test coverage improvements that we want to do before extending the production code to implement this enhancement. --> -- ``: `` - `` +- `k8s.io/kubernetes/pkg/scheduler`: 2022-05-24 - 75.0% +- `k8s.io/kubernetes/pkg/scheduler/framework`: 2022-05-24 - 76.3% +- `k8s.io/kubernetes/pkg/controller`: 2022-05-24 - 69.4% +- `k8s.io/kubernetes/pkg/kubelet`: 2022-05-24 - 64.5% ##### Integration tests - - -- : - ##### e2e tests -- : +End-to-end testing depends on a working resource driver and a container runtime +with CDI support. A [test driver](https://github.com/kubernetes/kubernetes/tree/master/test/e2e/dra/test-driver) +was developed in parallel to developing the +code in Kubernetes. + +That test driver simply takes parameters from ResourceClass +and ResourceClaim and turns them into environment variables that then get +checked inside containers. Tests for different behavior of an driver in various +scenarios can be simulated by running the control-plane part of it in the E2E +test itself. For interaction with kubelet, proxying of the gRPC interface can +be used, as in the +[csi-driver-host-path](https://github.com/kubernetes-csi/csi-driver-host-path/blob/16251932ab81ad94c9ec585867104400bf4f02e5/cmd/hostpathplugin/main.go#L61-L63): +then the kubelet plugin runs on the node(s), but the actual processing of gRPC +calls happens inside the E2E test. + +All tests that don't involve actually running a Pod can become part of +conformance testing. Those tests that run Pods cannot be because CDI support in +runtimes is not required. + +For beta: +- pre-merge with kind (optional, triggered for code which has an impact on DRA): https://testgrid.k8s.io/sig-node-dynamic-resource-allocation#pull-kind-dra +- periodic with kind: https://testgrid.k8s.io/sig-node-dynamic-resource-allocation#ci-kind-dra +- pre-merge with CRI-O: https://testgrid.k8s.io/sig-node-dynamic-resource-allocation#pull-node-dra +- periodic with CRI-O: https://testgrid.k8s.io/sig-node-dynamic-resource-allocation#ci-node-e2e-crio-dra + ### Graduation Criteria @@ -1400,10 +2738,13 @@ Major milestones might include: - when the KEP was retired or superseded --> +- Kubernetes 1.30: Code merged as "alpha" + ## Drawbacks DRA driver developers have to give up some flexibility with regards to -parameters. They have to learn and understand how structured models +parameters compared to opaque parameters in KEP #3063. +They have to learn and understand how structured models work to pick something which fits their needs. ## Alternatives @@ -1444,3 +2785,144 @@ several different ways: support of such a rebuilt CA binary. However, technically it [becomes possible](https://github.com/kubernetes-sigs/kube-scheduler-wasm-extension) with this KEP. + +### ResourceClaimTemplate + +Instead of creating a ResourceClaim from a template, the +PodStatus could be extended to hold the same information as a +ResourceClaimStatus. Every component which works with that information +then needs permission and extra code to work with PodStatus. Creating +an extra object seems simpler. + +### Reusing volume support as-is + +ResourceClaims are similar to PersistentVolumeClaims and also a lot of +the associated logic is similar. An [early +prototype](https://github.com/intel/proof-of-concept-cdi) used a +custom CSI driver to manage resources. + +The user experience with that approach is poor because per-resource +parameters must be stored in annotations of a PVC due to the lack of +custom per-PVC parameters. Passing annotations as additional parameters was [proposed +before](https://github.com/kubernetes-csi/external-provisioner/issues/86) +but were essentially [rejected by +SIG-Storage](https://github.com/kubernetes-csi/external-provisioner/issues/86#issuecomment-465836185) +because allowing apps to set custom parameters would make apps +non-portable. + +The current volume support also has open issues that affect the +“volume as resource” approach: Multiple different Pods on a node are +allowed to use the same +volume. https://github.com/kubernetes/enhancements/pull/2489 will +address that, but is still work in progress. Recovery from a bad node +selection during delayed binding may get stuck when a Pod has multiple +volumes because volumes are not getting deleted after a partial +provisioning. A proposal to fix that needs further work +(https://github.com/kubernetes/enhancements/pull/1703). Each “fake” +CSI driver would have to implement and install a scheduler extender +because storage capacity tracking only considers volume size as +criteria for selecting nodes, which is not applicable for custom +resources. + +### Extend volume support + +The StorageClass and PersistentVolumeClaim structs could be extended +to allow custom parameters. Together with an extension of the CSI +standard that would address the main objection against the previous +alternative. + +However, SIG-Storage and the CSI community would have to agree to this +kind of reuse and accept that some of the code maintained by them +becomes more complex because of these new use cases. + +### Extend Device Plugins + +The device plugins API could be extended to implement some of the +requirements mentioned in the “Motivation” section of this +document. There were certain attempts to do it, for example an attempt +to [add ‘Deallocate’ API call](https://github.com/kubernetes/enhancements/pull/1949) and [pass pod annotations to 'Allocate' API call](https://github.com/kubernetes/kubernetes/pull/61775) + +However, most of the requirements couldn’t be satisfied using this +approach as they would require major incompatible changes in the +Device Plugins API. For example: partial and optional resource +allocation couldn’t be done without changing the way resources are +currently declared on the Pod and Device Plugin level. + +Extending the device plugins API to use [Container Device Interface](https://github.com/container-orchestrated-devices/container-device-interface) +would help address some of the requirements, but not all of them. + +NodePrepareResource and NodeUnprepareResource could be added to the Device Plugins API and only get called for +resource claims. + +However, this would mean that +developers of the device plugins would have to implement mandatory +API calls (ListAndWatch, Allocate), which could create confusion +as those calls are meaningless for the Dynamic Resource Allocation +purposes. + +Even worse, existing device plugins would have to implement the new +calls with stubs that return errors because the generated Go interface +will require them. + +It should be also taken into account that device plugins API is +beta. Introducing incompatible changes to it may not be accepted by +the Kubernetes community. + +### Webhooks instead of ResourceClaim updates + +In the current design, scheduler and the third-party resource driver communicate by +updating fields in a ResourceClaim. This has several advantages compared to an +approach were kube-scheduler retrieves information from the resource driver +via HTTP: +* No need for a new webhook API. +* Simpler deployment of a resource driver because all it needs are + credentials to communicate with the apiserver. +* Current status can be checked by querying the ResourceClaim. + +The downside is higher load on the apiserver and an increase of the size of +ResourceClaim objects. + +### ResourceDriver + +Similar to CSIDriver for storage, a separate object describing a resource +driver might be useful at some point. At the moment it is not needed yet and +therefore not part of the v1alpha2 API. If it becomes necessary to describe +optional features of a resource driver, such a ResourceDriver type might look +like this: + +``` +type ResourceDriver struct { + // The name of the object is the unique driver name. + ObjectMeta + + // Features contains a list of features supported by the driver. + // New features may be added over time and must be ignored + // by code that does not know about them. + Features []ResourceDriverFeature +} + +type ResourceDriverFeature struct { + // Name is one of the pre-defined names for a feature. + Name ResourceDriverFeatureName + // Parameters might provide additional information about how + // the driver supports the feature. Boolean features have + // no parameters, merely listing them indicates support. + Parameters runtime.RawExtension +} +``` + +### Complex sharing of ResourceClaim + +At the moment, the allocation result marks as a claim as either "shareable" by +an unlimited number of consumers or "not shareable". More complex scenarios +might be useful like "may be shared by a certain number of consumers", but so +far such use cases have not come up yet. If they do, the `AllocationResult` can +be extended with new fields as defined by a follow-up KEP. + +## Infrastructure Needed + +Initially, all development will happen inside the main Kubernetes +repository. The mock driver can be developed inside test/e2e/dra. For the +generic part of that driver, i.e. the code that other drivers can reuse, and +other common code a new staging repo `k8s.io/dynamic-resource-allocation` is +needed. diff --git a/keps/sig-node/4381-dra-structured-parameters/components.png b/keps/sig-node/4381-dra-structured-parameters/components.png new file mode 100644 index 0000000000000000000000000000000000000000..417fe21ac42e7425dbf03de37c68e66b71fef412 GIT binary patch literal 74542 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 literal 0 HcmV?d00001 diff --git a/keps/sig-node/4381-dra-structured-parameters/components.puml b/keps/sig-node/4381-dra-structured-parameters/components.puml new file mode 100644 index 00000000000..36eb3afc16f --- /dev/null +++ b/keps/sig-node/4381-dra-structured-parameters/components.puml @@ -0,0 +1,60 @@ +@startuml +!theme reddress-lightblue +skinparam componentStyle rectangle +left to right direction + +cloud "resource driver" as resourcedriver { + component "CRD controller" as drivercrdcontroller + component "kubelet plugin" as driverplugin +} + +component Kubernetes { + component apiserver { + component namespaced { + file ResourceClaimTemplate + file Pod + file ResourceClaim + file DriverCRDParameters + file ResourceClaimParameters + } + component "cluster-scoped" as clusterscoped { + file ResourceClass + file ResourceSlice + } + } + component scheduler { + component "resource plugin" as k8sresourceplugin + } + component "controller-manager" as controllermanager { + component "resource claim controller" as k8sresourceclaimcontroller + } + component kubelet { + component "plugin manager" as pluginmanager + component "resource manager" as resourcemanager + } +} + +' Kubernetes ---> resourcedriver + +ResourceClaimTemplate <. Pod +Pod <. ResourceClaim: owned by\n(if created from template) +ResourceClaim .> ResourceClass +ResourceClaim .> DriverCRDParameters +ResourceClaimParameters .> DriverCRDParameters: generated from,\nowned by + +Pod -u-> k8sresourceclaimcontroller +ResourceClaimTemplate -u-> k8sresourceclaimcontroller +ResourceClaim <-u- k8sresourceclaimcontroller: create claim,\nclean up consumers,\ntrigger deallocation +k8sresourceplugin <- ResourceClaimParameters + +Pod <--> kubelet +Pod <--> scheduler +ResourceClaim <--> k8sresourceplugin + +ResourceClaimParameters <- drivercrdcontroller: create, update +DriverCRDParameters ---> drivercrdcontroller: read +resourcemanager <-> driverplugin: calls gRPC methods,\nreceives stream of\nresource information +ResourceSlice-> k8sresourceplugin: consumes +resourcemanager --> ResourceSlice: publishes +pluginmanager <-> driverplugin: registers +@enduml diff --git a/keps/sig-node/4381-dra-structured-parameters/kep.yaml b/keps/sig-node/4381-dra-structured-parameters/kep.yaml index 1c17a58f9ab..08b6dcf1c4b 100644 --- a/keps/sig-node/4381-dra-structured-parameters/kep.yaml +++ b/keps/sig-node/4381-dra-structured-parameters/kep.yaml @@ -25,7 +25,7 @@ stage: alpha # The most recent milestone for which work toward delivery of this KEP has been # done. This can be the current (upcoming) milestone, if it is being actively # worked on. -latest-milestone: "v1.30" +latest-milestone: "v1.31" # The milestone at which this feature was, or is targeted to be, at each stage. milestone: @@ -34,7 +34,12 @@ milestone: # The following PRR answers are required at alpha release # List the feature gate name and the components for which it must be enabled feature-gates: - # DynamicResourceAllocation, same as for DRA without this KEP + - name: DynamicResourceAllocation + components: + - kube-apiserver + - kube-controller-manager + - kube-scheduler + - kubelet disable-supported: true # The following PRR answers are required at beta release diff --git a/keps/sig-node/4381-dra-structured-parameters/kubelet.png b/keps/sig-node/4381-dra-structured-parameters/kubelet.png new file mode 100644 index 0000000000000000000000000000000000000000..3de5946d6d4e8d14b5ee6dc133497176e99fd29d GIT binary patch literal 26102 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

ehihuSs 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 literal 0 HcmV?d00001 diff --git a/keps/sig-node/3063-dynamic-resource-allocation/kubelet.puml b/keps/sig-node/4381-dra-structured-parameters/kubelet.puml similarity index 100% rename from keps/sig-node/3063-dynamic-resource-allocation/kubelet.puml rename to keps/sig-node/4381-dra-structured-parameters/kubelet.puml From d028f775ae50f60c66bfdcb7b28a8aedb4a6563b Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 22 Apr 2024 13:42:11 +0200 Subject: [PATCH 2/3] DRA: move DriverName into 3063 KEP Structured parameters don't use the ResourceClass driver name for anything. All that matters are the driver names in the individual requests and/or filters. --- .../README.md | 21 ++++++++-- .../4381-dra-structured-parameters/README.md | 42 ++++++++++++------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/keps/sig-node/3063-dynamic-resource-allocation/README.md b/keps/sig-node/3063-dynamic-resource-allocation/README.md index 730f6e5df9c..b47c1b83083 100644 --- a/keps/sig-node/3063-dynamic-resource-allocation/README.md +++ b/keps/sig-node/3063-dynamic-resource-allocation/README.md @@ -256,12 +256,18 @@ plane controller: type ResourceClass struct { ... - // If (and only if) allocation of claims using this class is handled - // by the DRA driver, ControlPlaneController must be set to true. + // ControllerName defines the name of the dynamic resource driver that is + // used for allocation of a ResourceClaim that uses this class. If empty, + // structured parameters are used for allocating claims using this class. + // + // Resource drivers have a unique name in forward domain order + // (acme.example.com). // // This is an alpha field and requires enabling the DRAControlPlaneController // feature gate. - ControlPlaneController bool + // + // +optional + ControllerName string } ``` @@ -353,6 +359,15 @@ const ( ``` type ResourceClaimStatus struct { ... + // ControllerName is a copy of the driver name from the ResourceClass at + // the time when allocation started. It is empty when the claim was + // allocated through structured parameters, + // + // This is an alpha field and requires enabling the DRAControlPlaneController + // feature gate. + // + // +optional + ControllerName string // DeallocationRequested indicates that a ResourceClaim is to be // deallocated. diff --git a/keps/sig-node/4381-dra-structured-parameters/README.md b/keps/sig-node/4381-dra-structured-parameters/README.md index 5a91ddea8bb..02f70a1796b 100644 --- a/keps/sig-node/4381-dra-structured-parameters/README.md +++ b/keps/sig-node/4381-dra-structured-parameters/README.md @@ -447,7 +447,6 @@ apiVersion: core.k8s.io/v1alpha2 kind: ResourceClass metadata: name: acme-gpu -driverName: gpu.example.com parametersRef: apiGroup: gpu.example.com kind: GPUInit @@ -1332,13 +1331,6 @@ type ResourceClass struct { // +optional metav1.ObjectMeta - // DriverName defines the name of the dynamic resource driver that is - // used for allocation of a ResourceClaim that uses this class. - // - // Resource drivers have a unique name in forward domain order - // (acme.example.com). - DriverName string - // ParametersRef references an arbitrary separate object that may hold // parameters that will be used by the driver when allocating a // resource that uses this class. A dynamic resource driver can @@ -1354,6 +1346,14 @@ type ResourceClass struct { // Setting this field is optional. If null, all nodes are candidates. // +optional SuitableNodes *core.NodeSelector + + // DefaultClaimParametersRef is an optional reference to an object that holds parameters + // used as default when allocating a claim which references this class. This field is utilized + // only when the ParametersRef of the claim is nil. If both ParametersRef + // and DefaultClaimParametersRef are nil, the claim requests no resources and thus + // can always be allocated. + // +optional + DefaultClaimParametersRef *ResourceClassParametersReference } ``` @@ -1402,7 +1402,7 @@ type NamedResourcesFilter struct { // resource instance is suitable. The language is as defined in // https://kubernetes.io/docs/reference/using-api/cel/ // - // In addition, for each type NamedResourcesin AttributeValue there is a map that + // In addition, for each type in NamedResourcesAttributeValue there is a map that // resolves to the corresponding value of the instance under evaluation. // For example: // @@ -1464,6 +1464,7 @@ type ResourceClaimSpec struct { // ResourceClassName references the driver and additional parameters // via the name of a ResourceClass that was created as part of the // driver deployment. + // +optional ResourceClassName string // ParametersRef references a separate object with arbitrary parameters @@ -1474,15 +1475,26 @@ type ResourceClaimSpec struct { // +optional ParametersRef *ResourceClaimParametersReference } +``` +The `ResourceClassName` field may be left empty. The parameters are sufficient +to determine which driver needs to provide resources. This leads to some corner cases: +- Empty `ResourceClassName` and nil `ParametersRef`: this is a claim which requests + no resources. Such a claim can always be allocated with an empty result. Allowing + this simplifies code generators which dynamically fill in the resource requests + because they are allowed to generate an empty claim. +- Non-empty `ResourceClassName`, nil `ParametersRef`, nil + `ResourceClass.DefaultClaimParametersRef`: this is handled the same way, the + only difference is that the cluster admin has decided that such claims need + no resources by not providing default parameters. + +There is no default ResourceClass. If that is desirable, then it can be +implemented with a mutating and/or admission webhook. + +``` // ResourceClaimStatus tracks whether the resource has been allocated and what // the resulting attributes are. type ResourceClaimStatus struct { - // DriverName is a copy of the driver name from the ResourceClass at - // the time when allocation started. - // +optional - DriverName string - // Allocation is set by the resource driver once a resource or set of // resources has been allocated successfully. If this is not specified, the // resources have not been allocated yet. @@ -1567,7 +1579,7 @@ type NamedResourcesRequest struct { // resource instance is suitable. The language is as defined in // https://kubernetes.io/docs/reference/using-api/cel/ // - // In addition, for each type NamedResourcesin AttributeValue there is a map that + // In addition, for each type in NamedResourcesAttributeValue there is a map that // resolves to the corresponding value of the instance under evaluation. // For example: // From e61fc514f9a498212864c2f316c2c76c64716fc4 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 29 Apr 2024 15:28:33 +0200 Subject: [PATCH 3/3] DRA: review feedback --- .../README.md | 10 ++-- .../4381-dra-structured-parameters/README.md | 57 ++++++++++--------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/keps/sig-node/3063-dynamic-resource-allocation/README.md b/keps/sig-node/3063-dynamic-resource-allocation/README.md index b47c1b83083..a6f6cd80523 100644 --- a/keps/sig-node/3063-dynamic-resource-allocation/README.md +++ b/keps/sig-node/3063-dynamic-resource-allocation/README.md @@ -769,11 +769,11 @@ to simulate the effect of allocating claims as part of scheduling and of creating or removing nodes. This is not possible with opaque parameters as described in this KEP. If a DRA -driver developer wants to support Cluster Autoscaler, they have to use semantic -parameters. Semantic parameters are an extension of this KEP that is defined in -[KEP #4381](https://github.com/kubernetes/enhancements/issues/4381). +driver developer wants to support Cluster Autoscaler, they have to use +structured parameters as defined in [KEP +#4381](https://github.com/kubernetes/enhancements/issues/4381). -Semantic parameters are not necessary for network-attached resources because +Structured parameters are not necessary for network-attached resources because adding or removing nodes doesn't change their availability and thus Cluster Autoscaler does not need to understand their parameters. @@ -934,7 +934,7 @@ For beta: - In normal scenarios, scheduling pods with claims must not block scheduling of other pods by doing blocking API calls -- Implement integration with Cluster Autoscaler through semantic parameters +- Implement integration with Cluster Autoscaler through structured parameters - Gather feedback from developers and surveys - Positive acknowledgment from 3 would-be implementors of a resource driver, from a diversity of companies or projects diff --git a/keps/sig-node/4381-dra-structured-parameters/README.md b/keps/sig-node/4381-dra-structured-parameters/README.md index 02f70a1796b..c754cd61572 100644 --- a/keps/sig-node/4381-dra-structured-parameters/README.md +++ b/keps/sig-node/4381-dra-structured-parameters/README.md @@ -297,9 +297,9 @@ address limitations of the current approach for the following use cases: - *Device initialization*: When starting a workload that uses an accelerator like an FPGA, I’d like to have the accelerator - reconfigured or reprogrammed for the workload before the workload - itself starts. For security reasons, workloads should not be able to - reconfigure devices directly. + reconfigured or reprogrammed without having to deploy my application + with full hardware access and/or root privileges. Running applications + with less privileges is better for overall security of the cluster. *Limitation*: Currently, it’s impossible to specify the desired device properties that are required for reconfiguring devices. @@ -315,28 +315,22 @@ address limitations of the current approach for the following use cases: *Limitation*: Post-stop actions are not supported. -- *Partial allocation*: When deploying a container I’d like to be able - to use part of the shareable device inside a container and other - containers should be able to use other free resources on the same - device. - - *Limitation*: For example, newer generations of NVIDIA GPUs have a mode of - operation called MIG, that allow them to be sub-divided into a set of - mini-GPUs (called MIG devices) with varying amounts of memory and compute - resources provided by each. From a hardware-standpoint, configuring a GPU - into a set of MIG devices is highly-dynamic and creating a MIG device - tailored to the resource needs of a particular application is well - supported. However, with the current device plugin API, the only way to make - use of this feature is to pre-partition a GPU into a set of MIG devices and - advertise them to the kubelet in the same way a full / static GPU is - advertised. The user must then pick from this set of pre-partitioned MIG - devices instead of having one created for them on the fly based on their - particular resource constraints. Without the ability to create MIG devices - dynamically (i.e. at the time they are requested) the set of pre-defined MIG - devices must be carefully tuned to ensure that GPU resources do not go unused - because some of the pre-partioned devices are in low-demand. It also puts - the burden on the user to pick a particular MIG device type, rather than - declaring the resource constraints more abstractly. +- *Partial allocation*: When workloads use only a portion of the device + capabilities, devices can be partitioned (e.g. using Nvidia MIG or SR-IOV) to + better match workload needs. Sharing the devices in this way can greatly + increase HW utilization / reduce costs. + +- *Limitation*: currently there's no API to request partial device + allocation. With the current device plugin API, devices need to be + pre-partitioned and advertised in the same way a full / static devices + are. User must then select a pre-partitioned device instead of having one + created for them on the fly based on their particular resource + constraints. Without the ability to create devices dynamically (i.e. at the + time they are requested) the set of pre-defined devices must be carefully + tuned to ensure that device resources do not go unused because some of the + pre-partioned devices are in low-demand. It also puts the burden on the user + to pick a particular device type, rather than declaring the resource + constraints more abstractly. - *Optional allocation*: When deploying a workload I’d like to specify soft(optional) device requirements. If a device exists and it’s @@ -873,8 +867,8 @@ parameters differently for reporting resource availability. This is the result of an attack against the resource driver, either from a container which uses a resource exposed by the driver, a compromised kubelet -which interacts with the plugin, or through a successful attack against the -node which led to root access. +which interacts with the plugin, or due to resource driver running on a node +with a compromised root account. The resource driver plugin only needs read access to objects described in this KEP, so compromising it does not interfere with dynamic resource allocation for @@ -1161,6 +1155,15 @@ resources. ### API +``` +<<[UNRESOLVED @pohly @johnbelamaric]>> +Before 1.31, we need to re-evaluate the API, including, but not limited to: +- Do we really need a separate ResourceClaim? +- Does "Device" instead of "Resource" make the API easier to understand? +- Avoid separate parameter objects if and when possible. +<<[/UNRESOLVED]>> +``` + The PodSpec gets extended. To minimize the changes in core/v1, all new types get defined in a new resource group. This makes it possible to revise those more experimental parts of the API in the future. The new fields in the