mirror of
https://github.com/k8snetworkplumbingwg/multus-cni.git
synced 2026-07-16 17:17:15 +00:00
feat: DRA resource.k8s.io/v1 integration via draclient
Migrate Dynamic Resource Allocation from kubelet PodResources / v1alpha2-style usage to the stable resource.k8s.io/v1 API (Kubernetes 1.34+). - Add pkg/draclient: fetch ResourceClaims and ResourceSlices, build pod resource map from device attributes (k8s.cni.cncf.io/deviceID, k8s.cni.cncf.io/resourceName) and ExtendedResourceClaimStatus - Wire GetPodResourceMap into k8sclient; remove DRA path from kubeletclient - RBAC: resourceclaims, resourceclaims/status, resourceslices (get, list) on multus ClusterRole - Docs: DRA / NAD usage; tests for draclient and k8sclient Co-authored-by: Sebastian Sch <sebassch@gmail.com> Signed-off-by: Fred Rolland <frolland@nvidia.com>
This commit is contained in:
@@ -70,7 +70,18 @@ rules:
|
||||
- pods/status
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- "resource.k8s.io"
|
||||
resources:
|
||||
- resourceclaims
|
||||
- resourceclaims/status
|
||||
- resourceslices
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
- events.k8s.io
|
||||
|
||||
@@ -73,6 +73,15 @@ rules:
|
||||
- list
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- "resource.k8s.io"
|
||||
resources:
|
||||
- resourceclaims
|
||||
- resourceclaims/status
|
||||
- resourceslices
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
- events.k8s.io
|
||||
|
||||
@@ -70,7 +70,18 @@ rules:
|
||||
- pods/status
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- "resource.k8s.io"
|
||||
resources:
|
||||
- resourceclaims
|
||||
- resourceclaims/status
|
||||
- resourceslices
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
- events.k8s.io
|
||||
|
||||
@@ -645,112 +645,139 @@ If you wish to have auto configuration use the `readinessindicatorfile` in the c
|
||||
|
||||
### Run pod with network annotation and Dynamic Resource Allocation driver
|
||||
|
||||
> :warning: Dynamic Resource Allocation (DRA) is [currently an alpha](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/),
|
||||
> and is subject to change. Please consider this functionality as a preview. The architecture and usage of DRA in
|
||||
> Multus CNI may change in the future as this technology matures.
|
||||
>
|
||||
> The current DRA integration is based on the DRA API for Kubernetes 1.26 to 1.30. With Kubernetes 1.31, the DRA API
|
||||
> will change and multus doesn't integrate with the new API yet.
|
||||
|
||||
Dynamic Resource Allocation is alternative mechanism to device plugin which allows to requests pod and container
|
||||
resources.
|
||||
Dynamic Resource Allocation is an alternative mechanism to device plugin which allows pods to request pod and container
|
||||
resources dynamically.
|
||||
|
||||
The following sections describe how to use DRA with multus and NVIDIA DRA driver. Other DRA networking driver vendors
|
||||
should follow similar concepts to make use of multus DRA support.
|
||||
The following sections describe how to use DRA with Multus. DRA networking driver vendors should follow similar
|
||||
concepts to make use of Multus DRA support.
|
||||
|
||||
#### Prerequisite
|
||||
|
||||
1. Kubernetes 1.27
|
||||
2. Container Runtime with CDI support enabled
|
||||
3. Kubernetes runtime-config=resource.k8s.io/v1alpha2
|
||||
4. Kubernetes feature-gates=DynamicResourceAllocation=True,KubeletPodResourcesDynamicResources=true
|
||||
1. Kubernetes 1.34+
|
||||
|
||||
#### Install DRA driver
|
||||
|
||||
The current example uses NVIDIA DRA driver for networking. This DRA driver is not publicly available. An alternative to
|
||||
this DRA driver is available at [dra-example-driver](https://github.com/kubernetes-sigs/dra-example-driver).
|
||||
You need to install a DRA driver that provides network devices. For example, you can use the SR-IOV DRA driver or
|
||||
other DRA networking drivers. Refer to your DRA driver documentation for installation instructions.
|
||||
|
||||
#### Create dynamic resource class with NVIDIA network DRA driver
|
||||
The DRA driver MUST expose the following attributes on each allocated **device** in `ResourceSlice`:
|
||||
- `k8s.cni.cncf.io/deviceID`: device ID that Multus passes to the CNI plugin.
|
||||
- `k8s.cni.cncf.io/resourceName`: **must exactly match** the value you put on the NetworkAttachmentDefinition
|
||||
`k8s.v1.cni.cncf.io/resourceName` annotation (same style as classic extended resources, e.g. `intel.com/sriov_vf`).
|
||||
If `k8s.cni.cncf.io/resourceName` is missing on the device, allocation processing fails.
|
||||
|
||||
The `ResourceClass` defines the resource pool of `sf-pool-1`.
|
||||
|
||||
```
|
||||
# Execute following command at Kubernetes master
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: resource.k8s.io/v1alpha2
|
||||
kind: ResourceClass
|
||||
metadata:
|
||||
name: sf-pool-1
|
||||
driverName: net.resource.nvidia.com
|
||||
EOF
|
||||
```
|
||||
For pods that use the **extended resource** feature gate, Multus uses `pod.status.extendedResourceClaimStatus`
|
||||
request mappings: the NAD `resourceName` matches `requestMappings[].resourceName`. The device attribute
|
||||
`k8s.cni.cncf.io/resourceName` must be set and must **equal** that same `requestMappings[].resourceName` value
|
||||
(Multus rejects a mismatch). Device lookup uses the same `ResourceClaim` / `ResourceSlice` flow.
|
||||
|
||||
#### Create network attachment definition with resource name
|
||||
|
||||
The `k8s.v1.cni.cncf.io/resourceName` should match the `ResourceClass` name defined in the section above.
|
||||
In this example it is `sf-pool-1`. Multus query the K8s PodResource API to fetch the `resourceClass` name and also
|
||||
query the NetworkAttachmentDefinition `k8s.v1.cni.cncf.io/resourceName`. If both has the same name multus send the
|
||||
CDI device name in the DeviceID argument.
|
||||
The `k8s.v1.cni.cncf.io/resourceName` annotation must be the **same string** as the `k8s.cni.cncf.io/resourceName`
|
||||
device attribute published by your DRA driver for that allocation. Multiple secondary networks use distinct values
|
||||
on each device (or the same value when multiple device IDs should be consumed from one NAD, matching the device-plugin
|
||||
model).
|
||||
|
||||
##### NetworkAttachmentDefinition for ovn-kubernetes example:
|
||||
Multus queries the ResourceClaim and ResourceSlices APIs. When the NAD annotation equals the device’s
|
||||
`k8s.cni.cncf.io/resourceName`, Multus passes the corresponding `k8s.cni.cncf.io/deviceID` to the CNI plugin.
|
||||
|
||||
Following command creates NetworkAttachmentDefinition. CNI config is in `config:` field.
|
||||
##### NetworkAttachmentDefinition for SR-IOV example:
|
||||
|
||||
Following command creates a NetworkAttachmentDefinition for SR-IOV. The `resourceName` annotation must match what the
|
||||
DRA driver sets on the allocated device (here `intel.com/sriov_vf`):
|
||||
|
||||
```
|
||||
# Execute following command at Kubernetes master
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: "k8s.cni.cncf.io/v1"
|
||||
apiVersion: k8s.cni.cncf.io/v1
|
||||
kind: NetworkAttachmentDefinition
|
||||
metadata:
|
||||
name: default
|
||||
name: sriov-net
|
||||
namespace: default
|
||||
annotations:
|
||||
k8s.v1.cni.cncf.io/resourceName: sf-pool-1
|
||||
k8s.v1.cni.cncf.io/resourceName: intel.com/sriov_vf
|
||||
spec:
|
||||
config: '{
|
||||
"cniVersion": "0.4.0",
|
||||
"dns": {},
|
||||
"ipam": {},
|
||||
"logFile": "/var/log/ovn-kubernetes/ovn-k8s-cni-overlay.log",
|
||||
"logLevel": "4",
|
||||
"logfile-maxage": 5,
|
||||
"logfile-maxbackups": 5,
|
||||
"logfile-maxsize": 100,
|
||||
"name": "ovn-kubernetes",
|
||||
"type": "ovn-k8s-cni-overlay"
|
||||
}'
|
||||
config: |-
|
||||
{
|
||||
"cniVersion": "1.0.0",
|
||||
"name": "sriov-net",
|
||||
"type": "sriov",
|
||||
"vlan": 0,
|
||||
"spoofchk": "on",
|
||||
"trust": "on",
|
||||
"vlanQoS": 0,
|
||||
"logLevel": "info",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{
|
||||
"subnet": "10.0.2.0/24"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Create DRA Resource Claim
|
||||
#### Create Device Class
|
||||
|
||||
Following command creates `ResourceClaim` `sf` which request resource from `ResourceClass` `sf-pool-1`.
|
||||
Following command creates a `DeviceClass` for the `ResourceClaimTemplate` to request devices from.
|
||||
|
||||
```
|
||||
# Execute following command at Kubernetes master
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: resource.k8s.io/v1alpha2
|
||||
kind: ResourceClaim
|
||||
apiVersion: resource.k8s.io/v1
|
||||
kind: DeviceClass
|
||||
metadata:
|
||||
name: sriovnetwork.openshift.io
|
||||
spec:
|
||||
selectors:
|
||||
- cel:
|
||||
expression: device.driver == sriovnetwork.openshift.io
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Create DRA Resource Claim Template
|
||||
|
||||
Following command creates a `ResourceClaimTemplate` that requests a VF device from the SR-IOV device class.
|
||||
The device request is named `vf`; the DRA driver should publish `k8s.cni.cncf.io/resourceName` on the device
|
||||
so it matches your NAD (e.g. `intel.com/sriov_vf`).
|
||||
|
||||
```
|
||||
# Execute following command at Kubernetes master
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: resource.k8s.io/v1
|
||||
kind: ResourceClaimTemplate
|
||||
metadata:
|
||||
namespace: default
|
||||
name: sf
|
||||
name: sriov-template
|
||||
spec:
|
||||
spec:
|
||||
resourceClassName: sf-pool-1
|
||||
devices:
|
||||
requests:
|
||||
- name: vf
|
||||
deviceClassName: sriovnetwork.openshift.io
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Launch pod with DRA Resource Claim
|
||||
|
||||
Following command Launch a Pod with primiry network `default` and `ResourceClaim` `sf`.
|
||||
Following command launches a Pod with the secondary network `sriov-net` and a DRA resource claim named `sriov`.
|
||||
The NAD `resourceName` must match the driver’s `k8s.cni.cncf.io/resourceName` on the allocated device.
|
||||
|
||||
```
|
||||
# Execute following command at Kubernetes master
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
namespace: default
|
||||
name: test-sf-claim
|
||||
name: sriov-pod
|
||||
annotations:
|
||||
v1.multus-cni.io/default-network: default
|
||||
k8s.v1.cni.cncf.io/networks: sriov-net
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
containers:
|
||||
@@ -759,9 +786,16 @@ spec:
|
||||
command: ["/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"]
|
||||
resources:
|
||||
claims:
|
||||
- name: resource
|
||||
- name: sriov
|
||||
resourceClaims:
|
||||
- name: resource
|
||||
source:
|
||||
resourceClaimName: sf
|
||||
- name: sriov
|
||||
resourceClaimTemplateName: sriov-template
|
||||
EOF
|
||||
```
|
||||
|
||||
In this example:
|
||||
- The pod has a resourceClaim named `sriov` that uses the `sriov-template`
|
||||
- The ResourceClaimTemplate has a device request named `vf`
|
||||
- The DRA driver must set `k8s.cni.cncf.io/resourceName: intel.com/sriov_vf` (and `deviceID`) on the allocated device
|
||||
- The NetworkAttachmentDefinition uses `resourceName: intel.com/sriov_vf` to match that device attribute
|
||||
- Multus will match these and provide the allocated deviceID to the SR-IOV CNI plugin
|
||||
|
||||
289
pkg/draclient/draclient.go
Normal file
289
pkg/draclient/draclient.go
Normal file
@@ -0,0 +1,289 @@
|
||||
// Copyright (c) 2026 Multus 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 draclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
resourcev1api "k8s.io/api/resource/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
resourcev1 "k8s.io/client-go/kubernetes/typed/resource/v1"
|
||||
|
||||
"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/logging"
|
||||
"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
multusDeviceIDAttr = "k8s.cni.cncf.io/deviceID"
|
||||
multusResourceNameAttr = "k8s.cni.cncf.io/resourceName"
|
||||
)
|
||||
|
||||
// namespacedClaimCacheKey avoids cache collisions: ResourceClaim is namespaced.
|
||||
func namespacedClaimCacheKey(namespace, claimName string) string {
|
||||
return namespace + "/" + claimName
|
||||
}
|
||||
|
||||
type deviceInfo struct {
|
||||
DeviceID string
|
||||
ResourceName string
|
||||
}
|
||||
|
||||
type ClientInterface interface {
|
||||
GetPodResourceMap(pod *v1.Pod, resourceMap map[string]*types.ResourceInfo) error
|
||||
}
|
||||
|
||||
type draClient struct {
|
||||
client resourcev1.ResourceV1Interface
|
||||
// One driver/pool may span multiple ResourceSlice objects (e.g. per NUMA zone).
|
||||
resourceSliceCache map[string][]*resourcev1api.ResourceSlice
|
||||
// Keys are namespace/claimName (ResourceClaim is namespaced).
|
||||
resourceClaimCache map[string]*resourcev1api.ResourceClaim
|
||||
}
|
||||
|
||||
func NewClient(client resourcev1.ResourceV1Interface) ClientInterface {
|
||||
logging.Debugf("NewClient: creating new DRA client")
|
||||
return &draClient{
|
||||
client: client,
|
||||
resourceSliceCache: make(map[string][]*resourcev1api.ResourceSlice),
|
||||
resourceClaimCache: make(map[string]*resourcev1api.ResourceClaim),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *draClient) GetPodResourceMap(pod *v1.Pod, resourceMap map[string]*types.ResourceInfo) error {
|
||||
logging.Verbosef("GetPodResourceMap: processing DRA resources for pod %s/%s", pod.Namespace, pod.Name)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for _, claimResource := range pod.Status.ResourceClaimStatuses {
|
||||
if claimResource.ResourceClaimName == nil {
|
||||
logging.Errorf("GetPodResourceMap: resource claim status has nil ResourceClaimName")
|
||||
continue
|
||||
}
|
||||
claimName := *claimResource.ResourceClaimName
|
||||
claimCacheKey := namespacedClaimCacheKey(pod.Namespace, claimName)
|
||||
logging.Debugf("GetPodResourceMap: processing resource claim: %s (ref name: %s)", claimName, claimResource.Name)
|
||||
|
||||
resourceClaim, ok := d.resourceClaimCache[claimCacheKey]
|
||||
if !ok {
|
||||
logging.Debugf("GetPodResourceMap: resource claim %s/%s not in cache, fetching from API", pod.Namespace, claimName)
|
||||
fetched, err := d.client.ResourceClaims(pod.Namespace).Get(ctx, *claimResource.ResourceClaimName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
logging.Errorf("GetPodResourceMap: failed to get resource claim %s: %v", claimName, err)
|
||||
return err
|
||||
}
|
||||
resourceClaim = fetched
|
||||
d.resourceClaimCache[claimCacheKey] = resourceClaim
|
||||
logging.Debugf("GetPodResourceMap: cached resource claim %s", claimCacheKey)
|
||||
} else {
|
||||
logging.Debugf("GetPodResourceMap: using cached resource claim %s", claimCacheKey)
|
||||
}
|
||||
|
||||
if resourceClaim.Status.Allocation == nil || resourceClaim.Status.Allocation.Devices.Results == nil {
|
||||
logging.Errorf("GetPodResourceMap: claim %s has no device allocation", claimName)
|
||||
return fmt.Errorf("claim %s has no device allocation", claimName)
|
||||
}
|
||||
|
||||
for _, result := range resourceClaim.Status.Allocation.Devices.Results {
|
||||
logging.Debugf("GetPodResourceMap: processing device allocation - driver: %s, pool: %s, device: %s, request: %s",
|
||||
result.Driver, result.Pool, result.Device, result.Request)
|
||||
|
||||
info, err := d.getDeviceInfo(ctx, result)
|
||||
if err != nil {
|
||||
logging.Errorf("GetPodResourceMap: failed to get device info for claim %s: %v", claimName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.ResourceName == "" {
|
||||
resErr := fmt.Errorf("device %s missing required attribute %s (must match NAD k8s.v1.cni.cncf.io/resourceName)", result.Device, multusResourceNameAttr)
|
||||
logging.Errorf("GetPodResourceMap: %v", resErr)
|
||||
return resErr
|
||||
}
|
||||
|
||||
resourceMapKey := info.ResourceName
|
||||
if rInfo, ok := resourceMap[resourceMapKey]; ok {
|
||||
rInfo.DeviceIDs = append(rInfo.DeviceIDs, info.DeviceID)
|
||||
logging.Debugf("GetPodResourceMap: appended device ID %s to existing resource map entry %s", info.DeviceID, resourceMapKey)
|
||||
} else {
|
||||
resourceMap[resourceMapKey] = &types.ResourceInfo{DeviceIDs: []string{info.DeviceID}}
|
||||
logging.Debugf("GetPodResourceMap: created new resource map entry %s with device ID %s", resourceMapKey, info.DeviceID)
|
||||
}
|
||||
}
|
||||
logging.Debugf("GetPodResourceMap: successfully processed resource claim %s", claimName)
|
||||
}
|
||||
|
||||
if pod.Status.ExtendedResourceClaimStatus != nil {
|
||||
if err := d.processExtendedResourceClaimStatus(ctx, pod, resourceMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logging.Verbosef("GetPodResourceMap: successfully processed all DRA resources for pod %s/%s, total resources: %d",
|
||||
pod.Namespace, pod.Name, len(resourceMap))
|
||||
return nil
|
||||
}
|
||||
|
||||
// processExtendedResourceClaimStatus fills the resource map for pods that use
|
||||
// the extended resource feature gate (pod.Status.ExtendedResourceClaimStatus).
|
||||
// Keys come from requestMappings[].resourceName (same as NAD annotation).
|
||||
func (d *draClient) processExtendedResourceClaimStatus(ctx context.Context, pod *v1.Pod, resourceMap map[string]*types.ResourceInfo) error {
|
||||
extStatus := pod.Status.ExtendedResourceClaimStatus
|
||||
claimName := extStatus.ResourceClaimName
|
||||
claimCacheKey := namespacedClaimCacheKey(pod.Namespace, claimName)
|
||||
logging.Debugf("GetPodResourceMap: processing extended resource claim: %s/%s", pod.Namespace, claimName)
|
||||
|
||||
resourceClaim, ok := d.resourceClaimCache[claimCacheKey]
|
||||
if !ok {
|
||||
fetched, err := d.client.ResourceClaims(pod.Namespace).Get(ctx, claimName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
logging.Errorf("GetPodResourceMap: failed to get extended resource claim %s/%s: %v", pod.Namespace, claimName, err)
|
||||
return err
|
||||
}
|
||||
resourceClaim = fetched
|
||||
d.resourceClaimCache[claimCacheKey] = resourceClaim
|
||||
logging.Debugf("GetPodResourceMap: cached extended resource claim %s", claimCacheKey)
|
||||
}
|
||||
|
||||
if resourceClaim.Status.Allocation == nil || resourceClaim.Status.Allocation.Devices.Results == nil {
|
||||
logging.Errorf("GetPodResourceMap: claim %s has no device allocation", claimName)
|
||||
return fmt.Errorf("claim %s has no device allocation", claimName)
|
||||
}
|
||||
|
||||
resultsByRequest := make(map[string][]resourcev1api.DeviceRequestAllocationResult)
|
||||
for _, result := range resourceClaim.Status.Allocation.Devices.Results {
|
||||
resultsByRequest[result.Request] = append(resultsByRequest[result.Request], result)
|
||||
}
|
||||
|
||||
for _, mapping := range extStatus.RequestMappings {
|
||||
results, ok := resultsByRequest[mapping.RequestName]
|
||||
if !ok || len(results) == 0 {
|
||||
logging.Errorf("GetPodResourceMap: extended resource request %s not found in claim %s", mapping.RequestName, claimName)
|
||||
return fmt.Errorf("request %s not found in claim %s", mapping.RequestName, claimName)
|
||||
}
|
||||
|
||||
resourceMapKey := mapping.ResourceName
|
||||
for _, result := range results {
|
||||
info, err := d.getDeviceInfo(ctx, result)
|
||||
if err != nil {
|
||||
logging.Errorf("GetPodResourceMap: failed to get device info for extended resource claim %s request %s: %v", claimName, mapping.RequestName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.ResourceName == "" {
|
||||
resErr := fmt.Errorf("device %s missing required attribute %s (must match NAD k8s.v1.cni.cncf.io/resourceName and extended mapping %q)",
|
||||
result.Device, multusResourceNameAttr, resourceMapKey)
|
||||
logging.Errorf("GetPodResourceMap: %v", resErr)
|
||||
return resErr
|
||||
}
|
||||
if info.ResourceName != mapping.ResourceName {
|
||||
resErr := fmt.Errorf("device %s: %s is %q but extended resource mapping for request %q is %q",
|
||||
result.Device, multusResourceNameAttr, info.ResourceName, mapping.RequestName, mapping.ResourceName)
|
||||
logging.Errorf("GetPodResourceMap: %v", resErr)
|
||||
return resErr
|
||||
}
|
||||
|
||||
if rInfo, ok := resourceMap[resourceMapKey]; ok {
|
||||
rInfo.DeviceIDs = append(rInfo.DeviceIDs, info.DeviceID)
|
||||
logging.Debugf("GetPodResourceMap: appended device ID %s to extended resource map entry %s", info.DeviceID, resourceMapKey)
|
||||
} else {
|
||||
resourceMap[resourceMapKey] = &types.ResourceInfo{DeviceIDs: []string{info.DeviceID}}
|
||||
logging.Debugf("GetPodResourceMap: created new extended resource map entry %s with device ID %s", resourceMapKey, info.DeviceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logging.Debugf("GetPodResourceMap: successfully processed extended resource claim %s", claimName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *draClient) getDeviceInfo(ctx context.Context, result resourcev1api.DeviceRequestAllocationResult) (*deviceInfo, error) {
|
||||
key := fmt.Sprintf("%s/%s", result.Driver, result.Pool)
|
||||
logging.Debugf("getDeviceInfo: looking up device for driver/pool: %s, device: %s", key, result.Device)
|
||||
|
||||
resourceSlices, ok := d.resourceSliceCache[key]
|
||||
if !ok {
|
||||
logging.Debugf("getDeviceInfo: resource slices for %s not in cache, fetching from API", key)
|
||||
// TODO: Use server-side field selector once spec.driver is supported by the API.
|
||||
// Currently, ResourceSlice does not support field selection on spec.driver,
|
||||
// requiring client-side filtering which may impact performance in very large clusters.
|
||||
listOptions := metav1.ListOptions{}
|
||||
allResourceSlices, err := d.client.ResourceSlices().List(ctx, listOptions)
|
||||
if err != nil {
|
||||
logging.Errorf("getDeviceInfo: failed to list resource slices: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchingSlices []*resourcev1api.ResourceSlice
|
||||
for i := range allResourceSlices.Items {
|
||||
slice := &allResourceSlices.Items[i]
|
||||
if slice.Spec.Driver == result.Driver && slice.Spec.Pool.Name == result.Pool {
|
||||
matchingSlices = append(matchingSlices, slice)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchingSlices) == 0 {
|
||||
listErr := fmt.Errorf("no resource slice found for driver/pool %s", key)
|
||||
logging.Errorf("getDeviceInfo: %v", listErr)
|
||||
return nil, listErr
|
||||
}
|
||||
resourceSlices = matchingSlices
|
||||
d.resourceSliceCache[key] = resourceSlices
|
||||
logging.Debugf("getDeviceInfo: cached %d resource slices for %s", len(resourceSlices), key)
|
||||
} else {
|
||||
logging.Debugf("getDeviceInfo: using cached %d resource slices for %s", len(resourceSlices), key)
|
||||
}
|
||||
|
||||
for _, resourceSlice := range resourceSlices {
|
||||
logging.Debugf("getDeviceInfo: searching for device %s in slice %s with %d devices", result.Device, resourceSlice.Name, len(resourceSlice.Spec.Devices))
|
||||
for _, device := range resourceSlice.Spec.Devices {
|
||||
if device.Name != result.Device {
|
||||
continue
|
||||
}
|
||||
logging.Debugf("getDeviceInfo: found device %s, checking attributes", device.Name)
|
||||
|
||||
devIDAttr, exists := device.Attributes[multusDeviceIDAttr]
|
||||
if !exists {
|
||||
logging.Warningf(
|
||||
"getDeviceInfo: allocated device %q (driver %q, pool %q) has no %q attribute; DRA drivers must publish this for Multus; skipping device",
|
||||
device.Name, result.Driver, result.Pool, multusDeviceIDAttr)
|
||||
continue
|
||||
}
|
||||
|
||||
if devIDAttr.StringValue == nil {
|
||||
logging.Warningf(
|
||||
"getDeviceInfo: allocated device %q (driver %q, pool %q) has %q with nil StringValue; skipping device",
|
||||
device.Name, result.Driver, result.Pool, multusDeviceIDAttr)
|
||||
continue
|
||||
}
|
||||
info := &deviceInfo{DeviceID: *devIDAttr.StringValue}
|
||||
|
||||
if resNameAttr, ok := device.Attributes[multusResourceNameAttr]; ok && resNameAttr.StringValue != nil {
|
||||
info.ResourceName = *resNameAttr.StringValue
|
||||
logging.Debugf("getDeviceInfo: device %s has %s %s", device.Name, multusResourceNameAttr, info.ResourceName)
|
||||
}
|
||||
|
||||
logging.Verbosef("getDeviceInfo: successfully retrieved info for device %s (driver/pool: %s): deviceID=%s, resourceName=%s",
|
||||
result.Device, key, info.DeviceID, info.ResourceName)
|
||||
return info, nil
|
||||
}
|
||||
}
|
||||
|
||||
notFoundErr := fmt.Errorf("device %s not found for claim resource %s/%s in any matching resource slice", result.Device, result.Driver, result.Pool)
|
||||
logging.Errorf("getDeviceInfo: %v", notFoundErr)
|
||||
return nil, notFoundErr
|
||||
}
|
||||
29
pkg/draclient/draclient_suite_test.go
Normal file
29
pkg/draclient/draclient_suite_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2025 Multus 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 draclient
|
||||
|
||||
// disable dot-imports only for testing
|
||||
//revive:disable:dot-imports
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDRAClient(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "draclient")
|
||||
}
|
||||
1552
pkg/draclient/draclient_test.go
Normal file
1552
pkg/draclient/draclient_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ import (
|
||||
netclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned"
|
||||
netlister "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/listers/k8s.cni.cncf.io/v1"
|
||||
netutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils"
|
||||
"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/draclient"
|
||||
"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/kubeletclient"
|
||||
"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/logging"
|
||||
"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/types"
|
||||
@@ -53,6 +54,10 @@ const (
|
||||
networkAttachmentAnnot = "k8s.v1.cni.cncf.io/networks"
|
||||
)
|
||||
|
||||
// getResourceClientFunc returns kubelet / device-plugin resource info for a pod.
|
||||
// It defaults to kubeletclient.GetResourceClient; unit tests may replace it to avoid a real kubelet checkpoint/socket.
|
||||
var getResourceClientFunc = kubeletclient.GetResourceClient
|
||||
|
||||
// NoK8sNetworkError indicates error, no network in kubernetes
|
||||
type NoK8sNetworkError struct {
|
||||
message string
|
||||
@@ -309,7 +314,7 @@ func getKubernetesDelegate(client *ClientInfo, net *types.NetworkSelectionElemen
|
||||
logging.Debugf("getKubernetesDelegate: found resourceName annotation : %s", resourceName)
|
||||
|
||||
if resourceMap == nil {
|
||||
ck, err := kubeletclient.GetResourceClient("")
|
||||
ck, err := getResourceClientFunc("")
|
||||
if err != nil {
|
||||
return nil, resourceMap, logging.Errorf("getKubernetesDelegate: failed to get a ResourceClient instance: %v", err)
|
||||
}
|
||||
@@ -317,6 +322,13 @@ func getKubernetesDelegate(client *ClientInfo, net *types.NetworkSelectionElemen
|
||||
if err != nil {
|
||||
return nil, resourceMap, logging.Errorf("getKubernetesDelegate: failed to get resourceMap from ResourceClient: %v", err)
|
||||
}
|
||||
|
||||
dc := draclient.NewClient(client.Client.ResourceV1())
|
||||
err = dc.GetPodResourceMap(pod, resourceMap)
|
||||
if err != nil {
|
||||
return nil, resourceMap, logging.Errorf("getKubernetesDelegate: failed to get resourceMap from DRA client: %v", err)
|
||||
}
|
||||
|
||||
logging.Debugf("getKubernetesDelegate: resourceMap instance: %+v", resourceMap)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package k8sclient
|
||||
// disable dot-imports only for testing
|
||||
//revive:disable:dot-imports
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -33,6 +34,9 @@ import (
|
||||
netfake "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/fake"
|
||||
netutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
resourcev1api "k8s.io/api/resource/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -52,6 +56,13 @@ func NewFakeClientInfo() *ClientInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// fakeEmptyResourceClient implements types.ResourceClient with no device allocations (stubs kubelet in tests).
|
||||
type fakeEmptyResourceClient struct{}
|
||||
|
||||
func (*fakeEmptyResourceClient) GetPodResourceMap(*v1.Pod) (map[string]*types.ResourceInfo, error) {
|
||||
return make(map[string]*types.ResourceInfo), nil
|
||||
}
|
||||
|
||||
var _ = Describe("k8sclient operations", func() {
|
||||
var tmpDir string
|
||||
var err error
|
||||
@@ -1553,4 +1564,572 @@ users:
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("DRA (Dynamic Resource Allocation) integration", func() {
|
||||
var tmpDir string
|
||||
var err error
|
||||
|
||||
BeforeEach(func() {
|
||||
tmpDir, err = os.MkdirTemp("", "multus_dra_test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
err := os.RemoveAll(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when pod has DRA resources with device plugin resources", func() {
|
||||
It("should combine DRA and device plugin resource maps", func() {
|
||||
// This test verifies that getKubernetesDelegate correctly integrates
|
||||
// DRA resources with traditional device plugin resources
|
||||
|
||||
const fakePodName string = "dra-test-pod"
|
||||
const fakeNamespace string = "default"
|
||||
|
||||
// Create a network attachment definition
|
||||
netAttachDef := `{
|
||||
"name": "sriov-net",
|
||||
"type": "sriov",
|
||||
"cniVersion": "0.3.1"
|
||||
}`
|
||||
|
||||
// Create fake pod with DRA resource claims
|
||||
claimName := "gpu-claim"
|
||||
claimNamePtr := &claimName
|
||||
fakePod := testutils.NewFakePod(fakePodName, "sriov-net", "")
|
||||
fakePod.Namespace = fakeNamespace
|
||||
fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
|
||||
{
|
||||
ResourceClaimName: claimNamePtr,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client
|
||||
clientInfo := NewFakeClientInfo()
|
||||
_, err = clientInfo.AddPod(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create network attachment definition (without resourceName annotation to avoid kubeletclient)
|
||||
nad := testutils.NewFakeNetAttachDef(fakeNamespace, "sriov-net", netAttachDef)
|
||||
_, err = clientInfo.AddNetAttachDef(nad)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create ResourceClaim
|
||||
deviceName := "gpu-1"
|
||||
driverName := "gpu.example.com"
|
||||
poolName := "gpu-pool"
|
||||
requestName := "gpu"
|
||||
deviceID := "pci:0000:00:01.0"
|
||||
|
||||
deviceIDValue := deviceID
|
||||
resourceSlice := &resourcev1api.ResourceSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-resource-slice",
|
||||
},
|
||||
Spec: resourcev1api.ResourceSliceSpec{
|
||||
Driver: driverName,
|
||||
Pool: resourcev1api.ResourcePool{
|
||||
Name: poolName,
|
||||
ResourceSliceCount: 1,
|
||||
},
|
||||
Devices: []resourcev1api.Device{
|
||||
{
|
||||
Name: deviceName,
|
||||
Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{
|
||||
"k8s.cni.cncf.io/deviceID": {
|
||||
StringValue: &deviceIDValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resourceClaim := &resourcev1api.ResourceClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: claimName,
|
||||
Namespace: fakeNamespace,
|
||||
},
|
||||
Status: resourcev1api.ResourceClaimStatus{
|
||||
Allocation: &resourcev1api.AllocationResult{
|
||||
Devices: resourcev1api.DeviceAllocationResult{
|
||||
Results: []resourcev1api.DeviceRequestAllocationResult{
|
||||
{
|
||||
Request: requestName,
|
||||
Driver: driverName,
|
||||
Pool: poolName,
|
||||
Device: deviceName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add DRA resources to fake client
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Get the network selection element
|
||||
net := &types.NetworkSelectionElement{
|
||||
Name: "sriov-net",
|
||||
Namespace: fakeNamespace,
|
||||
}
|
||||
|
||||
// Call getKubernetesDelegate
|
||||
// Note: Without resourceName annotation, DRA client is not invoked automatically
|
||||
// This test verifies the delegate is created successfully for pods with DRA claims
|
||||
delegate, resourceMap, err := getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(delegate).NotTo(BeNil())
|
||||
|
||||
// Verify resourceMap is initialized (but empty since no resourceName annotation)
|
||||
Expect(len(resourceMap)).To(Equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when GetNetworkDelegates is called with DRA-enabled pod", func() {
|
||||
It("should successfully retrieve delegates with DRA resources", func() {
|
||||
const fakePodName string = "dra-network-pod"
|
||||
const fakeNamespace string = "default"
|
||||
|
||||
// Create network attachment definition
|
||||
netAttachDef := `{
|
||||
"name": "dra-network",
|
||||
"type": "bridge",
|
||||
"cniVersion": "0.3.1"
|
||||
}`
|
||||
|
||||
// Create fake pod with DRA resource claims
|
||||
claimName := "network-claim"
|
||||
claimNamePtr := &claimName
|
||||
fakePod := testutils.NewFakePod(fakePodName, "dra-network", "")
|
||||
fakePod.Namespace = fakeNamespace
|
||||
fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
|
||||
{
|
||||
ResourceClaimName: claimNamePtr,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client
|
||||
clientInfo := NewFakeClientInfo()
|
||||
_, err = clientInfo.AddPod(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create network attachment definition with resource name annotation
|
||||
nad := testutils.NewFakeNetAttachDef(fakeNamespace, "dra-network", netAttachDef)
|
||||
nad.Annotations = map[string]string{
|
||||
resourceNameAnnot: "dra-network-claim/gpu",
|
||||
}
|
||||
_, err = clientInfo.AddNetAttachDef(nad)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create DRA resources
|
||||
deviceName := "nic-1"
|
||||
driverName := "network.example.com"
|
||||
poolName := "network-pool"
|
||||
requestName := "gpu"
|
||||
deviceID := "pci:0000:00:02.0"
|
||||
// NAD k8s.v1.cni.cncf.io/resourceName must match device attribute k8s.cni.cncf.io/resourceName
|
||||
nadResourceKey := "dra-network-claim/gpu"
|
||||
|
||||
deviceIDValue := deviceID
|
||||
nadResourceKeyVal := nadResourceKey
|
||||
resourceSlice := &resourcev1api.ResourceSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "network-resource-slice",
|
||||
},
|
||||
Spec: resourcev1api.ResourceSliceSpec{
|
||||
Driver: driverName,
|
||||
Pool: resourcev1api.ResourcePool{
|
||||
Name: poolName,
|
||||
ResourceSliceCount: 1,
|
||||
},
|
||||
Devices: []resourcev1api.Device{
|
||||
{
|
||||
Name: deviceName,
|
||||
Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{
|
||||
"k8s.cni.cncf.io/deviceID": {
|
||||
StringValue: &deviceIDValue,
|
||||
},
|
||||
"k8s.cni.cncf.io/resourceName": {
|
||||
StringValue: &nadResourceKeyVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resourceClaim := &resourcev1api.ResourceClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: claimName,
|
||||
Namespace: fakeNamespace,
|
||||
},
|
||||
Status: resourcev1api.ResourceClaimStatus{
|
||||
Allocation: &resourcev1api.AllocationResult{
|
||||
Devices: resourcev1api.DeviceAllocationResult{
|
||||
Results: []resourcev1api.DeviceRequestAllocationResult{
|
||||
{
|
||||
Request: requestName,
|
||||
Driver: driverName,
|
||||
Pool: poolName,
|
||||
Device: deviceName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add DRA resources to fake client
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Get pod network
|
||||
networks, err := GetPodNetwork(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(networks).NotTo(BeEmpty())
|
||||
|
||||
// Create NetConf
|
||||
conf := &types.NetConf{
|
||||
ConfDir: tmpDir,
|
||||
}
|
||||
|
||||
// Get network delegates
|
||||
resourceMap := make(map[string]*types.ResourceInfo)
|
||||
delegates, err := GetNetworkDelegates(clientInfo, fakePod, networks, conf, resourceMap)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(delegates).NotTo(BeEmpty())
|
||||
Expect(delegates[0]).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when DRA resources are used in TryLoadPodDelegates", func() {
|
||||
It("should successfully load delegates with DRA resources", func() {
|
||||
const fakePodName string = "dra-delegate-pod"
|
||||
const fakeNamespace string = "default"
|
||||
|
||||
// Create network attachment definition
|
||||
netAttachDef := `{
|
||||
"name": "dra-delegate-network",
|
||||
"type": "macvlan",
|
||||
"cniVersion": "0.3.1"
|
||||
}`
|
||||
|
||||
// Create fake pod with DRA resource claims
|
||||
claimName := "delegate-claim"
|
||||
claimNamePtr := &claimName
|
||||
fakePod := testutils.NewFakePod(fakePodName, "dra-delegate-network", "")
|
||||
fakePod.Namespace = fakeNamespace
|
||||
fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
|
||||
{
|
||||
ResourceClaimName: claimNamePtr,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client
|
||||
clientInfo := NewFakeClientInfo()
|
||||
_, err = clientInfo.AddPod(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create network attachment definition
|
||||
nad := testutils.NewFakeNetAttachDef(fakeNamespace, "dra-delegate-network", netAttachDef)
|
||||
_, err = clientInfo.AddNetAttachDef(nad)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create DRA resources
|
||||
deviceName := "macvlan-device"
|
||||
driverName := "macvlan.example.com"
|
||||
poolName := "macvlan-pool"
|
||||
requestName := "nic"
|
||||
deviceID := "pci:0000:00:03.0"
|
||||
|
||||
deviceIDValue := deviceID
|
||||
resourceSlice := &resourcev1api.ResourceSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "macvlan-resource-slice",
|
||||
},
|
||||
Spec: resourcev1api.ResourceSliceSpec{
|
||||
Driver: driverName,
|
||||
Pool: resourcev1api.ResourcePool{
|
||||
Name: poolName,
|
||||
ResourceSliceCount: 1,
|
||||
},
|
||||
Devices: []resourcev1api.Device{
|
||||
{
|
||||
Name: deviceName,
|
||||
Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{
|
||||
"k8s.cni.cncf.io/deviceID": {
|
||||
StringValue: &deviceIDValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resourceClaim := &resourcev1api.ResourceClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: claimName,
|
||||
Namespace: fakeNamespace,
|
||||
},
|
||||
Status: resourcev1api.ResourceClaimStatus{
|
||||
Allocation: &resourcev1api.AllocationResult{
|
||||
Devices: resourcev1api.DeviceAllocationResult{
|
||||
Results: []resourcev1api.DeviceRequestAllocationResult{
|
||||
{
|
||||
Request: requestName,
|
||||
Driver: driverName,
|
||||
Pool: poolName,
|
||||
Device: deviceName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add DRA resources to fake client
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create NetConf with a default delegate
|
||||
confStr := `{
|
||||
"name": "test-network",
|
||||
"type": "multus",
|
||||
"delegates": [{
|
||||
"name": "default-network",
|
||||
"type": "bridge"
|
||||
}],
|
||||
"confDir": "` + tmpDir + `"
|
||||
}`
|
||||
|
||||
conf, err := types.LoadNetConf([]byte(confStr))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Try loading pod delegates
|
||||
resourceMap := make(map[string]*types.ResourceInfo)
|
||||
count, updatedClient, err := TryLoadPodDelegates(fakePod, conf, clientInfo, resourceMap)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(updatedClient).NotTo(BeNil())
|
||||
Expect(count).To(BeNumerically(">", 0))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when pod has no DRA resources", func() {
|
||||
It("should handle pods without DRA resources gracefully", func() {
|
||||
const fakePodName string = "no-dra-pod"
|
||||
const fakeNamespace string = "default"
|
||||
|
||||
// Create network attachment definition
|
||||
netAttachDef := `{
|
||||
"name": "simple-network",
|
||||
"type": "bridge",
|
||||
"cniVersion": "0.3.1"
|
||||
}`
|
||||
|
||||
// Create fake pod WITHOUT DRA resource claims
|
||||
fakePod := testutils.NewFakePod(fakePodName, "simple-network", "")
|
||||
fakePod.Namespace = fakeNamespace
|
||||
fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{} // Empty
|
||||
|
||||
// Setup client
|
||||
clientInfo := NewFakeClientInfo()
|
||||
_, err = clientInfo.AddPod(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create network attachment definition
|
||||
nad := testutils.NewFakeNetAttachDef(fakeNamespace, "simple-network", netAttachDef)
|
||||
_, err = clientInfo.AddNetAttachDef(nad)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Get the network selection element
|
||||
net := &types.NetworkSelectionElement{
|
||||
Name: "simple-network",
|
||||
Namespace: fakeNamespace,
|
||||
}
|
||||
|
||||
// Call getKubernetesDelegate - should work without DRA
|
||||
delegate, resourceMap, err := getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(delegate).NotTo(BeNil())
|
||||
// ResourceMap should be empty (no DRA resources)
|
||||
Expect(len(resourceMap)).To(Equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when DRA client fails to get resources", func() {
|
||||
var origGetResourceClient func(string) (types.ResourceClient, error)
|
||||
|
||||
BeforeEach(func() {
|
||||
origGetResourceClient = getResourceClientFunc
|
||||
// Stub kubelet so we get past device-plugin checkpoint/socket and exercise draclient.
|
||||
getResourceClientFunc = func(string) (types.ResourceClient, error) {
|
||||
return &fakeEmptyResourceClient{}, nil
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
getResourceClientFunc = origGetResourceClient
|
||||
})
|
||||
|
||||
It("should return an error from getKubernetesDelegate", func() {
|
||||
const fakePodName string = "dra-fail-pod"
|
||||
const fakeNamespace string = "default"
|
||||
const resourceName = "example.com/gpu"
|
||||
|
||||
// Create network attachment definition
|
||||
netAttachDef := `{
|
||||
"name": "fail-network",
|
||||
"type": "bridge",
|
||||
"cniVersion": "0.3.1"
|
||||
}`
|
||||
|
||||
// Create fake pod with DRA resource claims that don't exist in the API
|
||||
claimName := "non-existent-claim"
|
||||
claimNamePtr := &claimName
|
||||
fakePod := testutils.NewFakePod(fakePodName, "fail-network", "")
|
||||
fakePod.Namespace = fakeNamespace
|
||||
fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
|
||||
{
|
||||
Name: claimName,
|
||||
ResourceClaimName: claimNamePtr,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client
|
||||
clientInfo := NewFakeClientInfo()
|
||||
_, err = clientInfo.AddPod(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// NAD must have resourceName annotation so getKubernetesDelegate runs the DRA client path
|
||||
nad := testutils.NewFakeNetAttachDef(fakeNamespace, "fail-network", netAttachDef)
|
||||
nad.Annotations = map[string]string{
|
||||
resourceNameAnnot: resourceName,
|
||||
}
|
||||
_, err = clientInfo.AddNetAttachDef(nad)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Do NOT create the ResourceClaim — draclient.GetPodResourceMap fails on API Get.
|
||||
|
||||
net := &types.NetworkSelectionElement{
|
||||
Name: "fail-network",
|
||||
Namespace: fakeNamespace,
|
||||
}
|
||||
|
||||
_, _, err = getKubernetesDelegate(clientInfo, net, tmpDir, fakePod, nil)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("failed to get resourceMap from DRA client"))
|
||||
Expect(err.Error()).To(ContainSubstring("not found"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when using getNetDelegate with DRA resources", func() {
|
||||
It("should retrieve delegates with DRA resources populated", func() {
|
||||
const fakePodName string = "netdelegate-dra-pod"
|
||||
const fakeNamespace string = "default"
|
||||
|
||||
// Create network attachment definition
|
||||
netAttachDef := `{
|
||||
"name": "netdelegate-network",
|
||||
"type": "ipvlan",
|
||||
"cniVersion": "0.3.1"
|
||||
}`
|
||||
|
||||
// Create fake pod with DRA resource claims
|
||||
claimName := "netdelegate-claim"
|
||||
claimNamePtr := &claimName
|
||||
fakePod := testutils.NewFakePod(fakePodName, "", "")
|
||||
fakePod.Namespace = fakeNamespace
|
||||
fakePod.Status.ResourceClaimStatuses = []v1.PodResourceClaimStatus{
|
||||
{
|
||||
ResourceClaimName: claimNamePtr,
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client
|
||||
clientInfo := NewFakeClientInfo()
|
||||
_, err = clientInfo.AddPod(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create network attachment definition
|
||||
nad := testutils.NewFakeNetAttachDef(fakeNamespace, "netdelegate-network", netAttachDef)
|
||||
_, err = clientInfo.AddNetAttachDef(nad)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create DRA resources
|
||||
deviceName := "ipvlan-device"
|
||||
driverName := "ipvlan.example.com"
|
||||
poolName := "ipvlan-pool"
|
||||
requestName := "vnic"
|
||||
deviceID := "pci:0000:00:04.0"
|
||||
|
||||
deviceIDValue := deviceID
|
||||
resourceSlice := &resourcev1api.ResourceSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ipvlan-resource-slice",
|
||||
},
|
||||
Spec: resourcev1api.ResourceSliceSpec{
|
||||
Driver: driverName,
|
||||
Pool: resourcev1api.ResourcePool{
|
||||
Name: poolName,
|
||||
ResourceSliceCount: 1,
|
||||
},
|
||||
Devices: []resourcev1api.Device{
|
||||
{
|
||||
Name: deviceName,
|
||||
Attributes: map[resourcev1api.QualifiedName]resourcev1api.DeviceAttribute{
|
||||
"k8s.cni.cncf.io/deviceID": {
|
||||
StringValue: &deviceIDValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resourceClaim := &resourcev1api.ResourceClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: claimName,
|
||||
Namespace: fakeNamespace,
|
||||
},
|
||||
Status: resourcev1api.ResourceClaimStatus{
|
||||
Allocation: &resourcev1api.AllocationResult{
|
||||
Devices: resourcev1api.DeviceAllocationResult{
|
||||
Results: []resourcev1api.DeviceRequestAllocationResult{
|
||||
{
|
||||
Request: requestName,
|
||||
Driver: driverName,
|
||||
Pool: poolName,
|
||||
Device: deviceName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add DRA resources to fake client
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceClaims(fakeNamespace).Create(context.TODO(), resourceClaim, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = clientInfo.Client.ResourceV1().ResourceSlices().Create(context.TODO(), resourceSlice, metav1.CreateOptions{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Call getNetDelegate
|
||||
resourceMap := make(map[string]*types.ResourceInfo)
|
||||
delegate, updatedResourceMap, err := getNetDelegate(clientInfo, fakePod, "netdelegate-network", tmpDir, fakeNamespace, resourceMap)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(delegate).NotTo(BeNil())
|
||||
Expect(updatedResourceMap).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
@@ -111,7 +110,6 @@ type kubeletClient struct {
|
||||
}
|
||||
|
||||
func (rc *kubeletClient) getPodResources(client podresourcesapi.PodResourcesListerClient) error {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -139,7 +137,6 @@ func (rc *kubeletClient) GetPodResourceMap(pod *v1.Pod) (map[string]*types.Resou
|
||||
if pr.Name == name && pr.Namespace == ns {
|
||||
for _, cnt := range pr.Containers {
|
||||
rc.getDevicePluginResources(cnt.Devices, resourceMap)
|
||||
rc.getDRAResources(cnt.DynamicResources, resourceMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,27 +154,6 @@ func (rc *kubeletClient) getDevicePluginResources(devices []*podresourcesapi.Con
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *kubeletClient) getDRAResources(dynamicResources []*podresourcesapi.DynamicResource, resourceMap map[string]*types.ResourceInfo) {
|
||||
for _, dynamicResource := range dynamicResources {
|
||||
var deviceIDs []string
|
||||
for _, claimResource := range dynamicResource.ClaimResources {
|
||||
for _, cdiDevice := range claimResource.CdiDevices {
|
||||
res := strings.Split(cdiDevice.Name, "=")
|
||||
if len(res) == 2 {
|
||||
deviceIDs = append(deviceIDs, res[1])
|
||||
} else {
|
||||
logging.Errorf("GetPodResourceMap: Invalid CDI format")
|
||||
}
|
||||
}
|
||||
}
|
||||
if rInfo, ok := resourceMap[dynamicResource.ClaimName]; ok {
|
||||
rInfo.DeviceIDs = append(rInfo.DeviceIDs, deviceIDs...)
|
||||
} else {
|
||||
resourceMap[dynamicResource.ClaimName] = &types.ResourceInfo{DeviceIDs: deviceIDs}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasKubeletAPIEndpoint(url *url.URL) bool {
|
||||
// Check for kubelet resource API socket file
|
||||
if _, err := os.Stat(url.Path); err != nil {
|
||||
|
||||
@@ -46,8 +46,8 @@ var (
|
||||
)
|
||||
|
||||
type fakeResourceServer struct {
|
||||
server *grpc.Server
|
||||
podresourcesapi.UnimplementedPodResourcesListerServer
|
||||
server *grpc.Server
|
||||
}
|
||||
|
||||
// TODO: This is stub code for test, but we may need to change for the testing we use this API in the future...
|
||||
@@ -81,7 +81,7 @@ func (m *fakeResourceServer) List(_ context.Context, _ *podresourcesapi.ListPodR
|
||||
{
|
||||
CdiDevices: cdiDevices,
|
||||
DriverName: draDriverName,
|
||||
PoolName: poolName,
|
||||
PoolName: poolName,
|
||||
DeviceName: deviceName,
|
||||
},
|
||||
}
|
||||
@@ -249,34 +249,6 @@ var _ = Describe("Kubelet resource endpoint data read operations", func() {
|
||||
Expect(resourceMap).To(Equal(outputRMap))
|
||||
})
|
||||
|
||||
It("should return no error with dynamic resource", func() {
|
||||
podUID := k8sTypes.UID("9f94e27b-4233-43d6-bd10-f73b4de6f456")
|
||||
fakePod := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "dynamic-resource-pod-name",
|
||||
Namespace: "dynamic-resource-pod-namespace",
|
||||
UID: podUID,
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "dynamic-resource-container-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
client, err := getKubeletClient(testKubeletSocket)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
outputRMap := map[string]*mtypes.ResourceInfo{
|
||||
"resource-claim": {DeviceIDs: []string{"cdi-resource"}},
|
||||
}
|
||||
resourceMap, err := client.GetPodResourceMap(fakePod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(resourceMap).ShouldNot(BeNil())
|
||||
Expect(resourceMap).To(Equal(outputRMap))
|
||||
})
|
||||
|
||||
It("should return an error with garbage socket value", func() {
|
||||
u, err := url.Parse("/badfilepath!?//")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
@@ -138,6 +138,11 @@ func Errorf(format string, a ...interface{}) error {
|
||||
return fmt.Errorf(format, a...)
|
||||
}
|
||||
|
||||
// Warningf prints a warning at the same visibility threshold as Errorf (prefix "warning:").
|
||||
func Warningf(format string, a ...interface{}) {
|
||||
printf(ErrorLevel, "warning: "+format, a...)
|
||||
}
|
||||
|
||||
// Panicf prints logging plus stack trace. This should be used only for unrecoverable error
|
||||
func Panicf(format string, a ...interface{}) {
|
||||
printf(PanicLevel, format, a...)
|
||||
|
||||
@@ -622,7 +622,7 @@ func delPlugins(exec invoke.Exec, pod *v1.Pod, args *skel.CmdArgs, k8sArgs *type
|
||||
|
||||
// Check if we had any errors, and send them all back.
|
||||
if len(errorstrings) > 0 {
|
||||
return fmt.Errorf("%s", strings.Join(errorstrings, " / "))
|
||||
return fmt.Errorf("errors: %s", strings.Join(errorstrings, " / "))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user