Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/dra-example-kubeletplugin/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (d *driver) PrepareResourceClaims(ctx context.Context, claims []*resourceap
func (d *driver) prepareResourceClaim(ctx context.Context, claim *resourceapi.ResourceClaim) kubeletplugin.PrepareResult {
logger := klog.FromContext(ctx)
logger.Info("Preparing claim", "uid", claim.UID, "namespace", claim.Namespace, "name", claim.Name)
preparedPBs, err := d.state.Prepare(claim)
preparedPBs, err := d.state.Prepare(ctx, claim)
if err != nil {
logger.Error(err, "Error preparing devices for claim", "uid", claim.UID)
return kubeletplugin.PrepareResult{
Expand Down
9 changes: 8 additions & 1 deletion cmd/dra-example-kubeletplugin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Flags struct {
driverName string
podUID string
gpuPartitions int
gpuDeviceStatus bool
}

type Config struct {
Expand All @@ -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.gpuDeviceStatus)
},
}

Expand Down Expand Up @@ -162,6 +163,12 @@ func newApp() *cli.App {
Destination: &flags.gpuPartitions,
EnvVars: []string{"GPU_PARTITIONS"},
},
&cli.BoolFlag{
Name: "gpu-device-status",
Usage: "Enable adding allocated device attributes (e.g., model, uuid, driverVersion) into ResourceClaim.status.devices[].data. Disabled by default.",
Destination: &flags.gpuDeviceStatus,
EnvVars: []string{"GPU_DEVICE_STATUS"},
},
}
cliFlags = append(cliFlags, flags.kubeClientConfig.Flags()...)
cliFlags = append(cliFlags, flags.loggingConfig.Flags()...)
Expand Down
78 changes: 74 additions & 4 deletions cmd/dra-example-kubeletplugin/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,25 @@
package main

import (
"context"
"fmt"
"path/filepath"
"slices"
"sync"

resourceapi "k8s.io/api/resource/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"

"k8s.io/apimachinery/pkg/types"
coreclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"
draclient "k8s.io/dynamic-resource-allocation/client"
"k8s.io/dynamic-resource-allocation/resourceslice"

"k8s.io/klog/v2"
drapbv1 "k8s.io/kubelet/pkg/apis/dra/v1beta1"
cdiapi "tags.cncf.io/container-device-interface/pkg/cdi"

Expand Down Expand Up @@ -71,6 +79,9 @@ type DeviceState struct {
checkpointPath string
checkpointDecoder runtime.Decoder
checkpointEncoder runtime.Encoder

coreClient coreclientset.Interface
gpuDeviceStatus bool
}

func NewDeviceState(config *Config) (*DeviceState, error) {
Expand Down Expand Up @@ -134,12 +145,14 @@ func NewDeviceState(config *Config) (*DeviceState, error) {
checkpointPath: filepath.Join(config.DriverPluginPath(), DriverPluginCheckpointFile),
checkpointDecoder: checkpointDecoder,
checkpointEncoder: checkpointEncoder,
coreClient: config.coreclient,
gpuDeviceStatus: config.flags.gpuDeviceStatus,
}

return state, nil
}

func (s *DeviceState) Prepare(claim *resourceapi.ResourceClaim) ([]*drapbv1.Device, error) {
func (s *DeviceState) Prepare(ctx context.Context, claim *resourceapi.ResourceClaim) ([]*drapbv1.Device, error) {
s.Lock()
defer s.Unlock()

Expand All @@ -155,7 +168,7 @@ func (s *DeviceState) Prepare(claim *resourceapi.ResourceClaim) ([]*drapbv1.Devi
return restoredDevices.GetDevices(), nil
}

preparedDevices, err := s.prepareDevices(claim)
preparedDevices, err := s.prepareDevices(ctx, claim)
if err != nil {
return nil, fmt.Errorf("prepare failed: %v", err)
}
Expand Down Expand Up @@ -202,8 +215,43 @@ func (s *DeviceState) Unprepare(claimUID types.UID) error {

// prepareDevices performs one-time setup for the devices allocated to a
// ResourceClaim before being consumed by a Pod.
func (s *DeviceState) prepareDevices(claim *resourceapi.ResourceClaim) (PreparedDevices, error) {
return s.computeDeviceConfig(claim)
func (s *DeviceState) prepareDevices(ctx context.Context, claim *resourceapi.ResourceClaim) (PreparedDevices, error) {
preparedDevices, err := s.computeDeviceConfig(claim)
if err != nil {
return nil, err
}

// Publish per-device status (e.g. uuid, model, driverVersion) into
// ResourceClaim.status.devices[].data when the profile implements
// [profiles.DeviceStatusBuilder]. This is a side-effect on the API server
// and therefore lives in prepareDevices (rather than computeDeviceConfig,
// which must be deterministic and side-effect free).
builder, ok := s.configHandler.(profiles.DeviceStatusBuilder)
if !ok {
return preparedDevices, nil
}

var deviceStatuses []resourceapi.AllocatedDeviceStatus
for _, result := range claim.Status.Allocation.Devices.Results {
if result.Driver != s.driverName {
continue
}
if status := builder.BuildDeviceStatus(s.allocatable, &result); status != nil {
deviceStatuses = append(deviceStatuses, *status)
}
}
if len(deviceStatuses) > 0 {
klog.FromContext(ctx).Info("Publishing device status to ResourceClaim",
"namespace", claim.Namespace, "name", claim.Name, "devices", len(deviceStatuses))
if err := s.updateDeviceStatus(ctx, claim.Namespace, claim.Name, deviceStatuses...); err != nil {
// A failure to publish status is non-fatal: the device is still
// prepared and the claim status will simply be missing the data.
klog.FromContext(ctx).Error(err, "Failed to update device status on ResourceClaim",
"namespace", claim.Namespace, "name", claim.Name)
}
}

return preparedDevices, nil
}

// unprepareDevices undoes any side-effects produced by
Expand Down Expand Up @@ -251,6 +299,7 @@ func (s *DeviceState) computeDeviceConfig(claim *resourceapi.ResourceClaim) (Pre
if _, exists := s.allocatable[result.Device]; !exists {
return nil, fmt.Errorf("requested device is not allocatable: %v", result.Device)
}

for _, c := range slices.Backward(configs) {
if len(c.Requests) == 0 || slices.Contains(c.Requests, result.Request) {
configResultsMap[c.Config] = append(configResultsMap[c.Config], &result)
Expand Down Expand Up @@ -422,3 +471,24 @@ func GetOpaqueDeviceConfigs(

return resultConfigs, nil
}

func (s *DeviceState) updateDeviceStatus(ctx context.Context, ns, name string, devices ...resourceapi.AllocatedDeviceStatus) error {
// Converting wrapper to use latest API types,
// converts to/from server-supported version.
c := draclient.New(s.coreClient)
rc := c.ResourceClaims(ns)

return retry.RetryOnConflict(retry.DefaultRetry, func() error {
claim, err := rc.Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}

// copy the object and update only status.devices
claim = claim.DeepCopy()
claim.Status.Devices = devices

_, err = rc.UpdateStatus(ctx, claim, metav1.UpdateOptions{})
return err
})
}
13 changes: 13 additions & 0 deletions deployments/helm/dra-example-driver/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ rules:
- apiGroups: ["resource.k8s.io"]
resources: ["resourceclaims"]
verbs: ["get"]
- apiGroups: ["resource.k8s.io"]
resources: ["resourceclaims/status"]
verbs: ["update"]
# Kubernetes 1.36+ enforces granular authorization for ResourceClaim status
# writes via the DRAResourceClaimGranularStatusAuthorization feature gate. As a
# node-local driver, we need the "associated-node" verbs on
# "resourceclaims/driver" for our own driver name. This rule is inert on older
# clusters that have not yet enabled the feature gate.
# See https://github.com/kubernetes/kubernetes/issues/138149
- apiGroups: ["resource.k8s.io"]
resources: ["resourceclaims/driver"]
verbs: ["associated-node:update", "associated-node:patch"]
resourceNames: ["{{ include "dra-example-driver.driverName" . }}"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ spec:
# Simulated number of devices the example driver will pretend to have.
- name: NUM_DEVICES
value: {{ .Values.kubeletPlugin.numDevices | quote }}
- name: GPU_DEVICE_STATUS
value: {{ .Values.gpuDeviceStatus | quote }}
{{- if .Values.kubeletPlugin.containers.plugin.healthcheckPort }}
- name: HEALTHCHECK_PORT
value: {{ .Values.kubeletPlugin.containers.plugin.healthcheckPort | quote }}
Expand Down
4 changes: 4 additions & 0 deletions deployments/helm/dra-example-driver/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ deviceClass:
# `deviceclass.resource.kubernetes.io/<deviceClassName>` is always available.
extendedResourceName: ""

# add allocated device attributes (e.g. model, uuid, driverVersion)
# into ResourceClaim.status.devices[].data via server-side apply.
gpuDeviceStatus: false

imagePullSecrets: []
image:
repository: registry.k8s.io/dra-example-driver/dra-example-driver
Expand Down
57 changes: 50 additions & 7 deletions internal/profiles/gpu/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package gpu

import (
"encoding/json"
"fmt"
"maps"
"math/rand"
Expand All @@ -27,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/dynamic-resource-allocation/resourceslice"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
cdiapi "tags.cncf.io/container-device-interface/pkg/cdi"
cdispec "tags.cncf.io/container-device-interface/specs-go"
Expand All @@ -38,16 +40,18 @@ import (
const ProfileName = "gpu"

type Profile struct {
nodeName string
numGPUs int
partitionsPerGPU int
nodeName string
numGPUs int
partitionsPerGPU int
enableDeviceStatus bool
}

func NewProfile(nodeName string, numGPUs int, partitionsPerGPU int) Profile {
func NewProfile(nodeName string, numGPUs int, partitionsPerGPU int, enableDeviceStatus bool) Profile {
return Profile{
nodeName: nodeName,
numGPUs: numGPUs,
partitionsPerGPU: partitionsPerGPU,
nodeName: nodeName,
numGPUs: numGPUs,
partitionsPerGPU: partitionsPerGPU,
enableDeviceStatus: enableDeviceStatus,
}
}

Expand Down Expand Up @@ -290,3 +294,42 @@ func applyGpuConfig(config *configapi.GpuConfig, results []*resourceapi.DeviceRe

return perDeviceEdits, nil
}

// BuildDeviceStatus implements [profiles.DeviceStatusBuilder]. It returns an
// [resourceapi.AllocatedDeviceStatus] populated with a subset of the device's
// attributes (uuid, model, driverVersion) to publish into
// ResourceClaim.status.devices[].data.
func (p Profile) BuildDeviceStatus(allocatable map[string]resourceapi.Device, result *resourceapi.DeviceRequestAllocationResult) *resourceapi.AllocatedDeviceStatus {
if !p.enableDeviceStatus {
return nil
}

deviceInfo := make(map[string]resourceapi.DeviceAttribute)
if d, ok := allocatable[result.Device]; ok {
for _, name := range []resourceapi.QualifiedName{"uuid", "model", "driverVersion"} {
if v, ok := d.Attributes[name]; ok {
deviceInfo[string(name)] = v
}
}
}

jsonBytes, err := json.Marshal(deviceInfo)
if err != nil {
klog.Errorf("Failed to marshal device data for %s: %v", result.Device, err)
jsonBytes = []byte("{}")
}

// Data records per-allocation metadata used for monitoring and debugging:
// - Pod->GPU mapping: makes it easier to see which GPU a given pod is
// using, which is not readily available elsewhere.
// - Device attributes (e.g. UUID, model, driverVersion): remain available
// even if the device is later removed from a ResourceSlice (for
// example, because it becomes unhealthy), so past allocations can
// still be correlated with later health or scheduling issues.
return &resourceapi.AllocatedDeviceStatus{
Device: result.Device,
Driver: result.Driver,
Pool: result.Pool,
Data: &runtime.RawExtension{Raw: jsonBytes},
}
}
Loading
Loading