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:
Omer Yahud
2026-07-24 11:23:28 +03:00
parent f7b9c23180
commit 9f0a577fc6
12 changed files with 380 additions and 13 deletions

View File

@@ -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.

View File

@@ -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},

View File

@@ -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

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"`
}

View File

@@ -55,6 +55,7 @@
| DRAConsumableCapacity | :ballot_box_with_check: 1.36+ | | 1.341.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:&nbsp;1.36+ | | 1.341.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:&nbsp;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:&nbsp;1.36+ | | 1.331.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:&nbsp;1.36+ | :closed_lock_with_key:&nbsp;1.37+ | 1.341.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) |

View File

@@ -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

View File

@@ -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{

View File

@@ -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) {