Merge pull request #130447 from pohly/dra-device-taints

device taints and tolerations (KEP 5055)
This commit is contained in:
Kubernetes Prow Robot 2025-03-19 13:00:32 -07:00 committed by GitHub
commit ab3cec0701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
149 changed files with 18346 additions and 484 deletions

View file

@ -1915,6 +1915,26 @@
"watch"
]
},
{
"resource": "devicetaintrules",
"responseKind": {
"group": "",
"kind": "DeviceTaintRule",
"version": ""
},
"scope": "Cluster",
"singularResource": "devicetaintrule",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"resource": "resourceclaims",
"responseKind": {

View file

@ -20,6 +20,23 @@
"watch"
]
},
{
"kind": "DeviceTaintRule",
"name": "devicetaintrules",
"namespaced": false,
"singularName": "devicetaintrule",
"storageVersionHash": "DJ3UJ0fj8MI=",
"verbs": [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch"
]
},
{
"kind": "ResourceClaim",
"name": "resourceclaims",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -193,6 +193,19 @@
},
"description": "Capacity defines the set of capacities for this device. The name of each capacity must be unique in that set.\n\nThe maximum number of attributes and capacities combined is 32.",
"type": "object"
},
"taints": {
"description": "If specified, these are the driver-defined taints.\n\nThe maximum number of taints is 8.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
"items": {
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.api.resource.v1beta1.DeviceTaint"
}
],
"default": {}
},
"type": "array",
"x-kubernetes-list-type": "atomic"
}
},
"type": "object"
@ -607,6 +620,19 @@
},
"type": "array",
"x-kubernetes-list-type": "atomic"
},
"tolerations": {
"description": "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
"items": {
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.api.resource.v1beta1.DeviceToleration"
}
],
"default": {}
},
"type": "array",
"x-kubernetes-list-type": "atomic"
}
},
"required": [
@ -640,6 +666,19 @@
"default": "",
"description": "Request is the name of the request in the claim which caused this device to be allocated. If it references a subrequest in the firstAvailable list on a DeviceRequest, this field must include both the name of the main request and the subrequest using the format <main request>/<subrequest>.\n\nMultiple devices may have been allocated per request.",
"type": "string"
},
"tolerations": {
"description": "A copy of all tolerations specified in the request at the time when the device got allocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
"items": {
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.api.resource.v1beta1.DeviceToleration"
}
],
"default": {}
},
"type": "array",
"x-kubernetes-list-type": "atomic"
}
},
"required": [
@ -698,6 +737,19 @@
},
"type": "array",
"x-kubernetes-list-type": "atomic"
},
"tolerations": {
"description": "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
"items": {
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.api.resource.v1beta1.DeviceToleration"
}
],
"default": {}
},
"type": "array",
"x-kubernetes-list-type": "atomic"
}
},
"required": [
@ -706,6 +758,66 @@
],
"type": "object"
},
"io.k8s.api.resource.v1beta1.DeviceTaint": {
"description": "The device this taint is attached to has the \"effect\" on any claim which does not tolerate the taint and, through the claim, to pods using the claim.",
"properties": {
"effect": {
"default": "",
"description": "The effect of the taint on claims that do not tolerate the taint and through such claims on the pods using them. Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for nodes is not valid here.",
"type": "string"
},
"key": {
"default": "",
"description": "The taint key to be applied to a device. Must be a label name.",
"type": "string"
},
"timeAdded": {
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time"
}
],
"description": "TimeAdded represents the time at which the taint was added. Added automatically during create or update if not set."
},
"value": {
"description": "The taint value corresponding to the taint key. Must be a label value.",
"type": "string"
}
},
"required": [
"key",
"effect"
],
"type": "object"
},
"io.k8s.api.resource.v1beta1.DeviceToleration": {
"description": "The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.",
"properties": {
"effect": {
"description": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule and NoExecute.",
"type": "string"
},
"key": {
"description": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. Must be a label name.",
"type": "string"
},
"operator": {
"default": "Equal",
"description": "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a ResourceClaim can tolerate all taints of a particular category.",
"type": "string"
},
"tolerationSeconds": {
"description": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. If larger than zero, the time when the pod needs to be evicted is calculated as <time when taint was adedd> + <toleration seconds>.",
"format": "int64",
"type": "integer"
},
"value": {
"description": "Value is the taint value the toleration matches to. If the operator is Exists, the value must be empty, otherwise just a regular string. Must be a label value.",
"type": "string"
}
},
"type": "object"
},
"io.k8s.api.resource.v1beta1.NetworkDeviceData": {
"description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
"properties": {

View file

@ -588,6 +588,7 @@ func NewControllerDescriptors() map[string]*ControllerDescriptor {
// feature gated
register(newStorageVersionGarbageCollectorControllerDescriptor())
register(newResourceClaimControllerDescriptor())
register(newDeviceTaintEvictionControllerDescriptor())
register(newLegacyServiceAccountTokenCleanerControllerDescriptor())
register(newValidatingAdmissionPolicyStatusControllerDescriptor())
register(newTaintEvictionControllerDescriptor())

View file

@ -93,6 +93,7 @@ func TestControllerNamesDeclaration(t *testing.T) {
names.EphemeralVolumeController,
names.StorageVersionGarbageCollectorController,
names.ResourceClaimController,
names.DeviceTaintEvictionController,
names.LegacyServiceAccountTokenCleanerController,
names.ValidatingAdmissionPolicyStatusController,
names.ServiceCIDRController,

View file

@ -41,6 +41,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/kubernetes/cmd/kube-controller-manager/names"
pkgcontroller "k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/controller/devicetainteviction"
endpointcontroller "k8s.io/kubernetes/pkg/controller/endpoint"
"k8s.io/kubernetes/pkg/controller/garbagecollector"
namespacecontroller "k8s.io/kubernetes/pkg/controller/namespace"
@ -231,6 +232,32 @@ func startTaintEvictionController(ctx context.Context, controllerContext Control
return nil, true, nil
}
func newDeviceTaintEvictionControllerDescriptor() *ControllerDescriptor {
return &ControllerDescriptor{
name: names.DeviceTaintEvictionController,
initFunc: startDeviceTaintEvictionController,
requiredFeatureGates: []featuregate.Feature{
// TODO update app.TestFeatureGatedControllersShouldNotDefineAliases when removing these feature gates.
features.DynamicResourceAllocation,
features.DRADeviceTaints,
},
}
}
func startDeviceTaintEvictionController(ctx context.Context, controllerContext ControllerContext, controllerName string) (controller.Interface, bool, error) {
deviceTaintEvictionController := devicetainteviction.New(
controllerContext.ClientBuilder.ClientOrDie(names.DeviceTaintEvictionController),
controllerContext.InformerFactory.Core().V1().Pods(),
controllerContext.InformerFactory.Resource().V1beta1().ResourceClaims(),
controllerContext.InformerFactory.Resource().V1beta1().ResourceSlices(),
controllerContext.InformerFactory.Resource().V1alpha3().DeviceTaintRules(),
controllerContext.InformerFactory.Resource().V1beta1().DeviceClasses(),
controllerName,
)
go deviceTaintEvictionController.Run(ctx)
return nil, true, nil
}
func newCloudNodeLifecycleControllerDescriptor() *ControllerDescriptor {
return &ControllerDescriptor{
name: cpnames.CloudNodeLifecycleController,

View file

@ -69,6 +69,7 @@ const (
NodeIpamController = "node-ipam-controller"
NodeLifecycleController = "node-lifecycle-controller"
TaintEvictionController = "taint-eviction-controller"
DeviceTaintEvictionController = "device-taint-eviction-controller"
PersistentVolumeBinderController = "persistentvolume-binder-controller"
PersistentVolumeAttachDetachController = "persistentvolume-attach-detach-controller"
PersistentVolumeExpanderController = "persistentvolume-expander-controller"

View file

@ -135,14 +135,20 @@ func TestDefaulting(t *testing.T) {
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}: {},
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: {},
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "DeviceTaintRule"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "DeviceTaintRuleList"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaim"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaimList"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaimTemplate"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceClaimTemplateList"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceSlice"}: {},
{Group: "resource.k8s.io", Version: "v1alpha3", Kind: "ResourceSliceList"}: {},
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceClaim"}: {},
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceClaimList"}: {},
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceClaimTemplate"}: {},
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceClaimTemplateList"}: {},
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceSlice"}: {},
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceSliceList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicy"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {},

View file

@ -17,6 +17,9 @@ limitations under the License.
package fuzzer
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/kubernetes/pkg/apis/resource"
@ -57,6 +60,24 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
}[c.Int31n(2)]
}
},
func(r *resource.DeviceToleration, c randfill.Continue) {
c.FillNoCustom(r)
if r.Operator == "" {
r.Operator = []resource.DeviceTolerationOperator{
resource.DeviceTolerationOpEqual,
resource.DeviceTolerationOpExists,
}[c.Int31n(2)]
}
},
func(r *resource.DeviceTaint, c randfill.Continue) {
c.FillNoCustom(r)
if r.TimeAdded == nil {
// Current time is more or less random.
// Truncate to seconds because sub-second resolution
// does not survive round-tripping.
r.TimeAdded = &metav1.Time{Time: time.Now().Truncate(time.Second)}
}
},
func(r *resource.OpaqueDeviceConfiguration, c randfill.Continue) {
c.FillNoCustom(r)
// Match the fuzzer default content for runtime.Object.

View file

@ -54,6 +54,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&DeviceClass{},
&DeviceClassList{},
&DeviceTaintRule{},
&DeviceTaintRuleList{},
&ResourceClaim{},
&ResourceClaimList{},
&ResourceClaimTemplate{},

View file

@ -219,6 +219,18 @@ type BasicDevice struct {
//
// +optional
Capacity map[QualifiedName]DeviceCapacity
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Taints []DeviceTaint
}
// DeviceCapacity describes a quantity associated with a device.
@ -297,6 +309,64 @@ type DeviceAttribute struct {
// DeviceAttributeMaxValueLength is the maximum length of a string or version attribute value.
const DeviceAttributeMaxValueLength = 64
// DeviceTaintsMaxLength is the maximum number of taints per device.
const DeviceTaintsMaxLength = 8
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
// to pods using the claim.
type DeviceTaint struct {
// The taint key to be applied to a device.
// Must be a label name.
//
// +required
Key string
// The taint value corresponding to the taint key.
// Must be a label value.
//
// +optional
Value string
// The effect of the taint on claims that do not tolerate the taint
// and through such claims on the pods using them.
// Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for
// nodes is not valid here.
//
// +required
Effect DeviceTaintEffect
// ^^^^
//
// Implementing PreferNoSchedule would depend on a scoring solution for DRA.
// It might get added as part of that.
// TimeAdded represents the time at which the taint was added.
// Added automatically during create or update if not set.
//
// +optional
TimeAdded *metav1.Time
// ^^^
//
// This field was defined as "It is only written for NoExecute taints." for node taints.
// But in practice, Kubernetes never did anything with it (no validation, no defaulting,
// ignored during pod eviction in pkg/controller/tainteviction).
}
// +enum
type DeviceTaintEffect string
const (
// Do not allow new pods to schedule which use a tainted device unless they tolerate the taint,
// but allow all pods submitted to Kubelet without going through the scheduler
// to start, and allow all already-running pods to continue running.
DeviceTaintEffectNoSchedule DeviceTaintEffect = "NoSchedule"
// Evict any already-running pods that do not tolerate the device taint.
DeviceTaintEffectNoExecute DeviceTaintEffect = "NoExecute"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// ResourceSliceList is a collection of ResourceSlices.
@ -511,6 +581,32 @@ type DeviceRequest struct {
// +listType=atomic
// +featureGate=DRAPrioritizedList
FirstAvailable []DeviceSubRequest
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration
}
// DeviceSubRequest describes a request for device provided in the
@ -583,11 +679,35 @@ type DeviceSubRequest struct {
// +optional
// +oneOf=AllocationMode
Count int64
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration
}
const (
DeviceSelectorsMaxSize = 32
FirstAvailableDeviceRequestMaxSize = 8
DeviceTolerationsMaxLength = 16
)
type DeviceAllocationMode string
@ -793,6 +913,59 @@ type OpaqueDeviceConfiguration struct {
// [OpaqueDeviceConfiguration.Parameters] field.
const OpaqueParametersMaxLength = 10 * 1024
// The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches
// the triple <key,value,effect> using the matching operator <operator>.
type DeviceToleration struct {
// Key is the taint key that the toleration applies to. Empty means match all taint keys.
// If the key is empty, operator must be Exists; this combination means to match all values and all keys.
// Must be a label name.
//
// +optional
Key string
// Operator represents a key's relationship to the value.
// Valid operators are Exists and Equal. Defaults to Equal.
// Exists is equivalent to wildcard for value, so that a ResourceClaim can
// tolerate all taints of a particular category.
//
// +optional
// +default="Equal"
Operator DeviceTolerationOperator
// Value is the taint value the toleration matches to.
// If the operator is Exists, the value must be empty, otherwise just a regular string.
// Must be a label value.
//
// +optional
Value string
// Effect indicates the taint effect to match. Empty means match all taint effects.
// When specified, allowed values are NoSchedule and NoExecute.
//
// +optional
Effect DeviceTaintEffect
// TolerationSeconds represents the period of time the toleration (which must be
// of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
// it is not set, which means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by the system.
// If larger than zero, the time when the pod needs to be evicted is calculated as <time when
// taint was adedd> + <toleration seconds>.
//
// +optional
TolerationSeconds *int64
}
// A toleration operator is the set of operators that can be used in a toleration.
//
// +enum
type DeviceTolerationOperator string
const (
DeviceTolerationOpExists DeviceTolerationOperator = "Exists"
DeviceTolerationOpEqual DeviceTolerationOperator = "Equal"
)
// ResourceClaimStatus tracks whether the resource has been allocated and what
// the result of that was.
type ResourceClaimStatus struct {
@ -963,6 +1136,19 @@ type DeviceRequestAllocationResult struct {
// +optional
// +featureGate=DRAAdminAccess
AdminAccess *bool
// A copy of all tolerations specified in the request at the time
// when the device got allocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration
}
// DeviceAllocationConfiguration gets embedded in an AllocationResult.
@ -1227,3 +1413,103 @@ type NetworkDeviceData struct {
// +optional
HardwareAddress string
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// DeviceTaintRule adds one taint to all devices which match the selector.
// This has the same effect as if the taint was specified directly
// in the ResourceSlice by the DRA driver.
type DeviceTaintRule struct {
metav1.TypeMeta
// Standard object metadata
// +optional
metav1.ObjectMeta
// Spec specifies the selector and one taint.
//
// Changing the spec automatically increments the metadata.generation number.
Spec DeviceTaintRuleSpec
// ^^^
// A spec gets added because adding a status seems likely.
// Such a status could provide feedback on applying the
// eviction and/or statistics (number of matching devices,
// affected allocated claims, pods remaining to be evicted,
// etc.).
}
// DeviceTaintRuleSpec specifies the selector and one taint.
type DeviceTaintRuleSpec struct {
// DeviceSelector defines which device(s) the taint is applied to.
// All selector criteria must be satified for a device to
// match. The empty selector matches all devices. Without
// a selector, no devices are matches.
//
// +optional
DeviceSelector *DeviceTaintSelector
// The taint that gets applied to matching devices.
//
// +required
Taint DeviceTaint
}
// DeviceTaintSelector defines which device(s) a DeviceTaintRule applies to.
// The empty selector matches all devices. Without a selector, no devices
// are matched.
type DeviceTaintSelector struct {
// If DeviceClassName is set, the selectors defined there must be
// satisfied by a device to be selected. This field corresponds
// to class.metadata.name.
//
// +optional
DeviceClassName *string
// If driver is set, only devices from that driver are selected.
// This fields corresponds to slice.spec.driver.
//
// +optional
Driver *string
// If pool is set, only devices in that pool are selected.
//
// Also setting the driver name may be useful to avoid
// ambiguity when different drivers use the same pool name,
// but this is not required because selecting pools from
// different drivers may also be useful, for example when
// drivers with node-local devices use the node name as
// their pool name.
//
// +optional
Pool *string
// If device is set, only devices with that name are selected.
// This field corresponds to slice.spec.devices[].name.
//
// Setting also driver and pool may be required to avoid ambiguity,
// but is not required.
//
// +optional
Device *string
// Selectors contains the same selection criteria as a ResourceClaim.
// Currently, CEL expressions are supported. All of these selectors
// must be satisfied.
//
// +optional
// +listType=atomic
Selectors []DeviceSelector
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// DeviceTaintRuleList is a collection of DeviceTaintRules.
type DeviceTaintRuleList struct {
metav1.TypeMeta
// Standard list metadata
// +optional
metav1.ListMeta
// Items is the list of DeviceTaintRules.
Items []DeviceTaintRule
}

View file

@ -17,7 +17,10 @@ limitations under the License.
package v1alpha3
import (
"time"
resourceapi "k8s.io/api/resource/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@ -50,3 +53,9 @@ func SetDefaults_DeviceSubRequest(obj *resourceapi.DeviceSubRequest) {
obj.Count = 1
}
}
func SetDefaults_DeviceTaint(obj *resourceapi.DeviceTaint) {
if obj.TimeAdded == nil {
obj.TimeAdded = &metav1.Time{Time: time.Now().Truncate(time.Second)}
}
}

View file

@ -19,11 +19,14 @@ package v1alpha3_test
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
v1alpha3 "k8s.io/api/resource/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
// ensure types are installed
"k8s.io/kubernetes/pkg/api/legacyscheme"
@ -134,6 +137,29 @@ func TestSetDefaultAllocationModeWithSubRequests(t *testing.T) {
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].FirstAvailable[1].Count)
}
func TestSetDefaultDeviceTaint(t *testing.T) {
slice := &v1alpha3.ResourceSlice{
Spec: v1alpha3.ResourceSliceSpec{
Devices: []v1alpha3.Device{{
Name: "device-0",
Basic: &v1alpha3.BasicDevice{
Taints: []v1alpha3.DeviceTaint{{}},
},
}},
},
}
// fields should be defaulted
output := roundTrip(t, slice).(*v1alpha3.ResourceSlice)
assert.WithinDuration(t, time.Now(), ptr.Deref(output.Spec.Devices[0].Basic.Taints[0].TimeAdded, metav1.Time{}).Time, time.Minute /* allow for some processing delay */, "time added default")
// field should not change
timeAdded, _ := time.ParseInLocation(time.RFC3339, "2006-01-02T15:04:05Z", time.UTC)
slice.Spec.Devices[0].Basic.Taints[0].TimeAdded = &metav1.Time{Time: timeAdded}
output = roundTrip(t, slice).(*v1alpha3.ResourceSlice)
assert.WithinDuration(t, timeAdded, ptr.Deref(output.Spec.Devices[0].Basic.Taints[0].TimeAdded, metav1.Time{}).Time, 0 /* semantically the same, different time zone allowed */, "time added fixed")
}
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
codec := legacyscheme.Codecs.LegacyCodec(v1alpha3.SchemeGroupVersion)
data, err := runtime.Encode(codec, obj)

View file

@ -242,6 +242,66 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.DeviceTaint)(nil), (*resource.DeviceTaint)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_DeviceTaint_To_resource_DeviceTaint(a.(*resourcev1alpha3.DeviceTaint), b.(*resource.DeviceTaint), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceTaint)(nil), (*resourcev1alpha3.DeviceTaint)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceTaint_To_v1alpha3_DeviceTaint(a.(*resource.DeviceTaint), b.(*resourcev1alpha3.DeviceTaint), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.DeviceTaintRule)(nil), (*resource.DeviceTaintRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_DeviceTaintRule_To_resource_DeviceTaintRule(a.(*resourcev1alpha3.DeviceTaintRule), b.(*resource.DeviceTaintRule), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceTaintRule)(nil), (*resourcev1alpha3.DeviceTaintRule)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceTaintRule_To_v1alpha3_DeviceTaintRule(a.(*resource.DeviceTaintRule), b.(*resourcev1alpha3.DeviceTaintRule), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.DeviceTaintRuleList)(nil), (*resource.DeviceTaintRuleList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_DeviceTaintRuleList_To_resource_DeviceTaintRuleList(a.(*resourcev1alpha3.DeviceTaintRuleList), b.(*resource.DeviceTaintRuleList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceTaintRuleList)(nil), (*resourcev1alpha3.DeviceTaintRuleList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceTaintRuleList_To_v1alpha3_DeviceTaintRuleList(a.(*resource.DeviceTaintRuleList), b.(*resourcev1alpha3.DeviceTaintRuleList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.DeviceTaintRuleSpec)(nil), (*resource.DeviceTaintRuleSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_DeviceTaintRuleSpec_To_resource_DeviceTaintRuleSpec(a.(*resourcev1alpha3.DeviceTaintRuleSpec), b.(*resource.DeviceTaintRuleSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceTaintRuleSpec)(nil), (*resourcev1alpha3.DeviceTaintRuleSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceTaintRuleSpec_To_v1alpha3_DeviceTaintRuleSpec(a.(*resource.DeviceTaintRuleSpec), b.(*resourcev1alpha3.DeviceTaintRuleSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.DeviceTaintSelector)(nil), (*resource.DeviceTaintSelector)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_DeviceTaintSelector_To_resource_DeviceTaintSelector(a.(*resourcev1alpha3.DeviceTaintSelector), b.(*resource.DeviceTaintSelector), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceTaintSelector)(nil), (*resourcev1alpha3.DeviceTaintSelector)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceTaintSelector_To_v1alpha3_DeviceTaintSelector(a.(*resource.DeviceTaintSelector), b.(*resourcev1alpha3.DeviceTaintSelector), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.DeviceToleration)(nil), (*resource.DeviceToleration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_DeviceToleration_To_resource_DeviceToleration(a.(*resourcev1alpha3.DeviceToleration), b.(*resource.DeviceToleration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceToleration)(nil), (*resourcev1alpha3.DeviceToleration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceToleration_To_v1alpha3_DeviceToleration(a.(*resource.DeviceToleration), b.(*resourcev1alpha3.DeviceToleration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1alpha3.NetworkDeviceData)(nil), (*resource.NetworkDeviceData)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha3_NetworkDeviceData_To_resource_NetworkDeviceData(a.(*resourcev1alpha3.NetworkDeviceData), b.(*resource.NetworkDeviceData), scope)
}); err != nil {
@ -466,6 +526,7 @@ func autoConvert_v1alpha3_BasicDevice_To_resource_BasicDevice(in *resourcev1alph
} else {
out.Capacity = nil
}
out.Taints = *(*[]resource.DeviceTaint)(unsafe.Pointer(&in.Taints))
return nil
}
@ -489,6 +550,7 @@ func autoConvert_resource_BasicDevice_To_v1alpha3_BasicDevice(in *resource.Basic
} else {
out.Capacity = nil
}
out.Taints = *(*[]resourcev1alpha3.DeviceTaint)(unsafe.Pointer(&in.Taints))
return nil
}
@ -825,6 +887,7 @@ func autoConvert_v1alpha3_DeviceRequest_To_resource_DeviceRequest(in *resourcev1
out.Count = in.Count
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.FirstAvailable = *(*[]resource.DeviceSubRequest)(unsafe.Pointer(&in.FirstAvailable))
out.Tolerations = *(*[]resource.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -841,6 +904,7 @@ func autoConvert_resource_DeviceRequest_To_v1alpha3_DeviceRequest(in *resource.D
out.Count = in.Count
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.FirstAvailable = *(*[]resourcev1alpha3.DeviceSubRequest)(unsafe.Pointer(&in.FirstAvailable))
out.Tolerations = *(*[]resourcev1alpha3.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -855,6 +919,7 @@ func autoConvert_v1alpha3_DeviceRequestAllocationResult_To_resource_DeviceReques
out.Pool = in.Pool
out.Device = in.Device
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.Tolerations = *(*[]resource.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -869,6 +934,7 @@ func autoConvert_resource_DeviceRequestAllocationResult_To_v1alpha3_DeviceReques
out.Pool = in.Pool
out.Device = in.Device
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.Tolerations = *(*[]resourcev1alpha3.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -903,6 +969,7 @@ func autoConvert_v1alpha3_DeviceSubRequest_To_resource_DeviceSubRequest(in *reso
out.Selectors = *(*[]resource.DeviceSelector)(unsafe.Pointer(&in.Selectors))
out.AllocationMode = resource.DeviceAllocationMode(in.AllocationMode)
out.Count = in.Count
out.Tolerations = *(*[]resource.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -917,6 +984,7 @@ func autoConvert_resource_DeviceSubRequest_To_v1alpha3_DeviceSubRequest(in *reso
out.Selectors = *(*[]resourcev1alpha3.DeviceSelector)(unsafe.Pointer(&in.Selectors))
out.AllocationMode = resourcev1alpha3.DeviceAllocationMode(in.AllocationMode)
out.Count = in.Count
out.Tolerations = *(*[]resourcev1alpha3.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -925,6 +993,162 @@ func Convert_resource_DeviceSubRequest_To_v1alpha3_DeviceSubRequest(in *resource
return autoConvert_resource_DeviceSubRequest_To_v1alpha3_DeviceSubRequest(in, out, s)
}
func autoConvert_v1alpha3_DeviceTaint_To_resource_DeviceTaint(in *resourcev1alpha3.DeviceTaint, out *resource.DeviceTaint, s conversion.Scope) error {
out.Key = in.Key
out.Value = in.Value
out.Effect = resource.DeviceTaintEffect(in.Effect)
out.TimeAdded = (*v1.Time)(unsafe.Pointer(in.TimeAdded))
return nil
}
// Convert_v1alpha3_DeviceTaint_To_resource_DeviceTaint is an autogenerated conversion function.
func Convert_v1alpha3_DeviceTaint_To_resource_DeviceTaint(in *resourcev1alpha3.DeviceTaint, out *resource.DeviceTaint, s conversion.Scope) error {
return autoConvert_v1alpha3_DeviceTaint_To_resource_DeviceTaint(in, out, s)
}
func autoConvert_resource_DeviceTaint_To_v1alpha3_DeviceTaint(in *resource.DeviceTaint, out *resourcev1alpha3.DeviceTaint, s conversion.Scope) error {
out.Key = in.Key
out.Value = in.Value
out.Effect = resourcev1alpha3.DeviceTaintEffect(in.Effect)
out.TimeAdded = (*v1.Time)(unsafe.Pointer(in.TimeAdded))
return nil
}
// Convert_resource_DeviceTaint_To_v1alpha3_DeviceTaint is an autogenerated conversion function.
func Convert_resource_DeviceTaint_To_v1alpha3_DeviceTaint(in *resource.DeviceTaint, out *resourcev1alpha3.DeviceTaint, s conversion.Scope) error {
return autoConvert_resource_DeviceTaint_To_v1alpha3_DeviceTaint(in, out, s)
}
func autoConvert_v1alpha3_DeviceTaintRule_To_resource_DeviceTaintRule(in *resourcev1alpha3.DeviceTaintRule, out *resource.DeviceTaintRule, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v1alpha3_DeviceTaintRuleSpec_To_resource_DeviceTaintRuleSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha3_DeviceTaintRule_To_resource_DeviceTaintRule is an autogenerated conversion function.
func Convert_v1alpha3_DeviceTaintRule_To_resource_DeviceTaintRule(in *resourcev1alpha3.DeviceTaintRule, out *resource.DeviceTaintRule, s conversion.Scope) error {
return autoConvert_v1alpha3_DeviceTaintRule_To_resource_DeviceTaintRule(in, out, s)
}
func autoConvert_resource_DeviceTaintRule_To_v1alpha3_DeviceTaintRule(in *resource.DeviceTaintRule, out *resourcev1alpha3.DeviceTaintRule, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_resource_DeviceTaintRuleSpec_To_v1alpha3_DeviceTaintRuleSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_resource_DeviceTaintRule_To_v1alpha3_DeviceTaintRule is an autogenerated conversion function.
func Convert_resource_DeviceTaintRule_To_v1alpha3_DeviceTaintRule(in *resource.DeviceTaintRule, out *resourcev1alpha3.DeviceTaintRule, s conversion.Scope) error {
return autoConvert_resource_DeviceTaintRule_To_v1alpha3_DeviceTaintRule(in, out, s)
}
func autoConvert_v1alpha3_DeviceTaintRuleList_To_resource_DeviceTaintRuleList(in *resourcev1alpha3.DeviceTaintRuleList, out *resource.DeviceTaintRuleList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]resource.DeviceTaintRule)(unsafe.Pointer(&in.Items))
return nil
}
// Convert_v1alpha3_DeviceTaintRuleList_To_resource_DeviceTaintRuleList is an autogenerated conversion function.
func Convert_v1alpha3_DeviceTaintRuleList_To_resource_DeviceTaintRuleList(in *resourcev1alpha3.DeviceTaintRuleList, out *resource.DeviceTaintRuleList, s conversion.Scope) error {
return autoConvert_v1alpha3_DeviceTaintRuleList_To_resource_DeviceTaintRuleList(in, out, s)
}
func autoConvert_resource_DeviceTaintRuleList_To_v1alpha3_DeviceTaintRuleList(in *resource.DeviceTaintRuleList, out *resourcev1alpha3.DeviceTaintRuleList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]resourcev1alpha3.DeviceTaintRule)(unsafe.Pointer(&in.Items))
return nil
}
// Convert_resource_DeviceTaintRuleList_To_v1alpha3_DeviceTaintRuleList is an autogenerated conversion function.
func Convert_resource_DeviceTaintRuleList_To_v1alpha3_DeviceTaintRuleList(in *resource.DeviceTaintRuleList, out *resourcev1alpha3.DeviceTaintRuleList, s conversion.Scope) error {
return autoConvert_resource_DeviceTaintRuleList_To_v1alpha3_DeviceTaintRuleList(in, out, s)
}
func autoConvert_v1alpha3_DeviceTaintRuleSpec_To_resource_DeviceTaintRuleSpec(in *resourcev1alpha3.DeviceTaintRuleSpec, out *resource.DeviceTaintRuleSpec, s conversion.Scope) error {
out.DeviceSelector = (*resource.DeviceTaintSelector)(unsafe.Pointer(in.DeviceSelector))
if err := Convert_v1alpha3_DeviceTaint_To_resource_DeviceTaint(&in.Taint, &out.Taint, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha3_DeviceTaintRuleSpec_To_resource_DeviceTaintRuleSpec is an autogenerated conversion function.
func Convert_v1alpha3_DeviceTaintRuleSpec_To_resource_DeviceTaintRuleSpec(in *resourcev1alpha3.DeviceTaintRuleSpec, out *resource.DeviceTaintRuleSpec, s conversion.Scope) error {
return autoConvert_v1alpha3_DeviceTaintRuleSpec_To_resource_DeviceTaintRuleSpec(in, out, s)
}
func autoConvert_resource_DeviceTaintRuleSpec_To_v1alpha3_DeviceTaintRuleSpec(in *resource.DeviceTaintRuleSpec, out *resourcev1alpha3.DeviceTaintRuleSpec, s conversion.Scope) error {
out.DeviceSelector = (*resourcev1alpha3.DeviceTaintSelector)(unsafe.Pointer(in.DeviceSelector))
if err := Convert_resource_DeviceTaint_To_v1alpha3_DeviceTaint(&in.Taint, &out.Taint, s); err != nil {
return err
}
return nil
}
// Convert_resource_DeviceTaintRuleSpec_To_v1alpha3_DeviceTaintRuleSpec is an autogenerated conversion function.
func Convert_resource_DeviceTaintRuleSpec_To_v1alpha3_DeviceTaintRuleSpec(in *resource.DeviceTaintRuleSpec, out *resourcev1alpha3.DeviceTaintRuleSpec, s conversion.Scope) error {
return autoConvert_resource_DeviceTaintRuleSpec_To_v1alpha3_DeviceTaintRuleSpec(in, out, s)
}
func autoConvert_v1alpha3_DeviceTaintSelector_To_resource_DeviceTaintSelector(in *resourcev1alpha3.DeviceTaintSelector, out *resource.DeviceTaintSelector, s conversion.Scope) error {
out.DeviceClassName = (*string)(unsafe.Pointer(in.DeviceClassName))
out.Driver = (*string)(unsafe.Pointer(in.Driver))
out.Pool = (*string)(unsafe.Pointer(in.Pool))
out.Device = (*string)(unsafe.Pointer(in.Device))
out.Selectors = *(*[]resource.DeviceSelector)(unsafe.Pointer(&in.Selectors))
return nil
}
// Convert_v1alpha3_DeviceTaintSelector_To_resource_DeviceTaintSelector is an autogenerated conversion function.
func Convert_v1alpha3_DeviceTaintSelector_To_resource_DeviceTaintSelector(in *resourcev1alpha3.DeviceTaintSelector, out *resource.DeviceTaintSelector, s conversion.Scope) error {
return autoConvert_v1alpha3_DeviceTaintSelector_To_resource_DeviceTaintSelector(in, out, s)
}
func autoConvert_resource_DeviceTaintSelector_To_v1alpha3_DeviceTaintSelector(in *resource.DeviceTaintSelector, out *resourcev1alpha3.DeviceTaintSelector, s conversion.Scope) error {
out.DeviceClassName = (*string)(unsafe.Pointer(in.DeviceClassName))
out.Driver = (*string)(unsafe.Pointer(in.Driver))
out.Pool = (*string)(unsafe.Pointer(in.Pool))
out.Device = (*string)(unsafe.Pointer(in.Device))
out.Selectors = *(*[]resourcev1alpha3.DeviceSelector)(unsafe.Pointer(&in.Selectors))
return nil
}
// Convert_resource_DeviceTaintSelector_To_v1alpha3_DeviceTaintSelector is an autogenerated conversion function.
func Convert_resource_DeviceTaintSelector_To_v1alpha3_DeviceTaintSelector(in *resource.DeviceTaintSelector, out *resourcev1alpha3.DeviceTaintSelector, s conversion.Scope) error {
return autoConvert_resource_DeviceTaintSelector_To_v1alpha3_DeviceTaintSelector(in, out, s)
}
func autoConvert_v1alpha3_DeviceToleration_To_resource_DeviceToleration(in *resourcev1alpha3.DeviceToleration, out *resource.DeviceToleration, s conversion.Scope) error {
out.Key = in.Key
out.Operator = resource.DeviceTolerationOperator(in.Operator)
out.Value = in.Value
out.Effect = resource.DeviceTaintEffect(in.Effect)
out.TolerationSeconds = (*int64)(unsafe.Pointer(in.TolerationSeconds))
return nil
}
// Convert_v1alpha3_DeviceToleration_To_resource_DeviceToleration is an autogenerated conversion function.
func Convert_v1alpha3_DeviceToleration_To_resource_DeviceToleration(in *resourcev1alpha3.DeviceToleration, out *resource.DeviceToleration, s conversion.Scope) error {
return autoConvert_v1alpha3_DeviceToleration_To_resource_DeviceToleration(in, out, s)
}
func autoConvert_resource_DeviceToleration_To_v1alpha3_DeviceToleration(in *resource.DeviceToleration, out *resourcev1alpha3.DeviceToleration, s conversion.Scope) error {
out.Key = in.Key
out.Operator = resourcev1alpha3.DeviceTolerationOperator(in.Operator)
out.Value = in.Value
out.Effect = resourcev1alpha3.DeviceTaintEffect(in.Effect)
out.TolerationSeconds = (*int64)(unsafe.Pointer(in.TolerationSeconds))
return nil
}
// Convert_resource_DeviceToleration_To_v1alpha3_DeviceToleration is an autogenerated conversion function.
func Convert_resource_DeviceToleration_To_v1alpha3_DeviceToleration(in *resource.DeviceToleration, out *resourcev1alpha3.DeviceToleration, s conversion.Scope) error {
return autoConvert_resource_DeviceToleration_To_v1alpha3_DeviceToleration(in, out, s)
}
func autoConvert_v1alpha3_NetworkDeviceData_To_resource_NetworkDeviceData(in *resourcev1alpha3.NetworkDeviceData, out *resource.NetworkDeviceData, s conversion.Scope) error {
out.InterfaceName = in.InterfaceName
out.IPs = *(*[]string)(unsafe.Pointer(&in.IPs))

View file

@ -30,6 +30,10 @@ import (
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.DeviceTaintRule{}, func(obj interface{}) { SetObjectDefaults_DeviceTaintRule(obj.(*resourcev1alpha3.DeviceTaintRule)) })
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.DeviceTaintRuleList{}, func(obj interface{}) {
SetObjectDefaults_DeviceTaintRuleList(obj.(*resourcev1alpha3.DeviceTaintRuleList))
})
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.ResourceClaim{}, func(obj interface{}) { SetObjectDefaults_ResourceClaim(obj.(*resourcev1alpha3.ResourceClaim)) })
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.ResourceClaimList{}, func(obj interface{}) { SetObjectDefaults_ResourceClaimList(obj.(*resourcev1alpha3.ResourceClaimList)) })
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.ResourceClaimTemplate{}, func(obj interface{}) {
@ -38,9 +42,22 @@ func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.ResourceClaimTemplateList{}, func(obj interface{}) {
SetObjectDefaults_ResourceClaimTemplateList(obj.(*resourcev1alpha3.ResourceClaimTemplateList))
})
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.ResourceSlice{}, func(obj interface{}) { SetObjectDefaults_ResourceSlice(obj.(*resourcev1alpha3.ResourceSlice)) })
scheme.AddTypeDefaultingFunc(&resourcev1alpha3.ResourceSliceList{}, func(obj interface{}) { SetObjectDefaults_ResourceSliceList(obj.(*resourcev1alpha3.ResourceSliceList)) })
return nil
}
func SetObjectDefaults_DeviceTaintRule(in *resourcev1alpha3.DeviceTaintRule) {
SetDefaults_DeviceTaint(&in.Spec.Taint)
}
func SetObjectDefaults_DeviceTaintRuleList(in *resourcev1alpha3.DeviceTaintRuleList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_DeviceTaintRule(a)
}
}
func SetObjectDefaults_ResourceClaim(in *resourcev1alpha3.ResourceClaim) {
for i := range in.Spec.Devices.Requests {
a := &in.Spec.Devices.Requests[i]
@ -48,6 +65,29 @@ func SetObjectDefaults_ResourceClaim(in *resourcev1alpha3.ResourceClaim) {
for j := range a.FirstAvailable {
b := &a.FirstAvailable[j]
SetDefaults_DeviceSubRequest(b)
for k := range b.Tolerations {
c := &b.Tolerations[k]
if c.Operator == "" {
c.Operator = "Equal"
}
}
}
for j := range a.Tolerations {
b := &a.Tolerations[j]
if b.Operator == "" {
b.Operator = "Equal"
}
}
}
if in.Status.Allocation != nil {
for i := range in.Status.Allocation.Devices.Results {
a := &in.Status.Allocation.Devices.Results[i]
for j := range a.Tolerations {
b := &a.Tolerations[j]
if b.Operator == "" {
b.Operator = "Equal"
}
}
}
}
}
@ -66,6 +106,18 @@ func SetObjectDefaults_ResourceClaimTemplate(in *resourcev1alpha3.ResourceClaimT
for j := range a.FirstAvailable {
b := &a.FirstAvailable[j]
SetDefaults_DeviceSubRequest(b)
for k := range b.Tolerations {
c := &b.Tolerations[k]
if c.Operator == "" {
c.Operator = "Equal"
}
}
}
for j := range a.Tolerations {
b := &a.Tolerations[j]
if b.Operator == "" {
b.Operator = "Equal"
}
}
}
}
@ -76,3 +128,22 @@ func SetObjectDefaults_ResourceClaimTemplateList(in *resourcev1alpha3.ResourceCl
SetObjectDefaults_ResourceClaimTemplate(a)
}
}
func SetObjectDefaults_ResourceSlice(in *resourcev1alpha3.ResourceSlice) {
for i := range in.Spec.Devices {
a := &in.Spec.Devices[i]
if a.Basic != nil {
for j := range a.Basic.Taints {
b := &a.Basic.Taints[j]
SetDefaults_DeviceTaint(b)
}
}
}
}
func SetObjectDefaults_ResourceSliceList(in *resourcev1alpha3.ResourceSliceList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_ResourceSlice(a)
}
}

View file

@ -17,7 +17,10 @@ limitations under the License.
package v1beta1
import (
"time"
resourceapi "k8s.io/api/resource/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@ -50,3 +53,9 @@ func SetDefaults_DeviceSubRequest(obj *resourceapi.DeviceSubRequest) {
obj.Count = 1
}
}
func SetDefaults_DeviceTaint(obj *resourceapi.DeviceTaint) {
if obj.TimeAdded == nil {
obj.TimeAdded = &metav1.Time{Time: time.Now().Truncate(time.Second)}
}
}

View file

@ -19,11 +19,14 @@ package v1beta1_test
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
v1beta1 "k8s.io/api/resource/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
// ensure types are installed
"k8s.io/kubernetes/pkg/api/legacyscheme"
@ -134,6 +137,29 @@ func TestSetDefaultAllocationModeWithSubRequests(t *testing.T) {
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].FirstAvailable[1].Count)
}
func TestSetDefaultDeviceTaint(t *testing.T) {
slice := &v1beta1.ResourceSlice{
Spec: v1beta1.ResourceSliceSpec{
Devices: []v1beta1.Device{{
Name: "device-0",
Basic: &v1beta1.BasicDevice{
Taints: []v1beta1.DeviceTaint{{}},
},
}},
},
}
// fields should be defaulted
output := roundTrip(t, slice).(*v1beta1.ResourceSlice)
assert.WithinDuration(t, time.Now(), ptr.Deref(output.Spec.Devices[0].Basic.Taints[0].TimeAdded, metav1.Time{}).Time, time.Minute /* allow for some processing delay */, "time added default")
// field should not change
timeAdded, _ := time.ParseInLocation(time.RFC3339, "2006-01-02T15:04:05Z", time.UTC)
slice.Spec.Devices[0].Basic.Taints[0].TimeAdded = &metav1.Time{Time: timeAdded}
output = roundTrip(t, slice).(*v1beta1.ResourceSlice)
assert.WithinDuration(t, timeAdded, ptr.Deref(output.Spec.Devices[0].Basic.Taints[0].TimeAdded, metav1.Time{}).Time, 0 /* semantically the same, different time zone allowed */, "time added fixed")
}
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
codec := legacyscheme.Codecs.LegacyCodec(v1beta1.SchemeGroupVersion)
data, err := runtime.Encode(codec, obj)

View file

@ -251,6 +251,26 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1beta1.DeviceTaint)(nil), (*resource.DeviceTaint)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_DeviceTaint_To_resource_DeviceTaint(a.(*resourcev1beta1.DeviceTaint), b.(*resource.DeviceTaint), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceTaint)(nil), (*resourcev1beta1.DeviceTaint)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceTaint_To_v1beta1_DeviceTaint(a.(*resource.DeviceTaint), b.(*resourcev1beta1.DeviceTaint), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1beta1.DeviceToleration)(nil), (*resource.DeviceToleration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_DeviceToleration_To_resource_DeviceToleration(a.(*resourcev1beta1.DeviceToleration), b.(*resource.DeviceToleration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resource.DeviceToleration)(nil), (*resourcev1beta1.DeviceToleration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_resource_DeviceToleration_To_v1beta1_DeviceToleration(a.(*resource.DeviceToleration), b.(*resourcev1beta1.DeviceToleration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*resourcev1beta1.NetworkDeviceData)(nil), (*resource.NetworkDeviceData)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1beta1_NetworkDeviceData_To_resource_NetworkDeviceData(a.(*resourcev1beta1.NetworkDeviceData), b.(*resource.NetworkDeviceData), scope)
}); err != nil {
@ -453,6 +473,7 @@ func Convert_resource_AllocationResult_To_v1beta1_AllocationResult(in *resource.
func autoConvert_v1beta1_BasicDevice_To_resource_BasicDevice(in *resourcev1beta1.BasicDevice, out *resource.BasicDevice, s conversion.Scope) error {
out.Attributes = *(*map[resource.QualifiedName]resource.DeviceAttribute)(unsafe.Pointer(&in.Attributes))
out.Capacity = *(*map[resource.QualifiedName]resource.DeviceCapacity)(unsafe.Pointer(&in.Capacity))
out.Taints = *(*[]resource.DeviceTaint)(unsafe.Pointer(&in.Taints))
return nil
}
@ -464,6 +485,7 @@ func Convert_v1beta1_BasicDevice_To_resource_BasicDevice(in *resourcev1beta1.Bas
func autoConvert_resource_BasicDevice_To_v1beta1_BasicDevice(in *resource.BasicDevice, out *resourcev1beta1.BasicDevice, s conversion.Scope) error {
out.Attributes = *(*map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceAttribute)(unsafe.Pointer(&in.Attributes))
out.Capacity = *(*map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceCapacity)(unsafe.Pointer(&in.Capacity))
out.Taints = *(*[]resourcev1beta1.DeviceTaint)(unsafe.Pointer(&in.Taints))
return nil
}
@ -804,6 +826,7 @@ func autoConvert_v1beta1_DeviceRequest_To_resource_DeviceRequest(in *resourcev1b
out.Count = in.Count
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.FirstAvailable = *(*[]resource.DeviceSubRequest)(unsafe.Pointer(&in.FirstAvailable))
out.Tolerations = *(*[]resource.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -820,6 +843,7 @@ func autoConvert_resource_DeviceRequest_To_v1beta1_DeviceRequest(in *resource.De
out.Count = in.Count
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.FirstAvailable = *(*[]resourcev1beta1.DeviceSubRequest)(unsafe.Pointer(&in.FirstAvailable))
out.Tolerations = *(*[]resourcev1beta1.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -834,6 +858,7 @@ func autoConvert_v1beta1_DeviceRequestAllocationResult_To_resource_DeviceRequest
out.Pool = in.Pool
out.Device = in.Device
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.Tolerations = *(*[]resource.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -848,6 +873,7 @@ func autoConvert_resource_DeviceRequestAllocationResult_To_v1beta1_DeviceRequest
out.Pool = in.Pool
out.Device = in.Device
out.AdminAccess = (*bool)(unsafe.Pointer(in.AdminAccess))
out.Tolerations = *(*[]resourcev1beta1.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -882,6 +908,7 @@ func autoConvert_v1beta1_DeviceSubRequest_To_resource_DeviceSubRequest(in *resou
out.Selectors = *(*[]resource.DeviceSelector)(unsafe.Pointer(&in.Selectors))
out.AllocationMode = resource.DeviceAllocationMode(in.AllocationMode)
out.Count = in.Count
out.Tolerations = *(*[]resource.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -896,6 +923,7 @@ func autoConvert_resource_DeviceSubRequest_To_v1beta1_DeviceSubRequest(in *resou
out.Selectors = *(*[]resourcev1beta1.DeviceSelector)(unsafe.Pointer(&in.Selectors))
out.AllocationMode = resourcev1beta1.DeviceAllocationMode(in.AllocationMode)
out.Count = in.Count
out.Tolerations = *(*[]resourcev1beta1.DeviceToleration)(unsafe.Pointer(&in.Tolerations))
return nil
}
@ -904,6 +932,60 @@ func Convert_resource_DeviceSubRequest_To_v1beta1_DeviceSubRequest(in *resource.
return autoConvert_resource_DeviceSubRequest_To_v1beta1_DeviceSubRequest(in, out, s)
}
func autoConvert_v1beta1_DeviceTaint_To_resource_DeviceTaint(in *resourcev1beta1.DeviceTaint, out *resource.DeviceTaint, s conversion.Scope) error {
out.Key = in.Key
out.Value = in.Value
out.Effect = resource.DeviceTaintEffect(in.Effect)
out.TimeAdded = (*v1.Time)(unsafe.Pointer(in.TimeAdded))
return nil
}
// Convert_v1beta1_DeviceTaint_To_resource_DeviceTaint is an autogenerated conversion function.
func Convert_v1beta1_DeviceTaint_To_resource_DeviceTaint(in *resourcev1beta1.DeviceTaint, out *resource.DeviceTaint, s conversion.Scope) error {
return autoConvert_v1beta1_DeviceTaint_To_resource_DeviceTaint(in, out, s)
}
func autoConvert_resource_DeviceTaint_To_v1beta1_DeviceTaint(in *resource.DeviceTaint, out *resourcev1beta1.DeviceTaint, s conversion.Scope) error {
out.Key = in.Key
out.Value = in.Value
out.Effect = resourcev1beta1.DeviceTaintEffect(in.Effect)
out.TimeAdded = (*v1.Time)(unsafe.Pointer(in.TimeAdded))
return nil
}
// Convert_resource_DeviceTaint_To_v1beta1_DeviceTaint is an autogenerated conversion function.
func Convert_resource_DeviceTaint_To_v1beta1_DeviceTaint(in *resource.DeviceTaint, out *resourcev1beta1.DeviceTaint, s conversion.Scope) error {
return autoConvert_resource_DeviceTaint_To_v1beta1_DeviceTaint(in, out, s)
}
func autoConvert_v1beta1_DeviceToleration_To_resource_DeviceToleration(in *resourcev1beta1.DeviceToleration, out *resource.DeviceToleration, s conversion.Scope) error {
out.Key = in.Key
out.Operator = resource.DeviceTolerationOperator(in.Operator)
out.Value = in.Value
out.Effect = resource.DeviceTaintEffect(in.Effect)
out.TolerationSeconds = (*int64)(unsafe.Pointer(in.TolerationSeconds))
return nil
}
// Convert_v1beta1_DeviceToleration_To_resource_DeviceToleration is an autogenerated conversion function.
func Convert_v1beta1_DeviceToleration_To_resource_DeviceToleration(in *resourcev1beta1.DeviceToleration, out *resource.DeviceToleration, s conversion.Scope) error {
return autoConvert_v1beta1_DeviceToleration_To_resource_DeviceToleration(in, out, s)
}
func autoConvert_resource_DeviceToleration_To_v1beta1_DeviceToleration(in *resource.DeviceToleration, out *resourcev1beta1.DeviceToleration, s conversion.Scope) error {
out.Key = in.Key
out.Operator = resourcev1beta1.DeviceTolerationOperator(in.Operator)
out.Value = in.Value
out.Effect = resourcev1beta1.DeviceTaintEffect(in.Effect)
out.TolerationSeconds = (*int64)(unsafe.Pointer(in.TolerationSeconds))
return nil
}
// Convert_resource_DeviceToleration_To_v1beta1_DeviceToleration is an autogenerated conversion function.
func Convert_resource_DeviceToleration_To_v1beta1_DeviceToleration(in *resource.DeviceToleration, out *resourcev1beta1.DeviceToleration, s conversion.Scope) error {
return autoConvert_resource_DeviceToleration_To_v1beta1_DeviceToleration(in, out, s)
}
func autoConvert_v1beta1_NetworkDeviceData_To_resource_NetworkDeviceData(in *resourcev1beta1.NetworkDeviceData, out *resource.NetworkDeviceData, s conversion.Scope) error {
out.InterfaceName = in.InterfaceName
out.IPs = *(*[]string)(unsafe.Pointer(&in.IPs))

View file

@ -38,6 +38,8 @@ func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&resourcev1beta1.ResourceClaimTemplateList{}, func(obj interface{}) {
SetObjectDefaults_ResourceClaimTemplateList(obj.(*resourcev1beta1.ResourceClaimTemplateList))
})
scheme.AddTypeDefaultingFunc(&resourcev1beta1.ResourceSlice{}, func(obj interface{}) { SetObjectDefaults_ResourceSlice(obj.(*resourcev1beta1.ResourceSlice)) })
scheme.AddTypeDefaultingFunc(&resourcev1beta1.ResourceSliceList{}, func(obj interface{}) { SetObjectDefaults_ResourceSliceList(obj.(*resourcev1beta1.ResourceSliceList)) })
return nil
}
@ -48,6 +50,29 @@ func SetObjectDefaults_ResourceClaim(in *resourcev1beta1.ResourceClaim) {
for j := range a.FirstAvailable {
b := &a.FirstAvailable[j]
SetDefaults_DeviceSubRequest(b)
for k := range b.Tolerations {
c := &b.Tolerations[k]
if c.Operator == "" {
c.Operator = "Equal"
}
}
}
for j := range a.Tolerations {
b := &a.Tolerations[j]
if b.Operator == "" {
b.Operator = "Equal"
}
}
}
if in.Status.Allocation != nil {
for i := range in.Status.Allocation.Devices.Results {
a := &in.Status.Allocation.Devices.Results[i]
for j := range a.Tolerations {
b := &a.Tolerations[j]
if b.Operator == "" {
b.Operator = "Equal"
}
}
}
}
}
@ -66,6 +91,18 @@ func SetObjectDefaults_ResourceClaimTemplate(in *resourcev1beta1.ResourceClaimTe
for j := range a.FirstAvailable {
b := &a.FirstAvailable[j]
SetDefaults_DeviceSubRequest(b)
for k := range b.Tolerations {
c := &b.Tolerations[k]
if c.Operator == "" {
c.Operator = "Equal"
}
}
}
for j := range a.Tolerations {
b := &a.Tolerations[j]
if b.Operator == "" {
b.Operator = "Equal"
}
}
}
}
@ -76,3 +113,22 @@ func SetObjectDefaults_ResourceClaimTemplateList(in *resourcev1beta1.ResourceCla
SetObjectDefaults_ResourceClaimTemplate(a)
}
}
func SetObjectDefaults_ResourceSlice(in *resourcev1beta1.ResourceSlice) {
for i := range in.Spec.Devices {
a := &in.Spec.Devices[i]
if a.Basic != nil {
for j := range a.Basic.Taints {
b := &a.Basic.Taints[j]
SetDefaults_DeviceTaint(b)
}
}
}
}
func SetObjectDefaults_ResourceSliceList(in *resourcev1beta1.ResourceSliceList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_ResourceSlice(a)
}
}

View file

@ -199,6 +199,10 @@ func validateDeviceRequest(request resource.DeviceRequest, fldPath *field.Path,
},
fldPath.Child("firstAvailable"))...)
}
for i, toleration := range request.Tolerations {
allErrs = append(allErrs, validateDeviceToleration(toleration, fldPath.Child("tolerations").Index(i))...)
}
return allErrs
}
@ -207,6 +211,9 @@ func validateDeviceSubRequest(subRequest resource.DeviceSubRequest, fldPath *fie
allErrs = append(allErrs, validateDeviceClass(subRequest.DeviceClassName, fldPath.Child("deviceClassName"))...)
allErrs = append(allErrs, validateSelectorSlice(subRequest.Selectors, fldPath.Child("selectors"), stored)...)
allErrs = append(allErrs, validateDeviceAllocationMode(subRequest.AllocationMode, subRequest.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...)
for i, toleration := range subRequest.Tolerations {
allErrs = append(allErrs, validateDeviceToleration(toleration, fldPath.Child("tolerations").Index(i))...)
}
return allErrs
}
@ -642,6 +649,9 @@ func validateBasicDevice(device resource.BasicDevice, fldPath *field.Path) field
if combinedLen, max := len(device.Attributes)+len(device.Capacity), resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice; combinedLen > max {
allErrs = append(allErrs, field.Invalid(fldPath, combinedLen, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", max)))
}
for i, taint := range device.Taints {
allErrs = append(allErrs, validateDeviceTaint(taint, fldPath.Child("taints").Index(i))...)
}
return allErrs
}
@ -675,19 +685,12 @@ func validateDeviceAttribute(attribute resource.DeviceAttribute, fldPath *field.
numFields++
}
if attribute.StringValue != nil {
if len(*attribute.StringValue) > resource.DeviceAttributeMaxValueLength {
allErrs = append(allErrs, field.TooLong(fldPath.Child("string"), "" /*unused*/, resource.DeviceAttributeMaxValueLength))
}
numFields++
allErrs = append(allErrs, validateDeviceAttributeStringValue(attribute.StringValue, fldPath.Child("string"))...)
}
if attribute.VersionValue != nil {
numFields++
if !semverRe.MatchString(*attribute.VersionValue) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
}
if len(*attribute.VersionValue) > resource.DeviceAttributeMaxValueLength {
allErrs = append(allErrs, field.TooLong(fldPath.Child("version"), "" /*unused*/, resource.DeviceAttributeMaxValueLength))
}
allErrs = append(allErrs, validateDeviceAttributeVersionValue(attribute.VersionValue, fldPath.Child("version"))...)
}
switch numFields {
@ -701,6 +704,25 @@ func validateDeviceAttribute(attribute resource.DeviceAttribute, fldPath *field.
return allErrs
}
func validateDeviceAttributeStringValue(value *string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(*value) > resource.DeviceAttributeMaxValueLength {
allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.DeviceAttributeMaxValueLength))
}
return allErrs
}
func validateDeviceAttributeVersionValue(value *string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if !semverRe.MatchString(*value) {
allErrs = append(allErrs, field.Invalid(fldPath, *value, "must be a string compatible with semver.org spec 2.0.0"))
}
if len(*value) > resource.DeviceAttributeMaxValueLength {
allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.DeviceAttributeMaxValueLength))
}
return allErrs
}
func validateDeviceCapacity(capacity resource.DeviceCapacity, fldPath *field.Path) field.ErrorList {
// Any parsed quantity is valid.
return nil
@ -893,3 +915,123 @@ func validateNetworkDeviceData(networkDeviceData *resource.NetworkDeviceData, fl
}, stringKey, fldPath.Child("ips"))...)
return allErrs
}
// ValidateDeviceTaintRule tests if a DeviceTaintRule object is valid.
func ValidateDeviceTaintRule(deviceTaint *resource.DeviceTaintRule) field.ErrorList {
allErrs := corevalidation.ValidateObjectMeta(&deviceTaint.ObjectMeta, false, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
allErrs = append(allErrs, validateDeviceTaintRuleSpec(&deviceTaint.Spec, nil, field.NewPath("spec"))...)
return allErrs
}
// ValidateDeviceTaintRuleUpdate tests if a DeviceTaintRule update is valid.
func ValidateDeviceTaintRuleUpdate(deviceTaint, oldDeviceTaint *resource.DeviceTaintRule) field.ErrorList {
allErrs := corevalidation.ValidateObjectMetaUpdate(&deviceTaint.ObjectMeta, &oldDeviceTaint.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, validateDeviceTaintRuleSpec(&deviceTaint.Spec, &oldDeviceTaint.Spec, field.NewPath("spec"))...)
return allErrs
}
func validateDeviceTaintRuleSpec(spec, oldSpec *resource.DeviceTaintRuleSpec, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
var oldFilter *resource.DeviceTaintSelector
if oldSpec != nil {
oldFilter = oldSpec.DeviceSelector // +k8s:verify-mutation:reason=clone
}
allErrs = append(allErrs, validateDeviceTaintSelector(spec.DeviceSelector, oldFilter, fldPath.Child("deviceSelector"))...)
allErrs = append(allErrs, validateDeviceTaint(spec.Taint, fldPath.Child("taint"))...)
return allErrs
}
func validateDeviceTaintSelector(filter, oldFilter *resource.DeviceTaintSelector, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if filter == nil {
return allErrs
}
if filter.DeviceClassName != nil {
allErrs = append(allErrs, validateDeviceClassName(*filter.DeviceClassName, fldPath.Child("deviceClassName"))...)
}
if filter.Driver != nil {
allErrs = append(allErrs, validateDriverName(*filter.Driver, fldPath.Child("driver"))...)
}
if filter.Pool != nil {
allErrs = append(allErrs, validatePoolName(*filter.Pool, fldPath.Child("pool"))...)
}
if filter.Device != nil {
allErrs = append(allErrs, validateDeviceName(*filter.Device, fldPath.Child("device"))...)
}
// If the selectors are exactly as before, we treat the CEL expressions as "stored".
// Any change, including merely reordering selectors, triggers validation as new
// expressions.
stored := false
if oldFilter != nil {
stored = apiequality.Semantic.DeepEqual(filter.Selectors, oldFilter.Selectors)
}
allErrs = append(allErrs, validateSlice(filter.Selectors, resource.DeviceSelectorsMaxSize,
func(selector resource.DeviceSelector, fldPath *field.Path) field.ErrorList {
return validateSelector(selector, fldPath, stored)
},
fldPath.Child("selectors"))...)
return allErrs
}
var validDeviceTolerationOperators = []resource.DeviceTolerationOperator{resource.DeviceTolerationOpEqual, resource.DeviceTolerationOpExists}
var validDeviceTaintEffects = sets.New(resource.DeviceTaintEffectNoSchedule, resource.DeviceTaintEffectNoExecute)
func validateDeviceTaint(taint resource.DeviceTaint, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, metav1validation.ValidateLabelName(taint.Key, fldPath.Child("key"))...) // Includes checking for non-empty.
if taint.Value != "" {
allErrs = append(allErrs, validateLabelValue(taint.Value, fldPath.Child("value"))...)
}
switch {
case taint.Effect == "":
allErrs = append(allErrs, field.Required(fldPath.Child("effect"), "")) // Required in a taint.
case !validDeviceTaintEffects.Has(taint.Effect):
allErrs = append(allErrs, field.NotSupported(fldPath.Child("effect"), taint.Effect, sets.List(validDeviceTaintEffects)))
}
return allErrs
}
func validateDeviceToleration(toleration resource.DeviceToleration, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if toleration.Key != "" {
allErrs = append(allErrs, metav1validation.ValidateLabelName(toleration.Key, fldPath.Child("key"))...)
}
switch toleration.Operator {
case resource.DeviceTolerationOpExists:
if toleration.Value != "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), toleration.Value, "must be empty for operator `Exists`"))
}
case resource.DeviceTolerationOpEqual:
allErrs = append(allErrs, validateLabelValue(toleration.Value, fldPath.Child("value"))...)
case "":
allErrs = append(allErrs, field.Required(fldPath.Child("operator"), ""))
default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("operator"), toleration.Operator, validDeviceTolerationOperators))
}
switch {
case toleration.Effect == "":
// Optional in a toleration.
case !validDeviceTaintEffects.Has(toleration.Effect):
allErrs = append(allErrs, field.NotSupported(fldPath.Child("effect"), toleration.Effect, sets.List(validDeviceTaintEffects)))
}
return allErrs
}
func validateLabelValue(value string, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
// There's no metav1validation.ValidateLabelValue.
for _, msg := range validation.IsValidLabelValue(value) {
allErrs = append(allErrs, field.Invalid(fldPath, value, msg))
}
return allErrs
}

View file

@ -0,0 +1,351 @@
/*
Copyright 2025 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 validation
import (
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
)
func testDeviceTaintRule(name string, spec resourceapi.DeviceTaintRuleSpec) *resourceapi.DeviceTaintRule {
return &resourceapi.DeviceTaintRule{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: *spec.DeepCopy(),
}
}
var validDeviceTaintRuleSpec = resourceapi.DeviceTaintRuleSpec{
DeviceSelector: &resourceapi.DeviceTaintSelector{
DeviceClassName: ptr.To(goodName),
Driver: ptr.To("test.example.com"),
Pool: ptr.To(goodName),
Device: ptr.To(goodName),
},
Taint: resourceapi.DeviceTaint{
Key: "example.com/taint",
Value: "tainted",
Effect: resourceapi.DeviceTaintEffectNoSchedule,
},
}
func TestValidateDeviceTaint(t *testing.T) {
goodName := "foo"
now := metav1.Now()
badName := "!@#$%^"
badValue := "spaces not allowed"
scenarios := map[string]struct {
taintRule *resourceapi.DeviceTaintRule
wantFailures field.ErrorList
}{
"good": {
taintRule: testDeviceTaintRule(goodName, validDeviceTaintRuleSpec),
},
"missing-name": {
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
taintRule: testDeviceTaintRule("", validDeviceTaintRuleSpec),
},
"bad-name": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
taintRule: testDeviceTaintRule(badName, validDeviceTaintRuleSpec),
},
"generate-name": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.GenerateName = "pvc-"
return taintRule
}(),
},
"uid": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
return taintRule
}(),
},
"resource-version": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.ResourceVersion = "1"
return taintRule
}(),
},
"generation": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Generation = 100
return taintRule
}(),
},
"creation-timestamp": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.CreationTimestamp = now
return taintRule
}(),
},
"deletion-grace-period-seconds": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.DeletionGracePeriodSeconds = ptr.To(int64(10))
return taintRule
}(),
},
"owner-references": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "pod",
Name: "foo",
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
},
}
return taintRule
}(),
},
"finalizers": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Finalizers = []string{
"example.com/foo",
}
return taintRule
}(),
},
"managed-fields": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.ManagedFields = []metav1.ManagedFieldsEntry{
{
FieldsType: "FieldsV1",
Operation: "Apply",
APIVersion: "apps/v1",
Manager: "foo",
},
}
return taintRule
}(),
},
"good-labels": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Labels = map[string]string{
"apps.kubernetes.io/name": "test",
}
return taintRule
}(),
},
"bad-labels": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Labels = map[string]string{
"hello-world": badValue,
}
return taintRule
}(),
},
"good-annotations": {
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Annotations = map[string]string{
"foo": "bar",
}
return taintRule
}(),
},
"bad-annotations": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Annotations = map[string]string{
badName: "hello world",
}
return taintRule
}(),
},
"bad-class": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "deviceSelector", "deviceClassName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Spec.DeviceSelector.DeviceClassName = ptr.To(badName)
return taintRule
}(),
},
"bad-driver": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "deviceSelector", "driver"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Spec.DeviceSelector.Driver = ptr.To(badName)
return taintRule
}(),
},
"bad-pool": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "deviceSelector", "pool"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Spec.DeviceSelector.Pool = ptr.To(badName)
return taintRule
}(),
},
"bad-device": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "deviceSelector", "device"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Spec.DeviceSelector.Device = ptr.To(badName)
return taintRule
}(),
},
"CEL-compile-errors": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "deviceSelector", "selectors").Index(1).Child("cel", "expression"), `device.attributes[true].someBoolean`, "compilation failed: ERROR: <input>:1:18: found no matching overload for '_[_]' applied to '(map(string, map(string, any)), bool)'\n | device.attributes[true].someBoolean\n | .................^"),
},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
taintRule.Spec.DeviceSelector.Selectors = []resourceapi.DeviceSelector{
{
// Good selector.
CEL: &resourceapi.CELDeviceSelector{
Expression: `device.driver == "dra.example.com"`,
},
},
{
// Bad selector.
CEL: &resourceapi.CELDeviceSelector{
Expression: `device.attributes[true].someBoolean`,
},
},
}
return taintRule
}(),
},
"CEL-length": {
wantFailures: field.ErrorList{
field.TooLong(field.NewPath("spec", "deviceSelector", "selectors").Index(1).Child("cel", "expression"), "" /*unused*/, resourceapi.CELSelectorExpressionMaxLength),
},
taintRule: func() *resourceapi.DeviceTaintRule {
taintRule := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
expression := `device.driver == ""`
taintRule.Spec.DeviceSelector.Selectors = []resourceapi.DeviceSelector{
{
// Good selector.
CEL: &resourceapi.CELDeviceSelector{
Expression: strings.ReplaceAll(expression, `""`, `"`+strings.Repeat("x", resourceapi.CELSelectorExpressionMaxLength-len(expression))+`"`),
},
},
{
// Too long by one selector.
CEL: &resourceapi.CELDeviceSelector{
Expression: strings.ReplaceAll(expression, `""`, `"`+strings.Repeat("x", resourceapi.CELSelectorExpressionMaxLength-len(expression)+1)+`"`),
},
},
}
return taintRule
}(),
},
"CEL-cost": {
wantFailures: field.ErrorList{
field.Forbidden(field.NewPath("spec", "deviceSelector", "selectors").Index(0).Child("cel", "expression"), "too complex, exceeds cost limit"),
},
taintRule: func() *resourceapi.DeviceTaintRule {
claim := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
claim.Spec.DeviceSelector.Selectors = []resourceapi.DeviceSelector{
{
CEL: &resourceapi.CELDeviceSelector{
// From https://github.com/kubernetes/kubernetes/blob/50fc400f178d2078d0ca46aee955ee26375fc437/test/integration/apiserver/cel/validatingadmissionpolicy_test.go#L2150.
Expression: `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(x, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(y, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z3, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].all(z5, int('1'.find('[0-9]*')) < 100)))))))`,
},
},
}
return claim
}(),
},
"valid-taint": {
taintRule: func() *resourceapi.DeviceTaintRule {
claim := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
claim.Spec.Taint = resourceapi.DeviceTaint{
Key: goodName,
Value: goodName,
Effect: resourceapi.DeviceTaintEffectNoExecute,
}
return claim
}(),
},
"invalid-taint": {
wantFailures: field.ErrorList{
field.Required(field.NewPath("spec", "taint", "effect"), ""),
},
taintRule: func() *resourceapi.DeviceTaintRule {
claim := testDeviceTaintRule(goodName, validDeviceTaintRuleSpec)
claim.Spec.Taint = resourceapi.DeviceTaint{
// Minimal test. Full coverage of validateDeviceTaint is in ResourceSlice test.
Key: goodName,
Value: goodName,
}
return claim
}(),
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
errs := ValidateDeviceTaintRule(scenario.taintRule)
assertFailures(t, scenario.wantFailures, errs)
})
}
}
func TestValidateDeviceTaintUpdate(t *testing.T) {
name := "valid"
validTaintRule := testDeviceTaintRule(name, validDeviceTaintRuleSpec)
scenarios := map[string]struct {
old *resourceapi.DeviceTaintRule
update func(patch *resourceapi.DeviceTaintRule) *resourceapi.DeviceTaintRule
wantFailures field.ErrorList
}{
"valid-no-op-update": {
old: validTaintRule,
update: func(taintRule *resourceapi.DeviceTaintRule) *resourceapi.DeviceTaintRule { return taintRule },
},
"invalid-name-update": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
old: validTaintRule,
update: func(taintRule *resourceapi.DeviceTaintRule) *resourceapi.DeviceTaintRule {
taintRule.Name += "-update"
return taintRule
},
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
scenario.old.ResourceVersion = "1"
errs := ValidateDeviceTaintRuleUpdate(scenario.update(scenario.old.DeepCopy()), scenario.old)
assertFailures(t, scenario.wantFailures, errs)
})
}
}

View file

@ -735,6 +735,70 @@ func TestValidateClaim(t *testing.T) {
return claim
}(),
},
"tolerations": {
wantFailures: func() field.ErrorList {
var allErrs field.ErrorList
fldPath := field.NewPath("spec", "devices", "requests").Index(0).Child("firstAvailable").Index(0).Child("tolerations")
allErrs = append(allErrs,
field.Required(fldPath.Index(0).Child("operator"), ""),
)
fldPath = field.NewPath("spec", "devices", "requests").Index(1).Child("tolerations")
allErrs = append(allErrs,
field.Required(fldPath.Index(3).Child("operator"), ""),
field.NotSupported(fldPath.Index(4).Child("operator"), resource.DeviceTolerationOperator("some-other-op"), []resource.DeviceTolerationOperator{resource.DeviceTolerationOpEqual, resource.DeviceTolerationOpExists}),
field.Invalid(fldPath.Index(5).Child("key"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
field.Invalid(fldPath.Index(5).Child("value"), badName, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')"),
field.NotSupported(fldPath.Index(5).Child("effect"), resource.DeviceTaintEffect("some-other-effect"), []resource.DeviceTaintEffect{resource.DeviceTaintEffectNoExecute, resource.DeviceTaintEffectNoSchedule}),
)
return allErrs
}(),
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpecWithFirstAvailable)
claim.Spec.Devices.Requests[0].FirstAvailable[0].Tolerations = []resource.DeviceToleration{
{
// One invalid case to verify the field path.
// Full test for validateDeviceToleration is below.
Operator: "",
},
}
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *validClaimSpec.Devices.Requests[0].DeepCopy())
claim.Spec.Devices.Requests[1].Name += "-other"
claim.Spec.Devices.Requests[1].Tolerations = []resource.DeviceToleration{
{
// Minimal valid toleration: match all taints.
Operator: resource.DeviceTolerationOpExists,
},
{
Key: "example.com/taint",
Operator: resource.DeviceTolerationOpExists,
Effect: resource.DeviceTaintEffectNoExecute,
},
{
Key: "example.com/taint",
Operator: resource.DeviceTolerationOpEqual,
Value: "tainted",
Effect: resource.DeviceTaintEffectNoSchedule,
},
{
// Invalid, operator is required.
Operator: "",
},
{
Key: goodName,
Operator: "some-other-op",
},
{
Key: badName,
Operator: resource.DeviceTolerationOpEqual,
Value: badName,
Effect: "some-other-effect",
},
}
return claim
}(),
},
}
for name, scenario := range scenarios {
@ -1645,11 +1709,6 @@ func TestValidateClaimStatusUpdate(t *testing.T) {
scenario.oldClaim.ResourceVersion = "1"
errs := ValidateResourceClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
if name == "invalid-data-device-status-limits-feature-gate" {
fmt.Println(errs)
fmt.Println(scenario.wantFailures)
}
assertFailures(t, scenario.wantFailures, errs)
})
}

View file

@ -447,6 +447,47 @@ func TestValidateResourceSlice(t *testing.T) {
return slice
}(),
},
"taints": {
wantFailures: func() field.ErrorList {
fldPath := field.NewPath("spec", "devices").Index(0).Child("basic", "taints")
return field.ErrorList{
field.Invalid(fldPath.Index(2).Child("key"), "", "name part must be non-empty"),
field.Invalid(fldPath.Index(2).Child("key"), "", "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
field.Required(fldPath.Index(2).Child("effect"), ""),
field.Invalid(fldPath.Index(3).Child("key"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
field.Invalid(fldPath.Index(3).Child("value"), badName, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')"),
field.NotSupported(fldPath.Index(3).Child("effect"), resourceapi.DeviceTaintEffect("some-other-op"), []resourceapi.DeviceTaintEffect{resourceapi.DeviceTaintEffectNoExecute, resourceapi.DeviceTaintEffectNoSchedule}),
}
}(),
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 3)
slice.Spec.Devices[0].Basic.Taints = []resourceapi.DeviceTaint{
{
// Minimal valid taint.
Key: "example.com/taint",
Effect: resourceapi.DeviceTaintEffectNoExecute,
},
{
// Full valid taint, other key and effect.
Key: "taint",
Value: "tainted",
Effect: resourceapi.DeviceTaintEffectNoSchedule,
TimeAdded: ptr.To(metav1.Now()),
},
{
// Invalid, all empty!
},
{
// Invalid strings.
Key: badName,
Value: badName,
Effect: "some-other-op",
},
}
return slice
}(),
},
}
for name, scenario := range scenarios {

View file

@ -99,6 +99,13 @@ func (in *BasicDevice) DeepCopyInto(out *BasicDevice) {
(*out)[key] = *val.DeepCopy()
}
}
if in.Taints != nil {
in, out := &in.Taints, &out.Taints
*out = make([]DeviceTaint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -489,6 +496,13 @@ func (in *DeviceRequest) DeepCopyInto(out *DeviceRequest) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -510,6 +524,13 @@ func (in *DeviceRequestAllocationResult) DeepCopyInto(out *DeviceRequestAllocati
*out = new(bool)
**out = **in
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -554,6 +575,13 @@ func (in *DeviceSubRequest) DeepCopyInto(out *DeviceSubRequest) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -567,6 +595,172 @@ func (in *DeviceSubRequest) DeepCopy() *DeviceSubRequest {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaint) DeepCopyInto(out *DeviceTaint) {
*out = *in
if in.TimeAdded != nil {
in, out := &in.TimeAdded, &out.TimeAdded
*out = (*in).DeepCopy()
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaint.
func (in *DeviceTaint) DeepCopy() *DeviceTaint {
if in == nil {
return nil
}
out := new(DeviceTaint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintRule) DeepCopyInto(out *DeviceTaintRule) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintRule.
func (in *DeviceTaintRule) DeepCopy() *DeviceTaintRule {
if in == nil {
return nil
}
out := new(DeviceTaintRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DeviceTaintRule) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintRuleList) DeepCopyInto(out *DeviceTaintRuleList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DeviceTaintRule, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintRuleList.
func (in *DeviceTaintRuleList) DeepCopy() *DeviceTaintRuleList {
if in == nil {
return nil
}
out := new(DeviceTaintRuleList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DeviceTaintRuleList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintRuleSpec) DeepCopyInto(out *DeviceTaintRuleSpec) {
*out = *in
if in.DeviceSelector != nil {
in, out := &in.DeviceSelector, &out.DeviceSelector
*out = new(DeviceTaintSelector)
(*in).DeepCopyInto(*out)
}
in.Taint.DeepCopyInto(&out.Taint)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintRuleSpec.
func (in *DeviceTaintRuleSpec) DeepCopy() *DeviceTaintRuleSpec {
if in == nil {
return nil
}
out := new(DeviceTaintRuleSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintSelector) DeepCopyInto(out *DeviceTaintSelector) {
*out = *in
if in.DeviceClassName != nil {
in, out := &in.DeviceClassName, &out.DeviceClassName
*out = new(string)
**out = **in
}
if in.Driver != nil {
in, out := &in.Driver, &out.Driver
*out = new(string)
**out = **in
}
if in.Pool != nil {
in, out := &in.Pool, &out.Pool
*out = new(string)
**out = **in
}
if in.Device != nil {
in, out := &in.Device, &out.Device
*out = new(string)
**out = **in
}
if in.Selectors != nil {
in, out := &in.Selectors, &out.Selectors
*out = make([]DeviceSelector, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintSelector.
func (in *DeviceTaintSelector) DeepCopy() *DeviceTaintSelector {
if in == nil {
return nil
}
out := new(DeviceTaintSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceToleration) DeepCopyInto(out *DeviceToleration) {
*out = *in
if in.TolerationSeconds != nil {
in, out := &in.TolerationSeconds, &out.TolerationSeconds
*out = new(int64)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceToleration.
func (in *DeviceToleration) DeepCopy() *DeviceToleration {
if in == nil {
return nil
}
out := new(DeviceToleration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkDeviceData) DeepCopyInto(out *NetworkDeviceData) {
*out = *in

View file

@ -0,0 +1,8 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-scheduling-maintainers
reviewers:
- sig-scheduling
labels:
- sig/scheduling

View file

@ -0,0 +1,916 @@
/*
Copyright 2025 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 devicetainteviction
import (
"context"
"fmt"
"math"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/go-cmp/cmp" //nolint:depguard // Discouraged for production use (https://github.com/kubernetes/kubernetes/issues/104821) but has no good alternative for logging.
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1beta1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
coreinformers "k8s.io/client-go/informers/core/v1"
resourcealphainformers "k8s.io/client-go/informers/resource/v1alpha3"
resourceinformers "k8s.io/client-go/informers/resource/v1beta1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
corelisters "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/dynamic-resource-allocation/resourceclaim"
resourceslicetracker "k8s.io/dynamic-resource-allocation/resourceslice/tracker"
"k8s.io/klog/v2"
apipod "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/controller/devicetainteviction/metrics"
"k8s.io/kubernetes/pkg/controller/tainteviction"
utilpod "k8s.io/kubernetes/pkg/util/pod"
)
const (
// retries is the number of times that the controller tries to delete a pod
// that needs to be evicted.
retries = 5
)
// Controller listens to Taint changes of DRA devices and Toleration changes of ResourceClaims,
// then deletes Pods which use ResourceClaims that don't tolerate a NoExecute taint.
// Pods which have already reached a final state (aka terminated) don't need to be deleted.
//
// All of the logic which identifies pods which need to be evicted runs in the
// handle* event handlers. They don't call any blocking method. All the blocking
// calls happen in a [tainteviction.TimedWorkerQueue], using the context passed to Run.
//
// The [resourceslicetracker] takes care of applying taints defined in DeviceTaintRules
// to ResourceSlices. This controller here receives modified ResourceSlices with all
// applicable taints from that tracker and doesn't need to care about where a
// taint came from, the DRA driver or a DeviceTaintRule.
type Controller struct {
name string
// logger is the general-purpose logger to be used for background activities.
logger klog.Logger
// handlerLogger is specifically for logging during event handling. It may be nil
// if no such logging is desired.
eventLogger *klog.Logger
client clientset.Interface
recorder record.EventRecorder
podInformer coreinformers.PodInformer
podLister corelisters.PodLister
claimInformer resourceinformers.ResourceClaimInformer
sliceInformer resourceinformers.ResourceSliceInformer
taintInformer resourcealphainformers.DeviceTaintRuleInformer
classInformer resourceinformers.DeviceClassInformer
haveSynced []cache.InformerSynced
metrics metrics.Metrics
// evictPod ensures that the pod gets evicted at the specified time.
// It doesn't block.
evictPod func(pod tainteviction.NamespacedObject, fireAt time.Time)
// cancelEvict cancels eviction set up with evictPod earlier.
// Idempotent, returns false if there was nothing to cancel.
cancelEvict func(pod tainteviction.NamespacedObject) bool
// allocatedClaims holds all currently known allocated claims.
allocatedClaims map[types.NamespacedName]allocatedClaim // A value is slightly more efficient in BenchmarkTaintUntaint (less allocations!).
// pools indexes all slices by driver and pool name.
pools map[poolID]pool
hasSynced atomic.Int32
}
type poolID struct {
driverName, poolName string
}
type pool struct {
slices sets.Set[*resourceapi.ResourceSlice]
maxGeneration int64
}
// addSlice adds one slice to the pool.
func (p *pool) addSlice(slice *resourceapi.ResourceSlice) {
if slice == nil {
return
}
if p.slices == nil {
p.slices = sets.New[*resourceapi.ResourceSlice]()
p.maxGeneration = math.MinInt64
}
p.slices.Insert(slice)
// Adding a slice can only increase the generation.
if slice.Spec.Pool.Generation > p.maxGeneration {
p.maxGeneration = slice.Spec.Pool.Generation
}
}
// removeSlice removes a slice. It must have been added before.
func (p *pool) removeSlice(slice *resourceapi.ResourceSlice) {
if slice == nil {
return
}
p.slices.Delete(slice)
// Removing a slice might have decreased the generation to
// that of some other slice.
if slice.Spec.Pool.Generation == p.maxGeneration {
maxGeneration := int64(math.MinInt64)
for slice := range p.slices {
if slice.Spec.Pool.Generation > maxGeneration {
maxGeneration = slice.Spec.Pool.Generation
}
}
p.maxGeneration = maxGeneration
}
}
// getTaintedDevices appends all device taints with NoExecute effect.
// The result is sorted by device name.
func (p pool) getTaintedDevices() []taintedDevice {
var buffer []taintedDevice
for slice := range p.slices {
if slice.Spec.Pool.Generation != p.maxGeneration {
continue
}
for _, device := range slice.Spec.Devices {
if device.Basic == nil {
// Unknown device type, not supported.
continue
}
for _, taint := range device.Basic.Taints {
if taint.Effect != resourceapi.DeviceTaintEffectNoExecute {
continue
}
buffer = append(buffer, taintedDevice{deviceName: device.Name, taint: taint})
}
}
}
// slices.SortFunc is more efficient than sort.Slice here.
slices.SortFunc(buffer, func(a, b taintedDevice) int {
return strings.Compare(a.deviceName, b.deviceName)
})
return buffer
}
// getDevice looks up one device by name. Out-dated slices are ignored.
func (p pool) getDevice(deviceName string) *resourceapi.BasicDevice {
for slice := range p.slices {
if slice.Spec.Pool.Generation != p.maxGeneration {
continue
}
for _, device := range slice.Spec.Devices {
if device.Basic == nil {
// Unknown device type, not supported.
continue
}
if device.Name == deviceName {
return device.Basic
}
}
}
return nil
}
type taintedDevice struct {
deviceName string
taint resourceapi.DeviceTaint
}
// allocatedClaim is a ResourceClaim which has an allocation result. It
// may or may not be tainted such that pods need to be evicted.
type allocatedClaim struct {
*resourceapi.ResourceClaim
// evictionTime, if non-nil, is the time at which pods using this claim need to be evicted.
// This is the smallest value of all such per-device values.
// For each device, the value is calculated as `<time of setting the taint> +
// <toleration seconds, 0 if not set>`.
evictionTime *metav1.Time
}
func (tc *Controller) deletePodHandler(c clientset.Interface, emitEventFunc func(tainteviction.NamespacedObject)) func(ctx context.Context, fireAt time.Time, args *tainteviction.WorkArgs) error {
return func(ctx context.Context, fireAt time.Time, args *tainteviction.WorkArgs) error {
klog.FromContext(ctx).Info("Deleting pod", "pod", args.Object)
var err error
for i := 0; i < retries; i++ {
err = addConditionAndDeletePod(ctx, c, args.Object, &emitEventFunc)
if apierrors.IsNotFound(err) {
// Not a problem, the work is done.
// But we didn't do it, so don't
// bump the metric.
return nil
}
if err == nil {
tc.metrics.PodDeletionsTotal.Inc()
tc.metrics.PodDeletionsLatency.Observe(float64(time.Since(fireAt).Seconds()))
return nil
}
time.Sleep(10 * time.Millisecond)
}
return err
}
}
func addConditionAndDeletePod(ctx context.Context, c clientset.Interface, podRef tainteviction.NamespacedObject, emitEventFunc *func(tainteviction.NamespacedObject)) (err error) {
pod, err := c.CoreV1().Pods(podRef.Namespace).Get(ctx, podRef.Name, metav1.GetOptions{})
if err != nil {
return err
}
if pod.UID != podRef.UID {
// This special error suppresses event logging in our caller and prevents further retries.
// We can stop because the pod we were meant to evict is already gone and happens to
// be replaced by some other pod which reuses the same name.
return apierrors.NewNotFound(v1.SchemeGroupVersion.WithResource("pods").GroupResource(), pod.Name)
}
// Emit the event only once, and only if we are actually doing something.
if *emitEventFunc != nil {
(*emitEventFunc)(podRef)
*emitEventFunc = nil
}
newStatus := pod.Status.DeepCopy()
updated := apipod.UpdatePodCondition(newStatus, &v1.PodCondition{
Type: v1.DisruptionTarget,
Status: v1.ConditionTrue,
Reason: "DeletionByDeviceTaintManager",
Message: "Device Taint manager: deleting due to NoExecute taint",
})
if updated {
if _, _, _, err := utilpod.PatchPodStatus(ctx, c, pod.Namespace, pod.Name, pod.UID, pod.Status, *newStatus); err != nil {
return err
}
}
// Unlikely, but it could happen that the pod we got above got replaced with
// another pod using the same name in the meantime. Include a precondition
// to prevent that race. This delete attempt then fails and the next one detects
// the new pod and stops retrying.
return c.CoreV1().Pods(podRef.Namespace).Delete(ctx, podRef.Name, metav1.DeleteOptions{
Preconditions: &metav1.Preconditions{
UID: &podRef.UID,
},
})
}
// New creates a new Controller that will use passed clientset to communicate with the API server.
// Spawns no goroutines. That happens in Run.
func New(c clientset.Interface, podInformer coreinformers.PodInformer, claimInformer resourceinformers.ResourceClaimInformer, sliceInformer resourceinformers.ResourceSliceInformer, taintInformer resourcealphainformers.DeviceTaintRuleInformer, classInformer resourceinformers.DeviceClassInformer, controllerName string) *Controller {
metrics.Register() // It would be nicer to pass the controller name here, but that probably would break generating https://kubernetes.io/docs/reference/instrumentation/metrics.
tc := &Controller{
name: controllerName,
client: c,
podInformer: podInformer,
podLister: podInformer.Lister(),
claimInformer: claimInformer,
sliceInformer: sliceInformer,
taintInformer: taintInformer,
classInformer: classInformer,
allocatedClaims: make(map[types.NamespacedName]allocatedClaim),
pools: make(map[poolID]pool),
// Instantiate all informers now to ensure that they get started.
haveSynced: []cache.InformerSynced{
podInformer.Informer().HasSynced,
claimInformer.Informer().HasSynced,
sliceInformer.Informer().HasSynced,
taintInformer.Informer().HasSynced,
classInformer.Informer().HasSynced,
},
metrics: metrics.Global,
}
return tc
}
// Run starts the controller which will run until the context is done.
func (tc *Controller) Run(ctx context.Context) {
defer utilruntime.HandleCrash()
logger := klog.FromContext(ctx)
logger.Info("Starting", "controller", tc.name)
defer logger.Info("Shutting down controller", "controller", tc.name)
tc.logger = logger
// Doing debug logging?
if loggerV := logger.V(6); loggerV.Enabled() {
tc.eventLogger = &loggerV
}
// Delayed construction of broadcaster because it spawns goroutines.
// tc.recorder.Eventf is a local in-memory operation which never
// blocks, so it is safe to call from an event handler. The
// actual API calls then happen in those spawned goroutines.
eventBroadcaster := record.NewBroadcaster(record.WithContext(ctx))
tc.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: tc.name}).WithLogger(logger)
defer eventBroadcaster.Shutdown()
taintEvictionQueue := tainteviction.CreateWorkerQueue(tc.deletePodHandler(tc.client, tc.emitPodDeletionEvent))
evictPod := tc.evictPod
tc.evictPod = func(podRef tainteviction.NamespacedObject, fireAt time.Time) {
// Only relevant for testing.
if evictPod != nil {
evictPod(podRef, fireAt)
}
taintEvictionQueue.UpdateWork(ctx, &tainteviction.WorkArgs{Object: podRef}, time.Now(), fireAt)
}
cancelEvict := tc.cancelEvict
tc.cancelEvict = func(podRef tainteviction.NamespacedObject) bool {
if cancelEvict != nil {
cancelEvict(podRef)
}
return taintEvictionQueue.CancelWork(logger, podRef.NamespacedName.String())
}
// Start events processing pipeline.
eventBroadcaster.StartStructuredLogging(3)
if tc.client != nil {
logger.Info("Sending events to api server")
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: tc.client.CoreV1().Events("")})
} else {
logger.Error(nil, "kubeClient is nil", "controller", tc.name)
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
defer eventBroadcaster.Shutdown()
// mutex serializes event processing.
var mutex sync.Mutex
claimHandler, _ := tc.claimInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj any) {
claim, ok := obj.(*resourceapi.ResourceClaim)
if !ok {
logger.Error(nil, "Expected ResourceClaim", "actual", fmt.Sprintf("%T", obj))
return
}
mutex.Lock()
defer mutex.Unlock()
tc.handleClaimChange(nil, claim)
},
UpdateFunc: func(oldObj, newObj any) {
oldClaim, ok := oldObj.(*resourceapi.ResourceClaim)
if !ok {
logger.Error(nil, "Expected ResourceClaim", "actual", fmt.Sprintf("%T", oldObj))
return
}
newClaim, ok := newObj.(*resourceapi.ResourceClaim)
if !ok {
logger.Error(nil, "Expected ResourceClaim", "actual", fmt.Sprintf("%T", newObj))
}
mutex.Lock()
defer mutex.Unlock()
tc.handleClaimChange(oldClaim, newClaim)
},
DeleteFunc: func(obj any) {
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = tombstone.Obj
}
claim, ok := obj.(*resourceapi.ResourceClaim)
if !ok {
logger.Error(nil, "Expected ResourceClaim", "actual", fmt.Sprintf("%T", obj))
return
}
mutex.Lock()
defer mutex.Unlock()
tc.handleClaimChange(claim, nil)
},
})
defer func() {
_ = tc.claimInformer.Informer().RemoveEventHandler(claimHandler)
}()
tc.haveSynced = append(tc.haveSynced, claimHandler.HasSynced)
podHandler, _ := tc.podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj any) {
pod, ok := obj.(*v1.Pod)
if !ok {
logger.Error(nil, "Expected ResourcePod", "actual", fmt.Sprintf("%T", obj))
return
}
mutex.Lock()
defer mutex.Unlock()
tc.handlePodChange(nil, pod)
},
UpdateFunc: func(oldObj, newObj any) {
oldPod, ok := oldObj.(*v1.Pod)
if !ok {
logger.Error(nil, "Expected Pod", "actual", fmt.Sprintf("%T", oldObj))
return
}
newPod, ok := newObj.(*v1.Pod)
if !ok {
logger.Error(nil, "Expected Pod", "actual", fmt.Sprintf("%T", newObj))
}
mutex.Lock()
defer mutex.Unlock()
tc.handlePodChange(oldPod, newPod)
},
DeleteFunc: func(obj any) {
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = tombstone.Obj
}
pod, ok := obj.(*v1.Pod)
if !ok {
logger.Error(nil, "Expected Pod", "actual", fmt.Sprintf("%T", obj))
return
}
mutex.Lock()
defer mutex.Unlock()
tc.handlePodChange(pod, nil)
},
})
defer func() {
_ = tc.podInformer.Informer().RemoveEventHandler(podHandler)
}()
tc.haveSynced = append(tc.haveSynced, podHandler.HasSynced)
opts := resourceslicetracker.Options{
EnableDeviceTaints: true,
SliceInformer: tc.sliceInformer,
TaintInformer: tc.taintInformer,
ClassInformer: tc.classInformer,
KubeClient: tc.client,
}
sliceTracker, err := resourceslicetracker.StartTracker(ctx, opts)
if err != nil {
logger.Info("Failed to initialize ResourceSlice tracker; device taint processing leading to Pod eviction is now paused", "err", err)
return
}
tc.haveSynced = append(tc.haveSynced, sliceTracker.HasSynced)
defer sliceTracker.Stop()
// Wait for tracker to sync before we react to events.
// This doesn't have to be perfect, it merely avoids unnecessary
// work which might be done as events get emitted for intermediate
// state.
if !cache.WaitForNamedCacheSyncWithContext(ctx, tc.haveSynced...) {
return
}
logger.V(1).Info("Underlying informers have synced")
_, _ = sliceTracker.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj any) {
slice, ok := obj.(*resourceapi.ResourceSlice)
if !ok {
logger.Error(nil, "Expected ResourceSlice", "actual", fmt.Sprintf("%T", obj))
return
}
mutex.Lock()
defer mutex.Unlock()
tc.handleSliceChange(nil, slice)
},
UpdateFunc: func(oldObj, newObj any) {
oldSlice, ok := oldObj.(*resourceapi.ResourceSlice)
if !ok {
logger.Error(nil, "Expected ResourceSlice", "actual", fmt.Sprintf("%T", oldObj))
return
}
newSlice, ok := newObj.(*resourceapi.ResourceSlice)
if !ok {
logger.Error(nil, "Expected ResourceSlice", "actual", fmt.Sprintf("%T", newObj))
}
mutex.Lock()
defer mutex.Unlock()
tc.handleSliceChange(oldSlice, newSlice)
},
DeleteFunc: func(obj any) {
// No need to check for DeletedFinalStateUnknown here, the resourceslicetracker doesn't use that.
slice, ok := obj.(*resourceapi.ResourceSlice)
if !ok {
logger.Error(nil, "Expected ResourceSlice", "actual", fmt.Sprintf("%T", obj))
return
}
mutex.Lock()
defer mutex.Unlock()
tc.handleSliceChange(slice, nil)
},
})
// sliceTracker.AddEventHandler blocked while delivering events for all known
// ResourceSlices. Therefore our own state is up-to-date once we get here.
tc.hasSynced.Store(1)
<-ctx.Done()
}
func (tc *Controller) handleClaimChange(oldClaim, newClaim *resourceapi.ResourceClaim) {
claim := newClaim
if claim == nil {
claim = oldClaim
}
name := newNamespacedName(claim)
if tc.eventLogger != nil {
// This is intentionally very verbose for debugging.
tc.eventLogger.Info("ResourceClaim changed", "claimObject", name, "oldClaim", klog.Format(oldClaim), "newClaim", klog.Format(newClaim), "diff", cmp.Diff(oldClaim, newClaim))
}
// Deleted?
if newClaim == nil {
delete(tc.allocatedClaims, name)
tc.handlePods(claim)
return
}
// Added?
if oldClaim == nil {
if claim.Status.Allocation == nil {
return
}
tc.allocatedClaims[name] = allocatedClaim{
ResourceClaim: claim,
evictionTime: tc.evictionTime(claim.Status.Allocation),
}
tc.handlePods(claim)
return
}
// If we have two claims, the UID might still be different. Unlikely, but not impossible...
// Treat this like a remove + add.
if oldClaim.UID != newClaim.UID {
tc.handleClaimChange(oldClaim, nil)
tc.handleClaimChange(nil, newClaim)
return
}
syncBothClaims := func() {
// ReservedFor may have changed. If it did, sync both old and new lists,
// otherwise only once (same list).
if !slices.Equal(oldClaim.Status.ReservedFor, newClaim.Status.ReservedFor) {
tc.handlePods(oldClaim)
tc.handlePods(newClaim)
} else {
tc.handlePods(claim)
}
}
// Allocation added?
if oldClaim.Status.Allocation == nil && newClaim.Status.Allocation != nil {
tc.allocatedClaims[name] = allocatedClaim{
ResourceClaim: claim,
evictionTime: tc.evictionTime(claim.Status.Allocation),
}
syncBothClaims()
return
}
// Allocation removed?
if oldClaim.Status.Allocation != nil && newClaim.Status.Allocation == nil {
delete(tc.allocatedClaims, name)
syncBothClaims()
return
}
// Allocated before and after?
if claim.Status.Allocation != nil {
// The Allocation is immutable, so we don't need to recompute the eviction
// time. Storing the newer claim is enough.
tc.allocatedClaims[name] = allocatedClaim{
ResourceClaim: claim,
evictionTime: tc.allocatedClaims[name].evictionTime,
}
syncBothClaims()
return
}
// If we get here, nothing changed.
}
// evictionTime returns the earliest TimeAdded of any NoExecute taint in any allocated device
// unless that taint is tolerated, nil if none.
func (tc *Controller) evictionTime(allocation *resourceapi.AllocationResult) *metav1.Time {
var evictionTime *metav1.Time
for _, allocatedDevice := range allocation.Devices.Results {
device := tc.pools[poolID{driverName: allocatedDevice.Driver, poolName: allocatedDevice.Pool}].getDevice(allocatedDevice.Device)
if device == nil {
// Unknown device? Can't be tainted...
continue
}
nextTaint:
for _, taint := range device.Taints {
if taint.Effect != resourceapi.DeviceTaintEffectNoExecute {
continue
}
newEvictionTime := taint.TimeAdded
haveToleration := false
tolerationSeconds := int64(math.MaxInt64)
for _, toleration := range allocatedDevice.Tolerations {
if toleration.Effect == resourceapi.DeviceTaintEffectNoExecute &&
resourceclaim.ToleratesTaint(toleration, taint) {
if toleration.TolerationSeconds == nil {
// Tolerate forever -> ignore taint.
continue nextTaint
}
newTolerationSeconds := *toleration.TolerationSeconds
if newTolerationSeconds < 0 {
newTolerationSeconds = 0
}
if newTolerationSeconds < tolerationSeconds {
tolerationSeconds = newTolerationSeconds
}
haveToleration = true
}
}
if haveToleration {
newEvictionTime = &metav1.Time{Time: newEvictionTime.Add(time.Duration(tolerationSeconds) * time.Second)}
}
if evictionTime == nil {
evictionTime = newEvictionTime
continue
}
if newEvictionTime != nil && newEvictionTime.Before(evictionTime) {
evictionTime = newEvictionTime
}
}
}
return evictionTime
}
func (tc *Controller) handleSliceChange(oldSlice, newSlice *resourceapi.ResourceSlice) {
slice := newSlice
if slice == nil {
slice = oldSlice
}
poolID := poolID{
driverName: slice.Spec.Driver,
poolName: slice.Spec.Pool.Name,
}
if tc.eventLogger != nil {
// This is intentionally very verbose for debugging.
tc.eventLogger.Info("ResourceSlice changed", "pool", poolID, "oldSlice", klog.Format(oldSlice), "newSlice", klog.Format(newSlice), "diff", cmp.Diff(oldSlice, newSlice))
}
// Determine old and new device taints. Only devices
// where something changes trigger additional checks for claims
// using them.
//
// The pre-allocated slices are small enough to be allocated on
// the stack (https://stackoverflow.com/a/69187698/222305).
p := tc.pools[poolID]
oldDeviceTaints := p.getTaintedDevices()
p.removeSlice(oldSlice)
p.addSlice(newSlice)
if len(p.slices) == 0 {
delete(tc.pools, poolID)
} else {
tc.pools[poolID] = p
}
newDeviceTaints := p.getTaintedDevices()
// Now determine differences. This depends on both slices having been sorted
// by device name.
if len(oldDeviceTaints) == 0 && len(newDeviceTaints) == 0 {
// Both empty, no changes.
return
}
modifiedDevices := sets.New[string]()
o, n := 0, 0
for o < len(oldDeviceTaints) || n < len(newDeviceTaints) {
// Iterate over devices in both slices with the same name.
for o < len(oldDeviceTaints) && n < len(newDeviceTaints) && oldDeviceTaints[o].deviceName == newDeviceTaints[n].deviceName {
if !apiequality.Semantic.DeepEqual(oldDeviceTaints[o].taint, newDeviceTaints[n].taint) { // TODO: hard-code the comparison?
modifiedDevices.Insert(oldDeviceTaints[o].deviceName)
}
o++
n++
}
// Step over old devices which were removed.
newDeviceName := ""
if n < len(newDeviceTaints) {
newDeviceName = newDeviceTaints[n].deviceName
}
for o < len(oldDeviceTaints) && oldDeviceTaints[o].deviceName != newDeviceName {
modifiedDevices.Insert(oldDeviceTaints[o].deviceName)
o++
}
// Step over new devices which were added.
oldDeviceName := ""
if o < len(oldDeviceTaints) {
oldDeviceName = oldDeviceTaints[o].deviceName
}
if n < len(newDeviceTaints) && newDeviceTaints[n].deviceName != oldDeviceName {
modifiedDevices.Insert(newDeviceTaints[n].deviceName)
n++
}
}
// Now find all claims using at least one modified device,
// update their eviction time, and handle their consuming pods.
for name, claim := range tc.allocatedClaims {
if !usesDevice(claim.Status.Allocation, poolID, modifiedDevices) {
continue
}
newEvictionTime := tc.evictionTime(claim.ResourceClaim.Status.Allocation)
if newEvictionTime.Equal(claim.evictionTime) {
// No change.
continue
}
claim.evictionTime = newEvictionTime
tc.allocatedClaims[name] = claim
// We could collect pods which depend on claims with changes.
// In practice, most pods probably depend on one claim, so
// it is probably more efficient to avoid building such a map
// to make the common case simple.
tc.handlePods(claim.ResourceClaim)
}
}
func usesDevice(allocation *resourceapi.AllocationResult, pool poolID, modifiedDevices sets.Set[string]) bool {
for _, device := range allocation.Devices.Results {
if device.Driver == pool.driverName &&
device.Pool == pool.poolName &&
modifiedDevices.Has(device.Device) {
return true
}
}
return false
}
func (tc *Controller) handlePodChange(oldPod, newPod *v1.Pod) {
pod := newPod
if pod == nil {
pod = oldPod
}
if tc.eventLogger != nil {
// This is intentionally very verbose for debugging.
tc.eventLogger.Info("Pod changed", "pod", klog.KObj(pod), "oldPod", klog.Format(oldPod), "newPod", klog.Format(newPod), "diff", cmp.Diff(oldPod, newPod))
}
if newPod == nil {
// Nothing left to do for it. No need to emit an event here, it's gone.
tc.cancelEvict(newObject(oldPod))
return
}
// Pods get updated quite frequently. There's no need
// to check them again unless something changed regarding
// their claims.
//
// In particular this prevents adding the pod again
// directly after the eviction condition got added
// to it.
if oldPod != nil &&
apiequality.Semantic.DeepEqual(oldPod.Status.ResourceClaimStatuses, newPod.Status.ResourceClaimStatuses) {
return
}
tc.handlePod(newPod)
}
func (tc *Controller) handlePods(claim *resourceapi.ResourceClaim) {
for _, consumer := range claim.Status.ReservedFor {
if consumer.APIGroup == "" && consumer.Resource == "pods" {
pod, err := tc.podInformer.Lister().Pods(claim.Namespace).Get(consumer.Name)
if err != nil {
if apierrors.IsNotFound(err) {
return
}
// Should not happen.
utilruntime.HandleErrorWithLogger(tc.logger, err, "retrieve pod from cache")
return
}
if pod.UID != consumer.UID {
// Not the pod we were looking for.
return
}
tc.handlePod(pod)
}
}
}
func (tc *Controller) handlePod(pod *v1.Pod) {
// Not scheduled yet? No need to evict.
if pod.Spec.NodeName == "" {
return
}
// If any claim in use by the pod is tainted such that the taint is not tolerated,
// the pod needs to be evicted.
var evictionTime *metav1.Time
for i := range pod.Spec.ResourceClaims {
claimName, mustCheckOwner, err := resourceclaim.Name(pod, &pod.Spec.ResourceClaims[i])
if err != nil {
// Not created yet or unsupported. Definitely not tainted.
continue
}
if claimName == nil {
// Claim not needed.
continue
}
allocatedClaim, ok := tc.allocatedClaims[types.NamespacedName{Namespace: pod.Namespace, Name: *claimName}]
if !ok {
// Referenced, but not found or not allocated. Also not tainted.
continue
}
if mustCheckOwner && resourceclaim.IsForPod(pod, allocatedClaim.ResourceClaim) != nil {
// Claim and pod don't match. Ignore the claim.
continue
}
if !resourceclaim.IsReservedForPod(pod, allocatedClaim.ResourceClaim) {
// The pod isn't the one which is allowed and/or supposed to use the claim.
// Perhaps that pod instance already got deleted and we are looking at its
// replacement under the same name. Either way, ignore.
continue
}
if allocatedClaim.evictionTime == nil {
continue
}
if evictionTime == nil || allocatedClaim.evictionTime.Before(evictionTime) {
evictionTime = allocatedClaim.evictionTime
}
}
podRef := newObject(pod)
if evictionTime != nil {
tc.evictPod(podRef, evictionTime.Time)
} else {
tc.cancelWorkWithEvent(podRef)
}
}
func (tc *Controller) cancelWorkWithEvent(podRef tainteviction.NamespacedObject) {
if tc.cancelEvict(podRef) {
tc.emitCancelPodDeletionEvent(podRef)
}
}
func (tc *Controller) emitPodDeletionEvent(podRef tainteviction.NamespacedObject) {
if tc.recorder == nil {
return
}
ref := &v1.ObjectReference{
APIVersion: "v1",
Kind: "Pod",
Name: podRef.Name,
Namespace: podRef.Namespace,
UID: podRef.UID,
}
tc.recorder.Eventf(ref, v1.EventTypeNormal, "DeviceTaintManagerEviction", "Marking for deletion")
}
func (tc *Controller) emitCancelPodDeletionEvent(podRef tainteviction.NamespacedObject) {
if tc.recorder == nil {
return
}
ref := &v1.ObjectReference{
APIVersion: "v1",
Kind: "Pod",
Name: podRef.Name,
Namespace: podRef.Namespace,
UID: podRef.UID,
}
tc.recorder.Eventf(ref, v1.EventTypeNormal, "DeviceTaintManagerEviction", "Cancelling deletion")
}
func newNamespacedName(obj metav1.Object) types.NamespacedName {
return types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
}
}
func newObject(obj metav1.Object) tainteviction.NamespacedObject {
return tainteviction.NamespacedObject{
NamespacedName: newNamespacedName(obj),
UID: obj.GetUID(),
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
/*
Copyright 2025 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 devicetainteviction contains the logic implementing taint-based eviction
// for Pods using tainted devices (https://github.com/kubernetes/enhancements/issues/5055).
package devicetainteviction

View file

@ -0,0 +1,88 @@
/*
Copyright 2025 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 metrics
import (
"sync"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
// controllerSubsystem must be kept in sync with the controller name in cmd/kube-controller-manager/names.
const controllerSubsystem = "device_taint_eviction_controller"
var (
Global = New()
)
var registerMetrics sync.Once
// Register registers TaintEvictionController metrics.
func Register() {
registerMetrics.Do(func() {
legacyregistry.MustRegister(Global.PodDeletionsTotal)
legacyregistry.MustRegister(Global.PodDeletionsLatency)
})
}
// New returns new instances of all metrics for testing in parallel.
// Optionally, buckets for the histograms can be specified.
func New(buckets ...float64) Metrics {
if len(buckets) == 0 {
buckets = []float64{0.005, 0.025, 0.1, 0.5, 1, 2.5, 10, 30, 60, 120, 180, 240} // 5ms to 4m
}
m := Metrics{
KubeRegistry: metrics.NewKubeRegistry(),
PodDeletionsTotal: metrics.NewCounter(
&metrics.CounterOpts{
Subsystem: controllerSubsystem,
Name: "pod_deletions_total",
Help: "Total number of Pods deleted by DeviceTaintEvictionController since its start.",
StabilityLevel: metrics.ALPHA,
},
),
PodDeletionsLatency: metrics.NewHistogram(
&metrics.HistogramOpts{
Subsystem: controllerSubsystem,
Name: "pod_deletion_duration_seconds",
Help: "Latency, in seconds, between the time when a device taint effect has been activated and a Pod's deletion via DeviceTaintEvictionController.",
Buckets: []float64{0.005, 0.025, 0.1, 0.5, 1, 2.5, 10, 30, 60, 120, 180, 240}, // 5ms to 4m,
StabilityLevel: metrics.ALPHA,
},
),
}
// This has to be done after construction, otherwise ./hack/update-generated-stable-metrics.sh
// fails to find the default buckets.
if len(buckets) > 0 {
m.PodDeletionsLatency.HistogramOpts.Buckets = buckets
}
m.KubeRegistry.MustRegister(m.PodDeletionsTotal, m.PodDeletionsLatency)
return m
}
// Metrics contains all metrics supported by the device taint eviction controller.
// It implements [metrics.Gatherer].
type Metrics struct {
metrics.KubeRegistry
PodDeletionsTotal *metrics.Counter
PodDeletionsLatency *metrics.Histogram
}
var _ metrics.Gatherer = Metrics{}

View file

@ -0,0 +1,50 @@
/*
Copyright 2025 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 tainteviction
import (
"k8s.io/apimachinery/pkg/types"
)
// NamespacedObject comprises a resource name with a mandatory namespace
// and optional UID. It gets rendered as "<namespace>/<name>[:<uid>]"
// (text output) or as an object (JSON output).
type NamespacedObject struct {
types.NamespacedName
UID types.UID
}
// String returns the general purpose string representation
func (n NamespacedObject) String() string {
if n.UID != "" {
return n.Namespace + string(types.Separator) + n.Name + ":" + string(n.UID)
}
return n.Namespace + string(types.Separator) + n.Name
}
// MarshalLog emits a struct containing required key/value pair
func (n NamespacedObject) MarshalLog() interface{} {
return struct {
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
UID types.UID `json:"uid,omitempty"`
}{
Name: n.Name,
Namespace: n.Namespace,
UID: n.UID,
}
}

View file

@ -106,11 +106,11 @@ type Controller struct {
func deletePodHandler(c clientset.Interface, emitEventFunc func(types.NamespacedName), controllerName string) func(ctx context.Context, fireAt time.Time, args *WorkArgs) error {
return func(ctx context.Context, fireAt time.Time, args *WorkArgs) error {
ns := args.NamespacedName.Namespace
name := args.NamespacedName.Name
klog.FromContext(ctx).Info("Deleting pod", "controller", controllerName, "pod", args.NamespacedName)
ns := args.Object.Namespace
name := args.Object.Name
klog.FromContext(ctx).Info("Deleting pod", "controller", controllerName, "pod", args.Object)
if emitEventFunc != nil {
emitEventFunc(args.NamespacedName)
emitEventFunc(args.Object.NamespacedName)
}
var err error
for i := 0; i < retries; i++ {

View file

@ -28,18 +28,23 @@ import (
// WorkArgs keeps arguments that will be passed to the function executed by the worker.
type WorkArgs struct {
NamespacedName types.NamespacedName
// Object is the work item. The UID is only set if it was set when adding the work item.
Object NamespacedObject
}
// KeyFromWorkArgs creates a key for the given `WorkArgs`
// KeyFromWorkArgs creates a key for the given `WorkArgs`.
//
// The key is the same as the NamespacedName of the object in the work item,
// i.e. the UID is ignored. There cannot be two different
// work items with the same NamespacedName and different UIDs.
func (w *WorkArgs) KeyFromWorkArgs() string {
return w.NamespacedName.String()
return w.Object.NamespacedName.String()
}
// NewWorkArgs is a helper function to create new `WorkArgs`
// NewWorkArgs is a helper function to create new `WorkArgs` without a UID.
func NewWorkArgs(name, namespace string) *WorkArgs {
return &WorkArgs{
NamespacedName: types.NamespacedName{Namespace: namespace, Name: name},
Object: NamespacedObject{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
}
}
@ -102,31 +107,59 @@ func CreateWorkerQueue(f func(ctx context.Context, fireAt time.Time, args *WorkA
func (q *TimedWorkerQueue) getWrappedWorkerFunc(key string) func(ctx context.Context, fireAt time.Time, args *WorkArgs) error {
return func(ctx context.Context, fireAt time.Time, args *WorkArgs) error {
logger := klog.FromContext(ctx)
logger.V(4).Info("Firing worker", "item", key, "firedTime", fireAt)
err := q.workFunc(ctx, fireAt, args)
q.Lock()
defer q.Unlock()
logger.V(4).Info("Worker finished, removing", "item", key, "err", err)
delete(q.workers, key)
return err
}
}
// AddWork adds a work to the WorkerQueue which will be executed not earlier than `fireAt`.
// If replace is false, an existing work item will not get replaced, otherwise it
// gets canceled and the new one is added instead.
func (q *TimedWorkerQueue) AddWork(ctx context.Context, args *WorkArgs, createdAt time.Time, fireAt time.Time) {
key := args.KeyFromWorkArgs()
logger := klog.FromContext(ctx)
logger.V(4).Info("Adding TimedWorkerQueue item and to be fired at firedTime", "item", key, "createTime", createdAt, "firedTime", fireAt)
q.Lock()
defer q.Unlock()
if _, exists := q.workers[key]; exists {
logger.Info("Trying to add already existing work, skipping", "args", args)
logger.V(4).Info("Trying to add already existing work, skipping", "item", key, "createTime", createdAt, "firedTime", fireAt)
return
}
logger.V(4).Info("Adding TimedWorkerQueue item and to be fired at firedTime", "item", key, "createTime", createdAt, "firedTime", fireAt)
worker := createWorker(ctx, args, createdAt, fireAt, q.getWrappedWorkerFunc(key), q.clock)
q.workers[key] = worker
}
// UpdateWork adds or replaces a work item such that it will be executed not earlier than `fireAt`.
// This is a cheap no-op when the old and new fireAt are the same.
func (q *TimedWorkerQueue) UpdateWork(ctx context.Context, args *WorkArgs, createdAt time.Time, fireAt time.Time) {
key := args.KeyFromWorkArgs()
logger := klog.FromContext(ctx)
q.Lock()
defer q.Unlock()
if worker, exists := q.workers[key]; exists {
if worker.FireAt.Compare(fireAt) == 0 {
logger.V(4).Info("Keeping existing work, same time", "item", key, "createTime", worker.CreatedAt, "firedTime", worker.FireAt)
return
}
logger.V(4).Info("Replacing existing work", "item", key, "createTime", worker.CreatedAt, "firedTime", worker.FireAt)
worker.Cancel()
}
logger.V(4).Info("Adding TimedWorkerQueue item and to be fired at firedTime", "item", key, "createTime", createdAt, "firedTime", fireAt)
worker := createWorker(ctx, args, createdAt, fireAt, q.getWrappedWorkerFunc(key), q.clock)
q.workers[key] = worker
}
// CancelWork removes scheduled function execution from the queue. Returns true if work was cancelled.
// The key must be the same as the one returned by WorkArgs.KeyFromWorkArgs, i.e.
// the result of NamespacedName.String.
func (q *TimedWorkerQueue) CancelWork(logger klog.Logger, key string) bool {
q.Lock()
defer q.Unlock()

View file

@ -220,6 +220,13 @@ const (
// is to move it into a separate KEP.
DRAAdminAccess featuregate.Feature = "DRAAdminAccess"
// owner: @pohly
// kep: http://kep.k8s.io/5055
//
// Marking devices as tainted can prevent using them for new pods and/or
// cause pods using them to stop. Users can decide to tolerate taints.
DRADeviceTaints featuregate.Feature = "DRADeviceTaints"
// owner: @mortent
// kep: http://kep.k8s.io/4816
//
@ -228,6 +235,13 @@ const (
// be selected.
DRAPrioritizedList featuregate.Feature = "DRAPrioritizedList"
// owner: @LionelJouin
// kep: http://kep.k8s.io/4817
//
// Enables support the ResourceClaim.status.devices field and for setting this
// status from DRA drivers.
DRAResourceClaimDeviceStatus featuregate.Feature = "DRAResourceClaimDeviceStatus"
// owner: @pohly
// kep: http://kep.k8s.io/4381
//
@ -236,13 +250,6 @@ const (
// based on "structured parameters".
DynamicResourceAllocation featuregate.Feature = "DynamicResourceAllocation"
// owner: @LionelJouin
// kep: http://kep.k8s.io/4817
//
// Enables support the ResourceClaim.status.devices field and for setting this
// status from DRA drivers.
DRAResourceClaimDeviceStatus featuregate.Feature = "DRAResourceClaimDeviceStatus"
// owner: @lauralorenz
// kep: https://kep.k8s.io/4603
//

View file

@ -178,13 +178,12 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
DRAPrioritizedList: {
DRADeviceTaints: {
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
},
DynamicResourceAllocation: {
{Version: version.MustParse("1.26"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Beta},
DRAPrioritizedList: {
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
},
DRAResourceClaimDeviceStatus: {
@ -192,6 +191,11 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta},
},
DynamicResourceAllocation: {
{Version: version.MustParse("1.26"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Beta},
},
KubeletCrashLoopBackOffMax: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},

View file

@ -935,6 +935,12 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/resource/v1alpha3.DeviceRequestAllocationResult": schema_k8sio_api_resource_v1alpha3_DeviceRequestAllocationResult(ref),
"k8s.io/api/resource/v1alpha3.DeviceSelector": schema_k8sio_api_resource_v1alpha3_DeviceSelector(ref),
"k8s.io/api/resource/v1alpha3.DeviceSubRequest": schema_k8sio_api_resource_v1alpha3_DeviceSubRequest(ref),
"k8s.io/api/resource/v1alpha3.DeviceTaint": schema_k8sio_api_resource_v1alpha3_DeviceTaint(ref),
"k8s.io/api/resource/v1alpha3.DeviceTaintRule": schema_k8sio_api_resource_v1alpha3_DeviceTaintRule(ref),
"k8s.io/api/resource/v1alpha3.DeviceTaintRuleList": schema_k8sio_api_resource_v1alpha3_DeviceTaintRuleList(ref),
"k8s.io/api/resource/v1alpha3.DeviceTaintRuleSpec": schema_k8sio_api_resource_v1alpha3_DeviceTaintRuleSpec(ref),
"k8s.io/api/resource/v1alpha3.DeviceTaintSelector": schema_k8sio_api_resource_v1alpha3_DeviceTaintSelector(ref),
"k8s.io/api/resource/v1alpha3.DeviceToleration": schema_k8sio_api_resource_v1alpha3_DeviceToleration(ref),
"k8s.io/api/resource/v1alpha3.NetworkDeviceData": schema_k8sio_api_resource_v1alpha3_NetworkDeviceData(ref),
"k8s.io/api/resource/v1alpha3.OpaqueDeviceConfiguration": schema_k8sio_api_resource_v1alpha3_OpaqueDeviceConfiguration(ref),
"k8s.io/api/resource/v1alpha3.ResourceClaim": schema_k8sio_api_resource_v1alpha3_ResourceClaim(ref),
@ -970,6 +976,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/resource/v1beta1.DeviceRequestAllocationResult": schema_k8sio_api_resource_v1beta1_DeviceRequestAllocationResult(ref),
"k8s.io/api/resource/v1beta1.DeviceSelector": schema_k8sio_api_resource_v1beta1_DeviceSelector(ref),
"k8s.io/api/resource/v1beta1.DeviceSubRequest": schema_k8sio_api_resource_v1beta1_DeviceSubRequest(ref),
"k8s.io/api/resource/v1beta1.DeviceTaint": schema_k8sio_api_resource_v1beta1_DeviceTaint(ref),
"k8s.io/api/resource/v1beta1.DeviceToleration": schema_k8sio_api_resource_v1beta1_DeviceToleration(ref),
"k8s.io/api/resource/v1beta1.NetworkDeviceData": schema_k8sio_api_resource_v1beta1_NetworkDeviceData(ref),
"k8s.io/api/resource/v1beta1.OpaqueDeviceConfiguration": schema_k8sio_api_resource_v1beta1_OpaqueDeviceConfiguration(ref),
"k8s.io/api/resource/v1beta1.ResourceClaim": schema_k8sio_api_resource_v1beta1_ResourceClaim(ref),
@ -47168,11 +47176,30 @@ func schema_k8sio_api_resource_v1alpha3_BasicDevice(ref common.ReferenceCallback
},
},
},
"taints": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "If specified, these are the driver-defined taints.\n\nThe maximum number of taints is 8.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceTaint"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceAttribute", "k8s.io/apimachinery/pkg/api/resource.Quantity"},
"k8s.io/api/resource/v1alpha3.DeviceAttribute", "k8s.io/api/resource/v1alpha3.DeviceTaint", "k8s.io/apimachinery/pkg/api/resource.Quantity"},
}
}
@ -47797,12 +47824,31 @@ func schema_k8sio_api_resource_v1alpha3_DeviceRequest(ref common.ReferenceCallba
},
},
},
"tolerations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceToleration"),
},
},
},
},
},
},
Required: []string{"name"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceSelector", "k8s.io/api/resource/v1alpha3.DeviceSubRequest"},
"k8s.io/api/resource/v1alpha3.DeviceSelector", "k8s.io/api/resource/v1alpha3.DeviceSubRequest", "k8s.io/api/resource/v1alpha3.DeviceToleration"},
}
}
@ -47852,10 +47898,31 @@ func schema_k8sio_api_resource_v1alpha3_DeviceRequestAllocationResult(ref common
Format: "",
},
},
"tolerations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "A copy of all tolerations specified in the request at the time when the device got allocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceToleration"),
},
},
},
},
},
},
Required: []string{"request", "driver", "pool", "device"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceToleration"},
}
}
@ -47936,15 +48003,317 @@ func schema_k8sio_api_resource_v1alpha3_DeviceSubRequest(ref common.ReferenceCal
Format: "int64",
},
},
"tolerations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceToleration"),
},
},
},
},
},
},
Required: []string{"name", "deviceClassName"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceSelector", "k8s.io/api/resource/v1alpha3.DeviceToleration"},
}
}
func schema_k8sio_api_resource_v1alpha3_DeviceTaint(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The device this taint is attached to has the \"effect\" on any claim which does not tolerate the taint and, through the claim, to pods using the claim.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key": {
SchemaProps: spec.SchemaProps{
Description: "The taint key to be applied to a device. Must be a label name.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "The taint value corresponding to the taint key. Must be a label value.",
Type: []string{"string"},
Format: "",
},
},
"effect": {
SchemaProps: spec.SchemaProps{
Description: "The effect of the taint on claims that do not tolerate the taint and through such claims on the pods using them. Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for nodes is not valid here.\n\n\nPossible enum values:\n - `\"NoExecute\"` Evict any already-running pods that do not tolerate the device taint.\n - `\"NoSchedule\"` Do not allow new pods to schedule which use a tainted device unless they tolerate the taint, but allow all pods submitted to Kubelet without going through the scheduler to start, and allow all already-running pods to continue running.",
Default: "",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"NoExecute", "NoSchedule"},
},
},
"timeAdded": {
SchemaProps: spec.SchemaProps{
Description: "TimeAdded represents the time at which the taint was added. Added automatically during create or update if not set.",
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
},
},
},
Required: []string{"key", "effect"},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
}
}
func schema_k8sio_api_resource_v1alpha3_DeviceTaintRule(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "DeviceTaintRule adds one taint to all devices which match the selector. This has the same effect as if the taint was specified directly in the ResourceSlice by the DRA driver.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard object metadata",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Spec specifies the selector and one taint.\n\nChanging the spec automatically increments the metadata.generation number.",
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceTaintRuleSpec"),
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceTaintRuleSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_k8sio_api_resource_v1alpha3_DeviceTaintRuleList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "DeviceTaintRuleList is a collection of DeviceTaintRules.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Description: "Standard list metadata",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Description: "Items is the list of DeviceTaintRules.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceTaintRule"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceTaintRule", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_k8sio_api_resource_v1alpha3_DeviceTaintRuleSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "DeviceTaintRuleSpec specifies the selector and one taint.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"deviceSelector": {
SchemaProps: spec.SchemaProps{
Description: "DeviceSelector defines which device(s) the taint is applied to. All selector criteria must be satified for a device to match. The empty selector matches all devices. Without a selector, no devices are matches.",
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceTaintSelector"),
},
},
"taint": {
SchemaProps: spec.SchemaProps{
Description: "The taint that gets applied to matching devices.",
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceTaint"),
},
},
},
Required: []string{"taint"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceTaint", "k8s.io/api/resource/v1alpha3.DeviceTaintSelector"},
}
}
func schema_k8sio_api_resource_v1alpha3_DeviceTaintSelector(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "DeviceTaintSelector defines which device(s) a DeviceTaintRule applies to. The empty selector matches all devices. Without a selector, no devices are matched.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"deviceClassName": {
SchemaProps: spec.SchemaProps{
Description: "If DeviceClassName is set, the selectors defined there must be satisfied by a device to be selected. This field corresponds to class.metadata.name.",
Type: []string{"string"},
Format: "",
},
},
"driver": {
SchemaProps: spec.SchemaProps{
Description: "If driver is set, only devices from that driver are selected. This fields corresponds to slice.spec.driver.",
Type: []string{"string"},
Format: "",
},
},
"pool": {
SchemaProps: spec.SchemaProps{
Description: "If pool is set, only devices in that pool are selected.\n\nAlso setting the driver name may be useful to avoid ambiguity when different drivers use the same pool name, but this is not required because selecting pools from different drivers may also be useful, for example when drivers with node-local devices use the node name as their pool name.",
Type: []string{"string"},
Format: "",
},
},
"device": {
SchemaProps: spec.SchemaProps{
Description: "If device is set, only devices with that name are selected. This field corresponds to slice.spec.devices[].name.\n\nSetting also driver and pool may be required to avoid ambiguity, but is not required.",
Type: []string{"string"},
Format: "",
},
},
"selectors": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "Selectors contains the same selection criteria as a ResourceClaim. Currently, CEL expressions are supported. All of these selectors must be satisfied.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1alpha3.DeviceSelector"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1alpha3.DeviceSelector"},
}
}
func schema_k8sio_api_resource_v1alpha3_DeviceToleration(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key": {
SchemaProps: spec.SchemaProps{
Description: "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. Must be a label name.",
Type: []string{"string"},
Format: "",
},
},
"operator": {
SchemaProps: spec.SchemaProps{
Description: "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a ResourceClaim can tolerate all taints of a particular category.\n\n\nPossible enum values:\n - `\"Equal\"`\n - `\"Exists\"`",
Default: "Equal",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"Equal", "Exists"},
},
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "Value is the taint value the toleration matches to. If the operator is Exists, the value must be empty, otherwise just a regular string. Must be a label value.",
Type: []string{"string"},
Format: "",
},
},
"effect": {
SchemaProps: spec.SchemaProps{
Description: "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule and NoExecute.\n\n\nPossible enum values:\n - `\"NoExecute\"` Evict any already-running pods that do not tolerate the device taint.\n - `\"NoSchedule\"` Do not allow new pods to schedule which use a tainted device unless they tolerate the taint, but allow all pods submitted to Kubelet without going through the scheduler to start, and allow all already-running pods to continue running.",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"NoExecute", "NoSchedule"},
},
},
"tolerationSeconds": {
SchemaProps: spec.SchemaProps{
Description: "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. If larger than zero, the time when the pod needs to be evicted is calculated as <time when taint was adedd> + <toleration seconds>.",
Type: []string{"integer"},
Format: "int64",
},
},
},
},
},
}
}
func schema_k8sio_api_resource_v1alpha3_NetworkDeviceData(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -48727,11 +49096,30 @@ func schema_k8sio_api_resource_v1beta1_BasicDevice(ref common.ReferenceCallback)
},
},
},
"taints": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "If specified, these are the driver-defined taints.\n\nThe maximum number of taints is 8.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1beta1.DeviceTaint"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1beta1.DeviceAttribute", "k8s.io/api/resource/v1beta1.DeviceCapacity"},
"k8s.io/api/resource/v1beta1.DeviceAttribute", "k8s.io/api/resource/v1beta1.DeviceCapacity", "k8s.io/api/resource/v1beta1.DeviceTaint"},
}
}
@ -49378,12 +49766,31 @@ func schema_k8sio_api_resource_v1beta1_DeviceRequest(ref common.ReferenceCallbac
},
},
},
"tolerations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1beta1.DeviceToleration"),
},
},
},
},
},
},
Required: []string{"name"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1beta1.DeviceSelector", "k8s.io/api/resource/v1beta1.DeviceSubRequest"},
"k8s.io/api/resource/v1beta1.DeviceSelector", "k8s.io/api/resource/v1beta1.DeviceSubRequest", "k8s.io/api/resource/v1beta1.DeviceToleration"},
}
}
@ -49433,10 +49840,31 @@ func schema_k8sio_api_resource_v1beta1_DeviceRequestAllocationResult(ref common.
Format: "",
},
},
"tolerations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "A copy of all tolerations specified in the request at the time when the device got allocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1beta1.DeviceToleration"),
},
},
},
},
},
},
Required: []string{"request", "driver", "pool", "device"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1beta1.DeviceToleration"},
}
}
@ -49517,12 +49945,128 @@ func schema_k8sio_api_resource_v1beta1_DeviceSubRequest(ref common.ReferenceCall
Format: "int64",
},
},
"tolerations": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/api/resource/v1beta1.DeviceToleration"),
},
},
},
},
},
},
Required: []string{"name", "deviceClassName"},
},
},
Dependencies: []string{
"k8s.io/api/resource/v1beta1.DeviceSelector"},
"k8s.io/api/resource/v1beta1.DeviceSelector", "k8s.io/api/resource/v1beta1.DeviceToleration"},
}
}
func schema_k8sio_api_resource_v1beta1_DeviceTaint(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The device this taint is attached to has the \"effect\" on any claim which does not tolerate the taint and, through the claim, to pods using the claim.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key": {
SchemaProps: spec.SchemaProps{
Description: "The taint key to be applied to a device. Must be a label name.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "The taint value corresponding to the taint key. Must be a label value.",
Type: []string{"string"},
Format: "",
},
},
"effect": {
SchemaProps: spec.SchemaProps{
Description: "The effect of the taint on claims that do not tolerate the taint and through such claims on the pods using them. Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for nodes is not valid here.\n\n\nPossible enum values:\n - `\"NoExecute\"` Evict any already-running pods that do not tolerate the device taint.\n - `\"NoSchedule\"` Do not allow new pods to schedule which use a tainted device unless they tolerate the taint, but allow all pods submitted to Kubelet without going through the scheduler to start, and allow all already-running pods to continue running.",
Default: "",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"NoExecute", "NoSchedule"},
},
},
"timeAdded": {
SchemaProps: spec.SchemaProps{
Description: "TimeAdded represents the time at which the taint was added. Added automatically during create or update if not set.",
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
},
},
},
Required: []string{"key", "effect"},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
}
}
func schema_k8sio_api_resource_v1beta1_DeviceToleration(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key": {
SchemaProps: spec.SchemaProps{
Description: "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. Must be a label name.",
Type: []string{"string"},
Format: "",
},
},
"operator": {
SchemaProps: spec.SchemaProps{
Description: "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a ResourceClaim can tolerate all taints of a particular category.\n\n\nPossible enum values:\n - `\"Equal\"`\n - `\"Exists\"`",
Default: "Equal",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"Equal", "Exists"},
},
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "Value is the taint value the toleration matches to. If the operator is Exists, the value must be empty, otherwise just a regular string. Must be a label value.",
Type: []string{"string"},
Format: "",
},
},
"effect": {
SchemaProps: spec.SchemaProps{
Description: "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule and NoExecute.\n\n\nPossible enum values:\n - `\"NoExecute\"` Evict any already-running pods that do not tolerate the device taint.\n - `\"NoSchedule\"` Do not allow new pods to schedule which use a tainted device unless they tolerate the taint, but allow all pods submitted to Kubelet without going through the scheduler to start, and allow all already-running pods to continue running.",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"NoExecute", "NoSchedule"},
},
},
"tolerationSeconds": {
SchemaProps: spec.SchemaProps{
Description: "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. If larger than zero, the time when the pod needs to be evicted is calculated as <time when taint was adedd> + <toleration seconds>.",
Type: []string{"integer"},
Format: "int64",
},
},
},
},
},
}
}

View file

@ -37,6 +37,7 @@ import (
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/apis/networking"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/storage"
"k8s.io/kubernetes/pkg/apis/storagemigration"
)
@ -89,6 +90,7 @@ func NewStorageFactoryConfigEffectiveVersion(effectiveVersion basecompatibility.
certificates.Resource("clustertrustbundles").WithVersion("v1beta1"),
storage.Resource("volumeattributesclasses").WithVersion("v1beta1"),
storagemigration.Resource("storagemigrations").WithVersion("v1alpha1"),
resource.Resource("devicetaintrules").WithVersion("v1alpha3"),
}
return &StorageFactoryConfig{

View file

@ -687,6 +687,22 @@ func AddHandlers(h printers.PrintHandler) {
_ = h.TableHandler(nodeResourceSliceColumnDefinitions, printResourceSlice)
_ = h.TableHandler(nodeResourceSliceColumnDefinitions, printResourceSliceList)
deviceTaintColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
// The filter criteria are not printed. They could be lengthy (CEL!) and in practice many of them
// will be empty. Instead, the admin could pick a descriptive name.
//
// The taint is more useful.
{Name: "Key", Type: "string", Description: resourceapi.DeviceTaint{}.SwaggerDoc()["key"]},
{Name: "Value", Type: "string", Description: resourceapi.DeviceTaint{}.SwaggerDoc()["value"]},
{Name: "Effect", Type: "string", Description: resourceapi.DeviceTaint{}.SwaggerDoc()["effect"]},
// TimeAdded and Age are often the same, but not necessarily.
{Name: "TimeAdded", Type: "string", Description: resourceapi.DeviceTaint{}.SwaggerDoc()["timeAdded"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(deviceTaintColumnDefinitions, printDeviceTaint)
_ = h.TableHandler(deviceTaintColumnDefinitions, printDeviceTaintRuleList)
serviceCIDRColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "CIDRs", Type: "string", Description: networkingv1beta1.ServiceCIDRSpec{}.SwaggerDoc()["cidrs"]},
@ -3172,6 +3188,32 @@ func printResourceSliceList(list *resource.ResourceSliceList, options printers.G
return rows, nil
}
func printDeviceTaint(obj *resource.DeviceTaintRule, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
timeAdded := ""
if added := obj.Spec.Taint.TimeAdded; added != nil {
timeAdded = translateTimestampSince(*added)
}
row.Cells = append(row.Cells, obj.Name, string(obj.Spec.Taint.Key), obj.Spec.Taint.Value, string(obj.Spec.Taint.Effect), timeAdded, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
func printDeviceTaintRuleList(list *resource.DeviceTaintRuleList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printDeviceTaint(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printStorageVersionMigration(obj *svmv1alpha1.StorageVersionMigration, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},

View file

@ -0,0 +1,56 @@
/*
Copyright 2025 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 storage
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/resource/devicetaintrule"
)
// REST implements a RESTStorage for DeviceTaintRule.
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against DeviceTaintRule.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &resource.DeviceTaintRule{} },
NewListFunc: func() runtime.Object { return &resource.DeviceTaintRuleList{} },
DefaultQualifiedResource: resource.Resource("devicetaintrules"),
SingularQualifiedResource: resource.Resource("devicetaintrule"),
CreateStrategy: devicetaintrule.Strategy,
UpdateStrategy: devicetaintrule.Strategy,
DeleteStrategy: devicetaintrule.Strategy,
ReturnDeletedObject: true,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
return &REST{store}, nil
}

View file

@ -0,0 +1,144 @@
/*
Copyright 2025 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 storage
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/resource"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, resource.Resource("devicetaintrules"))
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "devicetaintrules",
}
deviceTaintStorage, err := NewREST(restOptions)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return deviceTaintStorage, server
}
func validNewDeviceTaint(name string) *resource.DeviceTaintRule {
return &resource.DeviceTaintRule{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: resource.DeviceTaintRuleSpec{
Taint: resource.DeviceTaint{
Key: "example.com/taint",
Effect: resource.DeviceTaintEffectNoExecute,
},
},
}
}
func TestCreate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
patch := validNewDeviceTaint("foo")
patch.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
test.TestCreate(
// valid
patch,
// invalid
&resource.DeviceTaintRule{
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
},
)
}
func TestUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestUpdate(
// valid
validNewDeviceTaint("foo"),
// updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*resource.DeviceTaintRule)
object.Labels = map[string]string{"foo": "bar"}
return object
},
)
}
func TestDelete(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject()
test.TestDelete(validNewDeviceTaint("foo"))
}
func TestGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestGet(validNewDeviceTaint("foo"))
}
func TestList(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestList(validNewDeviceTaint("foo"))
}
func TestWatch(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestWatch(
validNewDeviceTaint("foo"),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"foo": "bar"},
},
// matching fields
[]fields.Set{
{"metadata.name": "foo"},
},
// not matching fields
[]fields.Set{
{"metadata.name": "bar"},
},
)
}

View file

@ -0,0 +1,84 @@
/*
Copyright 2025 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 devicetaintrule
import (
"context"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/validation"
)
// deviceTaintRuleStrategy implements behavior for DeviceTaintRule objects
type deviceTaintRuleStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
var Strategy = deviceTaintRuleStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
func (deviceTaintRuleStrategy) NamespaceScoped() bool {
return false
}
func (deviceTaintRuleStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
patch := obj.(*resource.DeviceTaintRule)
patch.Generation = 1
}
func (deviceTaintRuleStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
patch := obj.(*resource.DeviceTaintRule)
return validation.ValidateDeviceTaintRule(patch)
}
func (deviceTaintRuleStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (deviceTaintRuleStrategy) Canonicalize(obj runtime.Object) {
}
func (deviceTaintRuleStrategy) AllowCreateOnUpdate() bool {
return false
}
func (deviceTaintRuleStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
patch := obj.(*resource.DeviceTaintRule)
oldPatch := old.(*resource.DeviceTaintRule)
// Any changes to the spec increment the generation number.
if !apiequality.Semantic.DeepEqual(oldPatch.Spec, patch.Spec) {
patch.Generation = oldPatch.Generation + 1
}
}
func (deviceTaintRuleStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateDeviceTaintRuleUpdate(obj.(*resource.DeviceTaintRule), old.(*resource.DeviceTaintRule))
}
func (deviceTaintRuleStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (deviceTaintRuleStrategy) AllowUnconditionalUpdate() bool {
return true
}

View file

@ -0,0 +1,86 @@
/*
Copyright 2025 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 devicetaintrule
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/resource"
)
var patch = &resource.DeviceTaintRule{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-patch",
},
Spec: resource.DeviceTaintRuleSpec{
Taint: resource.DeviceTaint{
Key: "example.com/tainted",
Effect: resource.DeviceTaintEffectNoExecute,
},
},
}
func TestDeviceTaintRuleStrategy(t *testing.T) {
if Strategy.NamespaceScoped() {
t.Errorf("DeviceTaintRule must not be namespace scoped")
}
if Strategy.AllowCreateOnUpdate() {
t.Errorf("DeviceTaintRule should not allow create on update")
}
}
func TestDeviceTaintRuleStrategyCreate(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
patch := patch.DeepCopy()
Strategy.PrepareForCreate(ctx, patch)
errs := Strategy.Validate(ctx, patch)
if len(errs) != 0 {
t.Errorf("unexpected error validating for create %v", errs)
}
}
func TestDeviceTaintRuleStrategyUpdate(t *testing.T) {
t.Run("no-changes-okay", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
patch := patch.DeepCopy()
newPatch := patch.DeepCopy()
newPatch.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newPatch, patch)
errs := Strategy.ValidateUpdate(ctx, newPatch, patch)
if len(errs) != 0 {
t.Errorf("unexpected validation errors: %v", errs)
}
})
t.Run("name-change-not-allowed", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
patch := patch.DeepCopy()
newPatch := patch.DeepCopy()
newPatch.Name = "valid-patch-2"
newPatch.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newPatch, patch)
errs := Strategy.ValidateUpdate(ctx, newPatch, patch)
if len(errs) == 0 {
t.Errorf("expected a validation error")
}
})
}

View file

@ -335,3 +335,5 @@ func dropDeallocatedStatusDevices(newClaim, oldClaim *resource.ResourceClaim) {
newClaim.Status.Devices = nil
}
}
// TODO: add tests after partitionable devices is merged (code conflict!)

View file

@ -28,10 +28,12 @@ import (
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/validation"
"k8s.io/kubernetes/pkg/features"
)
// resourceSliceStrategy implements behavior for ResourceSlice objects
@ -49,6 +51,8 @@ func (resourceSliceStrategy) NamespaceScoped() bool {
func (resourceSliceStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
slice := obj.(*resource.ResourceSlice)
slice.Generation = 1
dropDisabledFields(slice, nil)
}
func (resourceSliceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
@ -75,6 +79,8 @@ func (resourceSliceStrategy) PrepareForUpdate(ctx context.Context, obj, old runt
if !apiequality.Semantic.DeepEqual(oldSlice.Spec, slice.Spec) {
slice.Generation = oldSlice.Generation + 1
}
dropDisabledFields(slice, oldSlice)
}
func (resourceSliceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
@ -147,3 +153,33 @@ func toSelectableFields(slice *resource.ResourceSlice) fields.Set {
// Adds one field.
return generic.AddObjectMetaFieldsSet(fields, &slice.ObjectMeta, false)
}
// dropDisabledFields removes fields which are covered by a feature gate.
func dropDisabledFields(newSlice, oldSlice *resource.ResourceSlice) {
dropDisabledDRADeviceTaintsFields(newSlice, oldSlice)
}
func dropDisabledDRADeviceTaintsFields(newSlice, oldSlice *resource.ResourceSlice) {
if utilfeature.DefaultFeatureGate.Enabled(features.DRADeviceTaints) || draDeviceTaintsFeatureInUse(oldSlice) {
return
}
for _, device := range newSlice.Spec.Devices {
if device.Basic != nil {
device.Basic.Taints = nil
}
}
}
func draDeviceTaintsFeatureInUse(slice *resource.ResourceSlice) bool {
if slice == nil {
return false
}
for _, device := range slice.Spec.Devices {
if device.Basic != nil && len(device.Basic.Taints) > 0 {
return true
}
}
return false
}

View file

@ -19,9 +19,14 @@ package resourceslice
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/features"
)
var slice = &resource.ResourceSlice{
@ -35,9 +40,22 @@ var slice = &resource.ResourceSlice{
Name: "valid-pool-name",
ResourceSliceCount: 1,
},
Devices: []resource.Device{{
Name: "device-0",
Basic: &resource.BasicDevice{},
}},
},
}
var sliceWithDeviceTaints = func() *resource.ResourceSlice {
slice := slice.DeepCopy()
slice.Spec.Devices[0].Basic.Taints = []resource.DeviceTaint{{
Key: "example.com/tainted",
Effect: resource.DeviceTaintEffectNoSchedule,
}}
return slice
}()
func TestResourceSliceStrategy(t *testing.T) {
if Strategy.NamespaceScoped() {
t.Errorf("ResourceSlice must not be namespace scoped")
@ -49,40 +67,186 @@ func TestResourceSliceStrategy(t *testing.T) {
func TestResourceSliceStrategyCreate(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
slice := slice.DeepCopy()
testCases := map[string]struct {
obj *resource.ResourceSlice
deviceTaints bool
expectedValidationError bool
expectObj *resource.ResourceSlice
}{
"simple": {
obj: slice,
expectObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.ObjectMeta.Generation = 1
return obj
}(),
},
"validation error": {
obj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.Name = "%#@$%$"
return obj
}(),
expectedValidationError: true,
},
"drop-fields-device-taints": {
obj: sliceWithDeviceTaints,
deviceTaints: false,
expectObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.Generation = 1
return obj
}(),
},
"keep-fields-device-taints": {
obj: sliceWithDeviceTaints,
deviceTaints: true,
expectObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ObjectMeta.Generation = 1
return obj
}(),
},
}
Strategy.PrepareForCreate(ctx, slice)
errs := Strategy.Validate(ctx, slice)
if len(errs) != 0 {
t.Errorf("unexpected error validating for create %v", errs)
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRADeviceTaints, tc.deviceTaints)
obj := tc.obj.DeepCopy()
Strategy.PrepareForCreate(ctx, obj)
if errs := Strategy.Validate(ctx, obj); len(errs) != 0 {
if !tc.expectedValidationError {
t.Fatalf("unexpected validation errors: %q", errs)
}
return
}
if warnings := Strategy.WarningsOnCreate(ctx, obj); len(warnings) != 0 {
t.Fatalf("unexpected warnings: %q", warnings)
}
Strategy.Canonicalize(obj)
assert.Equal(t, tc.expectObj, obj)
})
}
}
func TestResourceSliceStrategyUpdate(t *testing.T) {
t.Run("no-changes-okay", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
slice := slice.DeepCopy()
newSlice := slice.DeepCopy()
newSlice.ResourceVersion = "4"
ctx := genericapirequest.NewDefaultContext()
Strategy.PrepareForUpdate(ctx, newSlice, slice)
errs := Strategy.ValidateUpdate(ctx, newSlice, slice)
if len(errs) != 0 {
t.Errorf("unexpected validation errors: %v", errs)
}
})
testcases := map[string]struct {
oldObj *resource.ResourceSlice
newObj *resource.ResourceSlice
deviceTaints bool
expectValidationError bool
expectObj *resource.ResourceSlice
}{
"no-changes-okay": {
oldObj: slice,
newObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
expectObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
},
"name-change-not-allowed": {
oldObj: slice,
newObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.Name = "valid-slice-2"
obj.ResourceVersion = "4"
return obj
}(),
expectValidationError: true,
},
"drop-fields-device-taints": {
oldObj: slice,
newObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
deviceTaints: false,
expectObj: func() *resource.ResourceSlice {
obj := slice.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
},
"keep-fields-device-taints": {
oldObj: slice,
newObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
deviceTaints: true,
expectObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ResourceVersion = "4"
obj.Generation = 1
return obj
}(),
},
"keep-existing-fields-device-taints": {
oldObj: sliceWithDeviceTaints,
newObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
deviceTaints: true,
expectObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
},
"keep-existing-fields-device-taints-disabled-feature": {
oldObj: sliceWithDeviceTaints,
newObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
deviceTaints: false,
expectObj: func() *resource.ResourceSlice {
obj := sliceWithDeviceTaints.DeepCopy()
obj.ResourceVersion = "4"
return obj
}(),
},
}
t.Run("name-change-not-allowed", func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
slice := slice.DeepCopy()
newSlice := slice.DeepCopy()
newSlice.Name = "valid-slice-2"
newSlice.ResourceVersion = "4"
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRADeviceTaints, tc.deviceTaints)
Strategy.PrepareForUpdate(ctx, newSlice, slice)
errs := Strategy.ValidateUpdate(ctx, newSlice, slice)
if len(errs) == 0 {
t.Errorf("expected a validation error")
}
})
oldObj := tc.oldObj.DeepCopy()
newObj := tc.newObj.DeepCopy()
Strategy.PrepareForUpdate(ctx, newObj, oldObj)
if errs := Strategy.ValidateUpdate(ctx, newObj, oldObj); len(errs) != 0 {
if !tc.expectValidationError {
t.Fatalf("unexpected validation errors: %q", errs)
}
return
} else if tc.expectValidationError {
t.Fatal("expected validation error(s), got none")
}
if warnings := Strategy.WarningsOnUpdate(ctx, newObj, oldObj); len(warnings) != 0 {
t.Fatalf("unexpected warnings: %q", warnings)
}
Strategy.Canonicalize(newObj)
expectObj := tc.expectObj.DeepCopy()
assert.Equal(t, expectObj, newObj)
})
}
}

View file

@ -27,6 +27,7 @@ import (
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
deviceclassstore "k8s.io/kubernetes/pkg/registry/resource/deviceclass/storage"
devicetaintrulestore "k8s.io/kubernetes/pkg/registry/resource/devicetaintrule/storage"
resourceclaimstore "k8s.io/kubernetes/pkg/registry/resource/resourceclaim/storage"
resourceclaimtemplatestore "k8s.io/kubernetes/pkg/registry/resource/resourceclaimtemplate/storage"
resourceslicestore "k8s.io/kubernetes/pkg/registry/resource/resourceslice/storage"
@ -96,6 +97,14 @@ func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstora
storage[resource] = resourceSliceStorage
}
if resource := "devicetaintrules"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
deviceTaintStorage, err := devicetaintrulestore.NewREST(restOptionsGetter)
if err != nil {
return nil, err
}
storage[resource] = deviceTaintStorage
}
return storage, nil
}

View file

@ -33,6 +33,7 @@ import (
"k8s.io/client-go/tools/cache"
corev1helpers "k8s.io/component-helpers/scheduling/corev1"
corev1nodeaffinity "k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
resourceslicetracker "k8s.io/dynamic-resource-allocation/resourceslice/tracker"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/scheduler/backend/queue"
@ -366,6 +367,7 @@ func addAllEventHandlers(
informerFactory informers.SharedInformerFactory,
dynInformerFactory dynamicinformer.DynamicSharedInformerFactory,
resourceClaimCache *assumecache.AssumeCache,
resourceSliceTracker *resourceslicetracker.Tracker,
gvkMap map[framework.EventResource]framework.ActionType,
) error {
var (
@ -555,7 +557,7 @@ func addAllEventHandlers(
}
case framework.ResourceSlice:
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
if handlerRegistration, err = informerFactory.Resource().V1beta1().ResourceSlices().Informer().AddEventHandler(
if handlerRegistration, err = resourceSliceTracker.AddEventHandler(
buildEvtResHandler(at, framework.ResourceSlice),
); err != nil {
return err

View file

@ -28,12 +28,14 @@ import (
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
resourcealphaapi "k8s.io/api/resource/v1alpha3"
resourceapi "k8s.io/api/resource/v1beta1"
storagev1 "k8s.io/api/storage/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
resourceslicetracker "k8s.io/dynamic-resource-allocation/resourceslice/tracker"
"k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting"
@ -395,6 +397,7 @@ func TestAddAllEventHandlers(t *testing.T) {
name string
gvkMap map[framework.EventResource]framework.ActionType
enableDRA bool
enableDRADeviceTaints bool
expectStaticInformers map[reflect.Type]bool
expectDynamicInformers map[schema.GroupVersionResource]bool
}{
@ -423,7 +426,7 @@ func TestAddAllEventHandlers(t *testing.T) {
expectDynamicInformers: map[schema.GroupVersionResource]bool{},
},
{
name: "all DRA events enabled",
name: "core DRA events enabled",
gvkMap: map[framework.EventResource]framework.ActionType{
framework.ResourceClaim: framework.Add,
framework.ResourceSlice: framework.Add,
@ -440,6 +443,26 @@ func TestAddAllEventHandlers(t *testing.T) {
},
expectDynamicInformers: map[schema.GroupVersionResource]bool{},
},
{
name: "all DRA events enabled",
gvkMap: map[framework.EventResource]framework.ActionType{
framework.ResourceClaim: framework.Add,
framework.ResourceSlice: framework.Add,
framework.DeviceClass: framework.Add,
},
enableDRA: true,
enableDRADeviceTaints: true,
expectStaticInformers: map[reflect.Type]bool{
reflect.TypeOf(&v1.Pod{}): true,
reflect.TypeOf(&v1.Node{}): true,
reflect.TypeOf(&v1.Namespace{}): true,
reflect.TypeOf(&resourceapi.ResourceClaim{}): true,
reflect.TypeOf(&resourceapi.ResourceSlice{}): true,
reflect.TypeOf(&resourcealphaapi.DeviceTaintRule{}): true,
reflect.TypeOf(&resourceapi.DeviceClass{}): true,
},
expectDynamicInformers: map[schema.GroupVersionResource]bool{},
},
{
name: "add GVKs handlers defined in framework dynamically",
gvkMap: map[framework.EventResource]framework.ActionType{
@ -499,6 +522,7 @@ func TestAddAllEventHandlers(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DynamicResourceAllocation, tt.enableDRA)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRADeviceTaints, tt.enableDRADeviceTaints)
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
@ -515,12 +539,27 @@ func TestAddAllEventHandlers(t *testing.T) {
dynclient := dyfake.NewSimpleDynamicClient(scheme)
dynInformerFactory := dynamicinformer.NewDynamicSharedInformerFactory(dynclient, 0)
var resourceClaimCache *assumecache.AssumeCache
var resourceSliceTracker *resourceslicetracker.Tracker
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
resourceClaimInformer := informerFactory.Resource().V1beta1().ResourceClaims().Informer()
resourceClaimCache = assumecache.NewAssumeCache(logger, resourceClaimInformer, "ResourceClaim", "", nil)
var err error
opts := resourceslicetracker.Options{
EnableDeviceTaints: utilfeature.DefaultFeatureGate.Enabled(features.DRADeviceTaints),
SliceInformer: informerFactory.Resource().V1beta1().ResourceSlices(),
}
if opts.EnableDeviceTaints {
opts.TaintInformer = informerFactory.Resource().V1alpha3().DeviceTaintRules()
opts.ClassInformer = informerFactory.Resource().V1beta1().DeviceClasses()
}
resourceSliceTracker, err = resourceslicetracker.StartTracker(ctx, opts)
if err != nil {
t.Fatalf("couldn't start resource slice tracker: %v", err)
}
}
if err := addAllEventHandlers(&testSched, informerFactory, dynInformerFactory, resourceClaimCache, tt.gvkMap); err != nil {
if err := addAllEventHandlers(&testSched, informerFactory, dynInformerFactory, resourceClaimCache, resourceSliceTracker, tt.gvkMap); err != nil {
t.Fatalf("Add event handlers failed, error = %v", err)
}

View file

@ -72,7 +72,7 @@ func (c *shareListerContract) StorageInfos() framework.StorageInfoLister {
type resourceSliceListerContract struct{}
func (c *resourceSliceListerContract) List() ([]*resourceapi.ResourceSlice, error) {
func (c *resourceSliceListerContract) ListWithDeviceTaintRules() ([]*resourceapi.ResourceSlice, error) {
return nil, nil
}

View file

@ -50,8 +50,13 @@ type SharedLister interface {
// ResourceSliceLister can be used to obtain ResourceSlices.
type ResourceSliceLister interface {
// List returns a list of all ResourceSlices.
List() ([]*resourceapi.ResourceSlice, error)
// ListWithDeviceTaintRules returns a list of all ResourceSlices with DeviceTaintRules applied
// if the DRADeviceTaints feature is enabled, otherwise without them.
//
// k8s.io/dynamic-resource-allocation/resourceslice/tracker provides an implementation
// of the necessary logic. That tracker can be instantiated as a replacement for
// a normal ResourceSlice informer and provides a ListPatchedResourceSlices method.
ListWithDeviceTaintRules() ([]*resourceapi.ResourceSlice, error)
}
// DeviceClassLister can be used to obtain DeviceClasses.

View file

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/informers"
resourcelisters "k8s.io/client-go/listers/resource/v1beta1"
resourceslicetracker "k8s.io/dynamic-resource-allocation/resourceslice/tracker"
"k8s.io/dynamic-resource-allocation/structured"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/scheduler/framework"
@ -44,8 +45,9 @@ type DefaultDRAManager struct {
deviceClassLister *deviceClassLister
}
func NewDRAManager(ctx context.Context, claimsCache *assumecache.AssumeCache, informerFactory informers.SharedInformerFactory) *DefaultDRAManager {
func NewDRAManager(ctx context.Context, claimsCache *assumecache.AssumeCache, resourceSliceTracker *resourceslicetracker.Tracker, informerFactory informers.SharedInformerFactory) *DefaultDRAManager {
logger := klog.FromContext(ctx)
manager := &DefaultDRAManager{
resourceClaimTracker: &claimTracker{
cache: claimsCache,
@ -53,7 +55,7 @@ func NewDRAManager(ctx context.Context, claimsCache *assumecache.AssumeCache, in
allocatedDevices: newAllocatedDevices(logger),
logger: logger,
},
resourceSliceLister: &resourceSliceLister{sliceLister: informerFactory.Resource().V1beta1().ResourceSlices().Lister()},
resourceSliceLister: &resourceSliceLister{tracker: resourceSliceTracker},
deviceClassLister: &deviceClassLister{classLister: informerFactory.Resource().V1beta1().DeviceClasses().Lister()},
}
@ -79,11 +81,11 @@ func (s *DefaultDRAManager) DeviceClasses() framework.DeviceClassLister {
var _ framework.ResourceSliceLister = &resourceSliceLister{}
type resourceSliceLister struct {
sliceLister resourcelisters.ResourceSliceLister
tracker *resourceslicetracker.Tracker
}
func (l *resourceSliceLister) List() ([]*resourceapi.ResourceSlice, error) {
return l.sliceLister.List(labels.Everything())
func (l *resourceSliceLister) ListWithDeviceTaintRules() ([]*resourceapi.ResourceSlice, error) {
return l.tracker.ListPatchedResourceSlices()
}
var _ framework.DeviceClassLister = &deviceClassLister{}

View file

@ -106,6 +106,7 @@ type DynamicResources struct {
enableAdminAccess bool
enablePrioritizedList bool
enableSchedulingQueueHint bool
enableDeviceTaints bool
fh framework.Handle
clientset kubernetes.Interface
@ -123,6 +124,7 @@ func New(ctx context.Context, plArgs runtime.Object, fh framework.Handle, fts fe
pl := &DynamicResources{
enabled: true,
enableAdminAccess: fts.EnableDRAAdminAccess,
enableDeviceTaints: fts.EnableDRADeviceTaints,
enablePrioritizedList: fts.EnableDRAPrioritizedList,
enableSchedulingQueueHint: fts.EnableSchedulingQueueHint,
@ -448,11 +450,11 @@ func (pl *DynamicResources) PreFilter(ctx context.Context, state *framework.Cycl
if err != nil {
return nil, statusError(logger, err)
}
slices, err := pl.draManager.ResourceSlices().List()
slices, err := pl.draManager.ResourceSlices().ListWithDeviceTaintRules()
if err != nil {
return nil, statusError(logger, err)
}
allocator, err := structured.NewAllocator(ctx, pl.enableAdminAccess, pl.enablePrioritizedList, allocateClaims, allAllocatedDevices, pl.draManager.DeviceClasses(), slices, pl.celCache)
allocator, err := structured.NewAllocator(ctx, pl.enableAdminAccess, pl.enablePrioritizedList, pl.enableDeviceTaints, allocateClaims, allAllocatedDevices, pl.draManager.DeviceClasses(), slices, pl.celCache)
if err != nil {
return nil, statusError(logger, err)
}

View file

@ -38,6 +38,7 @@ import (
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
cgotesting "k8s.io/client-go/testing"
resourceslicetracker "k8s.io/dynamic-resource-allocation/resourceslice/tracker"
"k8s.io/kubernetes/pkg/scheduler/framework"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/feature"
"k8s.io/kubernetes/pkg/scheduler/framework/runtime"
@ -95,7 +96,7 @@ var (
// Node with "instance-1" device and no device attributes.
workerNode = &st.MakeNode().Name(nodeName).Label("kubernetes.io/hostname", nodeName).Node
workerNodeSlice = st.MakeResourceSlice(nodeName, driver).Device("instance-1", nil).Obj()
workerNodeSlice = st.MakeResourceSlice(nodeName, driver).Device("instance-1").Obj()
// Node with same device, but now with a "healthy" boolean attribute.
workerNode2 = &st.MakeNode().Name(node2Name).Label("kubernetes.io/hostname", node2Name).Node
@ -183,8 +184,22 @@ var (
otherAllocatedClaim = st.FromResourceClaim(otherClaim).
Allocation(allocationResult).
Obj()
deviceTaint = resourceapi.DeviceTaint{
Key: "taint-key",
Value: "taint-value",
Effect: resourceapi.DeviceTaintEffectNoSchedule,
}
)
func taintDevices(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice {
slice = slice.DeepCopy()
for i := range slice.Spec.Devices {
slice.Spec.Devices[i].Basic.Taints = append(slice.Spec.Devices[i].Basic.Taints, deviceTaint)
}
return slice
}
func reserve(claim *resourceapi.ResourceClaim, pod *v1.Pod) *resourceapi.ResourceClaim {
return st.FromResourceClaim(claim).
ReservedForPod(pod.Name, types.UID(pod.UID)).
@ -343,6 +358,7 @@ func TestPlugin(t *testing.T) {
disableDRA bool
enableDRAPrioritizedList bool
enableDRADeviceTaints bool
}{
"empty": {
pod: st.MakePod().Name("foo").Namespace("default").Obj(),
@ -604,6 +620,56 @@ func TestPlugin(t *testing.T) {
},
},
// The two test cases for device tainting only need to cover
// whether the feature gate is passed through to the allocator
// correctly. The actual logic around device taints and allocation
// is in the allocator.
"tainted-device-disabled": {
enableDRADeviceTaints: false,
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{taintDevices(workerNodeSlice)},
want: want{
reserve: result{
inFlightClaim: allocatedClaim,
},
prebind: result{
assumedClaim: reserve(allocatedClaim, podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
claim = claim.DeepCopy()
claim.Finalizers = allocatedClaim.Finalizers
claim.Status = inUseClaim.Status
}
return claim
},
},
},
postbind: result{
assumedClaim: reserve(allocatedClaim, podWithClaimName),
},
},
},
"tainted-device-enabled": {
enableDRADeviceTaints: true,
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{taintDevices(workerNodeSlice)},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `cannot allocate all claims`),
},
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `still not schedulable`),
},
},
},
"request-admin-access-with-DRAAdminAccess-featuregate": {
// When the DRAAdminAccess feature gate is enabled,
// Because the pending claim asks for admin access,
@ -920,6 +986,7 @@ func TestPlugin(t *testing.T) {
}
features := feature.Features{
EnableDRAAdminAccess: tc.enableDRAAdminAccess,
EnableDRADeviceTaints: tc.enableDRADeviceTaints,
EnableDynamicResourceAllocation: !tc.disableDRA,
EnableDRAPrioritizedList: tc.enableDRAPrioritizedList,
}
@ -1191,7 +1258,16 @@ func setup(t *testing.T, nodes []*v1.Node, claims []*resourceapi.ResourceClaim,
tc.client.PrependReactor("*", "*", reactor)
tc.informerFactory = informers.NewSharedInformerFactory(tc.client, 0)
tc.draManager = NewDRAManager(tCtx, assumecache.NewAssumeCache(tCtx.Logger(), tc.informerFactory.Resource().V1beta1().ResourceClaims().Informer(), "resource claim", "", nil), tc.informerFactory)
resourceSliceTrackerOpts := resourceslicetracker.Options{
EnableDeviceTaints: true,
SliceInformer: tc.informerFactory.Resource().V1beta1().ResourceSlices(),
TaintInformer: tc.informerFactory.Resource().V1alpha3().DeviceTaintRules(),
ClassInformer: tc.informerFactory.Resource().V1beta1().DeviceClasses(),
KubeClient: tc.client,
}
resourceSliceTracker, err := resourceslicetracker.StartTracker(tCtx, resourceSliceTrackerOpts)
require.NoError(t, err, "couldn't start resource slice tracker")
tc.draManager = NewDRAManager(tCtx, assumecache.NewAssumeCache(tCtx.Logger(), tc.informerFactory.Resource().V1beta1().ResourceClaims().Informer(), "resource claim", "", nil), resourceSliceTracker, tc.informerFactory)
opts := []runtime.Option{
runtime.WithClientSet(tc.client),
runtime.WithInformerFactory(tc.informerFactory),

View file

@ -22,6 +22,7 @@ package feature
type Features struct {
EnableDRAPrioritizedList bool
EnableDRAAdminAccess bool
EnableDRADeviceTaints bool
EnableDynamicResourceAllocation bool
EnableVolumeAttributesClass bool
EnableCSIMigrationPortworx bool

View file

@ -48,6 +48,7 @@ func NewInTreeRegistry() runtime.Registry {
fts := plfeature.Features{
EnableDRAPrioritizedList: feature.DefaultFeatureGate.Enabled(features.DRAPrioritizedList),
EnableDRAAdminAccess: feature.DefaultFeatureGate.Enabled(features.DRAAdminAccess),
EnableDRADeviceTaints: feature.DefaultFeatureGate.Enabled(features.DRADeviceTaints),
EnableDynamicResourceAllocation: feature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation),
EnableVolumeAttributesClass: feature.DefaultFeatureGate.Enabled(features.VolumeAttributesClass),
EnableCSIMigrationPortworx: feature.DefaultFeatureGate.Enabled(features.CSIMigrationPortworx),

View file

@ -33,6 +33,7 @@ import (
clientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
resourceslicetracker "k8s.io/dynamic-resource-allocation/resourceslice/tracker"
"k8s.io/klog/v2"
configv1 "k8s.io/kube-scheduler/config/v1"
"k8s.io/kubernetes/pkg/features"
@ -307,11 +308,27 @@ func New(ctx context.Context,
waitingPods := frameworkruntime.NewWaitingPodsMap()
var resourceClaimCache *assumecache.AssumeCache
var resourceSliceTracker *resourceslicetracker.Tracker
var draManager framework.SharedDRAManager
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
resourceClaimInformer := informerFactory.Resource().V1beta1().ResourceClaims().Informer()
resourceClaimCache = assumecache.NewAssumeCache(logger, resourceClaimInformer, "ResourceClaim", "", nil)
draManager = dynamicresources.NewDRAManager(ctx, resourceClaimCache, informerFactory)
resourceSliceTrackerOpts := resourceslicetracker.Options{
EnableDeviceTaints: utilfeature.DefaultFeatureGate.Enabled(features.DRADeviceTaints),
SliceInformer: informerFactory.Resource().V1beta1().ResourceSlices(),
KubeClient: client,
}
// If device taints are disabled, the additional informers are not needed and
// the tracker turns into a simple wrapper around the slice informer.
if resourceSliceTrackerOpts.EnableDeviceTaints {
resourceSliceTrackerOpts.TaintInformer = informerFactory.Resource().V1alpha3().DeviceTaintRules()
resourceSliceTrackerOpts.ClassInformer = informerFactory.Resource().V1beta1().DeviceClasses()
}
resourceSliceTracker, err = resourceslicetracker.StartTracker(ctx, resourceSliceTrackerOpts)
if err != nil {
return nil, fmt.Errorf("couldn't start resource slice tracker: %w", err)
}
draManager = dynamicresources.NewDRAManager(ctx, resourceClaimCache, resourceSliceTracker, informerFactory)
}
profiles, err := profile.NewMap(ctx, options.profiles, registry, recorderFactory,
@ -389,7 +406,7 @@ func New(ctx context.Context,
sched.NextPod = podQueue.Pop
sched.applyDefaultHandlers()
if err = addAllEventHandlers(sched, informerFactory, dynInformerFactory, resourceClaimCache, unionedGVKs(queueingHintsPerProfile)); err != nil {
if err = addAllEventHandlers(sched, informerFactory, dynInformerFactory, resourceClaimCache, resourceSliceTracker, unionedGVKs(queueingHintsPerProfile)); err != nil {
return nil, fmt.Errorf("adding event handlers: %w", err)
}

View file

@ -1188,9 +1188,23 @@ func (wrapper *ResourceSliceWrapper) Devices(names ...string) *ResourceSliceWrap
return wrapper
}
// Device sets the devices field of the inner object.
func (wrapper *ResourceSliceWrapper) Device(name string, attrs map[resourceapi.QualifiedName]resourceapi.DeviceAttribute) *ResourceSliceWrapper {
wrapper.Spec.Devices = append(wrapper.Spec.Devices, resourceapi.Device{Name: name, Basic: &resourceapi.BasicDevice{Attributes: attrs}})
// Device extends the devices field of the inner object.
// The device must have a name and may have arbitrary additional fields.
func (wrapper *ResourceSliceWrapper) Device(name string, otherFields ...any) *ResourceSliceWrapper {
device := resourceapi.Device{Name: name, Basic: &resourceapi.BasicDevice{}}
for _, field := range otherFields {
switch typedField := field.(type) {
case map[resourceapi.QualifiedName]resourceapi.DeviceAttribute:
device.Basic.Attributes = typedField
case map[resourceapi.QualifiedName]resourceapi.DeviceCapacity:
device.Basic.Capacity = typedField
case resourceapi.DeviceTaint:
device.Basic.Taints = append(device.Basic.Taints, typedField)
default:
panic(fmt.Sprintf("expected a type which matches a field in BasicDevice, got %T", field))
}
}
wrapper.Spec.Devices = append(wrapper.Spec.Devices, device)
return wrapper
}

View file

@ -215,6 +215,24 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
eventsRule(),
},
})
if utilfeature.DefaultFeatureGate.Enabled(features.DRADeviceTaints) {
addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{
// Same name as in k8s.io/kubernetes/cmd/kube-controller-manager/names.
ObjectMeta: metav1.ObjectMeta{Name: saRolePrefix + "device-taint-eviction-controller"},
Rules: []rbacv1.PolicyRule{
// Deletes pods to evict them.
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(legacyGroup).Resources("pods").RuleOrDie(),
// Sets pod conditions.
rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("pods/status").RuleOrDie(),
// The rest is read-only.
rbacv1helpers.NewRule("get", "list", "watch").Groups(resourceGroup).Resources("resourceclaims").RuleOrDie(),
rbacv1helpers.NewRule("get", "list", "watch").Groups(resourceGroup).Resources("resourceslices").RuleOrDie(),
rbacv1helpers.NewRule("get", "list", "watch").Groups(resourceGroup).Resources("deviceclasses").RuleOrDie(),
rbacv1helpers.NewRule("get", "list", "watch").Groups(resourceGroup).Resources("devicetaintrules").RuleOrDie(),
eventsRule(),
},
})
}
}
addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{

View file

@ -628,6 +628,9 @@ func ClusterRoles() []rbacv1.ClusterRole {
rbacv1helpers.NewRule(ReadUpdate...).Groups(legacyGroup).Resources("pods/finalizers").RuleOrDie(),
rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("resourceslices").RuleOrDie(),
)
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
kubeSchedulerRules = append(kubeSchedulerRules, rbacv1helpers.NewRule(Read...).Groups(resourceGroup).Resources("devicetaintrules").RuleOrDie())
}
}
roles = append(roles, rbacv1.ClusterRole{
// a role to use for the kube-scheduler

View file

@ -968,6 +968,14 @@ items:
- get
- list
- watch
- apiGroups:
- resource.k8s.io
resources:
- devicetaintrules
verbs:
- get
- list
- watch
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:

File diff suppressed because it is too large Load diff

View file

@ -113,6 +113,18 @@ message BasicDevice {
//
// +optional
map<string, .k8s.io.apimachinery.pkg.api.resource.Quantity> capacity = 2;
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceTaint taints = 3;
}
// CELDeviceSelector contains a CEL expression for selecting a device.
@ -520,6 +532,32 @@ message DeviceRequest {
// +listType=atomic
// +featureGate=DRAPrioritizedList
repeated DeviceSubRequest firstAvailable = 7;
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceToleration tolerations = 8;
}
// DeviceRequestAllocationResult contains the allocation result for one request.
@ -571,6 +609,19 @@ message DeviceRequestAllocationResult {
// +optional
// +featureGate=DRAAdminAccess
optional bool adminAccess = 5;
// A copy of all tolerations specified in the request at the time
// when the device got allocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceToleration tolerations = 6;
}
// DeviceSelector must have exactly one field set.
@ -652,6 +703,190 @@ message DeviceSubRequest {
// +optional
// +oneOf=AllocationMode
optional int64 count = 5;
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceToleration tolerations = 7;
}
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
// to pods using the claim.
message DeviceTaint {
// The taint key to be applied to a device.
// Must be a label name.
//
// +required
optional string key = 1;
// The taint value corresponding to the taint key.
// Must be a label value.
//
// +optional
optional string value = 2;
// The effect of the taint on claims that do not tolerate the taint
// and through such claims on the pods using them.
// Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for
// nodes is not valid here.
//
// +required
optional string effect = 3;
// TimeAdded represents the time at which the taint was added.
// Added automatically during create or update if not set.
//
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time timeAdded = 4;
}
// DeviceTaintRule adds one taint to all devices which match the selector.
// This has the same effect as if the taint was specified directly
// in the ResourceSlice by the DRA driver.
message DeviceTaintRule {
// Standard object metadata
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// Spec specifies the selector and one taint.
//
// Changing the spec automatically increments the metadata.generation number.
optional DeviceTaintRuleSpec spec = 2;
}
// DeviceTaintRuleList is a collection of DeviceTaintRules.
message DeviceTaintRuleList {
// Standard list metadata
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
// Items is the list of DeviceTaintRules.
repeated DeviceTaintRule items = 2;
}
// DeviceTaintRuleSpec specifies the selector and one taint.
message DeviceTaintRuleSpec {
// DeviceSelector defines which device(s) the taint is applied to.
// All selector criteria must be satified for a device to
// match. The empty selector matches all devices. Without
// a selector, no devices are matches.
//
// +optional
optional DeviceTaintSelector deviceSelector = 1;
// The taint that gets applied to matching devices.
//
// +required
optional DeviceTaint taint = 2;
}
// DeviceTaintSelector defines which device(s) a DeviceTaintRule applies to.
// The empty selector matches all devices. Without a selector, no devices
// are matched.
message DeviceTaintSelector {
// If DeviceClassName is set, the selectors defined there must be
// satisfied by a device to be selected. This field corresponds
// to class.metadata.name.
//
// +optional
optional string deviceClassName = 1;
// If driver is set, only devices from that driver are selected.
// This fields corresponds to slice.spec.driver.
//
// +optional
optional string driver = 2;
// If pool is set, only devices in that pool are selected.
//
// Also setting the driver name may be useful to avoid
// ambiguity when different drivers use the same pool name,
// but this is not required because selecting pools from
// different drivers may also be useful, for example when
// drivers with node-local devices use the node name as
// their pool name.
//
// +optional
optional string pool = 3;
// If device is set, only devices with that name are selected.
// This field corresponds to slice.spec.devices[].name.
//
// Setting also driver and pool may be required to avoid ambiguity,
// but is not required.
//
// +optional
optional string device = 4;
// Selectors contains the same selection criteria as a ResourceClaim.
// Currently, CEL expressions are supported. All of these selectors
// must be satisfied.
//
// +optional
// +listType=atomic
repeated DeviceSelector selectors = 5;
}
// The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches
// the triple <key,value,effect> using the matching operator <operator>.
message DeviceToleration {
// Key is the taint key that the toleration applies to. Empty means match all taint keys.
// If the key is empty, operator must be Exists; this combination means to match all values and all keys.
// Must be a label name.
//
// +optional
optional string key = 1;
// Operator represents a key's relationship to the value.
// Valid operators are Exists and Equal. Defaults to Equal.
// Exists is equivalent to wildcard for value, so that a ResourceClaim can
// tolerate all taints of a particular category.
//
// +optional
// +default="Equal"
optional string operator = 2;
// Value is the taint value the toleration matches to.
// If the operator is Exists, the value must be empty, otherwise just a regular string.
// Must be a label value.
//
// +optional
optional string value = 3;
// Effect indicates the taint effect to match. Empty means match all taint effects.
// When specified, allowed values are NoSchedule and NoExecute.
//
// +optional
optional string effect = 4;
// TolerationSeconds represents the period of time the toleration (which must be
// of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
// it is not set, which means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by the system.
// If larger than zero, the time when the pod needs to be evicted is calculated as <time when
// taint was adedd> + <toleration seconds>.
//
// +optional
optional int64 tolerationSeconds = 5;
}
// NetworkDeviceData provides network-related details for the allocated device.

View file

@ -52,6 +52,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&ResourceClaimTemplateList{},
&ResourceSlice{},
&ResourceSliceList{},
&DeviceTaintRule{},
&DeviceTaintRuleList{},
)
// Add the watch version that applies

View file

@ -223,6 +223,18 @@ type BasicDevice struct {
//
// +optional
Capacity map[QualifiedName]resource.Quantity `json:"capacity,omitempty" protobuf:"bytes,2,rep,name=capacity"`
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Taints []DeviceTaint `json:"taints,omitempty" protobuf:"bytes,3,rep,name=taints"`
}
// Limit for the sum of the number of entries in both attributes and capacity.
@ -290,6 +302,64 @@ type DeviceAttribute struct {
// DeviceAttributeMaxValueLength is the maximum length of a string or version attribute value.
const DeviceAttributeMaxValueLength = 64
// DeviceTaintsMaxLength is the maximum number of taints per device.
const DeviceTaintsMaxLength = 8
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
// to pods using the claim.
type DeviceTaint struct {
// The taint key to be applied to a device.
// Must be a label name.
//
// +required
Key string `json:"key" protobuf:"bytes,1,name=key"`
// The taint value corresponding to the taint key.
// Must be a label value.
//
// +optional
Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`
// The effect of the taint on claims that do not tolerate the taint
// and through such claims on the pods using them.
// Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for
// nodes is not valid here.
//
// +required
Effect DeviceTaintEffect `json:"effect" protobuf:"bytes,3,name=effect,casttype=DeviceTaintEffect"`
// ^^^^
//
// Implementing PreferNoSchedule would depend on a scoring solution for DRA.
// It might get added as part of that.
// TimeAdded represents the time at which the taint was added.
// Added automatically during create or update if not set.
//
// +optional
TimeAdded *metav1.Time `json:"timeAdded,omitempty" protobuf:"bytes,4,opt,name=timeAdded"`
// ^^^
//
// This field was defined as "It is only written for NoExecute taints." for node taints.
// But in practice, Kubernetes never did anything with it (no validation, no defaulting,
// ignored during pod eviction in pkg/controller/tainteviction).
}
// +enum
type DeviceTaintEffect string
const (
// Do not allow new pods to schedule which use a tainted device unless they tolerate the taint,
// but allow all pods submitted to Kubelet without going through the scheduler
// to start, and allow all already-running pods to continue running.
DeviceTaintEffectNoSchedule DeviceTaintEffect = "NoSchedule"
// Evict any already-running pods that do not tolerate the device taint.
DeviceTaintEffectNoExecute DeviceTaintEffect = "NoExecute"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.31
// +k8s:prerelease-lifecycle-gen:replacement=resource.k8s.io,v1beta1,ResourceSliceList
@ -508,6 +578,32 @@ type DeviceRequest struct {
// +listType=atomic
// +featureGate=DRAPrioritizedList
FirstAvailable []DeviceSubRequest `json:"firstAvailable,omitempty" protobuf:"bytes,7,name=firstAvailable"`
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,8,opt,name=tolerations"`
}
// DeviceSubRequest describes a request for device provided in the
@ -580,11 +676,35 @@ type DeviceSubRequest struct {
// +optional
// +oneOf=AllocationMode
Count int64 `json:"count,omitempty" protobuf:"bytes,5,opt,name=count"`
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,7,opt,name=tolerations"`
}
const (
DeviceSelectorsMaxSize = 32
FirstAvailableDeviceRequestMaxSize = 8
DeviceTolerationsMaxLength = 16
)
type DeviceAllocationMode string
@ -790,6 +910,59 @@ type OpaqueDeviceConfiguration struct {
// [OpaqueDeviceConfiguration.Parameters] field.
const OpaqueParametersMaxLength = 10 * 1024
// The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches
// the triple <key,value,effect> using the matching operator <operator>.
type DeviceToleration struct {
// Key is the taint key that the toleration applies to. Empty means match all taint keys.
// If the key is empty, operator must be Exists; this combination means to match all values and all keys.
// Must be a label name.
//
// +optional
Key string `json:"key,omitempty" protobuf:"bytes,1,opt,name=key"`
// Operator represents a key's relationship to the value.
// Valid operators are Exists and Equal. Defaults to Equal.
// Exists is equivalent to wildcard for value, so that a ResourceClaim can
// tolerate all taints of a particular category.
//
// +optional
// +default="Equal"
Operator DeviceTolerationOperator `json:"operator,omitempty" protobuf:"bytes,2,opt,name=operator,casttype=DeviceTolerationOperator"`
// Value is the taint value the toleration matches to.
// If the operator is Exists, the value must be empty, otherwise just a regular string.
// Must be a label value.
//
// +optional
Value string `json:"value,omitempty" protobuf:"bytes,3,opt,name=value"`
// Effect indicates the taint effect to match. Empty means match all taint effects.
// When specified, allowed values are NoSchedule and NoExecute.
//
// +optional
Effect DeviceTaintEffect `json:"effect,omitempty" protobuf:"bytes,4,opt,name=effect,casttype=DeviceTaintEffect"`
// TolerationSeconds represents the period of time the toleration (which must be
// of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
// it is not set, which means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by the system.
// If larger than zero, the time when the pod needs to be evicted is calculated as <time when
// taint was adedd> + <toleration seconds>.
//
// +optional
TolerationSeconds *int64 `json:"tolerationSeconds,omitempty" protobuf:"varint,5,opt,name=tolerationSeconds"`
}
// A toleration operator is the set of operators that can be used in a toleration.
//
// +enum
type DeviceTolerationOperator string
const (
DeviceTolerationOpExists DeviceTolerationOperator = "Exists"
DeviceTolerationOpEqual DeviceTolerationOperator = "Equal"
)
// ResourceClaimStatus tracks whether the resource has been allocated and what
// the result of that was.
type ResourceClaimStatus struct {
@ -960,6 +1133,19 @@ type DeviceRequestAllocationResult struct {
// +optional
// +featureGate=DRAAdminAccess
AdminAccess *bool `json:"adminAccess" protobuf:"bytes,5,name=adminAccess"`
// A copy of all tolerations specified in the request at the time
// when the device got allocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,6,opt,name=tolerations"`
}
// DeviceAllocationConfiguration gets embedded in an AllocationResult.
@ -1237,3 +1423,107 @@ type NetworkDeviceData struct {
// +optional
HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"`
}
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.33
// DeviceTaintRule adds one taint to all devices which match the selector.
// This has the same effect as if the taint was specified directly
// in the ResourceSlice by the DRA driver.
type DeviceTaintRule struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Spec specifies the selector and one taint.
//
// Changing the spec automatically increments the metadata.generation number.
Spec DeviceTaintRuleSpec `json:"spec" protobuf:"bytes,2,name=spec"`
// ^^^
// A spec gets added because adding a status seems likely.
// Such a status could provide feedback on applying the
// eviction and/or statistics (number of matching devices,
// affected allocated claims, pods remaining to be evicted,
// etc.).
}
// DeviceTaintRuleSpec specifies the selector and one taint.
type DeviceTaintRuleSpec struct {
// DeviceSelector defines which device(s) the taint is applied to.
// All selector criteria must be satified for a device to
// match. The empty selector matches all devices. Without
// a selector, no devices are matches.
//
// +optional
DeviceSelector *DeviceTaintSelector `json:"deviceSelector,omitempty" protobuf:"bytes,1,opt,name=deviceSelector"`
// The taint that gets applied to matching devices.
//
// +required
Taint DeviceTaint `json:"taint,omitempty" protobuf:"bytes,2,rep,name=taint"`
}
// DeviceTaintSelector defines which device(s) a DeviceTaintRule applies to.
// The empty selector matches all devices. Without a selector, no devices
// are matched.
type DeviceTaintSelector struct {
// If DeviceClassName is set, the selectors defined there must be
// satisfied by a device to be selected. This field corresponds
// to class.metadata.name.
//
// +optional
DeviceClassName *string `json:"deviceClassName,omitempty" protobuf:"bytes,1,opt,name=deviceClassName"`
// If driver is set, only devices from that driver are selected.
// This fields corresponds to slice.spec.driver.
//
// +optional
Driver *string `json:"driver,omitempty" protobuf:"bytes,2,opt,name=driver"`
// If pool is set, only devices in that pool are selected.
//
// Also setting the driver name may be useful to avoid
// ambiguity when different drivers use the same pool name,
// but this is not required because selecting pools from
// different drivers may also be useful, for example when
// drivers with node-local devices use the node name as
// their pool name.
//
// +optional
Pool *string `json:"pool,omitempty" protobuf:"bytes,3,opt,name=pool"`
// If device is set, only devices with that name are selected.
// This field corresponds to slice.spec.devices[].name.
//
// Setting also driver and pool may be required to avoid ambiguity,
// but is not required.
//
// +optional
Device *string `json:"device,omitempty" protobuf:"bytes,4,opt,name=device"`
// Selectors contains the same selection criteria as a ResourceClaim.
// Currently, CEL expressions are supported. All of these selectors
// must be satisfied.
//
// +optional
// +listType=atomic
Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,5,rep,name=selectors"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.33
// DeviceTaintRuleList is a collection of DeviceTaintRules.
type DeviceTaintRuleList struct {
metav1.TypeMeta `json:",inline"`
// Standard list metadata
// +optional
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Items is the list of DeviceTaintRules.
Items []DeviceTaintRule `json:"items" protobuf:"bytes,2,rep,name=items"`
}

View file

@ -55,6 +55,7 @@ var map_BasicDevice = map[string]string{
"": "BasicDevice defines one device instance.",
"attributes": "Attributes defines the set of attributes for this device. The name of each attribute must be unique in that set.\n\nThe maximum number of attributes and capacities combined is 32.",
"capacity": "Capacity defines the set of capacities for this device. The name of each capacity must be unique in that set.\n\nThe maximum number of attributes and capacities combined is 32.",
"taints": "If specified, these are the driver-defined taints.\n\nThe maximum number of taints is 8.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (BasicDevice) SwaggerDoc() map[string]string {
@ -198,6 +199,7 @@ var map_DeviceRequest = map[string]string{
"count": "Count is used only when the count mode is \"ExactCount\". Must be greater than zero. If AllocationMode is ExactCount and this field is not specified, the default is one.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.",
"adminAccess": "AdminAccess indicates that this is a claim for administrative access to the device(s). Claims with AdminAccess are expected to be used for monitoring or other management services for a device. They ignore all ordinary claims to the device with respect to access modes and any resource allocations.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRAAdminAccess feature gate. Admin access is disabled if this field is unset or set to false, otherwise it is enabled.",
"firstAvailable": "FirstAvailable contains subrequests, of which exactly one will be satisfied by the scheduler to satisfy this request. It tries to satisfy them in the order in which they are listed here. So if there are two entries in the list, the scheduler will only check the second one if it determines that the first one cannot be used.\n\nThis field may only be set in the entries of DeviceClaim.Requests.\n\nDRA does not yet implement scoring, so the scheduler will select the first set of devices that satisfies all the requests in the claim. And if the requirements can be satisfied on more than one node, other scheduling features will determine which node is chosen. This means that the set of devices allocated to a claim might not be the optimal set available to the cluster. Scoring will be implemented later.",
"tolerations": "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (DeviceRequest) SwaggerDoc() map[string]string {
@ -211,6 +213,7 @@ var map_DeviceRequestAllocationResult = map[string]string{
"pool": "This name together with the driver name and the device name field identify which device was allocated (`<driver name>/<pool name>/<device name>`).\n\nMust not be longer than 253 characters and may contain one or more DNS sub-domains separated by slashes.",
"device": "Device references one device instance via its name in the driver's resource pool. It must be a DNS label.",
"adminAccess": "AdminAccess indicates that this device was allocated for administrative access. See the corresponding request field for a definition of mode.\n\nThis is an alpha field and requires enabling the DRAAdminAccess feature gate. Admin access is disabled if this field is unset or set to false, otherwise it is enabled.",
"tolerations": "A copy of all tolerations specified in the request at the time when the device got allocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (DeviceRequestAllocationResult) SwaggerDoc() map[string]string {
@ -233,12 +236,81 @@ var map_DeviceSubRequest = map[string]string{
"selectors": "Selectors define criteria which must be satisfied by a specific device in order for that device to be considered for this request. All selectors must be satisfied for a device to be considered.",
"allocationMode": "AllocationMode and its related fields define how devices are allocated to satisfy this request. Supported values are:\n\n- ExactCount: This request is for a specific number of devices.\n This is the default. The exact number is provided in the\n count field.\n\n- All: This request is for all of the matching devices in a pool.\n Allocation will fail if some devices are already allocated,\n unless adminAccess is requested.\n\nIf AlloctionMode is not specified, the default mode is ExactCount. If the mode is ExactCount and count is not specified, the default count is one. Any other requests must specify this field.\n\nMore modes may get added in the future. Clients must refuse to handle requests with unknown modes.",
"count": "Count is used only when the count mode is \"ExactCount\". Must be greater than zero. If AllocationMode is ExactCount and this field is not specified, the default is one.",
"tolerations": "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (DeviceSubRequest) SwaggerDoc() map[string]string {
return map_DeviceSubRequest
}
var map_DeviceTaint = map[string]string{
"": "The device this taint is attached to has the \"effect\" on any claim which does not tolerate the taint and, through the claim, to pods using the claim.",
"key": "The taint key to be applied to a device. Must be a label name.",
"value": "The taint value corresponding to the taint key. Must be a label value.",
"effect": "The effect of the taint on claims that do not tolerate the taint and through such claims on the pods using them. Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for nodes is not valid here.",
"timeAdded": "TimeAdded represents the time at which the taint was added. Added automatically during create or update if not set.",
}
func (DeviceTaint) SwaggerDoc() map[string]string {
return map_DeviceTaint
}
var map_DeviceTaintRule = map[string]string{
"": "DeviceTaintRule adds one taint to all devices which match the selector. This has the same effect as if the taint was specified directly in the ResourceSlice by the DRA driver.",
"metadata": "Standard object metadata",
"spec": "Spec specifies the selector and one taint.\n\nChanging the spec automatically increments the metadata.generation number.",
}
func (DeviceTaintRule) SwaggerDoc() map[string]string {
return map_DeviceTaintRule
}
var map_DeviceTaintRuleList = map[string]string{
"": "DeviceTaintRuleList is a collection of DeviceTaintRules.",
"metadata": "Standard list metadata",
"items": "Items is the list of DeviceTaintRules.",
}
func (DeviceTaintRuleList) SwaggerDoc() map[string]string {
return map_DeviceTaintRuleList
}
var map_DeviceTaintRuleSpec = map[string]string{
"": "DeviceTaintRuleSpec specifies the selector and one taint.",
"deviceSelector": "DeviceSelector defines which device(s) the taint is applied to. All selector criteria must be satified for a device to match. The empty selector matches all devices. Without a selector, no devices are matches.",
"taint": "The taint that gets applied to matching devices.",
}
func (DeviceTaintRuleSpec) SwaggerDoc() map[string]string {
return map_DeviceTaintRuleSpec
}
var map_DeviceTaintSelector = map[string]string{
"": "DeviceTaintSelector defines which device(s) a DeviceTaintRule applies to. The empty selector matches all devices. Without a selector, no devices are matched.",
"deviceClassName": "If DeviceClassName is set, the selectors defined there must be satisfied by a device to be selected. This field corresponds to class.metadata.name.",
"driver": "If driver is set, only devices from that driver are selected. This fields corresponds to slice.spec.driver.",
"pool": "If pool is set, only devices in that pool are selected.\n\nAlso setting the driver name may be useful to avoid ambiguity when different drivers use the same pool name, but this is not required because selecting pools from different drivers may also be useful, for example when drivers with node-local devices use the node name as their pool name.",
"device": "If device is set, only devices with that name are selected. This field corresponds to slice.spec.devices[].name.\n\nSetting also driver and pool may be required to avoid ambiguity, but is not required.",
"selectors": "Selectors contains the same selection criteria as a ResourceClaim. Currently, CEL expressions are supported. All of these selectors must be satisfied.",
}
func (DeviceTaintSelector) SwaggerDoc() map[string]string {
return map_DeviceTaintSelector
}
var map_DeviceToleration = map[string]string{
"": "The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.",
"key": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. Must be a label name.",
"operator": "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a ResourceClaim can tolerate all taints of a particular category.",
"value": "Value is the taint value the toleration matches to. If the operator is Exists, the value must be empty, otherwise just a regular string. Must be a label value.",
"effect": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule and NoExecute.",
"tolerationSeconds": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. If larger than zero, the time when the pod needs to be evicted is calculated as <time when taint was adedd> + <toleration seconds>.",
}
func (DeviceToleration) SwaggerDoc() map[string]string {
return map_DeviceToleration
}
var map_NetworkDeviceData = map[string]string{
"": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",

View file

@ -100,6 +100,13 @@ func (in *BasicDevice) DeepCopyInto(out *BasicDevice) {
(*out)[key] = val.DeepCopy()
}
}
if in.Taints != nil {
in, out := &in.Taints, &out.Taints
*out = make([]DeviceTaint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -473,6 +480,13 @@ func (in *DeviceRequest) DeepCopyInto(out *DeviceRequest) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -494,6 +508,13 @@ func (in *DeviceRequestAllocationResult) DeepCopyInto(out *DeviceRequestAllocati
*out = new(bool)
**out = **in
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -538,6 +559,13 @@ func (in *DeviceSubRequest) DeepCopyInto(out *DeviceSubRequest) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -551,6 +579,172 @@ func (in *DeviceSubRequest) DeepCopy() *DeviceSubRequest {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaint) DeepCopyInto(out *DeviceTaint) {
*out = *in
if in.TimeAdded != nil {
in, out := &in.TimeAdded, &out.TimeAdded
*out = (*in).DeepCopy()
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaint.
func (in *DeviceTaint) DeepCopy() *DeviceTaint {
if in == nil {
return nil
}
out := new(DeviceTaint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintRule) DeepCopyInto(out *DeviceTaintRule) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintRule.
func (in *DeviceTaintRule) DeepCopy() *DeviceTaintRule {
if in == nil {
return nil
}
out := new(DeviceTaintRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DeviceTaintRule) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintRuleList) DeepCopyInto(out *DeviceTaintRuleList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DeviceTaintRule, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintRuleList.
func (in *DeviceTaintRuleList) DeepCopy() *DeviceTaintRuleList {
if in == nil {
return nil
}
out := new(DeviceTaintRuleList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DeviceTaintRuleList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintRuleSpec) DeepCopyInto(out *DeviceTaintRuleSpec) {
*out = *in
if in.DeviceSelector != nil {
in, out := &in.DeviceSelector, &out.DeviceSelector
*out = new(DeviceTaintSelector)
(*in).DeepCopyInto(*out)
}
in.Taint.DeepCopyInto(&out.Taint)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintRuleSpec.
func (in *DeviceTaintRuleSpec) DeepCopy() *DeviceTaintRuleSpec {
if in == nil {
return nil
}
out := new(DeviceTaintRuleSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaintSelector) DeepCopyInto(out *DeviceTaintSelector) {
*out = *in
if in.DeviceClassName != nil {
in, out := &in.DeviceClassName, &out.DeviceClassName
*out = new(string)
**out = **in
}
if in.Driver != nil {
in, out := &in.Driver, &out.Driver
*out = new(string)
**out = **in
}
if in.Pool != nil {
in, out := &in.Pool, &out.Pool
*out = new(string)
**out = **in
}
if in.Device != nil {
in, out := &in.Device, &out.Device
*out = new(string)
**out = **in
}
if in.Selectors != nil {
in, out := &in.Selectors, &out.Selectors
*out = make([]DeviceSelector, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaintSelector.
func (in *DeviceTaintSelector) DeepCopy() *DeviceTaintSelector {
if in == nil {
return nil
}
out := new(DeviceTaintSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceToleration) DeepCopyInto(out *DeviceToleration) {
*out = *in
if in.TolerationSeconds != nil {
in, out := &in.TolerationSeconds, &out.TolerationSeconds
*out = new(int64)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceToleration.
func (in *DeviceToleration) DeepCopy() *DeviceToleration {
if in == nil {
return nil
}
out := new(DeviceToleration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkDeviceData) DeepCopyInto(out *NetworkDeviceData) {
*out = *in

View file

@ -73,6 +73,42 @@ func (in *DeviceClassList) APILifecycleRemoved() (major, minor int) {
return 1, 37
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *DeviceTaintRule) APILifecycleIntroduced() (major, minor int) {
return 1, 33
}
// APILifecycleDeprecated is an autogenerated function, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:deprecated" tags in types.go or "k8s:prerelease-lifecycle-gen:introduced" plus three minor.
func (in *DeviceTaintRule) APILifecycleDeprecated() (major, minor int) {
return 1, 36
}
// APILifecycleRemoved is an autogenerated function, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:removed" tags in types.go or "k8s:prerelease-lifecycle-gen:deprecated" plus three minor.
func (in *DeviceTaintRule) APILifecycleRemoved() (major, minor int) {
return 1, 39
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *DeviceTaintRuleList) APILifecycleIntroduced() (major, minor int) {
return 1, 33
}
// APILifecycleDeprecated is an autogenerated function, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:deprecated" tags in types.go or "k8s:prerelease-lifecycle-gen:introduced" plus three minor.
func (in *DeviceTaintRuleList) APILifecycleDeprecated() (major, minor int) {
return 1, 36
}
// APILifecycleRemoved is an autogenerated function, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:removed" tags in types.go or "k8s:prerelease-lifecycle-gen:deprecated" plus three minor.
func (in *DeviceTaintRuleList) APILifecycleRemoved() (major, minor int) {
return 1, 39
}
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
func (in *ResourceClaim) APILifecycleIntroduced() (major, minor int) {

View file

@ -0,0 +1,35 @@
/*
Copyright 2025 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 v1beta1
import "fmt"
var _ fmt.Stringer = DeviceTaint{}
// String converts to a string in the format '<key>=<value>:<effect>', '<key>=<value>:', '<key>:<effect>', or '<key>'.
func (t DeviceTaint) String() string {
if len(t.Effect) == 0 {
if len(t.Value) == 0 {
return fmt.Sprintf("%v", t.Key)
}
return fmt.Sprintf("%v=%v:", t.Key, t.Value)
}
if len(t.Value) == 0 {
return fmt.Sprintf("%v:%v", t.Key, t.Effect)
}
return fmt.Sprintf("%v=%v:%v", t.Key, t.Value, t.Effect)
}

File diff suppressed because it is too large Load diff

View file

@ -113,6 +113,18 @@ message BasicDevice {
//
// +optional
map<string, DeviceCapacity> capacity = 2;
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceTaint taints = 3;
}
// CELDeviceSelector contains a CEL expression for selecting a device.
@ -529,6 +541,32 @@ message DeviceRequest {
// +listType=atomic
// +featureGate=DRAPrioritizedList
repeated DeviceSubRequest firstAvailable = 7;
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceToleration tolerations = 8;
}
// DeviceRequestAllocationResult contains the allocation result for one request.
@ -580,6 +618,19 @@ message DeviceRequestAllocationResult {
// +optional
// +featureGate=DRAAdminAccess
optional bool adminAccess = 5;
// A copy of all tolerations specified in the request at the time
// when the device got allocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceToleration tolerations = 6;
}
// DeviceSelector must have exactly one field set.
@ -661,6 +712,105 @@ message DeviceSubRequest {
// +optional
// +oneOf=AllocationMode
optional int64 count = 5;
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
repeated DeviceToleration tolerations = 7;
}
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
// to pods using the claim.
//
// +protobuf.options.(gogoproto.goproto_stringer)=false
message DeviceTaint {
// The taint key to be applied to a device.
// Must be a label name.
//
// +required
optional string key = 1;
// The taint value corresponding to the taint key.
// Must be a label value.
//
// +optional
optional string value = 2;
// The effect of the taint on claims that do not tolerate the taint
// and through such claims on the pods using them.
// Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for
// nodes is not valid here.
//
// +required
optional string effect = 3;
// TimeAdded represents the time at which the taint was added.
// Added automatically during create or update if not set.
//
// +optional
optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time timeAdded = 4;
}
// The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches
// the triple <key,value,effect> using the matching operator <operator>.
message DeviceToleration {
// Key is the taint key that the toleration applies to. Empty means match all taint keys.
// If the key is empty, operator must be Exists; this combination means to match all values and all keys.
// Must be a label name.
//
// +optional
optional string key = 1;
// Operator represents a key's relationship to the value.
// Valid operators are Exists and Equal. Defaults to Equal.
// Exists is equivalent to wildcard for value, so that a ResourceClaim can
// tolerate all taints of a particular category.
//
// +optional
// +default="Equal"
optional string operator = 2;
// Value is the taint value the toleration matches to.
// If the operator is Exists, the value must be empty, otherwise just a regular string.
// Must be a label value.
//
// +optional
optional string value = 3;
// Effect indicates the taint effect to match. Empty means match all taint effects.
// When specified, allowed values are NoSchedule and NoExecute.
//
// +optional
optional string effect = 4;
// TolerationSeconds represents the period of time the toleration (which must be
// of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
// it is not set, which means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by the system.
// If larger than zero, the time when the pod needs to be evicted is calculated as <time when
// taint was adedd> + <toleration seconds>.
//
// +optional
optional int64 tolerationSeconds = 5;
}
// NetworkDeviceData provides network-related details for the allocated device.

View file

@ -222,6 +222,18 @@ type BasicDevice struct {
//
// +optional
Capacity map[QualifiedName]DeviceCapacity `json:"capacity,omitempty" protobuf:"bytes,2,rep,name=capacity"`
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Taints []DeviceTaint `json:"taints,omitempty" protobuf:"bytes,3,rep,name=taints"`
}
// DeviceCapacity describes a quantity associated with a device.
@ -300,6 +312,66 @@ type DeviceAttribute struct {
// DeviceAttributeMaxValueLength is the maximum length of a string or version attribute value.
const DeviceAttributeMaxValueLength = 64
// DeviceTaintsMaxLength is the maximum number of taints per device.
const DeviceTaintsMaxLength = 8
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
// to pods using the claim.
//
// +protobuf.options.(gogoproto.goproto_stringer)=false
type DeviceTaint struct {
// The taint key to be applied to a device.
// Must be a label name.
//
// +required
Key string `json:"key" protobuf:"bytes,1,name=key"`
// The taint value corresponding to the taint key.
// Must be a label value.
//
// +optional
Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`
// The effect of the taint on claims that do not tolerate the taint
// and through such claims on the pods using them.
// Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for
// nodes is not valid here.
//
// +required
Effect DeviceTaintEffect `json:"effect" protobuf:"bytes,3,name=effect,casttype=DeviceTaintEffect"`
// ^^^^
//
// Implementing PreferNoSchedule would depend on a scoring solution for DRA.
// It might get added as part of that.
// TimeAdded represents the time at which the taint was added.
// Added automatically during create or update if not set.
//
// +optional
TimeAdded *metav1.Time `json:"timeAdded,omitempty" protobuf:"bytes,4,opt,name=timeAdded"`
// ^^^
//
// This field was defined as "It is only written for NoExecute taints." for node taints.
// But in practice, Kubernetes never did anything with it (no validation, no defaulting,
// ignored during pod eviction in pkg/controller/tainteviction).
}
// +enum
type DeviceTaintEffect string
const (
// Do not allow new pods to schedule which use a tainted device unless they tolerate the taint,
// but allow all pods submitted to Kubelet without going through the scheduler
// to start, and allow all already-running pods to continue running.
DeviceTaintEffectNoSchedule DeviceTaintEffect = "NoSchedule"
// Evict any already-running pods that do not tolerate the device taint.
DeviceTaintEffectNoExecute DeviceTaintEffect = "NoExecute"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
@ -517,6 +589,32 @@ type DeviceRequest struct {
// +listType=atomic
// +featureGate=DRAPrioritizedList
FirstAvailable []DeviceSubRequest `json:"firstAvailable,omitempty" protobuf:"bytes,7,name=firstAvailable"`
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,8,opt,name=tolerations"`
}
// DeviceSubRequest describes a request for device provided in the
@ -589,10 +687,35 @@ type DeviceSubRequest struct {
// +optional
// +oneOf=AllocationMode
Count int64 `json:"count,omitempty" protobuf:"bytes,5,opt,name=count"`
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
// device which has a taint with that effect. The same applies
// to NoExecute.
//
// In addition, should any of the allocated devices get tainted
// with NoExecute after allocation and that effect is not tolerated,
// then all pods consuming the ResourceClaim get deleted to evict
// them. The scheduler will not let new pods reserve the claim while
// it has these tainted devices. Once all pods are evicted, the
// claim will get deallocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,7,opt,name=tolerations"`
}
const (
DeviceSelectorsMaxSize = 32
DeviceSelectorsMaxSize = 32
FirstAvailableDeviceRequestMaxSize = 8
DeviceTolerationsMaxLength = 16
)
type DeviceAllocationMode string
@ -798,6 +921,59 @@ type OpaqueDeviceConfiguration struct {
// [OpaqueDeviceConfiguration.Parameters] field.
const OpaqueParametersMaxLength = 10 * 1024
// The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches
// the triple <key,value,effect> using the matching operator <operator>.
type DeviceToleration struct {
// Key is the taint key that the toleration applies to. Empty means match all taint keys.
// If the key is empty, operator must be Exists; this combination means to match all values and all keys.
// Must be a label name.
//
// +optional
Key string `json:"key,omitempty" protobuf:"bytes,1,opt,name=key"`
// Operator represents a key's relationship to the value.
// Valid operators are Exists and Equal. Defaults to Equal.
// Exists is equivalent to wildcard for value, so that a ResourceClaim can
// tolerate all taints of a particular category.
//
// +optional
// +default="Equal"
Operator DeviceTolerationOperator `json:"operator,omitempty" protobuf:"bytes,2,opt,name=operator,casttype=DeviceTolerationOperator"`
// Value is the taint value the toleration matches to.
// If the operator is Exists, the value must be empty, otherwise just a regular string.
// Must be a label value.
//
// +optional
Value string `json:"value,omitempty" protobuf:"bytes,3,opt,name=value"`
// Effect indicates the taint effect to match. Empty means match all taint effects.
// When specified, allowed values are NoSchedule and NoExecute.
//
// +optional
Effect DeviceTaintEffect `json:"effect,omitempty" protobuf:"bytes,4,opt,name=effect,casttype=DeviceTaintEffect"`
// TolerationSeconds represents the period of time the toleration (which must be
// of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
// it is not set, which means tolerate the taint forever (do not evict). Zero and
// negative values will be treated as 0 (evict immediately) by the system.
// If larger than zero, the time when the pod needs to be evicted is calculated as <time when
// taint was adedd> + <toleration seconds>.
//
// +optional
TolerationSeconds *int64 `json:"tolerationSeconds,omitempty" protobuf:"varint,5,opt,name=tolerationSeconds"`
}
// A toleration operator is the set of operators that can be used in a toleration.
//
// +enum
type DeviceTolerationOperator string
const (
DeviceTolerationOpExists DeviceTolerationOperator = "Exists"
DeviceTolerationOpEqual DeviceTolerationOperator = "Equal"
)
// ResourceClaimStatus tracks whether the resource has been allocated and what
// the result of that was.
type ResourceClaimStatus struct {
@ -968,6 +1144,19 @@ type DeviceRequestAllocationResult struct {
// +optional
// +featureGate=DRAAdminAccess
AdminAccess *bool `json:"adminAccess" protobuf:"bytes,5,name=adminAccess"`
// A copy of all tolerations specified in the request at the time
// when the device got allocated.
//
// The maximum number of tolerations is 16.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
//
// +optional
// +listType=atomic
// +featureGate=DRADeviceTaints
Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,6,opt,name=tolerations"`
}
// DeviceAllocationConfiguration gets embedded in an AllocationResult.

View file

@ -55,6 +55,7 @@ var map_BasicDevice = map[string]string{
"": "BasicDevice defines one device instance.",
"attributes": "Attributes defines the set of attributes for this device. The name of each attribute must be unique in that set.\n\nThe maximum number of attributes and capacities combined is 32.",
"capacity": "Capacity defines the set of capacities for this device. The name of each capacity must be unique in that set.\n\nThe maximum number of attributes and capacities combined is 32.",
"taints": "If specified, these are the driver-defined taints.\n\nThe maximum number of taints is 8.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (BasicDevice) SwaggerDoc() map[string]string {
@ -207,6 +208,7 @@ var map_DeviceRequest = map[string]string{
"count": "Count is used only when the count mode is \"ExactCount\". Must be greater than zero. If AllocationMode is ExactCount and this field is not specified, the default is one.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.",
"adminAccess": "AdminAccess indicates that this is a claim for administrative access to the device(s). Claims with AdminAccess are expected to be used for monitoring or other management services for a device. They ignore all ordinary claims to the device with respect to access modes and any resource allocations.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRAAdminAccess feature gate. Admin access is disabled if this field is unset or set to false, otherwise it is enabled.",
"firstAvailable": "FirstAvailable contains subrequests, of which exactly one will be satisfied by the scheduler to satisfy this request. It tries to satisfy them in the order in which they are listed here. So if there are two entries in the list, the scheduler will only check the second one if it determines that the first one cannot be used.\n\nThis field may only be set in the entries of DeviceClaim.Requests.\n\nDRA does not yet implement scoring, so the scheduler will select the first set of devices that satisfies all the requests in the claim. And if the requirements can be satisfied on more than one node, other scheduling features will determine which node is chosen. This means that the set of devices allocated to a claim might not be the optimal set available to the cluster. Scoring will be implemented later.",
"tolerations": "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis field can only be set when deviceClassName is set and no subrequests are specified in the firstAvailable list.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (DeviceRequest) SwaggerDoc() map[string]string {
@ -220,6 +222,7 @@ var map_DeviceRequestAllocationResult = map[string]string{
"pool": "This name together with the driver name and the device name field identify which device was allocated (`<driver name>/<pool name>/<device name>`).\n\nMust not be longer than 253 characters and may contain one or more DNS sub-domains separated by slashes.",
"device": "Device references one device instance via its name in the driver's resource pool. It must be a DNS label.",
"adminAccess": "AdminAccess indicates that this device was allocated for administrative access. See the corresponding request field for a definition of mode.\n\nThis is an alpha field and requires enabling the DRAAdminAccess feature gate. Admin access is disabled if this field is unset or set to false, otherwise it is enabled.",
"tolerations": "A copy of all tolerations specified in the request at the time when the device got allocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (DeviceRequestAllocationResult) SwaggerDoc() map[string]string {
@ -242,12 +245,38 @@ var map_DeviceSubRequest = map[string]string{
"selectors": "Selectors define criteria which must be satisfied by a specific device in order for that device to be considered for this subrequest. All selectors must be satisfied for a device to be considered.",
"allocationMode": "AllocationMode and its related fields define how devices are allocated to satisfy this subrequest. Supported values are:\n\n- ExactCount: This request is for a specific number of devices.\n This is the default. The exact number is provided in the\n count field.\n\n- All: This subrequest is for all of the matching devices in a pool.\n Allocation will fail if some devices are already allocated,\n unless adminAccess is requested.\n\nIf AlloctionMode is not specified, the default mode is ExactCount. If the mode is ExactCount and count is not specified, the default count is one. Any other subrequests must specify this field.\n\nMore modes may get added in the future. Clients must refuse to handle requests with unknown modes.",
"count": "Count is used only when the count mode is \"ExactCount\". Must be greater than zero. If AllocationMode is ExactCount and this field is not specified, the default is one.",
"tolerations": "If specified, the request's tolerations.\n\nTolerations for NoSchedule are required to allocate a device which has a taint with that effect. The same applies to NoExecute.\n\nIn addition, should any of the allocated devices get tainted with NoExecute after allocation and that effect is not tolerated, then all pods consuming the ResourceClaim get deleted to evict them. The scheduler will not let new pods reserve the claim while it has these tainted devices. Once all pods are evicted, the claim will get deallocated.\n\nThe maximum number of tolerations is 16.\n\nThis is an alpha field and requires enabling the DRADeviceTaints feature gate.",
}
func (DeviceSubRequest) SwaggerDoc() map[string]string {
return map_DeviceSubRequest
}
var map_DeviceTaint = map[string]string{
"": "The device this taint is attached to has the \"effect\" on any claim which does not tolerate the taint and, through the claim, to pods using the claim.",
"key": "The taint key to be applied to a device. Must be a label name.",
"value": "The taint value corresponding to the taint key. Must be a label value.",
"effect": "The effect of the taint on claims that do not tolerate the taint and through such claims on the pods using them. Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for nodes is not valid here.",
"timeAdded": "TimeAdded represents the time at which the taint was added. Added automatically during create or update if not set.",
}
func (DeviceTaint) SwaggerDoc() map[string]string {
return map_DeviceTaint
}
var map_DeviceToleration = map[string]string{
"": "The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.",
"key": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys. Must be a label name.",
"operator": "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a ResourceClaim can tolerate all taints of a particular category.",
"value": "Value is the taint value the toleration matches to. If the operator is Exists, the value must be empty, otherwise just a regular string. Must be a label value.",
"effect": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule and NoExecute.",
"tolerationSeconds": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system. If larger than zero, the time when the pod needs to be evicted is calculated as <time when taint was adedd> + <toleration seconds>.",
}
func (DeviceToleration) SwaggerDoc() map[string]string {
return map_DeviceToleration
}
var map_NetworkDeviceData = map[string]string{
"": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.",
"interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.",

View file

@ -99,6 +99,13 @@ func (in *BasicDevice) DeepCopyInto(out *BasicDevice) {
(*out)[key] = *val.DeepCopy()
}
}
if in.Taints != nil {
in, out := &in.Taints, &out.Taints
*out = make([]DeviceTaint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -489,6 +496,13 @@ func (in *DeviceRequest) DeepCopyInto(out *DeviceRequest) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -510,6 +524,13 @@ func (in *DeviceRequestAllocationResult) DeepCopyInto(out *DeviceRequestAllocati
*out = new(bool)
**out = **in
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -554,6 +575,13 @@ func (in *DeviceSubRequest) DeepCopyInto(out *DeviceSubRequest) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]DeviceToleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@ -567,6 +595,47 @@ func (in *DeviceSubRequest) DeepCopy() *DeviceSubRequest {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceTaint) DeepCopyInto(out *DeviceTaint) {
*out = *in
if in.TimeAdded != nil {
in, out := &in.TimeAdded, &out.TimeAdded
*out = (*in).DeepCopy()
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceTaint.
func (in *DeviceTaint) DeepCopy() *DeviceTaint {
if in == nil {
return nil
}
out := new(DeviceTaint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeviceToleration) DeepCopyInto(out *DeviceToleration) {
*out = *in
if in.TolerationSeconds != nil {
in, out := &in.TolerationSeconds, &out.TolerationSeconds
*out = new(int64)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceToleration.
func (in *DeviceToleration) DeepCopy() *DeviceToleration {
if in == nil {
return nil
}
out := new(DeviceToleration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkDeviceData) DeepCopyInto(out *NetworkDeviceData) {
*out = *in

View file

@ -0,0 +1,67 @@
{
"kind": "DeviceTaintRule",
"apiVersion": "resource.k8s.io/v1alpha3",
"metadata": {
"name": "nameValue",
"generateName": "generateNameValue",
"namespace": "namespaceValue",
"selfLink": "selfLinkValue",
"uid": "uidValue",
"resourceVersion": "resourceVersionValue",
"generation": 7,
"creationTimestamp": "2008-01-01T01:01:01Z",
"deletionTimestamp": "2009-01-01T01:01:01Z",
"deletionGracePeriodSeconds": 10,
"labels": {
"labelsKey": "labelsValue"
},
"annotations": {
"annotationsKey": "annotationsValue"
},
"ownerReferences": [
{
"apiVersion": "apiVersionValue",
"kind": "kindValue",
"name": "nameValue",
"uid": "uidValue",
"controller": true,
"blockOwnerDeletion": true
}
],
"finalizers": [
"finalizersValue"
],
"managedFields": [
{
"manager": "managerValue",
"operation": "operationValue",
"apiVersion": "apiVersionValue",
"time": "2004-01-01T01:01:01Z",
"fieldsType": "fieldsTypeValue",
"fieldsV1": {},
"subresource": "subresourceValue"
}
]
},
"spec": {
"deviceSelector": {
"deviceClassName": "deviceClassNameValue",
"driver": "driverValue",
"pool": "poolValue",
"device": "deviceValue",
"selectors": [
{
"cel": {
"expression": "expressionValue"
}
}
]
},
"taint": {
"key": "keyValue",
"value": "valueValue",
"effect": "effectValue",
"timeAdded": "2004-01-01T01:01:01Z"
}
}
}

View file

@ -0,0 +1,48 @@
apiVersion: resource.k8s.io/v1alpha3
kind: DeviceTaintRule
metadata:
annotations:
annotationsKey: annotationsValue
creationTimestamp: "2008-01-01T01:01:01Z"
deletionGracePeriodSeconds: 10
deletionTimestamp: "2009-01-01T01:01:01Z"
finalizers:
- finalizersValue
generateName: generateNameValue
generation: 7
labels:
labelsKey: labelsValue
managedFields:
- apiVersion: apiVersionValue
fieldsType: fieldsTypeValue
fieldsV1: {}
manager: managerValue
operation: operationValue
subresource: subresourceValue
time: "2004-01-01T01:01:01Z"
name: nameValue
namespace: namespaceValue
ownerReferences:
- apiVersion: apiVersionValue
blockOwnerDeletion: true
controller: true
kind: kindValue
name: nameValue
uid: uidValue
resourceVersion: resourceVersionValue
selfLink: selfLinkValue
uid: uidValue
spec:
deviceSelector:
device: deviceValue
deviceClassName: deviceClassNameValue
driver: driverValue
pool: poolValue
selectors:
- cel:
expression: expressionValue
taint:
effect: effectValue
key: keyValue
timeAdded: "2004-01-01T01:01:01Z"
value: valueValue

View file

@ -71,7 +71,25 @@
}
],
"allocationMode": "allocationModeValue",
"count": 5
"count": 5,
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
],
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
@ -115,7 +133,16 @@
"driver": "driverValue",
"pool": "poolValue",
"device": "deviceValue",
"adminAccess": true
"adminAccess": true,
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
],
"config": [

View file

@ -63,10 +63,22 @@ spec:
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
name: nameValue
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
status:
allocation:
devices:
@ -89,6 +101,12 @@ status:
driver: driverValue
pool: poolValue
request: requestValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
nodeSelector:
nodeSelectorTerms:
- matchExpressions:

View file

@ -114,7 +114,25 @@
}
],
"allocationMode": "allocationModeValue",
"count": 5
"count": 5,
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
],
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}

View file

@ -96,7 +96,19 @@ spec:
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
name: nameValue
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue

View file

@ -90,7 +90,15 @@
},
"capacity": {
"capacityKey": "0"
}
},
"taints": [
{
"key": "keyValue",
"value": "valueValue",
"effect": "effectValue",
"timeAdded": "2004-01-01T01:01:01Z"
}
]
}
}
]

View file

@ -44,6 +44,11 @@ spec:
version: versionValue
capacity:
capacityKey: "0"
taints:
- effect: effectValue
key: keyValue
timeAdded: "2004-01-01T01:01:01Z"
value: valueValue
name: nameValue
driver: driverValue
nodeName: nodeNameValue

View file

@ -71,7 +71,25 @@
}
],
"allocationMode": "allocationModeValue",
"count": 5
"count": 5,
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
],
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
@ -115,7 +133,16 @@
"driver": "driverValue",
"pool": "poolValue",
"device": "deviceValue",
"adminAccess": true
"adminAccess": true,
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
],
"config": [

View file

@ -63,10 +63,22 @@ spec:
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
name: nameValue
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
status:
allocation:
devices:
@ -89,6 +101,12 @@ status:
driver: driverValue
pool: poolValue
request: requestValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
nodeSelector:
nodeSelectorTerms:
- matchExpressions:

View file

@ -114,7 +114,25 @@
}
],
"allocationMode": "allocationModeValue",
"count": 5
"count": 5,
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}
],
"tolerations": [
{
"key": "keyValue",
"operator": "operatorValue",
"value": "valueValue",
"effect": "effectValue",
"tolerationSeconds": 5
}
]
}

View file

@ -96,7 +96,19 @@ spec:
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue
name: nameValue
selectors:
- cel:
expression: expressionValue
tolerations:
- effect: effectValue
key: keyValue
operator: operatorValue
tolerationSeconds: 5
value: valueValue

View file

@ -92,7 +92,15 @@
"capacityKey": {
"value": "0"
}
}
},
"taints": [
{
"key": "keyValue",
"value": "valueValue",
"effect": "effectValue",
"timeAdded": "2004-01-01T01:01:01Z"
}
]
}
}
]

View file

@ -45,6 +45,11 @@ spec:
capacity:
capacityKey:
value: "0"
taints:
- effect: effectValue
key: keyValue
timeAdded: "2004-01-01T01:01:01Z"
value: valueValue
name: nameValue
driver: driverValue
nodeName: nodeNameValue

View file

@ -12590,6 +12590,12 @@ var schemaYAML = typed.YAMLObject(`types:
map:
elementType:
namedType: io.k8s.apimachinery.pkg.api.resource.Quantity
- name: taints
type:
list:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceTaint
elementRelationship: atomic
- name: io.k8s.api.resource.v1alpha3.CELDeviceSelector
map:
fields:
@ -12768,6 +12774,12 @@ var schemaYAML = typed.YAMLObject(`types:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceSelector
elementRelationship: atomic
- name: tolerations
type:
list:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceToleration
elementRelationship: atomic
- name: io.k8s.api.resource.v1alpha3.DeviceRequestAllocationResult
map:
fields:
@ -12790,6 +12802,12 @@ var schemaYAML = typed.YAMLObject(`types:
type:
scalar: string
default: ""
- name: tolerations
type:
list:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceToleration
elementRelationship: atomic
- name: io.k8s.api.resource.v1alpha3.DeviceSelector
map:
fields:
@ -12819,6 +12837,96 @@ var schemaYAML = typed.YAMLObject(`types:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceSelector
elementRelationship: atomic
- name: tolerations
type:
list:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceToleration
elementRelationship: atomic
- name: io.k8s.api.resource.v1alpha3.DeviceTaint
map:
fields:
- name: effect
type:
scalar: string
default: ""
- name: key
type:
scalar: string
default: ""
- name: timeAdded
type:
namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time
- name: value
type:
scalar: string
- name: io.k8s.api.resource.v1alpha3.DeviceTaintRule
map:
fields:
- name: apiVersion
type:
scalar: string
- name: kind
type:
scalar: string
- name: metadata
type:
namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta
default: {}
- name: spec
type:
namedType: io.k8s.api.resource.v1alpha3.DeviceTaintRuleSpec
default: {}
- name: io.k8s.api.resource.v1alpha3.DeviceTaintRuleSpec
map:
fields:
- name: deviceSelector
type:
namedType: io.k8s.api.resource.v1alpha3.DeviceTaintSelector
- name: taint
type:
namedType: io.k8s.api.resource.v1alpha3.DeviceTaint
default: {}
- name: io.k8s.api.resource.v1alpha3.DeviceTaintSelector
map:
fields:
- name: device
type:
scalar: string
- name: deviceClassName
type:
scalar: string
- name: driver
type:
scalar: string
- name: pool
type:
scalar: string
- name: selectors
type:
list:
elementType:
namedType: io.k8s.api.resource.v1alpha3.DeviceSelector
elementRelationship: atomic
- name: io.k8s.api.resource.v1alpha3.DeviceToleration
map:
fields:
- name: effect
type:
scalar: string
- name: key
type:
scalar: string
- name: operator
type:
scalar: string
default: Equal
- name: tolerationSeconds
type:
scalar: numeric
- name: value
type:
scalar: string
- name: io.k8s.api.resource.v1alpha3.NetworkDeviceData
map:
fields:
@ -13052,6 +13160,12 @@ var schemaYAML = typed.YAMLObject(`types:
map:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceCapacity
- name: taints
type:
list:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceTaint
elementRelationship: atomic
- name: io.k8s.api.resource.v1beta1.CELDeviceSelector
map:
fields:
@ -13236,6 +13350,12 @@ var schemaYAML = typed.YAMLObject(`types:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceSelector
elementRelationship: atomic
- name: tolerations
type:
list:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceToleration
elementRelationship: atomic
- name: io.k8s.api.resource.v1beta1.DeviceRequestAllocationResult
map:
fields:
@ -13258,6 +13378,12 @@ var schemaYAML = typed.YAMLObject(`types:
type:
scalar: string
default: ""
- name: tolerations
type:
list:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceToleration
elementRelationship: atomic
- name: io.k8s.api.resource.v1beta1.DeviceSelector
map:
fields:
@ -13287,6 +13413,48 @@ var schemaYAML = typed.YAMLObject(`types:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceSelector
elementRelationship: atomic
- name: tolerations
type:
list:
elementType:
namedType: io.k8s.api.resource.v1beta1.DeviceToleration
elementRelationship: atomic
- name: io.k8s.api.resource.v1beta1.DeviceTaint
map:
fields:
- name: effect
type:
scalar: string
default: ""
- name: key
type:
scalar: string
default: ""
- name: timeAdded
type:
namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time
- name: value
type:
scalar: string
- name: io.k8s.api.resource.v1beta1.DeviceToleration
map:
fields:
- name: effect
type:
scalar: string
- name: key
type:
scalar: string
- name: operator
type:
scalar: string
default: Equal
- name: tolerationSeconds
type:
scalar: numeric
- name: value
type:
scalar: string
- name: io.k8s.api.resource.v1beta1.NetworkDeviceData
map:
fields:

View file

@ -28,6 +28,7 @@ import (
type BasicDeviceApplyConfiguration struct {
Attributes map[resourcev1alpha3.QualifiedName]DeviceAttributeApplyConfiguration `json:"attributes,omitempty"`
Capacity map[resourcev1alpha3.QualifiedName]resource.Quantity `json:"capacity,omitempty"`
Taints []DeviceTaintApplyConfiguration `json:"taints,omitempty"`
}
// BasicDeviceApplyConfiguration constructs a declarative configuration of the BasicDevice type for use with
@ -63,3 +64,16 @@ func (b *BasicDeviceApplyConfiguration) WithCapacity(entries map[resourcev1alpha
}
return b
}
// WithTaints adds the given value to the Taints field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Taints field.
func (b *BasicDeviceApplyConfiguration) WithTaints(values ...*DeviceTaintApplyConfiguration) *BasicDeviceApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithTaints")
}
b.Taints = append(b.Taints, *values[i])
}
return b
}

View file

@ -32,6 +32,7 @@ type DeviceRequestApplyConfiguration struct {
Count *int64 `json:"count,omitempty"`
AdminAccess *bool `json:"adminAccess,omitempty"`
FirstAvailable []DeviceSubRequestApplyConfiguration `json:"firstAvailable,omitempty"`
Tolerations []DeviceTolerationApplyConfiguration `json:"tolerations,omitempty"`
}
// DeviceRequestApplyConfiguration constructs a declarative configuration of the DeviceRequest type for use with
@ -105,3 +106,16 @@ func (b *DeviceRequestApplyConfiguration) WithFirstAvailable(values ...*DeviceSu
}
return b
}
// WithTolerations adds the given value to the Tolerations field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Tolerations field.
func (b *DeviceRequestApplyConfiguration) WithTolerations(values ...*DeviceTolerationApplyConfiguration) *DeviceRequestApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithTolerations")
}
b.Tolerations = append(b.Tolerations, *values[i])
}
return b
}

View file

@ -21,11 +21,12 @@ package v1alpha3
// DeviceRequestAllocationResultApplyConfiguration represents a declarative configuration of the DeviceRequestAllocationResult type for use
// with apply.
type DeviceRequestAllocationResultApplyConfiguration struct {
Request *string `json:"request,omitempty"`
Driver *string `json:"driver,omitempty"`
Pool *string `json:"pool,omitempty"`
Device *string `json:"device,omitempty"`
AdminAccess *bool `json:"adminAccess,omitempty"`
Request *string `json:"request,omitempty"`
Driver *string `json:"driver,omitempty"`
Pool *string `json:"pool,omitempty"`
Device *string `json:"device,omitempty"`
AdminAccess *bool `json:"adminAccess,omitempty"`
Tolerations []DeviceTolerationApplyConfiguration `json:"tolerations,omitempty"`
}
// DeviceRequestAllocationResultApplyConfiguration constructs a declarative configuration of the DeviceRequestAllocationResult type for use with
@ -73,3 +74,16 @@ func (b *DeviceRequestAllocationResultApplyConfiguration) WithAdminAccess(value
b.AdminAccess = &value
return b
}
// WithTolerations adds the given value to the Tolerations field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Tolerations field.
func (b *DeviceRequestAllocationResultApplyConfiguration) WithTolerations(values ...*DeviceTolerationApplyConfiguration) *DeviceRequestAllocationResultApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithTolerations")
}
b.Tolerations = append(b.Tolerations, *values[i])
}
return b
}

View file

@ -30,6 +30,7 @@ type DeviceSubRequestApplyConfiguration struct {
Selectors []DeviceSelectorApplyConfiguration `json:"selectors,omitempty"`
AllocationMode *resourcev1alpha3.DeviceAllocationMode `json:"allocationMode,omitempty"`
Count *int64 `json:"count,omitempty"`
Tolerations []DeviceTolerationApplyConfiguration `json:"tolerations,omitempty"`
}
// DeviceSubRequestApplyConfiguration constructs a declarative configuration of the DeviceSubRequest type for use with
@ -82,3 +83,16 @@ func (b *DeviceSubRequestApplyConfiguration) WithCount(value int64) *DeviceSubRe
b.Count = &value
return b
}
// WithTolerations adds the given value to the Tolerations field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Tolerations field.
func (b *DeviceSubRequestApplyConfiguration) WithTolerations(values ...*DeviceTolerationApplyConfiguration) *DeviceSubRequestApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithTolerations")
}
b.Tolerations = append(b.Tolerations, *values[i])
}
return b
}

Some files were not shown because too many files have changed in this diff Show more