mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-08-13 22:08:01 +00:00
DRA: add CompatibilityGroups to the resource.k8s.io API
Add compatibility groups to resource.k8s.io across v1, v1beta1, v1beta2 and the internal API: the driver-declared DeviceCounterConsumption.CompatibilityGroups field (an atomic list of opaque group names, declaratively unique) and the DeviceCompatibilityGroupsMaxSize constant bounding it. All compatibility-group validation is declarative, applied directly as recommended for net-new fields, with no handwritten counterparts. Also includes the DRADeviceCompatibilityGroups feature gate (registered in the versioned feature reference list), the apiserver drop strategy, and the apiserver-side tests: create/update strategy drop tests, declarative-validation coverage, and the max-size ResourceSlice test objects (the generated declarative rules and test fixtures land with the next commit; the integration wiring that enables the feature gate lands with the implementation commit).
This commit is contained in:
@@ -334,6 +334,10 @@ const ResourceSliceMaxDeviceCounterConsumptionsPerDevice = 2
|
||||
// per device counter consumption.
|
||||
const ResourceSliceMaxCountersPerDeviceCounterConsumption = 32
|
||||
|
||||
// Defines the maximum number of compatibility groups that can be
|
||||
// declared per device counter consumption.
|
||||
const DeviceCompatibilityGroupsMaxSize = 2
|
||||
|
||||
// Device represents one individual hardware instance that can be selected based
|
||||
// on its attributes. Besides the name, exactly one field must be set.
|
||||
type Device struct {
|
||||
@@ -588,6 +592,31 @@ type DeviceCounterConsumption struct {
|
||||
//
|
||||
// +required
|
||||
Counters map[string]Counter
|
||||
|
||||
// CompatibilityGroups is a list of opaque group names for
|
||||
// this counter set consumption.
|
||||
//
|
||||
// Devices that consume counters from the same counter set may only be
|
||||
// allocated at the same time ("co-allocated") if they all share at least
|
||||
// one common group: the intersection of the CompatibilityGroups of all
|
||||
// co-allocated devices on that counter set must be non-empty. Devices
|
||||
// that consume from different counter sets are never compared via this
|
||||
// field.
|
||||
//
|
||||
// An unset field, an explicit nil, and an empty list are equivalent and
|
||||
// mean "no groups": such a device is only co-allocatable with sibling
|
||||
// devices on the same counter set that also have no groups, and is never
|
||||
// co-allocatable with a device that declares one or more groups.
|
||||
//
|
||||
// Group names are opaque and meaningful only within the
|
||||
// publishing driver's pool.
|
||||
//
|
||||
// The maximum number of groups is 2, and the names must be unique.
|
||||
//
|
||||
// +optional
|
||||
// +listType=atomic
|
||||
// +featureGate=DRADeviceCompatibilityGroups
|
||||
CompatibilityGroups []string
|
||||
}
|
||||
|
||||
// DeviceCapacity describes a quantity associated with a device.
|
||||
|
||||
@@ -220,6 +220,20 @@ const (
|
||||
// enabled.
|
||||
DRADeviceBindingConditions featuregate.Feature = "DRADeviceBindingConditions"
|
||||
|
||||
// owner: @omeryahud
|
||||
// kep: https://kep.k8s.io/5963
|
||||
//
|
||||
// Enables drivers to declare opaque compatibility groups on each
|
||||
// device.consumesCounters[] entry of a ResourceSlice. The scheduler then
|
||||
// only co-allocates devices drawing from the same counter set when their
|
||||
// declared groups intersect, moving detection of incompatible co-allocation
|
||||
// (e.g. GPU MIG vs vGPU on one physical device) from preparation-time
|
||||
// failure to scheduling-time rejection.
|
||||
//
|
||||
// DRAPartitionableDevices also needs to be enabled, since the field lives
|
||||
// on consumesCounters[] entries which only exist for partitionable devices.
|
||||
DRADeviceCompatibilityGroups featuregate.Feature = "DRADeviceCompatibilityGroups"
|
||||
|
||||
// owner: @pohly
|
||||
// kep: http://kep.k8s.io/5055
|
||||
//
|
||||
@@ -1442,6 +1456,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
||||
{Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta},
|
||||
},
|
||||
|
||||
DRADeviceCompatibilityGroups: {
|
||||
{Version: version.MustParse("1.37"), Default: false, PreRelease: featuregate.Alpha},
|
||||
},
|
||||
|
||||
DRADeviceTaintRules: {
|
||||
{Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha},
|
||||
{Version: version.MustParse("1.36"), Default: false, PreRelease: featuregate.Beta}, // Depends on an off-by-default beta API.
|
||||
@@ -2549,6 +2567,8 @@ var defaultKubernetesFeatureGateDependencies = map[featuregate.Feature][]feature
|
||||
|
||||
DRADeviceBindingConditions: {DynamicResourceAllocation, DRAResourceClaimDeviceStatus},
|
||||
|
||||
DRADeviceCompatibilityGroups: {DynamicResourceAllocation, DRAPartitionableDevices},
|
||||
|
||||
DRADeviceTaintRules: {DRADeviceTaints}, // DynamicResourceAllocation is indirect.
|
||||
|
||||
DRADeviceTaints: {DynamicResourceAllocation},
|
||||
|
||||
@@ -204,6 +204,7 @@ func dropDisabledFields(newSlice, oldSlice *resource.ResourceSlice) {
|
||||
dropDisabledDRAPartitionableDevicesFields(newSlice, oldSlice)
|
||||
dropDisabledDRADeviceBindingConditionsFields(newSlice, oldSlice)
|
||||
dropDisabledDRAConsumableCapacityFields(newSlice, oldSlice)
|
||||
dropDisabledDRADeviceCompatibilityGroupsFields(newSlice, oldSlice)
|
||||
dropDisabledDRANodeAllocatableResourcesFields(newSlice, oldSlice)
|
||||
dropDisableDRAListTypeAttributesFields(newSlice, oldSlice)
|
||||
dropDisabledDRAPartitionableDevicesTypeFields(newSlice, oldSlice)
|
||||
@@ -352,6 +353,39 @@ func dropDisabledDRAConsumableCapacityFields(newSlice, oldSlice *resource.Resour
|
||||
}
|
||||
}
|
||||
|
||||
func draDeviceCompatibilityGroupsFeatureInUse(slice *resource.ResourceSlice) bool {
|
||||
if slice == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, device := range slice.Spec.Devices {
|
||||
for _, consumption := range device.ConsumesCounters {
|
||||
if len(consumption.CompatibilityGroups) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dropDisabledDRADeviceCompatibilityGroupsFields drops the CompatibilityGroups
|
||||
// field from each device.consumesCounters[] entry of the new slice if the
|
||||
// DRADeviceCompatibilityGroups feature is disabled and the field was not
|
||||
// already in use in the old slice.
|
||||
func dropDisabledDRADeviceCompatibilityGroupsFields(newSlice, oldSlice *resource.ResourceSlice) {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.DRADeviceCompatibilityGroups) ||
|
||||
draDeviceCompatibilityGroupsFeatureInUse(oldSlice) {
|
||||
// No need to drop anything.
|
||||
return
|
||||
}
|
||||
|
||||
for i := range newSlice.Spec.Devices {
|
||||
for j := range newSlice.Spec.Devices[i].ConsumesCounters {
|
||||
newSlice.Spec.Devices[i].ConsumesCounters[j].CompatibilityGroups = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dropDisabledDRANodeAllocatableResourcesFields(newSlice, oldSlice *resource.ResourceSlice) {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.DRANodeAllocatableResources) || draNodeAllocatableResourcesFeatureInUse(oldSlice) {
|
||||
return
|
||||
|
||||
@@ -176,6 +176,12 @@ var sliceWithCapacity = func() *resource.ResourceSlice {
|
||||
return obj
|
||||
}()
|
||||
|
||||
var sliceWithCompatibilityGroups = func() *resource.ResourceSlice {
|
||||
obj := sliceWithPartitionableDevicesConsumesCounters.DeepCopy()
|
||||
obj.Spec.Devices[0].ConsumesCounters[0].CompatibilityGroups = []string{"mig"}
|
||||
return obj
|
||||
}()
|
||||
|
||||
var sliceWithConsumableCapacity = func() *resource.ResourceSlice {
|
||||
obj := sliceWithCapacity.DeepCopy()
|
||||
obj.Spec.Devices[0].AllowMultipleAllocations = ptr.To(true)
|
||||
@@ -458,6 +464,24 @@ func TestResourceSliceStrategyCreate(t *testing.T) {
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
"keep-fields-compatibility-groups": {
|
||||
obj: sliceWithCompatibilityGroups,
|
||||
featureOverrides: featuregatetesting.FeatureOverrides{features.DRAPartitionableDevices: true, features.DRADeviceCompatibilityGroups: true},
|
||||
expectObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.Generation = 1
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
"drop-fields-compatibility-groups-disabled-feature": {
|
||||
obj: sliceWithCompatibilityGroups,
|
||||
featureOverrides: featuregatetesting.FeatureOverrides{features.DRAPartitionableDevices: true, features.DRADeviceCompatibilityGroups: false},
|
||||
expectObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithPartitionableDevicesConsumesCounters.DeepCopy()
|
||||
obj.Generation = 1
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
@@ -1004,6 +1028,67 @@ func TestResourceSliceStrategyUpdate(t *testing.T) {
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
"drop-fields-compatibility-groups": {
|
||||
oldObj: sliceWithPartitionableDevicesConsumesCounters,
|
||||
newObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
return obj
|
||||
}(),
|
||||
featureOverrides: featuregatetesting.FeatureOverrides{features.DRAPartitionableDevices: true, features.DRADeviceCompatibilityGroups: false},
|
||||
expectObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithPartitionableDevicesConsumesCounters.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
// The generation is bumped for the incoming spec change even
|
||||
// though dropping the disabled field makes the stored spec
|
||||
// identical again, like in the partitionable drop case above.
|
||||
obj.Generation = 1
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
"keep-fields-compatibility-groups": {
|
||||
oldObj: sliceWithPartitionableDevicesConsumesCounters,
|
||||
newObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
return obj
|
||||
}(),
|
||||
featureOverrides: featuregatetesting.FeatureOverrides{features.DRAPartitionableDevices: true, features.DRADeviceCompatibilityGroups: true},
|
||||
expectObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
obj.Generation = 1
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
"keep-existing-fields-compatibility-groups": {
|
||||
oldObj: sliceWithCompatibilityGroups,
|
||||
newObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
return obj
|
||||
}(),
|
||||
featureOverrides: featuregatetesting.FeatureOverrides{features.DRAPartitionableDevices: true, features.DRADeviceCompatibilityGroups: true},
|
||||
expectObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
"keep-existing-fields-compatibility-groups-disabled-feature": {
|
||||
oldObj: sliceWithCompatibilityGroups,
|
||||
newObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
return obj
|
||||
}(),
|
||||
featureOverrides: featuregatetesting.FeatureOverrides{features.DRAPartitionableDevices: true, features.DRADeviceCompatibilityGroups: false},
|
||||
expectObj: func() *resource.ResourceSlice {
|
||||
obj := sliceWithCompatibilityGroups.DeepCopy()
|
||||
obj.ResourceVersion = "4"
|
||||
return obj
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testcases {
|
||||
|
||||
@@ -368,6 +368,10 @@ const ResourceSliceMaxDeviceCounterConsumptionsPerDevice = 2
|
||||
// per device counter consumption.
|
||||
const ResourceSliceMaxCountersPerDeviceCounterConsumption = 32
|
||||
|
||||
// Defines the maximum number of compatibility groups that can be
|
||||
// declared per device counter consumption.
|
||||
const DeviceCompatibilityGroupsMaxSize = 2
|
||||
|
||||
// Device represents one individual hardware instance that can be selected based
|
||||
// on its attributes. Besides the name, exactly one field must be set.
|
||||
type Device struct {
|
||||
@@ -652,6 +656,36 @@ type DeviceCounterConsumption struct {
|
||||
// +k8s:beta(since: "1.37")=+k8s:required
|
||||
// +k8s:beta(since: "1.37")=+k8s:eachKey=+k8s:format=k8s-short-name
|
||||
Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"`
|
||||
|
||||
// CompatibilityGroups is a list of opaque group names for
|
||||
// this counter set consumption.
|
||||
//
|
||||
// Devices that consume counters from the same counter set may only be
|
||||
// allocated at the same time ("co-allocated") if they all share at least
|
||||
// one common group: the intersection of the CompatibilityGroups of all
|
||||
// co-allocated devices on that counter set must be non-empty. Devices
|
||||
// that consume from different counter sets are never compared via this
|
||||
// field.
|
||||
//
|
||||
// An unset field, an explicit nil, and an empty list are equivalent and
|
||||
// mean "no groups": such a device is only co-allocatable with sibling
|
||||
// devices on the same counter set that also have no groups, and is never
|
||||
// co-allocatable with a device that declares one or more groups.
|
||||
//
|
||||
// Group names are opaque and meaningful only within the
|
||||
// publishing driver's pool.
|
||||
//
|
||||
// The maximum number of groups is 2, and the names must be unique.
|
||||
//
|
||||
// +optional
|
||||
// +listType=atomic
|
||||
// +featureGate=DRADeviceCompatibilityGroups
|
||||
// +k8s:listType=atomic
|
||||
// +k8s:optional
|
||||
// +k8s:maxItems=2
|
||||
// +k8s:unique=set
|
||||
// +k8s:eachVal=+k8s:format=k8s-short-name
|
||||
CompatibilityGroups []string `json:"compatibilityGroups,omitempty" protobuf:"bytes,3,rep,name=compatibilityGroups"`
|
||||
}
|
||||
|
||||
// DeviceCapacity describes a quantity associated with a device.
|
||||
|
||||
@@ -361,6 +361,10 @@ const ResourceSliceMaxDeviceCounterConsumptionsPerDevice = 2
|
||||
// per device counter consumption.
|
||||
const ResourceSliceMaxCountersPerDeviceCounterConsumption = 32
|
||||
|
||||
// Defines the maximum number of compatibility groups that can be
|
||||
// declared per device counter consumption.
|
||||
const DeviceCompatibilityGroupsMaxSize = 2
|
||||
|
||||
// Device represents one individual hardware instance that can be selected based
|
||||
// on its attributes. Besides the name, exactly one field must be set.
|
||||
type Device struct {
|
||||
@@ -654,6 +658,36 @@ type DeviceCounterConsumption struct {
|
||||
// +k8s:beta(since: "1.37")=+k8s:required
|
||||
// +k8s:beta(since: "1.37")=+k8s:eachKey=+k8s:format=k8s-short-name
|
||||
Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"`
|
||||
|
||||
// CompatibilityGroups is a list of opaque group names for
|
||||
// this counter set consumption.
|
||||
//
|
||||
// Devices that consume counters from the same counter set may only be
|
||||
// allocated at the same time ("co-allocated") if they all share at least
|
||||
// one common group: the intersection of the CompatibilityGroups of all
|
||||
// co-allocated devices on that counter set must be non-empty. Devices
|
||||
// that consume from different counter sets are never compared via this
|
||||
// field.
|
||||
//
|
||||
// An unset field, an explicit nil, and an empty list are equivalent and
|
||||
// mean "no groups": such a device is only co-allocatable with sibling
|
||||
// devices on the same counter set that also have no groups, and is never
|
||||
// co-allocatable with a device that declares one or more groups.
|
||||
//
|
||||
// Group names are opaque and meaningful only within the
|
||||
// publishing driver's pool.
|
||||
//
|
||||
// The maximum number of groups is 2, and the names must be unique.
|
||||
//
|
||||
// +optional
|
||||
// +listType=atomic
|
||||
// +featureGate=DRADeviceCompatibilityGroups
|
||||
// +k8s:listType=atomic
|
||||
// +k8s:optional
|
||||
// +k8s:maxItems=2
|
||||
// +k8s:unique=set
|
||||
// +k8s:eachVal=+k8s:format=k8s-short-name
|
||||
CompatibilityGroups []string `json:"compatibilityGroups,omitempty" protobuf:"bytes,3,rep,name=compatibilityGroups"`
|
||||
}
|
||||
|
||||
// DeviceCapacity describes a quantity associated with a device.
|
||||
|
||||
@@ -353,6 +353,10 @@ const ResourceSliceMaxDeviceCounterConsumptionsPerDevice = 2
|
||||
// per device counter consumption.
|
||||
const ResourceSliceMaxCountersPerDeviceCounterConsumption = 32
|
||||
|
||||
// Defines the maximum number of compatibility groups that can be
|
||||
// declared per device counter consumption.
|
||||
const DeviceCompatibilityGroupsMaxSize = 2
|
||||
|
||||
// Device represents one individual hardware instance that can be selected based
|
||||
// on its attributes. Besides the name, exactly one field must be set.
|
||||
type Device struct {
|
||||
@@ -637,6 +641,36 @@ type DeviceCounterConsumption struct {
|
||||
// +k8s:beta(since: "1.37")=+k8s:required
|
||||
// +k8s:beta(since: "1.37")=+k8s:eachKey=+k8s:format=k8s-short-name
|
||||
Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"`
|
||||
|
||||
// CompatibilityGroups is a list of opaque group names for
|
||||
// this counter set consumption.
|
||||
//
|
||||
// Devices that consume counters from the same counter set may only be
|
||||
// allocated at the same time ("co-allocated") if they all share at least
|
||||
// one common group: the intersection of the CompatibilityGroups of all
|
||||
// co-allocated devices on that counter set must be non-empty. Devices
|
||||
// that consume from different counter sets are never compared via this
|
||||
// field.
|
||||
//
|
||||
// An unset field, an explicit nil, and an empty list are equivalent and
|
||||
// mean "no groups": such a device is only co-allocatable with sibling
|
||||
// devices on the same counter set that also have no groups, and is never
|
||||
// co-allocatable with a device that declares one or more groups.
|
||||
//
|
||||
// Group names are opaque and meaningful only within the
|
||||
// publishing driver's pool.
|
||||
//
|
||||
// The maximum number of groups is 2, and the names must be unique.
|
||||
//
|
||||
// +optional
|
||||
// +listType=atomic
|
||||
// +featureGate=DRADeviceCompatibilityGroups
|
||||
// +k8s:listType=atomic
|
||||
// +k8s:optional
|
||||
// +k8s:maxItems=2
|
||||
// +k8s:unique=set
|
||||
// +k8s:eachVal=+k8s:format=k8s-short-name
|
||||
CompatibilityGroups []string `json:"compatibilityGroups,omitempty" protobuf:"bytes,3,rep,name=compatibilityGroups"`
|
||||
}
|
||||
|
||||
// DeviceCapacity describes a quantity associated with a device.
|
||||
|
||||
@@ -78,6 +78,7 @@ type Device struct {
|
||||
}
|
||||
|
||||
type DeviceCounterConsumption struct {
|
||||
CounterSet UniqueString
|
||||
Counters map[string]resourceapi.Counter `json:",omitempty"`
|
||||
CounterSet UniqueString
|
||||
Counters map[string]resourceapi.Counter `json:",omitempty"`
|
||||
CompatibilityGroups []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
| DRAConsumableCapacity | :ballot_box_with_check: 1.36+ | | 1.34–1.35 | 1.36– | | | DynamicResourceAllocation | [code](https://cs.k8s.io/?q=%5CbDRAConsumableCapacity%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRAConsumableCapacity%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
| DRADerivedAttributes | | | 1.37– | | | | DynamicResourceAllocation | [code](https://cs.k8s.io/?q=%5CbDRADerivedAttributes%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRADerivedAttributes%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
| DRADeviceBindingConditions | :ballot_box_with_check: 1.36+ | | 1.34–1.35 | 1.36– | | | DRAResourceClaimDeviceStatus<br>DynamicResourceAllocation | [code](https://cs.k8s.io/?q=%5CbDRADeviceBindingConditions%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRADeviceBindingConditions%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
| DRADeviceCompatibilityGroups | | | 1.37– | | | | DRAPartitionableDevices<br>DynamicResourceAllocation | [code](https://cs.k8s.io/?q=%5CbDRADeviceCompatibilityGroups%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRADeviceCompatibilityGroups%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
| DRADeviceTaintRules | :ballot_box_with_check: 1.37+ | | 1.35 | 1.36 | 1.37– | | DRADeviceTaints | [code](https://cs.k8s.io/?q=%5CbDRADeviceTaintRules%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRADeviceTaintRules%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
| DRADeviceTaints | :ballot_box_with_check: 1.36+ | | 1.33–1.35 | 1.36 | 1.37– | | DynamicResourceAllocation | [code](https://cs.k8s.io/?q=%5CbDRADeviceTaints%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRADeviceTaints%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
| DRAExtendedResource | :ballot_box_with_check: 1.36+ | :closed_lock_with_key: 1.37+ | 1.34–1.35 | 1.36 | 1.37– | | DynamicResourceAllocation | [code](https://cs.k8s.io/?q=%5CbDRAExtendedResource%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/kubernetes) [KEPs](https://cs.k8s.io/?q=%5CbDRAExtendedResource%5Cb&i=nope&files=&excludeFiles=CHANGELOG&repos=kubernetes/enhancements) |
|
||||
|
||||
@@ -529,6 +529,12 @@
|
||||
lockToDefault: false
|
||||
preRelease: Beta
|
||||
version: "1.36"
|
||||
- name: DRADeviceCompatibilityGroups
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
lockToDefault: false
|
||||
preRelease: Alpha
|
||||
version: "1.37"
|
||||
- name: DRADeviceTaintRules
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
|
||||
@@ -526,6 +526,28 @@ func TestDeclarativeValidate(t *testing.T) {
|
||||
field.Duplicate(field.NewPath("spec", "skipNodeOperations").Index(1), resource.SkipNodeOperationAll),
|
||||
},
|
||||
},
|
||||
// spec.devices.consumesCounters.compatibilityGroups
|
||||
"valid: at limit device consumes counters compatibility groups": {
|
||||
input: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups(compatibilityGroupNames(resource.DeviceCompatibilityGroupsMaxSize)...)),
|
||||
},
|
||||
"invalid: too many device consumes counters compatibility groups": {
|
||||
input: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups(compatibilityGroupNames(resource.DeviceCompatibilityGroupsMaxSize + 1)...)),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.TooMany(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("compatibilityGroups"), resource.DeviceCompatibilityGroupsMaxSize+1, resource.DeviceCompatibilityGroupsMaxSize).WithOrigin("maxItems"),
|
||||
},
|
||||
},
|
||||
"invalid: duplicate device consumes counters compatibility groups": {
|
||||
input: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups("duplicate-group", "duplicate-group")),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.Duplicate(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("compatibilityGroups").Index(1), "duplicate-group"),
|
||||
},
|
||||
},
|
||||
"invalid: device consumes counters compatibility group bad format": {
|
||||
input: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups("InvalidKey")),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("compatibilityGroups").Index(0), "InvalidKey", "").WithOrigin("format=k8s-short-name"),
|
||||
},
|
||||
},
|
||||
// TODO: Add more test cases
|
||||
}
|
||||
|
||||
@@ -803,6 +825,32 @@ func TestDeclarativeValidateUpdate(t *testing.T) {
|
||||
field.Duplicate(field.NewPath("spec", "skipNodeOperations").Index(1), resource.SkipNodeOperationAll),
|
||||
},
|
||||
},
|
||||
// spec.devices.consumesCounters.compatibilityGroups
|
||||
"valid update: at limit device consumes counters compatibility groups": {
|
||||
old: mkResourceSliceWithDevices(),
|
||||
update: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups(compatibilityGroupNames(resource.DeviceCompatibilityGroupsMaxSize)...)),
|
||||
},
|
||||
"invalid update: too many device consumes counters compatibility groups": {
|
||||
old: mkResourceSliceWithDevices(),
|
||||
update: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups(compatibilityGroupNames(resource.DeviceCompatibilityGroupsMaxSize + 1)...)),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.TooMany(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("compatibilityGroups"), resource.DeviceCompatibilityGroupsMaxSize+1, resource.DeviceCompatibilityGroupsMaxSize).WithOrigin("maxItems"),
|
||||
},
|
||||
},
|
||||
"invalid update: duplicate device consumes counters compatibility groups": {
|
||||
old: mkResourceSliceWithDevices(),
|
||||
update: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups("duplicate-group", "duplicate-group")),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.Duplicate(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("compatibilityGroups").Index(1), "duplicate-group"),
|
||||
},
|
||||
},
|
||||
"invalid update: device consumes counters compatibility group bad format": {
|
||||
old: mkResourceSliceWithDevices(),
|
||||
update: mkResourceSliceWithDevices(tweakDeviceConsumesCountersCompatibilityGroups("InvalidKey")),
|
||||
expectedErrs: field.ErrorList{
|
||||
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("compatibilityGroups").Index(0), "InvalidKey", "").WithOrigin("format=k8s-short-name"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, tc := range testCases {
|
||||
|
||||
@@ -984,6 +1032,31 @@ func tweakDeviceConsumesCountersCounterSetName(counterSets ...string) func(*reso
|
||||
}
|
||||
}
|
||||
|
||||
func tweakDeviceConsumesCountersCompatibilityGroups(groups ...string) func(*resource.ResourceSlice) {
|
||||
return func(rs *resource.ResourceSlice) {
|
||||
rs.Spec.Devices[0].ConsumesCounters = []resource.DeviceCounterConsumption{
|
||||
{
|
||||
CounterSet: "shared-counter-set",
|
||||
Counters: map[string]resource.Counter{
|
||||
"valid-key": {},
|
||||
},
|
||||
CompatibilityGroups: groups,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compatibilityGroupNames returns count distinct, well-formed compatibility
|
||||
// group names for exercising the maxItems (DeviceCompatibilityGroupsMaxSize)
|
||||
// limit.
|
||||
func compatibilityGroupNames(count int) []string {
|
||||
groups := make([]string, count)
|
||||
for i := range groups {
|
||||
groups[i] = fmt.Sprintf("group-%d", i)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func tweakSharedCounter(counters map[string]resource.Counter) func(*resource.ResourceSlice) {
|
||||
return func(rs *resource.ResourceSlice) {
|
||||
rs.Spec.SharedCounters = []resource.CounterSet{
|
||||
|
||||
@@ -33,11 +33,16 @@ import (
|
||||
// NewMaxResourceSlices creates slices that are as large as possible given the current validation constraints.
|
||||
func NewMaxResourceSlices() map[string]*resourceapi.ResourceSlice {
|
||||
slices := map[string]*resourceapi.ResourceSlice{
|
||||
"basic": newBasicResourceSlice(resourceapi.ResourceSliceMaxDevices),
|
||||
"with-taints-and-consumes-counters": newResourceSliceWithTaintsAndConsumesCounters(),
|
||||
"with-shared-counters": newSharedCountersResourceSlice(),
|
||||
"with-list-values": newResourceSliceWithListValues(),
|
||||
"with-taints-and-consumes-counters-and-list-values": newResourceSliceWithTaintsAndConsumesCountersAndListValues(),
|
||||
"basic": newBasicResourceSlice(resourceapi.ResourceSliceMaxDevices),
|
||||
// advanced combines all device-level features (taints, consumes counters
|
||||
// with compatibility groups, and list values) into the largest possible
|
||||
// device-based slice.
|
||||
// Compatibility groups grow this maximal slice by ~16,640 B (1,190,621 B -> ~1,207,261 B, +1.4%),
|
||||
// which stays well within the object size limit.
|
||||
"advanced": newAdvancedResourceSlice(),
|
||||
// shared-counters is a distinct slice kind: SharedCounters and Devices are
|
||||
// mutually exclusive per slice, so it cannot be merged into "advanced".
|
||||
"shared-counters": newSharedCountersResourceSlice(),
|
||||
}
|
||||
return slices
|
||||
}
|
||||
@@ -123,16 +128,27 @@ func newSharedCountersResourceSlice() *resourceapi.ResourceSlice {
|
||||
return slice
|
||||
}
|
||||
|
||||
func newResourceSliceWithListValues() *resourceapi.ResourceSlice {
|
||||
slice := newBasicResourceSlice(resourceapi.ResourceSliceMaxDevicesWithAdvancedFeatures)
|
||||
// newAdvancedResourceSlice builds the maximal device-feature slice: taints,
|
||||
// consumes counters, compatibility groups, and list values combined.
|
||||
func newAdvancedResourceSlice() *resourceapi.ResourceSlice {
|
||||
slice := newResourceSliceWithTaintsAndConsumesCounters()
|
||||
addListValues(slice)
|
||||
addCompatibilityGroups(slice)
|
||||
return slice
|
||||
}
|
||||
|
||||
func newResourceSliceWithTaintsAndConsumesCountersAndListValues() *resourceapi.ResourceSlice {
|
||||
slice := newResourceSliceWithTaintsAndConsumesCounters()
|
||||
addListValues(slice)
|
||||
return slice
|
||||
// addCompatibilityGroups declares the maximum number of compatibility groups
|
||||
// on each device counter consumption.
|
||||
func addCompatibilityGroups(slice *resourceapi.ResourceSlice) {
|
||||
for i := range slice.Spec.Devices {
|
||||
for j := range slice.Spec.Devices[i].ConsumesCounters {
|
||||
groups := make([]string, 0, resourceapi.DeviceCompatibilityGroupsMaxSize)
|
||||
for k := range resourceapi.DeviceCompatibilityGroupsMaxSize {
|
||||
groups = append(groups, maxDNSLabel(k))
|
||||
}
|
||||
slice.Spec.Devices[i].ConsumesCounters[j].CompatibilityGroups = groups
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addListValues(slice *resourceapi.ResourceSlice) {
|
||||
|
||||
Reference in New Issue
Block a user