From dfca0bd4f477cf9b8de21123b73d45b1aa7ae127 Mon Sep 17 00:00:00 2001 From: Tsubasa Watanabe Date: Tue, 12 May 2026 14:08:07 +0900 Subject: [PATCH 1/3] feat: add configurable binding conditions support Add --enable-binding-conditions flag and GPU profile support for binding conditions processing in DRA driver. Signed-off-by: Tsubasa Watanabe --- README.md | 118 ++++++++++++- cmd/dra-example-kubeletplugin/main.go | 10 +- demo/binding-conditions.yaml | 83 ++++++++++ demo/scripts/kind-cluster-config.yaml | 1 + demo/test-binding-conditions.sh | 156 ++++++++++++++++++ .../templates/kubeletplugin.yaml | 2 + .../helm/dra-example-driver/values.yaml | 2 + internal/profiles/gpu/gpu.go | 23 ++- internal/profiles/gpu/gpu_test.go | 22 +-- internal/profiles/profiles.go | 5 + 10 files changed, 403 insertions(+), 19 deletions(-) create mode 100644 demo/binding-conditions.yaml create mode 100755 demo/test-binding-conditions.sh diff --git a/README.md b/README.md index d91aaab8..fbe96cb0 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,120 @@ To run this demo: This demonstration shows the end-to-end flow of the DRA AdminAccess feature. In a production environment, drivers could use this admin access indication to provide additional privileged capabilities or information to authorized workloads. +### Demo DRA Device Binding Conditions Feature +This example driver includes support for the [DRA Device Binding Conditions feature](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/#device-binding-conditions), which allows drivers to declare conditions that must be met before a device is considered fully bound and ready for use. +When enabled, each GPU device in the `ResourceSlice` is published with a `bindingConditions` list and a `bindingFailureConditions` list. +The scheduler uses these to track the binding lifecycle of allocated devices. + +#### Usage Example + +1. **Enable binding conditions**: Binding conditions are controlled by the `bindingConditions` flag. Set `kubeletPlugin.bindingConditions` to `true` in `values.yaml` or via `--set`: + ```bash + helm upgrade -i \ + --create-namespace \ + --namespace dra-example-driver \ + --set kubeletPlugin.bindingConditions=true \ + dra-example-driver \ + deployments/helm/dra-example-driver + ``` + + **Note**: The `DRADeviceBindingConditions` feature gate must also be enabled on the Kubernetes cluster. + The demo Kind cluster configuration (`demo/scripts/kind-cluster-config.yaml`) already includes this gate. + +2. **Verify the ResourceSlice**: After installing the driver with binding conditions enabled, inspect the `ResourceSlice` to confirm that devices are published with binding condition fields: + ```bash + kubectl get resourceslice -o yaml + ``` + + Each device should contain: + ```yaml + bindsToNode: true + bindingConditions: + - BindingConditions + bindingFailureConditions: + - BindingFailureConditions + ``` + +3. **Create the demo pod**: Apply the example manifest and confirm that the pod stays `Pending` because the binding conditions have not yet been satisfied: + ```bash + kubectl apply -f demo/binding-conditions.yaml + ``` + + ```console + $ kubectl get pod -n binding-conditions + NAME READY STATUS RESTARTS AGE + pod0 0/1 Pending 0 14s + ``` + Also verify that the `ResourceClaim` has been allocated and that `bindingConditions` and `bindingFailureConditions` appear in its status: + ```console + $ kubectl get resourceclaim -n binding-conditions + NAME STATE AGE + pod0-gpu-5bnfq allocated,reserved 5m14s + ``` + + ```console + $ kubectl get resourceclaim -n binding-conditions -o yaml + ... + status: + allocation: + devices: + results: + - bindingConditions: + - BindingConditions + bindingFailureConditions: + - BindingFailureConditions + device: gpu-0 + driver: gpu.example.com + pool: dra-example-driver-cluster-worker + request: gpu + reservedFor: + - name: pod0 + resource: pods + ... + ``` + +4. **Set the binding condition**: In a real driver, an external controller or other component would update the `ResourceClaim` status to signal that the device is ready. In this demo, simulate that by editing the status directly. First get the current timestamp in UTC: + ```bash + date -u +"%Y-%m-%dT%H:%M:%SZ" + ``` + + Then edit the `ResourceClaim` status subresource (replace `` with the name obtained in step 3, e.g. `pod0-gpu-5bnfq`) and add a `devices` entry with a `BindingConditions` condition whose `status` is `"True"`: + ```bash + kubectl edit resourceclaim --subresource=status -n binding-conditions + ``` + + Add the following under `status:`: + ```yaml + status: + ... + devices: + - conditions: + - lastTransitionTime: "" + message: Device gpu-0 condition BindingConditions updated + reason: BindingConditionsUpdated + status: "True" + type: BindingConditions + device: gpu-0 + driver: gpu.example.com + pool: dra-example-driver-cluster-worker + ``` + + Once saved, the scheduler recognizes the binding condition as satisfied and the pod should transition out of `Pending`: + ```console + $ kubectl get pod -n binding-conditions + NAME READY STATUS RESTARTS AGE + pod0 1/1 Running 0 2m + ``` + +#### Testing + +To run the full end-to-end demo automatically: +```bash +./demo/test-binding-conditions.sh +``` + +This script applies the demo manifest, verifies the pod stays `Pending` while binding conditions are unset, patches the `ResourceClaim` status to satisfy the condition, and confirms the pod transitions to `Running`. + ### Clean Up Once you have verified everything is running correctly, delete all of the @@ -401,7 +515,8 @@ kubectl delete --wait=false --filename=demo/basic-resourceclaimtemplate.yaml \ --filename=demo/basic-shared-claim-across-containers.yaml \ --filename=demo/basic-shared-claim-across-pods.yaml \ --filename=demo/basic-resourceclaim-opaque-config.yaml \ - --filename=demo/admin-access.yaml + --filename=demo/admin-access.yaml \ + --filename=demo/binding-conditions.yaml ``` And wait for them to terminate: @@ -417,6 +532,7 @@ basic-shared-claim-across-containers pod1 1/1 Terminating 0 3 basic-shared-claim-across-pods pod0 1/1 Terminating 0 31m basic-resourceclaim-opaque-config pod0 4/4 Terminating 0 31m admin-access pod0 1/1 Terminating 0 31m +binding-conditions pod0 1/1 Terminating 0 31m ... ``` diff --git a/cmd/dra-example-kubeletplugin/main.go b/cmd/dra-example-kubeletplugin/main.go index 2630fc7e..34aa9534 100644 --- a/cmd/dra-example-kubeletplugin/main.go +++ b/cmd/dra-example-kubeletplugin/main.go @@ -54,6 +54,7 @@ type Flags struct { driverName string podUID string gpuPartitions int + bindingConditions bool } type Config struct { @@ -66,7 +67,7 @@ type Config struct { var validProfiles = map[string]func(flags Flags) profiles.Profile{ gpu.ProfileName: func(flags Flags) profiles.Profile { - return gpu.NewProfile(flags.nodeName, flags.numDevices, flags.gpuPartitions) + return gpu.NewProfile(flags.nodeName, flags.numDevices, flags.gpuPartitions, flags.bindingConditions) }, } @@ -162,6 +163,13 @@ func newApp() *cli.App { Destination: &flags.gpuPartitions, EnvVars: []string{"GPU_PARTITIONS"}, }, + &cli.BoolFlag{ + Name: "binding-conditions", + Usage: "Enable or disable binding conditions processing in the DRA driver.", + Value: false, + Destination: &flags.bindingConditions, + EnvVars: []string{"BINDING_CONDITIONS"}, + }, } cliFlags = append(cliFlags, flags.kubeClientConfig.Flags()...) cliFlags = append(cliFlags, flags.loggingConfig.Flags()...) diff --git a/demo/binding-conditions.yaml b/demo/binding-conditions.yaml new file mode 100644 index 00000000..ed64f206 --- /dev/null +++ b/demo/binding-conditions.yaml @@ -0,0 +1,83 @@ +# Example: DRA Device Binding Conditions +# +# One namespace, one pod with one container requesting a single GPU. +# This demo shows the DRA Device Binding Conditions feature. +# +# When binding conditions are enabled on the driver, each device in the +# ResourceSlice is published with: +# bindsToNode: true +# bindingConditions: +# - BindingConditions +# bindingFailureConditions: +# - BindingFailureConditions +# +# The scheduler uses these fields to track the binding lifecycle of allocated +# devices, deferring final binding decisions until the driver confirms device +# readiness on the node. +# +# Key requirements: +# - The driver must be installed with bindingConditions=true: +# helm upgrade -i ... --set kubeletPlugin.bindingConditions=true +# +# Verify binding condition fields are published in the ResourceSlice with: +# kubectl get resourceslice -o yaml +# Each device entry should contain: +# bindsToNode: true +# bindingConditions: +# - BindingConditions +# bindingFailureConditions: +# - BindingFailureConditions +# +# Driver requirements: +# Profile: gpu +# GPUs: 1 +# +# Cluster requirements: +# Kubernetes 1.34+ +# Feature gate: DRADeviceBindingConditions + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: binding-conditions + +--- +apiVersion: resource.k8s.io/v1 +kind: ResourceClaimTemplate +metadata: + namespace: binding-conditions + name: single-gpu +spec: + spec: + devices: + requests: + - name: gpu + exactly: + deviceClassName: gpu.example.com + +--- +apiVersion: v1 +kind: Pod +metadata: + namespace: binding-conditions + name: pod0 +spec: + containers: + - name: ctr0 + image: ubuntu:22.04 + command: ["bash", "-c"] + args: + - | + export + echo "=== DRA Device Binding Conditions Demo ===" + echo "" + echo "=== Sleeping to allow inspection ===" + trap 'exit 0' TERM + sleep 9999 & wait + resources: + claims: + - name: gpu + resourceClaims: + - name: gpu + resourceClaimTemplateName: single-gpu diff --git a/demo/scripts/kind-cluster-config.yaml b/demo/scripts/kind-cluster-config.yaml index 5f8dab5e..7ba1c4d3 100644 --- a/demo/scripts/kind-cluster-config.yaml +++ b/demo/scripts/kind-cluster-config.yaml @@ -7,6 +7,7 @@ featureGates: GangScheduling: true GenericWorkload: true DRAExtendedResource: true + DRADeviceBindingConditions: true containerdConfigPatches: # Enable CDI as described in # https://tags.cncf.io/container-device-interface#containerd-configuration diff --git a/demo/test-binding-conditions.sh b/demo/test-binding-conditions.sh new file mode 100755 index 00000000..858a4f63 --- /dev/null +++ b/demo/test-binding-conditions.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash + +# Copyright 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. + +# This script demonstrates the DRA Device Binding Conditions feature by: +# 1. Deploying the demo manifest (demo/binding-conditions.yaml) +# 2. Verifying the pod stays Pending until binding conditions are met +# 3. Patching the ResourceClaim status to satisfy the binding condition +# 4. Verifying the pod transitions to Running + +set -e + +NAMESPACE="binding-conditions" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== DRA Device Binding Conditions Feature Demo ===" +echo + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo "❌ kubectl is not available. Please install kubectl and ensure cluster access." + exit 1 +fi + +# Check if the cluster is accessible +if ! kubectl cluster-info &> /dev/null; then + echo "❌ Unable to access Kubernetes cluster. Please check your kubeconfig." + exit 1 +fi + +echo "✅ Kubernetes cluster is accessible" +echo + +# Verify ResourceSlice has binding condition fields +echo "=== ResourceSlice binding condition fields ===" +kubectl get resourceslice -o jsonpath='{range .items[*]}{range .spec.devices[*]}{.name}: bindsToNode={.bindsToNode}, bindingConditions={.bindingConditions}, bindingFailureConditions={.bindingFailureConditions}{"\n"}{end}{end}' +echo +echo + +# Apply the demo manifest +echo "📦 Applying binding-conditions.yaml demo..." +kubectl apply -f "${SCRIPT_DIR}/binding-conditions.yaml" +echo + +# Verify the pod is Pending (binding conditions not yet satisfied) +echo "⏳ Waiting briefly for the pod to be scheduled and stay Pending..." +sleep 5 + +echo +echo "=== Pod Status (expected: Pending) ===" +kubectl get pod pod0 -n "${NAMESPACE}" + +POD_PHASE=$(kubectl get pod pod0 -n "${NAMESPACE}" -o jsonpath='{.status.phase}') +if [[ "${POD_PHASE}" == "Pending" ]]; then + echo "✅ pod0 is Pending as expected (binding conditions not yet satisfied)" +else + echo "⚠️ pod0 phase is '${POD_PHASE}' (expected Pending)" +fi +echo + +# Find the ResourceClaim created for pod0 +echo "=== ResourceClaim Status ===" +RC_NAME=$(kubectl get resourceclaim -n "${NAMESPACE}" -o jsonpath='{.items[0].metadata.name}') +if [[ -z "${RC_NAME}" ]]; then + echo "❌ No ResourceClaim found in namespace ${NAMESPACE}" + exit 1 +fi +echo "ResourceClaim: ${RC_NAME}" +kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" +echo +echo "--- ResourceClaim status (allocation and reservedFor) ---" +kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" -o json | jq '{ + status: { + allocation: { + devices: { + results: [.status.allocation.devices.results[] | { + bindingConditions, + bindingFailureConditions, + device, driver, pool, request + }] + } + }, + reservedFor: [.status.reservedFor[] | {name, resource}] + } +}' +echo + +# Patch the ResourceClaim status to satisfy the binding condition +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +DEVICE=$(kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ + -o jsonpath='{.status.allocation.devices.results[0].device}') +DRIVER=$(kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ + -o jsonpath='{.status.allocation.devices.results[0].driver}') +POOL=$(kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ + -o jsonpath='{.status.allocation.devices.results[0].pool}') + +echo "🔧 Patching ResourceClaim status to satisfy BindingConditions..." +echo " Device: ${DEVICE}, Driver: ${DRIVER}, Pool: ${POOL}, Timestamp: ${TIMESTAMP}" + +kubectl patch resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ + --subresource=status \ + --type=merge \ + -p "{ + \"status\": { + \"devices\": [ + { + \"conditions\": [ + { + \"lastTransitionTime\": \"${TIMESTAMP}\", + \"message\": \"Device ${DEVICE} condition BindingConditions updated\", + \"reason\": \"BindingConditionsUpdated\", + \"status\": \"True\", + \"type\": \"BindingConditions\" + } + ], + \"device\": \"${DEVICE}\", + \"driver\": \"${DRIVER}\", + \"pool\": \"${POOL}\" + } + ] + } +}" +echo + +# Wait for the pod to become Running +echo "⏳ Waiting for pod0 to become Running..." +kubectl wait --for=condition=Ready pod/pod0 -n "${NAMESPACE}" --timeout=60s + +echo +echo "=== Pod Status (expected: Running) ===" +kubectl get pod pod0 -n "${NAMESPACE}" + +POD_PHASE=$(kubectl get pod pod0 -n "${NAMESPACE}" -o jsonpath='{.status.phase}') +if [[ "${POD_PHASE}" == "Running" ]]; then + echo "✅ pod0 is Running — binding conditions were satisfied successfully" +else + echo "❌ pod0 phase is '${POD_PHASE}' (expected Running)" + exit 1 +fi +echo + +echo +echo "=== Demo Complete ===" +echo "To clean up, run: kubectl delete namespace ${NAMESPACE}" diff --git a/deployments/helm/dra-example-driver/templates/kubeletplugin.yaml b/deployments/helm/dra-example-driver/templates/kubeletplugin.yaml index 82da8494..42ba3423 100644 --- a/deployments/helm/dra-example-driver/templates/kubeletplugin.yaml +++ b/deployments/helm/dra-example-driver/templates/kubeletplugin.yaml @@ -90,6 +90,8 @@ spec: valueFrom: fieldRef: fieldPath: metadata.uid + - name: BINDING_CONDITIONS + value: {{ .Values.kubeletPlugin.bindingConditions | quote }} volumeMounts: - name: plugins-registry mountPath: {{ .Values.kubeletPlugin.kubeletRegistrarDirectoryPath | quote }} diff --git a/deployments/helm/dra-example-driver/values.yaml b/deployments/helm/dra-example-driver/values.yaml index a9cf324f..016fc914 100644 --- a/deployments/helm/dra-example-driver/values.yaml +++ b/deployments/helm/dra-example-driver/values.yaml @@ -67,6 +67,8 @@ kubeletPlugin: affinity: {} kubeletRegistrarDirectoryPath: /var/lib/kubelet/plugins_registry kubeletPluginsDirectoryPath: /var/lib/kubelet/plugins + # bindingConditions enables or disables binding conditions processing in the DRA driver + bindingConditions: false containers: init: securityContext: {} diff --git a/internal/profiles/gpu/gpu.go b/internal/profiles/gpu/gpu.go index 17ddce13..1cc26e60 100644 --- a/internal/profiles/gpu/gpu.go +++ b/internal/profiles/gpu/gpu.go @@ -38,16 +38,18 @@ import ( const ProfileName = "gpu" type Profile struct { - nodeName string - numGPUs int - partitionsPerGPU int + nodeName string + numGPUs int + partitionsPerGPU int + bindingConditions bool } -func NewProfile(nodeName string, numGPUs int, partitionsPerGPU int) Profile { +func NewProfile(nodeName string, numGPUs int, partitionsPerGPU int, bindingConditions bool) Profile { return Profile{ - nodeName: nodeName, - numGPUs: numGPUs, - partitionsPerGPU: partitionsPerGPU, + nodeName: nodeName, + numGPUs: numGPUs, + partitionsPerGPU: partitionsPerGPU, + bindingConditions: bindingConditions, } } @@ -166,6 +168,13 @@ func (p Profile) EnumerateDevices() (resourceslice.DriverResources, error) { } } + if p.bindingConditions { + for i := range devices { + devices[i].BindingConditions = []string{profiles.BindingConditions} + devices[i].BindingFailureConditions = []string{profiles.BindingFailureConditions} + } + } + slices := []resourceslice.Slice{{Devices: devices}} if len(sharedCounters) > 0 { slices = []resourceslice.Slice{ diff --git a/internal/profiles/gpu/gpu_test.go b/internal/profiles/gpu/gpu_test.go index f6e31a1e..6168c14c 100644 --- a/internal/profiles/gpu/gpu_test.go +++ b/internal/profiles/gpu/gpu_test.go @@ -26,23 +26,25 @@ import ( ) func TestNewProfile(t *testing.T) { - profile := NewProfile("test-node", 4, 0) + profile := NewProfile("test-node", 4, 0, false) assert.Equal(t, "test-node", profile.nodeName) assert.Equal(t, 4, profile.numGPUs) assert.Equal(t, 0, profile.partitionsPerGPU) + assert.Equal(t, false, profile.bindingConditions) } -func TestNewProfile_WithPartitions(t *testing.T) { - profile := NewProfile("test-node", 2, 4) +func TestNewProfile_WithAllOptions(t *testing.T) { + profile := NewProfile("test-node", 2, 4, true) assert.Equal(t, "test-node", profile.nodeName) assert.Equal(t, 2, profile.numGPUs) assert.Equal(t, 4, profile.partitionsPerGPU) + assert.Equal(t, true, profile.bindingConditions) } func TestEnumerateDevices_Standard(t *testing.T) { - profile := NewProfile("test-node", 2, 0) + profile := NewProfile("test-node", 2, 0, false) resources, err := profile.EnumerateDevices() require.NoError(t, err) @@ -85,7 +87,7 @@ func TestEnumerateDevices_Standard(t *testing.T) { } func TestEnumerateDevices_Partitionable(t *testing.T) { - profile := NewProfile("test-node", 2, 4) + profile := NewProfile("test-node", 2, 4, false) resources, err := profile.EnumerateDevices() require.NoError(t, err) @@ -131,7 +133,7 @@ func TestEnumerateDevices_Partitionable(t *testing.T) { } func TestEnumerateDevices_PartitionableDeviceAttributes(t *testing.T) { - profile := NewProfile("test-node", 1, 2) + profile := NewProfile("test-node", 1, 2, false) resources, err := profile.EnumerateDevices() require.NoError(t, err) @@ -184,8 +186,8 @@ func TestEnumerateDevices_PartitionableDeviceAttributes(t *testing.T) { func TestEnumerateDevices_ConsistentUUIDs(t *testing.T) { // UUIDs should be consistent for the same node name - profile1 := NewProfile("test-node", 2, 0) - profile2 := NewProfile("test-node", 2, 0) + profile1 := NewProfile("test-node", 2, 0, false) + profile2 := NewProfile("test-node", 2, 0, false) resources1, err := profile1.EnumerateDevices() require.NoError(t, err) @@ -203,8 +205,8 @@ func TestEnumerateDevices_ConsistentUUIDs(t *testing.T) { } func TestEnumerateDevices_DifferentNodesHaveDifferentUUIDs(t *testing.T) { - profile1 := NewProfile("node-1", 1, 0) - profile2 := NewProfile("node-2", 1, 0) + profile1 := NewProfile("node-1", 1, 0, false) + profile2 := NewProfile("node-2", 1, 0, false) resources1, err := profile1.EnumerateDevices() require.NoError(t, err) diff --git a/internal/profiles/profiles.go b/internal/profiles/profiles.go index 3864f247..64888a93 100644 --- a/internal/profiles/profiles.go +++ b/internal/profiles/profiles.go @@ -25,6 +25,11 @@ import ( cdiapi "tags.cncf.io/container-device-interface/pkg/cdi" ) +const ( + BindingConditions = "BindingConditions" + BindingFailureConditions = "BindingFailureConditions" +) + type PerDeviceCDIContainerEdits map[string]*cdiapi.ContainerEdits // Profile describes a kind of device that can be managed by the driver. From 8b0f50e0439d7ee783d413744e3e5b18b6a05529 Mon Sep 17 00:00:00 2001 From: Tsubasa Watanabe Date: Fri, 15 May 2026 15:04:16 +0900 Subject: [PATCH 2/3] Add BindingConditions controller plugin with e2e tests - Add dra-example-controller that automatically satisfies device binding conditions on allocated ResourceClaims using controller-runtime - Add Helm templates, Dockerfile, and RBAC for the controller deployment - Add e2e tests gated by BINDING_CONDITIONS=true to verify ResourceSlice fields and pod readiness Signed-off-by: Tsubasa Watanabe --- README.md | 116 +------------ cmd/dra-example-controller/controller.go | 92 +++++++++++ cmd/dra-example-controller/main.go | 122 ++++++++++++++ .../plugins/bindingconditions.go | 136 +++++++++++++++ demo/binding-conditions/README.md | 116 +++++++++++++ .../binding-conditions.yaml | 0 demo/test-binding-conditions.sh | 156 ------------------ deployments/container/Dockerfile | 1 + .../templates/clusterrole.yaml | 9 +- .../templates/controller-deployment.yaml | 38 +++++ .../helm/dra-example-driver/values.yaml | 6 + go.mod | 4 + go.sum | 12 ++ test/e2e/e2e_setup_test.go | 23 +++ test/e2e/e2e_test.go | 26 +++ test/e2e/setup-e2e.sh | 8 + 16 files changed, 593 insertions(+), 272 deletions(-) create mode 100644 cmd/dra-example-controller/controller.go create mode 100644 cmd/dra-example-controller/main.go create mode 100644 cmd/dra-example-controller/plugins/bindingconditions.go create mode 100644 demo/binding-conditions/README.md rename demo/{ => binding-conditions}/binding-conditions.yaml (100%) delete mode 100755 demo/test-binding-conditions.sh create mode 100644 deployments/helm/dra-example-driver/templates/controller-deployment.yaml diff --git a/README.md b/README.md index fbe96cb0..814a05c3 100644 --- a/README.md +++ b/README.md @@ -391,120 +391,6 @@ To run this demo: This demonstration shows the end-to-end flow of the DRA AdminAccess feature. In a production environment, drivers could use this admin access indication to provide additional privileged capabilities or information to authorized workloads. -### Demo DRA Device Binding Conditions Feature -This example driver includes support for the [DRA Device Binding Conditions feature](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/#device-binding-conditions), which allows drivers to declare conditions that must be met before a device is considered fully bound and ready for use. -When enabled, each GPU device in the `ResourceSlice` is published with a `bindingConditions` list and a `bindingFailureConditions` list. -The scheduler uses these to track the binding lifecycle of allocated devices. - -#### Usage Example - -1. **Enable binding conditions**: Binding conditions are controlled by the `bindingConditions` flag. Set `kubeletPlugin.bindingConditions` to `true` in `values.yaml` or via `--set`: - ```bash - helm upgrade -i \ - --create-namespace \ - --namespace dra-example-driver \ - --set kubeletPlugin.bindingConditions=true \ - dra-example-driver \ - deployments/helm/dra-example-driver - ``` - - **Note**: The `DRADeviceBindingConditions` feature gate must also be enabled on the Kubernetes cluster. - The demo Kind cluster configuration (`demo/scripts/kind-cluster-config.yaml`) already includes this gate. - -2. **Verify the ResourceSlice**: After installing the driver with binding conditions enabled, inspect the `ResourceSlice` to confirm that devices are published with binding condition fields: - ```bash - kubectl get resourceslice -o yaml - ``` - - Each device should contain: - ```yaml - bindsToNode: true - bindingConditions: - - BindingConditions - bindingFailureConditions: - - BindingFailureConditions - ``` - -3. **Create the demo pod**: Apply the example manifest and confirm that the pod stays `Pending` because the binding conditions have not yet been satisfied: - ```bash - kubectl apply -f demo/binding-conditions.yaml - ``` - - ```console - $ kubectl get pod -n binding-conditions - NAME READY STATUS RESTARTS AGE - pod0 0/1 Pending 0 14s - ``` - Also verify that the `ResourceClaim` has been allocated and that `bindingConditions` and `bindingFailureConditions` appear in its status: - ```console - $ kubectl get resourceclaim -n binding-conditions - NAME STATE AGE - pod0-gpu-5bnfq allocated,reserved 5m14s - ``` - - ```console - $ kubectl get resourceclaim -n binding-conditions -o yaml - ... - status: - allocation: - devices: - results: - - bindingConditions: - - BindingConditions - bindingFailureConditions: - - BindingFailureConditions - device: gpu-0 - driver: gpu.example.com - pool: dra-example-driver-cluster-worker - request: gpu - reservedFor: - - name: pod0 - resource: pods - ... - ``` - -4. **Set the binding condition**: In a real driver, an external controller or other component would update the `ResourceClaim` status to signal that the device is ready. In this demo, simulate that by editing the status directly. First get the current timestamp in UTC: - ```bash - date -u +"%Y-%m-%dT%H:%M:%SZ" - ``` - - Then edit the `ResourceClaim` status subresource (replace `` with the name obtained in step 3, e.g. `pod0-gpu-5bnfq`) and add a `devices` entry with a `BindingConditions` condition whose `status` is `"True"`: - ```bash - kubectl edit resourceclaim --subresource=status -n binding-conditions - ``` - - Add the following under `status:`: - ```yaml - status: - ... - devices: - - conditions: - - lastTransitionTime: "" - message: Device gpu-0 condition BindingConditions updated - reason: BindingConditionsUpdated - status: "True" - type: BindingConditions - device: gpu-0 - driver: gpu.example.com - pool: dra-example-driver-cluster-worker - ``` - - Once saved, the scheduler recognizes the binding condition as satisfied and the pod should transition out of `Pending`: - ```console - $ kubectl get pod -n binding-conditions - NAME READY STATUS RESTARTS AGE - pod0 1/1 Running 0 2m - ``` - -#### Testing - -To run the full end-to-end demo automatically: -```bash -./demo/test-binding-conditions.sh -``` - -This script applies the demo manifest, verifies the pod stays `Pending` while binding conditions are unset, patches the `ResourceClaim` status to satisfy the condition, and confirms the pod transitions to `Running`. - ### Clean Up Once you have verified everything is running correctly, delete all of the @@ -516,7 +402,7 @@ kubectl delete --wait=false --filename=demo/basic-resourceclaimtemplate.yaml \ --filename=demo/basic-shared-claim-across-pods.yaml \ --filename=demo/basic-resourceclaim-opaque-config.yaml \ --filename=demo/admin-access.yaml \ - --filename=demo/binding-conditions.yaml + --filename=demo/binding-conditions/binding-conditions.yaml ``` And wait for them to terminate: diff --git a/cmd/dra-example-controller/controller.go b/cmd/dra-example-controller/controller.go new file mode 100644 index 00000000..47b32212 --- /dev/null +++ b/cmd/dra-example-controller/controller.go @@ -0,0 +1,92 @@ +/* + * Copyright 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. + */ + +package main + +import ( + "context" + "fmt" + + resourceapi "k8s.io/api/resource/v1" + "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Plugin processes a ResourceClaim that has been allocated for this driver. +// Each plugin is responsible for updating the claim status if needed. +// New functionality can be added by implementing this interface and +// registering the plugin in main(). +type Plugin interface { + Reconcile(ctx context.Context, c client.Client, claim *resourceapi.ResourceClaim) error +} + +// ClaimReconciler watches ResourceClaims and runs registered Plugins on +// claims allocated for its driver. +type ClaimReconciler struct { + client client.Client + driverName string + plugins []Plugin +} + +func NewClaimReconciler(mgr ctrl.Manager, driverName string, plugins []Plugin) *ClaimReconciler { + return &ClaimReconciler{ + client: mgr.GetClient(), + driverName: driverName, + plugins: plugins, + } +} + +func (r *ClaimReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&resourceapi.ResourceClaim{}). + Complete(r) +} + +func (r *ClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var claim resourceapi.ResourceClaim + if err := r.client.Get(ctx, req.NamespacedName, &claim); err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("get claim: %w", err) + } + + if !r.isRelevant(&claim) { + return ctrl.Result{}, nil + } + + for _, p := range r.plugins { + if err := p.Reconcile(ctx, r.client, &claim); err != nil { + return ctrl.Result{}, fmt.Errorf("plugin failed: %w", err) + } + } + + return ctrl.Result{}, nil +} + +// isRelevant returns true if the claim has any allocation results for this driver. +func (r *ClaimReconciler) isRelevant(claim *resourceapi.ResourceClaim) bool { + if claim.Status.Allocation == nil { + return false + } + for _, result := range claim.Status.Allocation.Devices.Results { + if result.Driver == r.driverName { + return true + } + } + return false +} diff --git a/cmd/dra-example-controller/main.go b/cmd/dra-example-controller/main.go new file mode 100644 index 00000000..61540191 --- /dev/null +++ b/cmd/dra-example-controller/main.go @@ -0,0 +1,122 @@ +/* + * Copyright 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. + */ + +package main + +import ( + "flag" + "fmt" + "os" + "sort" + "strings" + + resourceapi "k8s.io/api/resource/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "sigs.k8s.io/dra-example-driver/cmd/dra-example-controller/plugins" +) + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(resourceapi.AddToScheme(scheme)) +} + +const ( + PluginBindingConditions = "BindingConditions" +) + +// PluginFactory creates a Plugin for the given driver name. +type PluginFactory func(driverName string) Plugin + +// pluginRegistry maps plugin names to their factory functions. +// Add new plugins here. +var pluginRegistry = map[string]PluginFactory{ + PluginBindingConditions: func(driverName string) Plugin { + return plugins.NewBindingConditionsPlugin(driverName) + }, +} + +// pluginNames returns sorted plugin names for help text. +func pluginNames() []string { + names := make([]string, 0, len(pluginRegistry)) + for name := range pluginRegistry { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// enablePlugins is a flag.Value that collects --enable-plugin values. +// Values can be specified as comma-separated or by repeating the flag. +type enablePlugins []string + +func (e *enablePlugins) String() string { return strings.Join(*e, ",") } +func (e *enablePlugins) Set(v string) error { + for _, name := range strings.Split(v, ",") { + name = strings.TrimSpace(name) + if name == "" { + continue + } + if _, ok := pluginRegistry[name]; !ok { + return fmt.Errorf("unknown plugin %q (available: %s)", name, strings.Join(pluginNames(), ", ")) + } + *e = append(*e, name) + } + return nil +} + +func main() { + var driverName string + var enabled enablePlugins + flag.StringVar(&driverName, "driver-name", "gpu.example.com", "The driver name to filter ResourceClaims by.") + flag.Var(&enabled, "enable-plugin", + fmt.Sprintf("Enable a plugin (can be specified multiple times). Available: %s", strings.Join(pluginNames(), ", "))) + opts := zap.Options{Development: true} + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating manager: %v\n", err) + os.Exit(1) + } + + // Build the plugin list from flags. + var plugins []Plugin + for _, name := range enabled { + plugins = append(plugins, pluginRegistry[name](driverName)) + } + + if err := NewClaimReconciler(mgr, driverName, plugins).SetupWithManager(mgr); err != nil { + fmt.Fprintf(os.Stderr, "Error setting up controller: %v\n", err) + os.Exit(1) + } + + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + fmt.Fprintf(os.Stderr, "Error running manager: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/dra-example-controller/plugins/bindingconditions.go b/cmd/dra-example-controller/plugins/bindingconditions.go new file mode 100644 index 00000000..80da0f9c --- /dev/null +++ b/cmd/dra-example-controller/plugins/bindingconditions.go @@ -0,0 +1,136 @@ +/* + * Copyright 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. + */ + +package plugins + +import ( + "context" + + resourceapi "k8s.io/api/resource/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// BindingConditionsPlugin satisfies binding conditions for allocated devices +// by marking them as ready. In a real driver this would check actual device +// readiness before setting the condition. +type BindingConditionsPlugin struct { + driverName string +} + +func NewBindingConditionsPlugin(driverName string) *BindingConditionsPlugin { + return &BindingConditionsPlugin{driverName: driverName} +} + +func (p *BindingConditionsPlugin) Reconcile(ctx context.Context, c client.Client, claim *resourceapi.ResourceClaim) error { + if claim.Status.Allocation == nil { + return nil + } + + logger := log.FromContext(ctx) + modified := false + now := metav1.Now() + + for _, result := range claim.Status.Allocation.Devices.Results { + if result.Driver != p.driverName || len(result.BindingConditions) == 0 { + continue + } + for _, condType := range result.BindingConditions { + if isConditionTrue(claim, result, condType) { + continue + } + setDeviceCondition(claim, result, condType, now) + logger.Info("Set binding condition", + "device", result.Device, + "condition", condType, + ) + modified = true + } + } + + if !modified { + return nil + } + + logger.Info("Updating ResourceClaim status", "name", claim.Name) + return c.Status().Update(ctx, claim) +} + +// isConditionTrue checks whether a device already has the given condition set to True. +func isConditionTrue( + claim *resourceapi.ResourceClaim, + result resourceapi.DeviceRequestAllocationResult, + condType string, +) bool { + for _, d := range claim.Status.Devices { + if d.Driver == result.Driver && d.Pool == result.Pool && d.Device == result.Device { + for _, c := range d.Conditions { + if c.Type == condType && c.Status == metav1.ConditionTrue { + return true + } + } + } + } + return false +} + +// setDeviceCondition adds or updates a condition for a device in the claim status. +func setDeviceCondition( + claim *resourceapi.ResourceClaim, + result resourceapi.DeviceRequestAllocationResult, + condType string, + now metav1.Time, +) { + // Find existing device status entry. + for i := range claim.Status.Devices { + d := &claim.Status.Devices[i] + if d.Driver == result.Driver && d.Pool == result.Pool && d.Device == result.Device { + // Update or append the condition within this entry. + for j := range d.Conditions { + if d.Conditions[j].Type == condType { + d.Conditions[j].Status = metav1.ConditionTrue + d.Conditions[j].Reason = "Ready" + d.Conditions[j].Message = "Device is ready" + d.Conditions[j].LastTransitionTime = now + return + } + } + d.Conditions = append(d.Conditions, metav1.Condition{ + Type: condType, + Status: metav1.ConditionTrue, + Reason: "Ready", + Message: "Device is ready", + LastTransitionTime: now, + }) + return + } + } + + // No existing entry; create a new one. + claim.Status.Devices = append(claim.Status.Devices, resourceapi.AllocatedDeviceStatus{ + Driver: result.Driver, + Pool: result.Pool, + Device: result.Device, + Conditions: []metav1.Condition{{ + Type: condType, + Status: metav1.ConditionTrue, + Reason: "Ready", + Message: "Device is ready", + LastTransitionTime: now, + }}, + }) +} diff --git a/demo/binding-conditions/README.md b/demo/binding-conditions/README.md new file mode 100644 index 00000000..4e34bf67 --- /dev/null +++ b/demo/binding-conditions/README.md @@ -0,0 +1,116 @@ +### Demo DRA Device Binding Conditions Feature +This example driver includes support for the [DRA Device Binding Conditions feature](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/#device-binding-conditions), which allows drivers to declare conditions that must be met before a device is considered fully bound and ready for use. +When enabled, each GPU device in the `ResourceSlice` is published with a `bindingConditions` list and a `bindingFailureConditions` list. +The scheduler uses these to track the binding lifecycle of allocated devices. + +#### Usage Example + +1. **Enable binding conditions**: Binding conditions require two settings: `kubeletPlugin.bindingConditions` to publish binding conditions in the `ResourceSlice`, and `controller.plugins={BindingConditions}` to deploy the controller that automatically satisfies them. Set both in `values.yaml` or via `--set`: + ```bash + helm upgrade -i \ + --create-namespace \ + --namespace dra-example-driver \ + --set kubeletPlugin.bindingConditions=true \ + --set controller.plugins={BindingConditions} \ + dra-example-driver \ + deployments/helm/dra-example-driver + ``` + + **Note**: The `DRADeviceBindingConditions` feature gate must also be enabled on the Kubernetes cluster. + The demo Kind cluster configuration (`demo/scripts/kind-cluster-config.yaml`) already includes this gate. + +2. **Verify the ResourceSlice**: After installing the driver with binding conditions enabled, inspect the `ResourceSlice` to confirm that devices are published with binding condition fields: + ```bash + kubectl get resourceslice -o yaml + ``` + + Each device should contain: + ```yaml + bindsToNode: true + bindingConditions: + - BindingConditions + bindingFailureConditions: + - BindingFailureConditions + ``` + +3. **Create the demo pod**: Apply the example manifest and confirm that the pod stays `Pending` because the binding conditions have not yet been satisfied: + ```bash + kubectl apply -f demo/binding-conditions/binding-conditions.yaml + ``` + + ```console + $ kubectl get pod -n binding-conditions + NAME READY STATUS RESTARTS AGE + pod0 0/1 Pending 0 14s + ``` + Also verify that the `ResourceClaim` has been allocated and that `bindingConditions` and `bindingFailureConditions` appear in its status: + ```console + $ kubectl get resourceclaim -n binding-conditions + NAME STATE AGE + pod0-gpu-5bnfq allocated,reserved 5m14s + ``` + + ```console + $ kubectl get resourceclaim -n binding-conditions -o yaml + ... + status: + allocation: + devices: + results: + - bindingConditions: + - BindingConditions + bindingFailureConditions: + - BindingFailureConditions + device: gpu-0 + driver: gpu.example.com + pool: dra-example-driver-cluster-worker + request: gpu + reservedFor: + - name: pod0 + resource: pods + ... + ``` + +4. **Binding condition is satisfied automatically**: The `BindingConditions` controller plugin watches allocated `ResourceClaim` objects and automatically marks binding conditions as satisfied. After a short reconciliation period, the pod should transition out of `Pending`: + ```console + $ kubectl get pod -n binding-conditions + NAME READY STATUS RESTARTS AGE + pod0 1/1 Running 0 2m + ``` + + Verify that `status.devices` on the `ResourceClaim` now contains the `BindingConditions` condition set to `True`: + ```bash + kubectl get resourceclaim -n binding-conditions -o yaml + ``` + + ```yaml + status: + devices: + - conditions: + - lastTransitionTime: "2026-05-15T14:00:00Z" + message: Device is ready + reason: Ready + status: "True" + type: BindingConditions + device: gpu-0 + driver: gpu.example.com + pool: dra-example-driver-cluster-worker + ``` + +#### Testing + +E2e tests cover binding conditions validation. To run them: + +```bash +# Set up the cluster with binding conditions enabled +BINDING_CONDITIONS=true make setup-e2e + +# Run the e2e tests (includes binding conditions tests) +BINDING_CONDITIONS=true make test-e2e + +# Run only the binding conditions tests +BINDING_CONDITIONS=true go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e --focus="BindingConditions" ./test/e2e/... + +# Tear down the cluster +make teardown-e2e +``` diff --git a/demo/binding-conditions.yaml b/demo/binding-conditions/binding-conditions.yaml similarity index 100% rename from demo/binding-conditions.yaml rename to demo/binding-conditions/binding-conditions.yaml diff --git a/demo/test-binding-conditions.sh b/demo/test-binding-conditions.sh deleted file mode 100755 index 858a4f63..00000000 --- a/demo/test-binding-conditions.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 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. - -# This script demonstrates the DRA Device Binding Conditions feature by: -# 1. Deploying the demo manifest (demo/binding-conditions.yaml) -# 2. Verifying the pod stays Pending until binding conditions are met -# 3. Patching the ResourceClaim status to satisfy the binding condition -# 4. Verifying the pod transitions to Running - -set -e - -NAMESPACE="binding-conditions" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -echo "=== DRA Device Binding Conditions Feature Demo ===" -echo - -# Check if kubectl is available -if ! command -v kubectl &> /dev/null; then - echo "❌ kubectl is not available. Please install kubectl and ensure cluster access." - exit 1 -fi - -# Check if the cluster is accessible -if ! kubectl cluster-info &> /dev/null; then - echo "❌ Unable to access Kubernetes cluster. Please check your kubeconfig." - exit 1 -fi - -echo "✅ Kubernetes cluster is accessible" -echo - -# Verify ResourceSlice has binding condition fields -echo "=== ResourceSlice binding condition fields ===" -kubectl get resourceslice -o jsonpath='{range .items[*]}{range .spec.devices[*]}{.name}: bindsToNode={.bindsToNode}, bindingConditions={.bindingConditions}, bindingFailureConditions={.bindingFailureConditions}{"\n"}{end}{end}' -echo -echo - -# Apply the demo manifest -echo "📦 Applying binding-conditions.yaml demo..." -kubectl apply -f "${SCRIPT_DIR}/binding-conditions.yaml" -echo - -# Verify the pod is Pending (binding conditions not yet satisfied) -echo "⏳ Waiting briefly for the pod to be scheduled and stay Pending..." -sleep 5 - -echo -echo "=== Pod Status (expected: Pending) ===" -kubectl get pod pod0 -n "${NAMESPACE}" - -POD_PHASE=$(kubectl get pod pod0 -n "${NAMESPACE}" -o jsonpath='{.status.phase}') -if [[ "${POD_PHASE}" == "Pending" ]]; then - echo "✅ pod0 is Pending as expected (binding conditions not yet satisfied)" -else - echo "⚠️ pod0 phase is '${POD_PHASE}' (expected Pending)" -fi -echo - -# Find the ResourceClaim created for pod0 -echo "=== ResourceClaim Status ===" -RC_NAME=$(kubectl get resourceclaim -n "${NAMESPACE}" -o jsonpath='{.items[0].metadata.name}') -if [[ -z "${RC_NAME}" ]]; then - echo "❌ No ResourceClaim found in namespace ${NAMESPACE}" - exit 1 -fi -echo "ResourceClaim: ${RC_NAME}" -kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" -echo -echo "--- ResourceClaim status (allocation and reservedFor) ---" -kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" -o json | jq '{ - status: { - allocation: { - devices: { - results: [.status.allocation.devices.results[] | { - bindingConditions, - bindingFailureConditions, - device, driver, pool, request - }] - } - }, - reservedFor: [.status.reservedFor[] | {name, resource}] - } -}' -echo - -# Patch the ResourceClaim status to satisfy the binding condition -TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -DEVICE=$(kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ - -o jsonpath='{.status.allocation.devices.results[0].device}') -DRIVER=$(kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ - -o jsonpath='{.status.allocation.devices.results[0].driver}') -POOL=$(kubectl get resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ - -o jsonpath='{.status.allocation.devices.results[0].pool}') - -echo "🔧 Patching ResourceClaim status to satisfy BindingConditions..." -echo " Device: ${DEVICE}, Driver: ${DRIVER}, Pool: ${POOL}, Timestamp: ${TIMESTAMP}" - -kubectl patch resourceclaim -n "${NAMESPACE}" "${RC_NAME}" \ - --subresource=status \ - --type=merge \ - -p "{ - \"status\": { - \"devices\": [ - { - \"conditions\": [ - { - \"lastTransitionTime\": \"${TIMESTAMP}\", - \"message\": \"Device ${DEVICE} condition BindingConditions updated\", - \"reason\": \"BindingConditionsUpdated\", - \"status\": \"True\", - \"type\": \"BindingConditions\" - } - ], - \"device\": \"${DEVICE}\", - \"driver\": \"${DRIVER}\", - \"pool\": \"${POOL}\" - } - ] - } -}" -echo - -# Wait for the pod to become Running -echo "⏳ Waiting for pod0 to become Running..." -kubectl wait --for=condition=Ready pod/pod0 -n "${NAMESPACE}" --timeout=60s - -echo -echo "=== Pod Status (expected: Running) ===" -kubectl get pod pod0 -n "${NAMESPACE}" - -POD_PHASE=$(kubectl get pod pod0 -n "${NAMESPACE}" -o jsonpath='{.status.phase}') -if [[ "${POD_PHASE}" == "Running" ]]; then - echo "✅ pod0 is Running — binding conditions were satisfied successfully" -else - echo "❌ pod0 phase is '${POD_PHASE}' (expected Running)" - exit 1 -fi -echo - -echo -echo "=== Demo Complete ===" -echo "To clean up, run: kubectl delete namespace ${NAMESPACE}" diff --git a/deployments/container/Dockerfile b/deployments/container/Dockerfile index 7e791b04..e942b23b 100644 --- a/deployments/container/Dockerfile +++ b/deployments/container/Dockerfile @@ -35,4 +35,5 @@ LABEL summary="Example DRA resource driver for Kubernetes" LABEL description="See summary" COPY --from=build /artifacts/dra-example-kubeletplugin /usr/bin/dra-example-kubeletplugin +COPY --from=build /artifacts/dra-example-controller /usr/bin/dra-example-controller COPY --from=build /artifacts/dra-example-webhook /usr/bin/dra-example-webhook diff --git a/deployments/helm/dra-example-driver/templates/clusterrole.yaml b/deployments/helm/dra-example-driver/templates/clusterrole.yaml index f00003dc..23d4f4e2 100644 --- a/deployments/helm/dra-example-driver/templates/clusterrole.yaml +++ b/deployments/helm/dra-example-driver/templates/clusterrole.yaml @@ -7,7 +7,14 @@ metadata: rules: - apiGroups: ["resource.k8s.io"] resources: ["resourceclaims"] - verbs: ["get"] + verbs: ["get", "list", "watch"] +- apiGroups: ["resource.k8s.io"] + resources: ["resourceclaims/status"] + verbs: ["get", "update", "patch"] +- apiGroups: ["resource.k8s.io"] + resources: ["resourceclaims/driver"] + resourceNames: [{{ include "dra-example-driver.driverName" . | quote }}] + verbs: ["associated-node:update", "arbitrary-node:update"] - apiGroups: [""] resources: ["nodes"] verbs: ["get"] diff --git a/deployments/helm/dra-example-driver/templates/controller-deployment.yaml b/deployments/helm/dra-example-driver/templates/controller-deployment.yaml new file mode 100644 index 00000000..b4c9b3a9 --- /dev/null +++ b/deployments/helm/dra-example-driver/templates/controller-deployment.yaml @@ -0,0 +1,38 @@ +{{- if .Values.controller.plugins }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "dra-example-driver.fullname" . }}-controller + namespace: {{ include "dra-example-driver.namespace" . }} + labels: + {{- include "dra-example-driver.labels" . | nindent 4 }} + app.kubernetes.io/component: controller +spec: + replicas: 1 + selector: + matchLabels: + {{- include "dra-example-driver.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: controller + template: + metadata: + labels: + {{- include "dra-example-driver.templateLabels" . | nindent 8 }} + app.kubernetes.io/component: controller + spec: + priorityClassName: system-cluster-critical + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "dra-example-driver.serviceAccountName" . }} + containers: + - name: controller + image: {{ include "dra-example-driver.fullimage" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["dra-example-controller"] + args: + - --driver-name={{ include "dra-example-driver.driverName" . }} + {{- range .Values.controller.plugins }} + - --enable-plugin={{ . }} + {{- end }} +{{- end }} diff --git a/deployments/helm/dra-example-driver/values.yaml b/deployments/helm/dra-example-driver/values.yaml index 016fc914..1e17e212 100644 --- a/deployments/helm/dra-example-driver/values.yaml +++ b/deployments/helm/dra-example-driver/values.yaml @@ -81,6 +81,12 @@ kubeletPlugin: # Set to a negative value to disable the service and the probe. healthcheckPort: 51515 +controller: + # plugins is a list of plugins to enable in the controller. + # When non-empty, the controller Deployment is created. + # Available plugins: ["BindingConditions"] + plugins: [] + webhook: enabled: false servicePort: 443 diff --git a/go.mod b/go.mod index f2797944..0a9c0d45 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( k8s.io/klog/v2 v2.140.0 k8s.io/kubelet v0.36.1 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 + sigs.k8s.io/controller-runtime v0.24.1 tags.cncf.io/container-device-interface v1.1.0 tags.cncf.io/container-device-interface/specs-go v1.1.0 ) @@ -30,6 +31,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -78,11 +80,13 @@ require ( golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.44.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.36.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index 2ac406d8..62d64b6f 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -55,6 +59,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -108,6 +114,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84= github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -205,6 +213,8 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= @@ -241,6 +251,8 @@ k8s.io/kubelet v0.36.1 h1:FcHiG9wv92xerRPNxztuhYWqwS4IilOQNPxTPQewYgo= k8s.io/kubelet v0.36.1/go.mod h1:e6IeoCwqc2TbneCKu6P8HjmWLi7U6SOh3Pocs32iGFM= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/test/e2e/e2e_setup_test.go b/test/e2e/e2e_setup_test.go index e741bdd8..fe8230b7 100644 --- a/test/e2e/e2e_setup_test.go +++ b/test/e2e/e2e_setup_test.go @@ -638,3 +638,26 @@ func verifySharedGPUGroup(ctx context.Context, namespace string, group sharingGr }, checkPodLogsTimeout, checkPodLogsInterval).Should(Succeed()) } } + +// verifyResourceSliceBindingConditions verifies that all devices published in +// ResourceSlices for the example driver have bindingConditions set. +func verifyResourceSliceBindingConditions(ctx context.Context) { + GinkgoHelper() + Eventually(func(g Gomega) { + slices, err := clientset.ResourceV1().ResourceSlices().List(ctx, metav1.ListOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(slices.Items).NotTo(BeEmpty(), "No ResourceSlices found") + + for _, slice := range slices.Items { + if slice.Spec.Driver != "gpu.example.com" { + continue + } + for _, device := range slice.Spec.Devices { + g.Expect(device.BindingConditions).NotTo(BeEmpty(), + "Device %s in ResourceSlice %s: bindingConditions should not be empty", device.Name, slice.Name) + fmt.Fprintf(GinkgoWriter, "Device %s in ResourceSlice %s: bindingConditions=%v\n", + device.Name, slice.Name, device.BindingConditions) + } + } + }, "30s", "2s").Should(Succeed()) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index b4632159..9cdbd72f 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -21,6 +21,7 @@ package e2e import ( "context" "fmt" + "os" "path/filepath" . "github.com/onsi/ginkgo/v2" @@ -284,4 +285,29 @@ var _ = Describe("Test GPU allocation", func() { }) } }) + + Context("BindingConditions", func() { + BeforeEach(func() { + if os.Getenv("BINDING_CONDITIONS") != "true" { + Skip("BINDING_CONDITIONS is not enabled; skipping binding conditions tests") + } + }) + + It("should publish bindingConditions on devices in ResourceSlices", func(ctx SpecContext) { + verifyResourceSliceBindingConditions(ctx) + }) + + It("should allocate a GPU and make the pod Running with binding conditions", func(ctx SpecContext) { + namespace := "binding-conditions" + pods := []string{"pod0"} + containerName := "ctr0" + expectedGPUCount := 1 + + deployManifest(ctx, namespace, "binding-conditions/binding-conditions.yaml") + checkPodsReadyAndRunning(ctx, namespace, pods) + + observedGPUs := make(map[string]string) + verifyGPUAllocation(ctx, namespace, pods[0], containerName, expectedGPUCount, observedGPUs) + }) + }) }) diff --git a/test/e2e/setup-e2e.sh b/test/e2e/setup-e2e.sh index d1266f79..cfbfde1d 100755 --- a/test/e2e/setup-e2e.sh +++ b/test/e2e/setup-e2e.sh @@ -21,6 +21,13 @@ set -e # Example: HELM_CHART_PATH="oci://registry.k8s.io/dra-example-driver/charts/dra-example-driver" make setup-e2e HELM_CHART_PATH="${HELM_CHART_PATH:-deployments/helm/dra-example-driver}" +# Enable BindingConditions controller plugin and kubelet plugin flag when requested. +# Usage: BINDING_CONDITIONS=true make setup-e2e +BINDING_CONDITIONS_OPTS="" +if [[ "${BINDING_CONDITIONS:-false}" == "true" ]]; then + BINDING_CONDITIONS_OPTS="--set kubeletPlugin.bindingConditions=true --set controller.plugins={BindingConditions}" +fi + # Skip building local driver image if using OCI registry chart if [[ "${HELM_CHART_PATH}" != oci://* ]]; then bash demo/build-driver.sh @@ -43,5 +50,6 @@ helm upgrade -i \ --set webhook.enabled=true \ --set kubeletPlugin.numDevices=14 \ --set deviceClass.extendedResourceName=example.com/gpu \ + ${BINDING_CONDITIONS_OPTS} \ dra-example-driver \ ${HELM_CHART_PATH} From 9bc0bbe795e16315dc1bbee0bcd32d661e41bea8 Mon Sep 17 00:00:00 2001 From: Tsubasa Watanabe Date: Sat, 16 May 2026 15:39:28 +0900 Subject: [PATCH 3/3] refactor: support parallel plugin execution - Change from sequential to parallel plugin execution using goroutines - Collect all plugin errors instead of failing on first error Signed-off-by: Tsubasa Watanabe --- cmd/dra-example-controller/controller.go | 28 +++++++++++++++++-- cmd/dra-example-controller/main.go | 6 +--- .../plugins/bindingconditions.go | 4 +++ cmd/dra-example-controller/plugins/plugins.go | 21 ++++++++++++++ 4 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 cmd/dra-example-controller/plugins/plugins.go diff --git a/cmd/dra-example-controller/controller.go b/cmd/dra-example-controller/controller.go index 47b32212..89adb511 100644 --- a/cmd/dra-example-controller/controller.go +++ b/cmd/dra-example-controller/controller.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + "sync" resourceapi "k8s.io/api/resource/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -31,6 +32,8 @@ import ( // New functionality can be added by implementing this interface and // registering the plugin in main(). type Plugin interface { + // Name returns the name of the plugin. + Name() string Reconcile(ctx context.Context, c client.Client, claim *resourceapi.ResourceClaim) error } @@ -69,10 +72,29 @@ func (r *ClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, nil } + var wg sync.WaitGroup + errChan := make(chan error, len(r.plugins)) + for _, p := range r.plugins { - if err := p.Reconcile(ctx, r.client, &claim); err != nil { - return ctrl.Result{}, fmt.Errorf("plugin failed: %w", err) - } + wg.Add(1) + go func(plugin Plugin) { + defer wg.Done() + if err := plugin.Reconcile(ctx, r.client, &claim); err != nil { + errChan <- fmt.Errorf("%s: %w", plugin.Name(), err) + } + }(p) + } + + wg.Wait() + close(errChan) + + var errs []error + for err := range errChan { + errs = append(errs, err) + } + + if len(errs) > 0 { + return ctrl.Result{}, fmt.Errorf("plugins failed: %v", errs) } return ctrl.Result{}, nil diff --git a/cmd/dra-example-controller/main.go b/cmd/dra-example-controller/main.go index 61540191..cd2771df 100644 --- a/cmd/dra-example-controller/main.go +++ b/cmd/dra-example-controller/main.go @@ -40,17 +40,13 @@ func init() { utilruntime.Must(resourceapi.AddToScheme(scheme)) } -const ( - PluginBindingConditions = "BindingConditions" -) - // PluginFactory creates a Plugin for the given driver name. type PluginFactory func(driverName string) Plugin // pluginRegistry maps plugin names to their factory functions. // Add new plugins here. var pluginRegistry = map[string]PluginFactory{ - PluginBindingConditions: func(driverName string) Plugin { + plugins.BindingConditions: func(driverName string) Plugin { return plugins.NewBindingConditionsPlugin(driverName) }, } diff --git a/cmd/dra-example-controller/plugins/bindingconditions.go b/cmd/dra-example-controller/plugins/bindingconditions.go index 80da0f9c..1c57659d 100644 --- a/cmd/dra-example-controller/plugins/bindingconditions.go +++ b/cmd/dra-example-controller/plugins/bindingconditions.go @@ -36,6 +36,10 @@ func NewBindingConditionsPlugin(driverName string) *BindingConditionsPlugin { return &BindingConditionsPlugin{driverName: driverName} } +func (p *BindingConditionsPlugin) Name() string { + return BindingConditions +} + func (p *BindingConditionsPlugin) Reconcile(ctx context.Context, c client.Client, claim *resourceapi.ResourceClaim) error { if claim.Status.Allocation == nil { return nil diff --git a/cmd/dra-example-controller/plugins/plugins.go b/cmd/dra-example-controller/plugins/plugins.go new file mode 100644 index 00000000..e04240a7 --- /dev/null +++ b/cmd/dra-example-controller/plugins/plugins.go @@ -0,0 +1,21 @@ +/* + * Copyright 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. + */ + +package plugins + +const ( + BindingConditions = "BindingConditions" +)