Add resource v1beta2 API

This commit is contained in:
Morten Torkildsen
2025-03-20 07:04:41 +00:00
parent 16abcd78bd
commit 39507d911f
50 changed files with 3860 additions and 523 deletions

View File

@@ -63,6 +63,11 @@ API rule violation: names_match,k8s.io/api/resource/v1beta1,DeviceAttribute,IntV
API rule violation: names_match,k8s.io/api/resource/v1beta1,DeviceAttribute,StringValue
API rule violation: names_match,k8s.io/api/resource/v1beta1,DeviceAttribute,VersionValue
API rule violation: names_match,k8s.io/api/resource/v1beta1,NetworkDeviceData,IPs
API rule violation: names_match,k8s.io/api/resource/v1beta2,DeviceAttribute,BoolValue
API rule violation: names_match,k8s.io/api/resource/v1beta2,DeviceAttribute,IntValue
API rule violation: names_match,k8s.io/api/resource/v1beta2,DeviceAttribute,StringValue
API rule violation: names_match,k8s.io/api/resource/v1beta2,DeviceAttribute,VersionValue
API rule violation: names_match,k8s.io/api/resource/v1beta2,NetworkDeviceData,IPs
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource

View File

@@ -51,6 +51,7 @@ var apiVersionPriorities = merge(controlplaneapiserver.DefaultGenericAPIServiceP
{Group: "node.k8s.io", Version: "v1"}: {Group: 16300, Version: 15},
{Group: "node.k8s.io", Version: "v1alpha1"}: {Group: 16300, Version: 1},
{Group: "node.k8s.io", Version: "v1beta1"}: {Group: 16300, Version: 9},
{Group: "resource.k8s.io", Version: "v1beta2"}: {Group: 16200, Version: 15},
{Group: "resource.k8s.io", Version: "v1beta1"}: {Group: 16200, Version: 9},
{Group: "resource.k8s.io", Version: "v1alpha3"}: {Group: 16200, Version: 1},
// Append a new group to the end of the list if unsure.

View File

@@ -94,6 +94,7 @@ coordination.k8s.io/v1beta1 \
coordination.k8s.io/v1 \
discovery.k8s.io/v1 \
discovery.k8s.io/v1beta1 \
resource.k8s.io/v1beta2 \
resource.k8s.io/v1beta1 \
resource.k8s.io/v1alpha3 \
extensions/v1beta1 \

View File

@@ -149,6 +149,12 @@ func TestDefaulting(t *testing.T) {
{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: "resource.k8s.io", Version: "v1beta2", Kind: "ResourceClaim"}: {},
{Group: "resource.k8s.io", Version: "v1beta2", Kind: "ResourceClaimList"}: {},
{Group: "resource.k8s.io", Version: "v1beta2", Kind: "ResourceClaimTemplate"}: {},
{Group: "resource.k8s.io", Version: "v1beta2", Kind: "ResourceClaimTemplateList"}: {},
{Group: "resource.k8s.io", Version: "v1beta2", Kind: "ResourceSlice"}: {},
{Group: "resource.k8s.io", Version: "v1beta2", 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

@@ -32,7 +32,7 @@ import (
// leads to errors during roundtrip tests.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(r *resource.DeviceRequest, c randfill.Continue) {
func(r *resource.ExactDeviceRequest, c randfill.Continue) {
c.FillNoCustom(r) // fuzz self without calling this function again
if r.AllocationMode == "" {
@@ -44,6 +44,7 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
},
func(r *resource.DeviceSubRequest, c randfill.Continue) {
c.FillNoCustom(r) // fuzz self without calling this function again
if r.AllocationMode == "" {
r.AllocationMode = []resource.DeviceAllocationMode{
resource.DeviceAllocationModeAll,
@@ -94,5 +95,16 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
// might be valid JSON which changes during re-encoding.
r.Data = &runtime.RawExtension{Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`)}
},
func(r *resource.ResourceSliceSpec, c randfill.Continue) {
c.FillNoCustom(r)
// Setting AllNodes to false is not allowed. It must be
// either true or nil.
if r.AllNodes != nil && !*r.AllNodes {
r.AllNodes = nil
}
if r.NodeName != nil && *r.NodeName == "" {
r.NodeName = nil
}
},
}
}

View File

@@ -25,6 +25,7 @@ import (
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/apis/resource/v1alpha3"
"k8s.io/kubernetes/pkg/apis/resource/v1beta1"
"k8s.io/kubernetes/pkg/apis/resource/v1beta2"
)
func init() {
@@ -36,5 +37,8 @@ func Install(scheme *runtime.Scheme) {
utilruntime.Must(resource.AddToScheme(scheme))
utilruntime.Must(v1alpha3.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1beta1.SchemeGroupVersion, v1alpha3.SchemeGroupVersion))
utilruntime.Must(v1beta2.AddToScheme(scheme))
// TODO(https://github.com/kubernetes/kubernetes/issues/129889) We should
// change the serialization version to v1beta2 for 1.34.
utilruntime.Must(scheme.SetVersionPriority(v1beta1.SchemeGroupVersion, v1beta2.SchemeGroupVersion, v1alpha3.SchemeGroupVersion))
}

View File

@@ -111,7 +111,7 @@ type ResourceSliceSpec struct {
//
// +optional
// +oneOf=NodeSelection
NodeName string
NodeName *string
// NodeSelector defines which nodes have access to the resources in the pool,
// when that pool is not limited to a single node.
@@ -130,7 +130,7 @@ type ResourceSliceSpec struct {
//
// +optional
// +oneOf=NodeSelection
AllNodes bool
AllNodes *bool
// Devices lists some or all of the devices in this pool.
//
@@ -157,7 +157,7 @@ type ResourceSliceSpec struct {
//
// The names of the SharedCounters must be unique in the ResourceSlice.
//
// The maximum number of SharedCounters is 32.
// The maximum number of counters in all sets is 32.
//
// +optional
// +listType=atomic
@@ -183,7 +183,7 @@ type CounterSet struct {
// Counters defines the set of counters for this CounterSet
// The name of each counter must be unique in that set and must be a DNS label.
//
// The maximum number of counters is 32.
// The maximum number of counters in all sets is 32.
//
// +required
Counters map[string]Counter
@@ -234,28 +234,10 @@ const ResourceSliceMaxSharedCapacity = 128
const ResourceSliceMaxDevices = 128
const PoolNameMaxLength = validation.DNS1123SubdomainMaxLength // Same as for a single node name.
// Defines the max number of SharedCounters that can be specified
// in a ResourceSlice. This is used to validate the fields:
// * spec.sharedCounters
// Defines the max number of shared counters that can be specified
// in a ResourceSlice. The number is summed up across all sets.
const ResourceSliceMaxSharedCounters = 32
// Defines the max number of Counters from which a device
// can consume. This is used to validate the fields:
// * spec.devices[].consumesCounter
const ResourceSliceMaxDeviceCounterConsumptions = 32
// Defines the max number of counters
// that can be specified for sharedCounters in a ResourceSlice.
// This is used to validate the fields:
// * spec.sharedCounters[].counters
const ResourceSliceMaxSharedCountersCounters = 32
// Defines the max number of counters
// that can be specified for consumesCounter in a ResourceSlice.
// This is used to validate the fields:
// * spec.devices[].consumesCounter[].counters
const ResourceSliceMaxDeviceCounterConsumptionCounters = 32
// 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 {
@@ -265,15 +247,6 @@ type Device struct {
// +required
Name string
// Basic defines one device instance.
//
// +optional
// +oneOf=deviceType
Basic *BasicDevice
}
// BasicDevice defines one device instance.
type BasicDevice struct {
// Attributes defines the set of attributes for this device.
// The name of each attribute must be unique in that set.
//
@@ -290,20 +263,21 @@ type BasicDevice struct {
// +optional
Capacity map[QualifiedName]DeviceCapacity
// ConsumesCounter defines a list of references to sharedCounters
// ConsumesCounters defines a list of references to sharedCounters
// and the set of counters that the device will
// consume from those counter sets.
//
// There can only be a single entry per counterSet.
//
// The maximum number of device counter consumption entries
// is 32. This is the same as the maximum number of shared counters
// allowed in a ResourceSlice.
// The total number of device counter consumption entries
// must be <= 32. In addition, the total number in the
// entire ResourceSlice must be <= 1024 (for example,
// 64 devices with 16 counters each).
//
// +optional
// +listType=atomic
// +featureGate=DRAPartitionableDevices
ConsumesCounter []DeviceCounterConsumption
ConsumesCounters []DeviceCounterConsumption
// NodeName identifies the node where the device is available.
//
@@ -317,6 +291,8 @@ type BasicDevice struct {
// NodeSelector defines the nodes where the device is available.
//
// Must use exactly one term.
//
// Must only be set if Spec.PerDeviceNodeSelection is set to true.
// At most one of NodeName, NodeSelector and AllNodes can be set.
//
@@ -337,7 +313,7 @@ type BasicDevice struct {
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
// The maximum number of taints is 4.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
@@ -351,17 +327,18 @@ type BasicDevice struct {
// DeviceCounterConsumption defines a set of counters that
// a device will consume from a CounterSet.
type DeviceCounterConsumption struct {
// SharedCounter defines the shared counter from which the
// CounterSet is the name of the set from which the
// counters defined will be consumed.
//
// +required
SharedCounter string
CounterSet string
// Counters defines the Counter that will be consumed by
// the device.
// Counters defines the counters that will be consumed by the device.
//
//
// The maximum number of Counters is 32.
// The maximum number counters in a device is 32.
// In addition, the maximum number of all counters
// in all devices is 1024 (for example, 64 devices with
// 16 counters each).
//
// +required
Counters map[string]Counter
@@ -389,6 +366,14 @@ type Counter struct {
// Limit for the sum of the number of entries in both attributes and capacity.
const ResourceSliceMaxAttributesAndCapacitiesPerDevice = 32
// Limit for the total number of counters in each device.
const ResourceSliceMaxCountersPerDevice = 32
// Limit for the total number of counters defined in devices in
// a ResourceSlice. We want to allow up to 64 devices to specify
// up to 16 counters, so the limit for the ResourceSlice will be 1024.
const ResourceSliceMaxDeviceCountersPerSlice = 1024 // 64 * 16
// QualifiedName is the name of a device attribute or capacity.
//
// Attributes and capacities are defined either by the owner of the specific
@@ -452,7 +437,7 @@ type DeviceAttribute struct {
const DeviceAttributeMaxValueLength = 64
// DeviceTaintsMaxLength is the maximum number of taints per device.
const DeviceTaintsMaxLength = 8
const DeviceTaintsMaxLength = 4
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
@@ -608,25 +593,60 @@ const (
// DeviceRequest is a request for devices required for a claim.
// This is typically a request for a single resource like a device, but can
// also ask for several identical devices.
// also ask for several identical devices. With FirstAvailable it is also
// possible to provide a prioritized list of requests.
type DeviceRequest struct {
// Name can be used to reference this request in a pod.spec.containers[].resources.claims
// entry and in a constraint of the claim.
//
// Must be a DNS label and unique among all DeviceRequests in a
// ResourceClaim.
// References using the name in the DeviceRequest will uniquely
// identify a request when the Exactly field is set. When the
// FirstAvailable field is set, a reference to the name of the
// DeviceRequest will match whatever subrequest is chosen by the
// scheduler.
//
// Must be a DNS label.
//
// +required
Name string
// Exactly specifies the details for a single request that must
// be met exactly for the request to be satisfied.
//
// One of Exactly or FirstAvailable must be set.
//
// +optional
// +oneOf=deviceRequestType
Exactly *ExactDeviceRequest
// FirstAvailable contains subrequests, of which exactly one will be
// selected by the scheduler. 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 can not be used.
//
// DRA 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.
//
// +optional
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
FirstAvailable []DeviceSubRequest
}
// ExactDeviceRequest is a request for one or more identical devices.
type ExactDeviceRequest struct {
// DeviceClassName references a specific DeviceClass, which can define
// additional configuration and selectors to be inherited by this
// request.
//
// A class is required if no subrequests are specified in the
// firstAvailable list and no class can be set if subrequests
// are specified in the firstAvailable list.
// Which classes are available depends on the cluster.
// A DeviceClassName is required.
//
// Administrators may use this to restrict which devices may get
// requested by only installing classes with selectors for permitted
@@ -634,8 +654,7 @@ type DeviceRequest struct {
// then administrators can create an empty DeviceClass for users
// to reference.
//
// +optional
// +oneOf=deviceRequestType
// +required
DeviceClassName string
// Selectors define criteria which must be satisfied by a specific
@@ -643,9 +662,6 @@ type DeviceRequest struct {
// request. All selectors must be satisfied for a device to be
// considered.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// +optional
// +listType=atomic
Selectors []DeviceSelector
@@ -666,9 +682,6 @@ type DeviceRequest struct {
// the mode is ExactCount and count is not specified, the default count is
// one. Any other requests must specify this field.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// More modes may get added in the future. Clients must refuse to handle
// requests with unknown modes.
//
@@ -678,9 +691,6 @@ type DeviceRequest struct {
// 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.
//
// This field can only be set when deviceClassName is set and no subrequests
// are specified in the firstAvailable list.
//
// +optional
// +oneOf=AllocationMode
Count int64
@@ -691,9 +701,6 @@ type DeviceRequest struct {
// all ordinary claims to the device with respect to access modes and
// any resource allocations.
//
// 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 DRAAdminAccess
// feature gate. Admin access is disabled if this field is unset or
// set to false, otherwise it is enabled.
@@ -702,28 +709,6 @@ type DeviceRequest struct {
// +featureGate=DRAAdminAccess
AdminAccess *bool
// 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.
//
// This field may only be set in the entries of DeviceClaim.Requests.
//
// DRA 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.
//
// +optional
// +oneOf=deviceRequestType
// +listType=atomic
// +featureGate=DRAPrioritizedList
FirstAvailable []DeviceSubRequest
// If specified, the request's tolerations.
//
// Tolerations for NoSchedule are required to allocate a
@@ -739,9 +724,6 @@ type DeviceRequest struct {
//
// 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.
//
@@ -756,10 +738,9 @@ type DeviceRequest struct {
// is typically a request for a single resource like a device, but can
// also ask for several identical devices.
//
// DeviceSubRequest is similar to Request, but doesn't expose the AdminAccess
// or FirstAvailable fields, as those can only be set on the top-level request.
// AdminAccess is not supported for requests with a prioritized list, and
// recursive FirstAvailable fields are not supported.
// DeviceSubRequest is similar to ExactDeviceRequest, but doesn't expose the
// AdminAccess field as that one is only supported when requesting a
// specific device.
type DeviceSubRequest struct {
// Name can be used to reference this subrequest in the list of constraints
// or the list of configurations for the claim. References must use the
@@ -805,7 +786,7 @@ type DeviceSubRequest struct {
// Allocation will fail if some devices are already allocated,
// unless adminAccess is requested.
//
// If AlloctionMode is not specified, the default mode is ExactCount. If
// If AllocationMode 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.
//

View File

@@ -18,10 +18,14 @@ package v1alpha3
import (
"fmt"
unsafe "unsafe"
corev1 "k8s.io/api/core/v1"
resourcev1alpha3 "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/api/resource"
conversion "k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
core "k8s.io/kubernetes/pkg/apis/core"
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
)
@@ -50,3 +54,252 @@ func Convert_resource_Quantity_To_resource_DeviceCapacity(in *resource.Quantity,
out.Value = *in
return nil
}
func Convert_v1alpha3_DeviceRequest_To_resource_DeviceRequest(in *resourcev1alpha3.DeviceRequest, out *resourceapi.DeviceRequest, s conversion.Scope) error {
if err := autoConvert_v1alpha3_DeviceRequest_To_resource_DeviceRequest(in, out, s); err != nil {
return err
}
// If any fields on the main request is set, we create a ExactDeviceRequest
// and set the Exactly field. It might be invalid but that will be caught in validation.
if hasAnyMainRequestFieldsSet(in) {
var exactDeviceRequest resourceapi.ExactDeviceRequest
exactDeviceRequest.DeviceClassName = in.DeviceClassName
if in.Selectors != nil {
selectors := make([]resourceapi.DeviceSelector, 0, len(in.Selectors))
for i := range in.Selectors {
var selector resourceapi.DeviceSelector
err := Convert_v1alpha3_DeviceSelector_To_resource_DeviceSelector(&in.Selectors[i], &selector, s)
if err != nil {
return err
}
selectors = append(selectors, selector)
}
exactDeviceRequest.Selectors = selectors
}
exactDeviceRequest.AllocationMode = resourceapi.DeviceAllocationMode(in.AllocationMode)
exactDeviceRequest.Count = in.Count
exactDeviceRequest.AdminAccess = in.AdminAccess
var tolerations []resourceapi.DeviceToleration
for _, e := range in.Tolerations {
var toleration resourceapi.DeviceToleration
if err := Convert_v1alpha3_DeviceToleration_To_resource_DeviceToleration(&e, &toleration, s); err != nil {
return err
}
tolerations = append(tolerations, toleration)
}
exactDeviceRequest.Tolerations = tolerations
out.Exactly = &exactDeviceRequest
}
return nil
}
func hasAnyMainRequestFieldsSet(deviceRequest *resourcev1alpha3.DeviceRequest) bool {
return deviceRequest.DeviceClassName != "" ||
deviceRequest.Selectors != nil ||
deviceRequest.AllocationMode != "" ||
deviceRequest.Count != 0 ||
deviceRequest.AdminAccess != nil ||
deviceRequest.Tolerations != nil
}
func Convert_resource_DeviceRequest_To_v1alpha3_DeviceRequest(in *resourceapi.DeviceRequest, out *resourcev1alpha3.DeviceRequest, s conversion.Scope) error {
if err := autoConvert_resource_DeviceRequest_To_v1alpha3_DeviceRequest(in, out, s); err != nil {
return err
}
if in.Exactly != nil {
out.DeviceClassName = in.Exactly.DeviceClassName
if in.Exactly.Selectors != nil {
selectors := make([]resourcev1alpha3.DeviceSelector, 0, len(in.Exactly.Selectors))
for i := range in.Exactly.Selectors {
var selector resourcev1alpha3.DeviceSelector
err := Convert_resource_DeviceSelector_To_v1alpha3_DeviceSelector(&in.Exactly.Selectors[i], &selector, s)
if err != nil {
return err
}
selectors = append(selectors, selector)
}
out.Selectors = selectors
}
out.AllocationMode = resourcev1alpha3.DeviceAllocationMode(in.Exactly.AllocationMode)
out.Count = in.Exactly.Count
out.AdminAccess = in.Exactly.AdminAccess
var tolerations []resourcev1alpha3.DeviceToleration
for _, e := range in.Exactly.Tolerations {
var toleration resourcev1alpha3.DeviceToleration
if err := Convert_resource_DeviceToleration_To_v1alpha3_DeviceToleration(&e, &toleration, s); err != nil {
return err
}
tolerations = append(tolerations, toleration)
}
out.Tolerations = tolerations
}
return nil
}
func Convert_v1alpha3_ResourceSliceSpec_To_resource_ResourceSliceSpec(in *resourcev1alpha3.ResourceSliceSpec, out *resourceapi.ResourceSliceSpec, s conversion.Scope) error {
if err := autoConvert_v1alpha3_ResourceSliceSpec_To_resource_ResourceSliceSpec(in, out, s); err != nil {
return err
}
if in.NodeName == "" {
out.NodeName = nil
} else {
out.NodeName = &in.NodeName
}
if !in.AllNodes {
out.AllNodes = nil
} else {
out.AllNodes = &in.AllNodes
}
return nil
}
func Convert_resource_ResourceSliceSpec_To_v1alpha3_ResourceSliceSpec(in *resourceapi.ResourceSliceSpec, out *resourcev1alpha3.ResourceSliceSpec, s conversion.Scope) error {
if err := autoConvert_resource_ResourceSliceSpec_To_v1alpha3_ResourceSliceSpec(in, out, s); err != nil {
return err
}
if in.NodeName == nil {
out.NodeName = ""
} else {
out.NodeName = *in.NodeName
}
if in.AllNodes == nil {
out.AllNodes = false
} else {
out.AllNodes = *in.AllNodes
}
return nil
}
func Convert_v1alpha3_Device_To_resource_Device(in *resourcev1alpha3.Device, out *resourceapi.Device, s conversion.Scope) error {
if err := autoConvert_v1alpha3_Device_To_resource_Device(in, out, s); err != nil {
return err
}
if in.Basic != nil {
basic := in.Basic
if len(basic.Attributes) > 0 {
attributes := make(map[resourceapi.QualifiedName]resourceapi.DeviceAttribute)
if err := convert_v1alpha3_Attributes_To_resource_Attributes(basic.Attributes, attributes, s); err != nil {
return err
}
out.Attributes = attributes
}
if len(basic.Capacity) > 0 {
capacity := make(map[resourceapi.QualifiedName]resourceapi.DeviceCapacity)
if err := convert_v1alpha3_Capacity_To_resource_Capacity(basic.Capacity, capacity, s); err != nil {
return err
}
out.Capacity = capacity
}
var consumesCounters []resourceapi.DeviceCounterConsumption
for _, e := range basic.ConsumesCounters {
var deviceCounterConsumption resourceapi.DeviceCounterConsumption
if err := Convert_v1alpha3_DeviceCounterConsumption_To_resource_DeviceCounterConsumption(&e, &deviceCounterConsumption, s); err != nil {
return err
}
consumesCounters = append(consumesCounters, deviceCounterConsumption)
}
out.ConsumesCounters = consumesCounters
out.NodeName = basic.NodeName
out.NodeSelector = (*core.NodeSelector)(unsafe.Pointer(basic.NodeSelector))
out.AllNodes = basic.AllNodes
var taints []resourceapi.DeviceTaint
for _, e := range basic.Taints {
var taint resourceapi.DeviceTaint
if err := Convert_v1alpha3_DeviceTaint_To_resource_DeviceTaint(&e, &taint, s); err != nil {
return err
}
taints = append(taints, taint)
}
out.Taints = taints
}
return nil
}
func Convert_resource_Device_To_v1alpha3_Device(in *resourceapi.Device, out *resourcev1alpha3.Device, s conversion.Scope) error {
if err := autoConvert_resource_Device_To_v1alpha3_Device(in, out, s); err != nil {
return err
}
out.Basic = &resourcev1alpha3.BasicDevice{}
if len(in.Attributes) > 0 {
attributes := make(map[resourcev1alpha3.QualifiedName]resourcev1alpha3.DeviceAttribute)
if err := convert_resource_Attributes_To_v1alpha3_Attributes(in.Attributes, attributes, s); err != nil {
return err
}
out.Basic.Attributes = attributes
}
if len(in.Capacity) > 0 {
capacity := make(map[resourcev1alpha3.QualifiedName]resource.Quantity)
if err := convert_resource_Capacity_To_v1alpha3_Capacity(in.Capacity, capacity, s); err != nil {
return err
}
out.Basic.Capacity = capacity
}
var consumesCounters []resourcev1alpha3.DeviceCounterConsumption
for _, e := range in.ConsumesCounters {
var deviceCounterConsumption resourcev1alpha3.DeviceCounterConsumption
if err := Convert_resource_DeviceCounterConsumption_To_v1alpha3_DeviceCounterConsumption(&e, &deviceCounterConsumption, s); err != nil {
return err
}
consumesCounters = append(consumesCounters, deviceCounterConsumption)
}
out.Basic.ConsumesCounters = consumesCounters
out.Basic.NodeName = in.NodeName
out.Basic.NodeSelector = (*corev1.NodeSelector)(unsafe.Pointer(in.NodeSelector))
out.Basic.AllNodes = in.AllNodes
var taints []resourcev1alpha3.DeviceTaint
for _, e := range in.Taints {
var taint resourcev1alpha3.DeviceTaint
if err := Convert_resource_DeviceTaint_To_v1alpha3_DeviceTaint(&e, &taint, s); err != nil {
return err
}
taints = append(taints, taint)
}
out.Basic.Taints = taints
return nil
}
func convert_resource_Attributes_To_v1alpha3_Attributes(in map[resourceapi.QualifiedName]resourceapi.DeviceAttribute, out map[resourcev1alpha3.QualifiedName]resourcev1alpha3.DeviceAttribute, s conversion.Scope) error {
for k, v := range in {
var a resourcev1alpha3.DeviceAttribute
if err := Convert_resource_DeviceAttribute_To_v1alpha3_DeviceAttribute(&v, &a, s); err != nil {
return err
}
out[resourcev1alpha3.QualifiedName(k)] = a
}
return nil
}
func convert_resource_Capacity_To_v1alpha3_Capacity(in map[resourceapi.QualifiedName]resourceapi.DeviceCapacity, out map[resourcev1alpha3.QualifiedName]resource.Quantity, s conversion.Scope) error {
for k, v := range in {
var c resource.Quantity
if err := Convert_resource_DeviceCapacity_To_resource_Quantity(&v, &c, s); err != nil {
return err
}
out[resourcev1alpha3.QualifiedName(k)] = c
}
return nil
}
func convert_v1alpha3_Attributes_To_resource_Attributes(in map[resourcev1alpha3.QualifiedName]resourcev1alpha3.DeviceAttribute, out map[resourceapi.QualifiedName]resourceapi.DeviceAttribute, s conversion.Scope) error {
for k, v := range in {
var a resourceapi.DeviceAttribute
if err := Convert_v1alpha3_DeviceAttribute_To_resource_DeviceAttribute(&v, &a, s); err != nil {
return err
}
out[resourceapi.QualifiedName(k)] = a
}
return nil
}
func convert_v1alpha3_Capacity_To_resource_Capacity(in map[resourcev1alpha3.QualifiedName]resource.Quantity, out map[resourceapi.QualifiedName]resourceapi.DeviceCapacity, s conversion.Scope) error {
for k, v := range in {
var c resourceapi.DeviceCapacity
if err := Convert_resource_Quantity_To_resource_DeviceCapacity(&v, &c, s); err != nil {
return err
}
out[resourceapi.QualifiedName(k)] = c
}
return nil
}

View File

@@ -18,16 +18,21 @@ package v1beta1
import (
"fmt"
unsafe "unsafe"
resourceapi "k8s.io/api/resource/v1beta1"
corev1 "k8s.io/api/core/v1"
resourcev1beta1 "k8s.io/api/resource/v1beta1"
conversion "k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
core "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/resource"
)
func addConversionFuncs(scheme *runtime.Scheme) error {
if err := scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("ResourceSlice"),
func(label, value string) (string, string, error) {
switch label {
case "metadata.name", resourceapi.ResourceSliceSelectorNodeName, resourceapi.ResourceSliceSelectorDriver:
case "metadata.name", resourcev1beta1.ResourceSliceSelectorNodeName, resourcev1beta1.ResourceSliceSelectorDriver:
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported for %s: %s", SchemeGroupVersion.WithKind("ResourceSlice"), label)
@@ -38,3 +43,251 @@ func addConversionFuncs(scheme *runtime.Scheme) error {
return nil
}
func Convert_v1beta1_DeviceRequest_To_resource_DeviceRequest(in *resourcev1beta1.DeviceRequest, out *resource.DeviceRequest, s conversion.Scope) error {
if err := autoConvert_v1beta1_DeviceRequest_To_resource_DeviceRequest(in, out, s); err != nil {
return err
}
// If any fields on the main request is set, we create a ExactDeviceRequest
// and set the Exactly field. It might be invalid but that will be caught in validation.
if hasAnyMainRequestFieldsSet(in) {
var exactDeviceRequest resource.ExactDeviceRequest
exactDeviceRequest.DeviceClassName = in.DeviceClassName
if in.Selectors != nil {
selectors := make([]resource.DeviceSelector, 0, len(in.Selectors))
for i := range in.Selectors {
var selector resource.DeviceSelector
err := Convert_v1beta1_DeviceSelector_To_resource_DeviceSelector(&in.Selectors[i], &selector, s)
if err != nil {
return err
}
selectors = append(selectors, selector)
}
exactDeviceRequest.Selectors = selectors
}
exactDeviceRequest.AllocationMode = resource.DeviceAllocationMode(in.AllocationMode)
exactDeviceRequest.Count = in.Count
exactDeviceRequest.AdminAccess = in.AdminAccess
var tolerations []resource.DeviceToleration
for _, e := range in.Tolerations {
var toleration resource.DeviceToleration
if err := Convert_v1beta1_DeviceToleration_To_resource_DeviceToleration(&e, &toleration, s); err != nil {
return err
}
tolerations = append(tolerations, toleration)
}
exactDeviceRequest.Tolerations = tolerations
out.Exactly = &exactDeviceRequest
}
return nil
}
func hasAnyMainRequestFieldsSet(deviceRequest *resourcev1beta1.DeviceRequest) bool {
return deviceRequest.DeviceClassName != "" ||
deviceRequest.Selectors != nil ||
deviceRequest.AllocationMode != "" ||
deviceRequest.Count != 0 ||
deviceRequest.AdminAccess != nil ||
deviceRequest.Tolerations != nil
}
func Convert_resource_DeviceRequest_To_v1beta1_DeviceRequest(in *resource.DeviceRequest, out *resourcev1beta1.DeviceRequest, s conversion.Scope) error {
if err := autoConvert_resource_DeviceRequest_To_v1beta1_DeviceRequest(in, out, s); err != nil {
return err
}
if in.Exactly != nil {
out.DeviceClassName = in.Exactly.DeviceClassName
if in.Exactly.Selectors != nil {
selectors := make([]resourcev1beta1.DeviceSelector, 0, len(in.Exactly.Selectors))
for i := range in.Exactly.Selectors {
var selector resourcev1beta1.DeviceSelector
err := Convert_resource_DeviceSelector_To_v1beta1_DeviceSelector(&in.Exactly.Selectors[i], &selector, s)
if err != nil {
return err
}
selectors = append(selectors, selector)
}
out.Selectors = selectors
}
out.AllocationMode = resourcev1beta1.DeviceAllocationMode(in.Exactly.AllocationMode)
out.Count = in.Exactly.Count
out.AdminAccess = in.Exactly.AdminAccess
var tolerations []resourcev1beta1.DeviceToleration
for _, e := range in.Exactly.Tolerations {
var toleration resourcev1beta1.DeviceToleration
if err := Convert_resource_DeviceToleration_To_v1beta1_DeviceToleration(&e, &toleration, s); err != nil {
return err
}
tolerations = append(tolerations, toleration)
}
out.Tolerations = tolerations
}
return nil
}
func Convert_v1beta1_ResourceSliceSpec_To_resource_ResourceSliceSpec(in *resourcev1beta1.ResourceSliceSpec, out *resource.ResourceSliceSpec, s conversion.Scope) error {
if err := autoConvert_v1beta1_ResourceSliceSpec_To_resource_ResourceSliceSpec(in, out, s); err != nil {
return err
}
if in.NodeName == "" {
out.NodeName = nil
} else {
out.NodeName = &in.NodeName
}
if !in.AllNodes {
out.AllNodes = nil
} else {
out.AllNodes = &in.AllNodes
}
return nil
}
func Convert_resource_ResourceSliceSpec_To_v1beta1_ResourceSliceSpec(in *resource.ResourceSliceSpec, out *resourcev1beta1.ResourceSliceSpec, s conversion.Scope) error {
if err := autoConvert_resource_ResourceSliceSpec_To_v1beta1_ResourceSliceSpec(in, out, s); err != nil {
return err
}
if in.NodeName == nil {
out.NodeName = ""
} else {
out.NodeName = *in.NodeName
}
if in.AllNodes == nil {
out.AllNodes = false
} else {
out.AllNodes = *in.AllNodes
}
return nil
}
func Convert_v1beta1_Device_To_resource_Device(in *resourcev1beta1.Device, out *resource.Device, s conversion.Scope) error {
if err := autoConvert_v1beta1_Device_To_resource_Device(in, out, s); err != nil {
return err
}
if in.Basic != nil {
basic := in.Basic
if len(basic.Attributes) > 0 {
attributes := make(map[resource.QualifiedName]resource.DeviceAttribute)
if err := convert_v1beta1_Attributes_To_resource_Attributes(basic.Attributes, attributes, s); err != nil {
return err
}
out.Attributes = attributes
}
if len(basic.Capacity) > 0 {
capacity := make(map[resource.QualifiedName]resource.DeviceCapacity)
if err := convert_v1beta1_Capacity_To_resource_Capacity(basic.Capacity, capacity, s); err != nil {
return err
}
out.Capacity = capacity
}
var consumesCounters []resource.DeviceCounterConsumption
for _, e := range basic.ConsumesCounters {
var deviceCounterConsumption resource.DeviceCounterConsumption
if err := Convert_v1beta1_DeviceCounterConsumption_To_resource_DeviceCounterConsumption(&e, &deviceCounterConsumption, s); err != nil {
return err
}
consumesCounters = append(consumesCounters, deviceCounterConsumption)
}
out.ConsumesCounters = consumesCounters
out.NodeName = basic.NodeName
out.NodeSelector = (*core.NodeSelector)(unsafe.Pointer(basic.NodeSelector))
out.AllNodes = basic.AllNodes
var taints []resource.DeviceTaint
for _, e := range basic.Taints {
var taint resource.DeviceTaint
if err := Convert_v1beta1_DeviceTaint_To_resource_DeviceTaint(&e, &taint, s); err != nil {
return err
}
taints = append(taints, taint)
}
out.Taints = taints
}
return nil
}
func Convert_resource_Device_To_v1beta1_Device(in *resource.Device, out *resourcev1beta1.Device, s conversion.Scope) error {
if err := autoConvert_resource_Device_To_v1beta1_Device(in, out, s); err != nil {
return err
}
out.Basic = &resourcev1beta1.BasicDevice{}
if len(in.Attributes) > 0 {
attributes := make(map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceAttribute)
if err := convert_resource_Attributes_To_v1beta1_Attributes(in.Attributes, attributes, s); err != nil {
return err
}
out.Basic.Attributes = attributes
}
if len(in.Capacity) > 0 {
capacity := make(map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceCapacity)
if err := convert_resource_Capacity_To_v1beta1_Capacity(in.Capacity, capacity, s); err != nil {
return err
}
out.Basic.Capacity = capacity
}
var consumesCounters []resourcev1beta1.DeviceCounterConsumption
for _, e := range in.ConsumesCounters {
var deviceCounterConsumption resourcev1beta1.DeviceCounterConsumption
if err := Convert_resource_DeviceCounterConsumption_To_v1beta1_DeviceCounterConsumption(&e, &deviceCounterConsumption, s); err != nil {
return err
}
consumesCounters = append(consumesCounters, deviceCounterConsumption)
}
out.Basic.ConsumesCounters = consumesCounters
out.Basic.NodeName = in.NodeName
out.Basic.NodeSelector = (*corev1.NodeSelector)(unsafe.Pointer(in.NodeSelector))
out.Basic.AllNodes = in.AllNodes
var taints []resourcev1beta1.DeviceTaint
for _, e := range in.Taints {
var taint resourcev1beta1.DeviceTaint
if err := Convert_resource_DeviceTaint_To_v1beta1_DeviceTaint(&e, &taint, s); err != nil {
return err
}
taints = append(taints, taint)
}
out.Basic.Taints = taints
return nil
}
func convert_resource_Attributes_To_v1beta1_Attributes(in map[resource.QualifiedName]resource.DeviceAttribute, out map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceAttribute, s conversion.Scope) error {
for k, v := range in {
var a resourcev1beta1.DeviceAttribute
if err := Convert_resource_DeviceAttribute_To_v1beta1_DeviceAttribute(&v, &a, s); err != nil {
return err
}
out[resourcev1beta1.QualifiedName(k)] = a
}
return nil
}
func convert_resource_Capacity_To_v1beta1_Capacity(in map[resource.QualifiedName]resource.DeviceCapacity, out map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceCapacity, s conversion.Scope) error {
for k, v := range in {
var c resourcev1beta1.DeviceCapacity
if err := Convert_resource_DeviceCapacity_To_v1beta1_DeviceCapacity(&v, &c, s); err != nil {
return err
}
out[resourcev1beta1.QualifiedName(k)] = c
}
return nil
}
func convert_v1beta1_Attributes_To_resource_Attributes(in map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceAttribute, out map[resource.QualifiedName]resource.DeviceAttribute, s conversion.Scope) error {
for k, v := range in {
var a resource.DeviceAttribute
if err := Convert_v1beta1_DeviceAttribute_To_resource_DeviceAttribute(&v, &a, s); err != nil {
return err
}
out[resource.QualifiedName(k)] = a
}
return nil
}
func convert_v1beta1_Capacity_To_resource_Capacity(in map[resourcev1beta1.QualifiedName]resourcev1beta1.DeviceCapacity, out map[resource.QualifiedName]resource.DeviceCapacity, s conversion.Scope) error {
for k, v := range in {
var c resource.DeviceCapacity
if err := Convert_v1beta1_DeviceCapacity_To_resource_DeviceCapacity(&v, &c, s); err != nil {
return err
}
out[resource.QualifiedName(k)] = c
}
return nil
}

View File

@@ -0,0 +1,333 @@
/*
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 (
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
resourcev1beta1 "k8s.io/api/resource/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/apis/resource"
)
func TestConversion(t *testing.T) {
testcases := []struct {
name string
in runtime.Object
out runtime.Object
expectOut runtime.Object
expectErr string
}{
{
name: "v1beta1 to internal without alternatives",
in: &resourcev1beta1.ResourceClaim{
Spec: resourcev1beta1.ResourceClaimSpec{
Devices: resourcev1beta1.DeviceClaim{
Requests: []resourcev1beta1.DeviceRequest{
{
Name: "foo",
DeviceClassName: "class-a",
Selectors: []resourcev1beta1.DeviceSelector{
{
CEL: &resourcev1beta1.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resourcev1beta1.DeviceAllocationModeExactCount,
Count: 2,
},
},
},
},
},
out: &resource.ResourceClaim{},
expectOut: &resource.ResourceClaim{
Spec: resource.ResourceClaimSpec{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "foo",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class-a",
Selectors: []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 2,
},
},
},
},
},
},
},
{
name: "internal to v1beta1 without alternatives",
in: &resource.ResourceClaim{
Spec: resource.ResourceClaimSpec{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "foo",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class-a",
Selectors: []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 2,
},
},
},
},
},
},
out: &resourcev1beta1.ResourceClaim{},
expectOut: &resourcev1beta1.ResourceClaim{
Spec: resourcev1beta1.ResourceClaimSpec{
Devices: resourcev1beta1.DeviceClaim{
Requests: []resourcev1beta1.DeviceRequest{
{
Name: "foo",
DeviceClassName: "class-a",
Selectors: []resourcev1beta1.DeviceSelector{
{
CEL: &resourcev1beta1.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resourcev1beta1.DeviceAllocationModeExactCount,
Count: 2,
},
},
},
},
},
},
{
name: "v1beta1 to internal with alternatives",
in: &resourcev1beta1.ResourceClaim{
Spec: resourcev1beta1.ResourceClaimSpec{
Devices: resourcev1beta1.DeviceClaim{
Requests: []resourcev1beta1.DeviceRequest{
{
Name: "foo",
FirstAvailable: []resourcev1beta1.DeviceSubRequest{
{
Name: "sub-1",
DeviceClassName: "class-a",
Selectors: []resourcev1beta1.DeviceSelector{
{
CEL: &resourcev1beta1.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resourcev1beta1.DeviceAllocationModeExactCount,
Count: 2,
},
{
Name: "sub-2",
DeviceClassName: "class-a",
Selectors: []resourcev1beta1.DeviceSelector{
{
CEL: &resourcev1beta1.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resourcev1beta1.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
},
},
},
out: &resource.ResourceClaim{},
expectOut: &resource.ResourceClaim{
Spec: resource.ResourceClaimSpec{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "foo",
FirstAvailable: []resource.DeviceSubRequest{
{
Name: "sub-1",
DeviceClassName: "class-a",
Selectors: []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 2,
},
{
Name: "sub-2",
DeviceClassName: "class-a",
Selectors: []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
},
},
},
},
{
name: "v1beta1 to internal with alternatives",
in: &resource.ResourceClaim{
Spec: resource.ResourceClaimSpec{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "foo",
FirstAvailable: []resource.DeviceSubRequest{
{
Name: "sub-1",
DeviceClassName: "class-a",
Selectors: []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 2,
},
{
Name: "sub-2",
DeviceClassName: "class-a",
Selectors: []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
},
},
},
out: &resourcev1beta1.ResourceClaim{},
expectOut: &resourcev1beta1.ResourceClaim{
Spec: resourcev1beta1.ResourceClaimSpec{
Devices: resourcev1beta1.DeviceClaim{
Requests: []resourcev1beta1.DeviceRequest{
{
Name: "foo",
FirstAvailable: []resourcev1beta1.DeviceSubRequest{
{
Name: "sub-1",
DeviceClassName: "class-a",
Selectors: []resourcev1beta1.DeviceSelector{
{
CEL: &resourcev1beta1.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resourcev1beta1.DeviceAllocationModeExactCount,
Count: 2,
},
{
Name: "sub-2",
DeviceClassName: "class-a",
Selectors: []resourcev1beta1.DeviceSelector{
{
CEL: &resourcev1beta1.CELDeviceSelector{
Expression: `device.attributes["driver-a"].exists`,
},
},
},
AllocationMode: resourcev1beta1.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
},
},
},
},
}
scheme := runtime.NewScheme()
if err := resource.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := AddToScheme(scheme); err != nil {
t.Fatal(err)
}
for i := range testcases {
name := testcases[i].name
tc := testcases[i]
t.Run(name, func(t *testing.T) {
err := scheme.Convert(tc.in, tc.out, nil)
if err != nil {
if len(tc.expectErr) == 0 {
t.Fatalf("unexpected error %v", err)
}
if !strings.Contains(err.Error(), tc.expectErr) {
t.Fatalf("expected error %s, got %v", tc.expectErr, err)
}
return
}
if len(tc.expectErr) > 0 {
t.Fatalf("expected error %s, got none", tc.expectErr)
}
if !reflect.DeepEqual(tc.out, tc.expectOut) {
t.Fatalf("unexpected result:\n %s", cmp.Diff(tc.expectOut, tc.out))
}
})
}
}

View File

@@ -0,0 +1,40 @@
/*
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 v1beta2
import (
"fmt"
resourceapi "k8s.io/api/resource/v1beta2"
"k8s.io/apimachinery/pkg/runtime"
)
func addConversionFuncs(scheme *runtime.Scheme) error {
if err := scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("ResourceSlice"),
func(label, value string) (string, string, error) {
switch label {
case "metadata.name", resourceapi.ResourceSliceSelectorNodeName, resourceapi.ResourceSliceSelectorDriver:
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported for %s: %s", SchemeGroupVersion.WithKind("ResourceSlice"), label)
}
}); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,55 @@
/*
Copyright 2022 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 v1beta2
import (
"time"
resourceapi "k8s.io/api/resource/v1beta2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func addDefaultingFuncs(scheme *runtime.Scheme) error {
return RegisterDefaults(scheme)
}
func SetDefaults_ExactDeviceRequest(obj *resourceapi.ExactDeviceRequest) {
if obj.AllocationMode == "" {
obj.AllocationMode = resourceapi.DeviceAllocationModeExactCount
}
if obj.AllocationMode == resourceapi.DeviceAllocationModeExactCount && obj.Count == 0 {
obj.Count = 1
}
}
func SetDefaults_DeviceSubRequest(obj *resourceapi.DeviceSubRequest) {
if obj.AllocationMode == "" {
obj.AllocationMode = resourceapi.DeviceAllocationModeExactCount
}
if obj.AllocationMode == resourceapi.DeviceAllocationModeExactCount && obj.Count == 0 {
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

@@ -0,0 +1,179 @@
/*
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 v1beta2_test
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
v1beta2 "k8s.io/api/resource/v1beta2"
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"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
)
func TestSetDefaultAllocationMode(t *testing.T) {
claim := &v1beta2.ResourceClaim{
Spec: v1beta2.ResourceClaimSpec{
Devices: v1beta2.DeviceClaim{
Requests: []v1beta2.DeviceRequest{
{
Exactly: &v1beta2.ExactDeviceRequest{},
},
},
},
},
}
// fields should be defaulted
defaultMode := v1beta2.DeviceAllocationModeExactCount
defaultCount := int64(1)
output := roundTrip(t, runtime.Object(claim)).(*v1beta2.ResourceClaim)
assert.Equal(t, defaultMode, output.Spec.Devices.Requests[0].Exactly.AllocationMode)
assert.Equal(t, defaultCount, output.Spec.Devices.Requests[0].Exactly.Count)
// field should not change
nonDefaultMode := v1beta2.DeviceAllocationModeExactCount
nonDefaultCount := int64(10)
claim = &v1beta2.ResourceClaim{
Spec: v1beta2.ResourceClaimSpec{
Devices: v1beta2.DeviceClaim{
Requests: []v1beta2.DeviceRequest{{
Exactly: &v1beta2.ExactDeviceRequest{
AllocationMode: nonDefaultMode,
Count: nonDefaultCount,
},
}},
},
},
}
output = roundTrip(t, runtime.Object(claim)).(*v1beta2.ResourceClaim)
assert.Equal(t, nonDefaultMode, output.Spec.Devices.Requests[0].Exactly.AllocationMode)
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].Exactly.Count)
}
func TestSetDefaultAllocationModeWithSubRequests(t *testing.T) {
claim := &v1beta2.ResourceClaim{
Spec: v1beta2.ResourceClaimSpec{
Devices: v1beta2.DeviceClaim{
Requests: []v1beta2.DeviceRequest{
{
Name: "req-1",
FirstAvailable: []v1beta2.DeviceSubRequest{
{
Name: "subReq-1",
},
{
Name: "subReq-2",
},
},
},
},
},
},
}
defaultMode := v1beta2.DeviceAllocationModeExactCount
defaultCount := int64(1)
output := roundTrip(t, runtime.Object(claim)).(*v1beta2.ResourceClaim)
// the exactly field is not set.
assert.Nil(t, output.Spec.Devices.Requests[0].Exactly)
// fields on the subRequests should be defaulted.
assert.Equal(t, defaultMode, output.Spec.Devices.Requests[0].FirstAvailable[0].AllocationMode)
assert.Equal(t, defaultCount, output.Spec.Devices.Requests[0].FirstAvailable[0].Count)
assert.Equal(t, defaultMode, output.Spec.Devices.Requests[0].FirstAvailable[1].AllocationMode)
assert.Equal(t, defaultCount, output.Spec.Devices.Requests[0].FirstAvailable[1].Count)
// field should not change
nonDefaultMode := v1beta2.DeviceAllocationModeExactCount
nonDefaultCount := int64(10)
claim = &v1beta2.ResourceClaim{
Spec: v1beta2.ResourceClaimSpec{
Devices: v1beta2.DeviceClaim{
Requests: []v1beta2.DeviceRequest{{
Name: "req-1",
FirstAvailable: []v1beta2.DeviceSubRequest{
{
Name: "subReq-1",
AllocationMode: nonDefaultMode,
Count: nonDefaultCount,
},
{
Name: "subReq-2",
AllocationMode: nonDefaultMode,
Count: nonDefaultCount,
},
},
}},
},
},
}
output = roundTrip(t, runtime.Object(claim)).(*v1beta2.ResourceClaim)
assert.Equal(t, nonDefaultMode, output.Spec.Devices.Requests[0].FirstAvailable[0].AllocationMode)
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].FirstAvailable[0].Count)
assert.Equal(t, nonDefaultMode, output.Spec.Devices.Requests[0].FirstAvailable[1].AllocationMode)
assert.Equal(t, nonDefaultCount, output.Spec.Devices.Requests[0].FirstAvailable[1].Count)
}
func TestSetDefaultDeviceTaint(t *testing.T) {
slice := &v1beta2.ResourceSlice{
Spec: v1beta2.ResourceSliceSpec{
Devices: []v1beta2.Device{{
Name: "device-0",
Taints: []v1beta2.DeviceTaint{{}},
}},
},
}
// fields should be defaulted
output := roundTrip(t, slice).(*v1beta2.ResourceSlice)
assert.WithinDuration(t, time.Now(), ptr.Deref(output.Spec.Devices[0].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].Taints[0].TimeAdded = &metav1.Time{Time: timeAdded}
output = roundTrip(t, slice).(*v1beta2.ResourceSlice)
assert.WithinDuration(t, timeAdded, ptr.Deref(output.Spec.Devices[0].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(v1beta2.SchemeGroupVersion)
data, err := runtime.Encode(codec, obj)
if err != nil {
t.Errorf("%v\n %#v", err, obj)
return nil
}
obj2, err := runtime.Decode(codec, data)
if err != nil {
t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj)
return nil
}
obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
err = legacyscheme.Scheme.Convert(obj2, obj3, nil)
if err != nil {
t.Errorf("%v\nSource: %#v", err, obj2)
return nil
}
return obj3
}

View File

@@ -0,0 +1,23 @@
/*
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.
*/
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/resource
// +k8s:conversion-gen-external-types=k8s.io/api/resource/v1beta2
// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=k8s.io/api/resource/v1beta2
// Package v1beta2 is the v1beta2 version of the resource API.
package v1beta2

View File

@@ -0,0 +1,46 @@
/*
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 v1beta2
import (
"k8s.io/api/resource/v1beta2"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
localSchemeBuilder = &v1beta2.SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addDefaultingFuncs, addConversionFuncs)
}
// TODO: remove these global variables
// GroupName is the group name use in this package
const GroupName = "resource.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta2"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}

View File

@@ -173,27 +173,9 @@ func gatherAllocatedDevices(allocationResult *resource.DeviceAllocationResult) s
func validateDeviceRequest(request resource.DeviceRequest, fldPath *field.Path, stored bool) field.ErrorList {
allErrs := validateRequestName(request.Name, fldPath.Child("name"))
if request.DeviceClassName == "" && len(request.FirstAvailable) == 0 {
allErrs = append(allErrs, field.Required(fldPath, "exactly one of `deviceClassName` or `firstAvailable` must be specified"))
} else if request.DeviceClassName != "" && len(request.FirstAvailable) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath, nil, "exactly one of `deviceClassName` or `firstAvailable` must be specified"))
} else if request.DeviceClassName != "" {
allErrs = append(allErrs, validateDeviceClass(request.DeviceClassName, fldPath.Child("deviceClassName"))...)
allErrs = append(allErrs, validateSelectorSlice(request.Selectors, fldPath.Child("selectors"), stored)...)
allErrs = append(allErrs, validateDeviceAllocationMode(request.AllocationMode, request.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...)
} else if len(request.FirstAvailable) > 0 {
if request.Selectors != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("selectors"), request.Selectors, "must not be specified when firstAvailable is set"))
}
if request.AllocationMode != "" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("allocationMode"), request.AllocationMode, "must not be specified when firstAvailable is set"))
}
if request.Count != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("count"), request.Count, "must not be specified when firstAvailable is set"))
}
if request.AdminAccess != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("adminAccess"), request.AdminAccess, "must not be specified when firstAvailable is set"))
}
numDeviceRequestType := 0
if len(request.FirstAvailable) > 0 {
numDeviceRequestType++
allErrs = append(allErrs, validateSet(request.FirstAvailable, resource.FirstAvailableDeviceRequestMaxSize,
func(subRequest resource.DeviceSubRequest, fldPath *field.Path) field.ErrorList {
return validateDeviceSubRequest(subRequest, fldPath, stored)
@@ -203,10 +185,19 @@ 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))...)
if request.Exactly != nil {
numDeviceRequestType++
allErrs = append(allErrs, validateExactDeviceRequest(*request.Exactly, fldPath.Child("exactly"), stored)...)
}
switch numDeviceRequestType {
case 0:
allErrs = append(allErrs, field.Required(fldPath, "exactly one of `exactly` or `firstAvailable` is required"))
case 1:
default:
allErrs = append(allErrs, field.Invalid(fldPath, nil, "exactly one of `exactly` or `firstAvailable` is required, but multiple fields are set"))
}
return allErrs
}
@@ -221,6 +212,17 @@ func validateDeviceSubRequest(subRequest resource.DeviceSubRequest, fldPath *fie
return allErrs
}
func validateExactDeviceRequest(request resource.ExactDeviceRequest, fldPath *field.Path, stored bool) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateDeviceClass(request.DeviceClassName, fldPath.Child("deviceClassName"))...)
allErrs = append(allErrs, validateSelectorSlice(request.Selectors, fldPath.Child("selectors"), stored)...)
allErrs = append(allErrs, validateDeviceAllocationMode(request.AllocationMode, request.Count, fldPath.Child("allocationMode"), fldPath.Child("count"))...)
for i, toleration := range request.Tolerations {
allErrs = append(allErrs, validateDeviceToleration(toleration, fldPath.Child("tolerations").Index(i))...)
}
return allErrs
}
func validateDeviceAllocationMode(deviceAllocationMode resource.DeviceAllocationMode, count int64, allocModeFldPath, countFldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
switch deviceAllocationMode {
@@ -587,9 +589,14 @@ func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPat
}
setFields := make([]string, 0, 4)
if spec.NodeName != "" {
setFields = append(setFields, "`nodeName`")
allErrs = append(allErrs, validateNodeName(spec.NodeName, fldPath.Child("nodeName"))...)
if spec.NodeName != nil {
if *spec.NodeName != "" {
setFields = append(setFields, "`nodeName`")
allErrs = append(allErrs, validateNodeName(*spec.NodeName, fldPath.Child("nodeName"))...)
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *spec.NodeName,
"must be either unset or set to a non-empty string"))
}
}
if spec.NodeSelector != nil {
setFields = append(setFields, "`nodeSelector`")
@@ -600,9 +607,16 @@ func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPat
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeSelector", "nodeSelectorTerms"), spec.NodeSelector.NodeSelectorTerms, "must have exactly one node selector term"))
}
}
if spec.AllNodes {
setFields = append(setFields, "`allNodes`")
if spec.AllNodes != nil {
if *spec.AllNodes {
setFields = append(setFields, "`allNodes`")
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("allNodes"), *spec.AllNodes,
"must be either unset or set to true"))
}
}
if spec.PerDeviceNodeSelection != nil {
if *spec.PerDeviceNodeSelection {
setFields = append(setFields, "`perDeviceNodeSelection`")
@@ -610,8 +624,8 @@ func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPat
allErrs = append(allErrs, field.Invalid(fldPath.Child("perDeviceNodeSelection"), *spec.PerDeviceNodeSelection,
"must be either unset or set to true"))
}
}
switch len(setFields) {
case 0:
allErrs = append(allErrs, field.Required(fldPath, "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required"))
@@ -630,7 +644,26 @@ func validateResourceSliceSpec(spec, oldSpec *resource.ResourceSliceSpec, fldPat
return device.Name, "name"
}, fldPath.Child("devices"))...)
allErrs = append(allErrs, validateSet(spec.SharedCounters, resource.ResourceSliceMaxSharedCounters,
// Size limit for total number of counters in devices enforced here.
numDeviceCounters := 0
for _, device := range spec.Devices {
for _, c := range device.ConsumesCounters {
numDeviceCounters += len(c.Counters)
}
}
if numDeviceCounters > resource.ResourceSliceMaxDeviceCountersPerSlice {
allErrs = append(allErrs, field.Invalid(fldPath.Child("devices"), numDeviceCounters, fmt.Sprintf("the total number of counters in devices must not exceed %d", resource.ResourceSliceMaxDeviceCountersPerSlice)))
}
// Size limit for all shared counters enforced here.
numCounters := 0
for _, set := range spec.SharedCounters {
numCounters += len(set.Counters)
}
if numCounters > resource.ResourceSliceMaxSharedCounters {
allErrs = append(allErrs, field.Invalid(fldPath.Child("sharedCounters"), numCounters, fmt.Sprintf("the total number of shared counters must not exceed %d", resource.ResourceSliceMaxSharedCounters)))
}
allErrs = append(allErrs, validateSet(spec.SharedCounters, -1,
validateCounterSet,
func(counterSet resource.CounterSet) (string, string) {
return counterSet.Name, "name"
@@ -649,7 +682,8 @@ func validateCounterSet(counterSet resource.CounterSet, fldPath *field.Path) fie
if len(counterSet.Counters) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("counters"), ""))
} else {
allErrs = append(allErrs, validateMap(counterSet.Counters, resource.ResourceSliceMaxSharedCountersCounters, attributeAndCapacityMaxKeyLength,
// The size limit is enforced for across all sets by the caller.
allErrs = append(allErrs, validateMap(counterSet.Counters, -1, validation.DNS1123LabelMaxLength,
validateCounterName, validateDeviceCounter, fldPath.Child("counters"))...)
}
@@ -658,12 +692,12 @@ func validateCounterSet(counterSet resource.CounterSet, fldPath *field.Path) fie
func gatherSharedCounterCounterNames(sharedCounters []resource.CounterSet) map[string]sets.Set[string] {
sharedCounterToCounterMap := make(map[string]sets.Set[string])
for _, sharedCounter := range sharedCounters {
for _, counterSet := range sharedCounters {
counterNames := sets.New[string]()
for counterName := range sharedCounter.Counters {
for counterName := range counterSet.Counters {
counterNames.Insert(counterName)
}
sharedCounterToCounterMap[sharedCounter.Name] = counterNames
sharedCounterToCounterMap[counterSet.Name] = counterNames
}
return sharedCounterToCounterMap
}
@@ -683,44 +717,42 @@ func validateResourcePool(pool resource.ResourcePool, fldPath *field.Path) field
func validateDevice(device resource.Device, fldPath *field.Path, sharedCounterToCounterNames map[string]sets.Set[string], perDeviceNodeSelection *bool) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, validateDeviceName(device.Name, fldPath.Child("name"))...)
if device.Basic == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("basic"), ""))
} else {
allErrs = append(allErrs, validateBasicDevice(*device.Basic, fldPath.Child("basic"), sharedCounterToCounterNames, perDeviceNodeSelection)...)
}
return allErrs
}
func validateBasicDevice(device resource.BasicDevice, fldPath *field.Path, sharedCounterToCounterNames map[string]sets.Set[string], perDeviceNodeSelection *bool) field.ErrorList {
var allErrs field.ErrorList
// Warn about exceeding the maximum length only once. If any individual
// field is too large, then so is the combination.
attributeAndCapacityLength := len(device.Attributes) + len(device.Capacity)
if attributeAndCapacityLength > resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice {
allErrs = append(allErrs, field.Invalid(fldPath, attributeAndCapacityLength, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", resource.ResourceSliceMaxAttributesAndCapacitiesPerDevice)))
}
allErrs = append(allErrs, validateMap(device.Attributes, -1, attributeAndCapacityMaxKeyLength, validateQualifiedName, validateDeviceAttribute, fldPath.Child("attributes"))...)
allErrs = append(allErrs, validateMap(device.Capacity, -1, attributeAndCapacityMaxKeyLength, validateQualifiedName, validateDeviceCapacity, fldPath.Child("capacity"))...)
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))...)
}
allErrs = append(allErrs, validateSlice(device.Taints, resource.DeviceTaintsMaxLength, validateDeviceTaint, fldPath.Child("taints"))...)
allErrs = append(allErrs, validateSet(device.ConsumesCounter, resource.ResourceSliceMaxDeviceCounterConsumptions,
allErrs = append(allErrs, validateSet(device.ConsumesCounters, -1,
validateDeviceCounterConsumption,
func(deviceCapacityConsumption resource.DeviceCounterConsumption) (string, string) {
return deviceCapacityConsumption.SharedCounter, "sharedCounter"
}, fldPath.Child("consumesCounter"))...)
return deviceCapacityConsumption.CounterSet, "counterSet"
}, fldPath.Child("consumesCounters"))...)
for i, deviceCounterConsumption := range device.ConsumesCounter {
if capacityNames, exists := sharedCounterToCounterNames[deviceCounterConsumption.SharedCounter]; exists {
var countersLength int
for _, set := range device.ConsumesCounters {
countersLength += len(set.Counters)
}
if countersLength > resource.ResourceSliceMaxCountersPerDevice {
allErrs = append(allErrs, field.Invalid(fldPath, countersLength, fmt.Sprintf("the total number of counters must not exceed %d", resource.ResourceSliceMaxCountersPerDevice)))
}
for i, deviceCounterConsumption := range device.ConsumesCounters {
if capacityNames, exists := sharedCounterToCounterNames[deviceCounterConsumption.CounterSet]; exists {
for capacityName := range deviceCounterConsumption.Counters {
if !capacityNames.Has(string(capacityName)) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("consumesCounter").Index(i).Child("counters"),
allErrs = append(allErrs, field.Invalid(fldPath.Child("consumesCounters").Index(i).Child("counters"),
capacityName, "must reference a counter defined in the ResourceSlice sharedCounters"))
}
}
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("consumesCounter").Index(i).Child("sharedCounter"),
deviceCounterConsumption.SharedCounter, "must reference a counterSet defined in the ResourceSlice sharedCounters"))
allErrs = append(allErrs, field.Invalid(fldPath.Child("consumesCounters").Index(i).Child("counterSet"),
deviceCounterConsumption.CounterSet, "must reference a counterSet defined in the ResourceSlice sharedCounters"))
}
}
@@ -756,20 +788,20 @@ func validateBasicDevice(device resource.BasicDevice, fldPath *field.Path, share
} else if (perDeviceNodeSelection == nil || !*perDeviceNodeSelection) && (device.NodeName != nil || device.NodeSelector != nil || device.AllNodes != nil) {
allErrs = append(allErrs, field.Invalid(fldPath, nil, "`nodeName`, `nodeSelector` and `allNodes` can only be set if `perDeviceNodeSelection` is set to true in the ResourceSlice spec"))
}
return allErrs
}
func validateDeviceCounterConsumption(deviceCounterConsumption resource.DeviceCounterConsumption, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if len(deviceCounterConsumption.SharedCounter) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("sharedCounter"), ""))
if len(deviceCounterConsumption.CounterSet) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("counterSet"), ""))
}
if deviceCounterConsumption.Counters == nil {
if len(deviceCounterConsumption.Counters) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("counters"), ""))
} else {
allErrs = append(allErrs, validateMap(deviceCounterConsumption.Counters, resource.ResourceSliceMaxDeviceCounterConsumptionCounters, attributeAndCapacityMaxKeyLength,
// The size limit is enforced for the entire device.
allErrs = append(allErrs, validateMap(deviceCounterConsumption.Counters, -1, validation.DNS1123LabelMaxLength,
validateCounterName, validateDeviceCounter, fldPath.Child("counters"))...)
}
return allErrs

View File

@@ -59,10 +59,12 @@ var (
validClaimSpec = resource.ResourceClaimSpec{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{{
Name: goodName,
DeviceClassName: goodName,
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 1,
Name: goodName,
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: goodName,
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 1,
},
}},
},
}
@@ -238,18 +240,18 @@ func TestValidateClaim(t *testing.T) {
}(),
},
"bad-classname": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "devices", "requests").Index(0).Child("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])?)*')")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "devices", "requests").Index(0).Child("exactly", "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])?)*')")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests[0].DeviceClassName = badName
claim.Spec.Devices.Requests[0].Exactly.DeviceClassName = badName
return claim
}(),
},
"missing-classname-and-firstavailable": {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "devices", "requests").Index(0), "exactly one of `deviceClassName` or `firstAvailable` must be specified")},
"missing-classname": {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "devices", "requests").Index(0).Child("exactly", "deviceClassName"), "")},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests[0].DeviceClassName = ""
claim.Spec.Devices.Requests[0].Exactly.DeviceClassName = ""
return claim
}(),
},
@@ -382,9 +384,9 @@ func TestValidateClaim(t *testing.T) {
},
"allocation-mode": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices", "requests").Index(2).Child("count"), int64(-1), "must be greater than zero"),
field.NotSupported(field.NewPath("spec", "devices", "requests").Index(3).Child("allocationMode"), resource.DeviceAllocationMode("other"), []resource.DeviceAllocationMode{resource.DeviceAllocationModeAll, resource.DeviceAllocationModeExactCount}),
field.Invalid(field.NewPath("spec", "devices", "requests").Index(4).Child("count"), int64(2), "must not be specified when allocationMode is 'All'"),
field.Invalid(field.NewPath("spec", "devices", "requests").Index(2).Child("exactly", "count"), int64(-1), "must be greater than zero"),
field.NotSupported(field.NewPath("spec", "devices", "requests").Index(3).Child("exactly", "allocationMode"), resource.DeviceAllocationMode("other"), []resource.DeviceAllocationMode{resource.DeviceAllocationModeAll, resource.DeviceAllocationModeExactCount}),
field.Invalid(field.NewPath("spec", "devices", "requests").Index(4).Child("exactly", "count"), int64(2), "must not be specified when allocationMode is 'All'"),
field.Duplicate(field.NewPath("spec", "devices", "requests").Index(5).Child("name"), "foo"),
},
claim: func() *resource.ResourceClaim {
@@ -392,30 +394,30 @@ func TestValidateClaim(t *testing.T) {
goodReq := &claim.Spec.Devices.Requests[0]
goodReq.Name = "foo"
goodReq.AllocationMode = resource.DeviceAllocationModeExactCount
goodReq.Count = 1
goodReq.Exactly.AllocationMode = resource.DeviceAllocationModeExactCount
goodReq.Exactly.Count = 1
req := goodReq.DeepCopy()
req.Name += "2"
req.AllocationMode = resource.DeviceAllocationModeAll
req.Count = 0
req.Exactly.AllocationMode = resource.DeviceAllocationModeAll
req.Exactly.Count = 0
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
req = goodReq.DeepCopy()
req.Name += "3"
req.AllocationMode = resource.DeviceAllocationModeExactCount
req.Count = -1
req.Exactly.AllocationMode = resource.DeviceAllocationModeExactCount
req.Exactly.Count = -1
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
req = goodReq.DeepCopy()
req.Name += "4"
req.AllocationMode = resource.DeviceAllocationMode("other")
req.Exactly.AllocationMode = resource.DeviceAllocationMode("other")
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
req = goodReq.DeepCopy()
req.Name += "5"
req.AllocationMode = resource.DeviceAllocationModeAll
req.Count = 2
req.Exactly.AllocationMode = resource.DeviceAllocationModeAll
req.Exactly.Count = 2
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *req)
req = goodReq.DeepCopy()
@@ -503,13 +505,13 @@ func TestValidateClaim(t *testing.T) {
},
"CEL-compile-errors": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices", "requests").Index(1).Child("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 | .................^"),
field.Invalid(field.NewPath("spec", "devices", "requests").Index(1).Child("exactly", "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 | .................^"),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, claim.Spec.Devices.Requests[0])
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *claim.Spec.Devices.Requests[0].DeepCopy())
claim.Spec.Devices.Requests[1].Name += "-2"
claim.Spec.Devices.Requests[1].Selectors = []resource.DeviceSelector{
claim.Spec.Devices.Requests[1].Exactly.Selectors = []resource.DeviceSelector{
{
// Good selector.
CEL: &resource.CELDeviceSelector{
@@ -528,14 +530,14 @@ func TestValidateClaim(t *testing.T) {
},
"CEL-length": {
wantFailures: field.ErrorList{
field.TooLong(field.NewPath("spec", "devices", "requests").Index(1).Child("selectors").Index(1).Child("cel", "expression"), "" /*unused*/, resource.CELSelectorExpressionMaxLength),
field.TooLong(field.NewPath("spec", "devices", "requests").Index(1).Child("exactly", "selectors").Index(1).Child("cel", "expression"), "" /*unused*/, resource.CELSelectorExpressionMaxLength),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, claim.Spec.Devices.Requests[0])
claim.Spec.Devices.Requests = append(claim.Spec.Devices.Requests, *claim.Spec.Devices.Requests[0].DeepCopy())
claim.Spec.Devices.Requests[1].Name += "-2"
expression := `device.driver == ""`
claim.Spec.Devices.Requests[1].Selectors = []resource.DeviceSelector{
claim.Spec.Devices.Requests[1].Exactly.Selectors = []resource.DeviceSelector{
{
// Good selector.
CEL: &resource.CELDeviceSelector{
@@ -554,11 +556,11 @@ func TestValidateClaim(t *testing.T) {
},
"CEL-cost": {
wantFailures: field.ErrorList{
field.Forbidden(field.NewPath("spec", "devices", "requests").Index(0).Child("selectors").Index(0).Child("cel", "expression"), "too complex, exceeds cost limit"),
field.Forbidden(field.NewPath("spec", "devices", "requests").Index(0).Child("exactly", "selectors").Index(0).Child("cel", "expression"), "too complex, exceeds cost limit"),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests[0].Selectors = []resource.DeviceSelector{
claim.Spec.Devices.Requests[0].Exactly.Selectors = []resource.DeviceSelector{
{
CEL: &resource.CELDeviceSelector{
// From https://github.com/kubernetes/kubernetes/blob/50fc400f178d2078d0ca46aee955ee26375fc437/test/integration/apiserver/cel/validatingadmissionpolicy_test.go#L2150.
@@ -576,17 +578,20 @@ func TestValidateClaim(t *testing.T) {
return claim
}(),
},
"prioritized-list-field-on-parent": {
"prioritized-list-both-first-available-and-exactly-set": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices", "requests").Index(0), nil, "exactly one of `deviceClassName` or `firstAvailable` must be specified"),
field.Invalid(field.NewPath("spec", "devices", "requests").Index(0), nil, "exactly one of `exactly` or `firstAvailable` is required, but multiple fields are set"),
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpecWithFirstAvailable)
claim.Spec.Devices.Requests[0].DeviceClassName = goodName
claim.Spec.Devices.Requests[0].Selectors = validSelector
claim.Spec.Devices.Requests[0].AllocationMode = resource.DeviceAllocationModeAll
claim.Spec.Devices.Requests[0].Count = 2
claim.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
claim.Spec.Devices.Requests[0].Exactly = &resource.ExactDeviceRequest{
DeviceClassName: goodName,
Selectors: validSelector,
AllocationMode: resource.DeviceAllocationModeExactCount,
Count: 2,
AdminAccess: ptr.To(true),
}
return claim
}(),
},
@@ -620,9 +625,7 @@ func TestValidateClaim(t *testing.T) {
},
claim: func() *resource.ResourceClaim {
claim := testClaim(goodName, goodNS, validClaimSpec)
claim.Spec.Devices.Requests[0].DeviceClassName = ""
claim.Spec.Devices.Requests[0].AllocationMode = ""
claim.Spec.Devices.Requests[0].Count = 0
claim.Spec.Devices.Requests[0].Exactly = nil
var subRequests []resource.DeviceSubRequest
for i := 0; i <= 8; i++ {
subRequests = append(subRequests, resource.DeviceSubRequest{
@@ -742,7 +745,7 @@ func TestValidateClaim(t *testing.T) {
allErrs = append(allErrs,
field.Required(fldPath.Index(0).Child("operator"), ""),
)
fldPath = field.NewPath("spec", "devices", "requests").Index(1).Child("tolerations")
fldPath = field.NewPath("spec", "devices", "requests").Index(1).Child("exactly", "tolerations")
allErrs = append(allErrs,
field.Required(fldPath.Index(3).Child("operator"), ""),
@@ -765,7 +768,7 @@ func TestValidateClaim(t *testing.T) {
}
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{
claim.Spec.Devices.Requests[1].Exactly.Tolerations = []resource.DeviceToleration{
{
// Minimal valid toleration: match all taints.
Operator: resource.DeviceTolerationOpExists,
@@ -822,12 +825,12 @@ func TestValidateClaimUpdate(t *testing.T) {
"invalid-update": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
spec := validClaim.Spec.DeepCopy()
spec.Devices.Requests[0].DeviceClassName += "2"
spec.Devices.Requests[0].Exactly.DeviceClassName += "2"
return *spec
}(), "field is immutable")},
oldClaim: validClaim,
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
claim.Spec.Devices.Requests[0].DeviceClassName += "2"
claim.Spec.Devices.Requests[0].Exactly.DeviceClassName += "2"
return claim
},
},

View File

@@ -178,10 +178,10 @@ func TestValidateClaimTemplate(t *testing.T) {
}(),
},
"bad-classname": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "devices", "requests").Index(0).Child("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])?)*')")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "devices", "requests").Index(0).Child("exactly", "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])?)*')")},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, validClaimSpec)
template.Spec.Spec.Devices.Requests[0].DeviceClassName = badName
template.Spec.Spec.Devices.Requests[0].Exactly.DeviceClassName = badName
return template
}(),
},
@@ -189,11 +189,16 @@ func TestValidateClaimTemplate(t *testing.T) {
wantFailures: nil,
template: testClaimTemplate(goodName, goodNS, validClaimSpecWithFirstAvailable),
},
"proritized-list-class-name-on-parent": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "devices", "requests").Index(0), nil, "exactly one of `deviceClassName` or `firstAvailable` must be specified")},
"prioritized-list-both-first-available-and-exactly-set": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "spec", "devices", "requests").Index(0), nil, "exactly one of `exactly` or `firstAvailable` is required, but multiple fields are set"),
},
template: func() *resource.ResourceClaimTemplate {
template := testClaimTemplate(goodName, goodNS, validClaimSpecWithFirstAvailable)
template.Spec.Spec.Devices.Requests[0].DeviceClassName = goodName
template.Spec.Spec.Devices.Requests[0].Exactly = &resource.ExactDeviceRequest{
DeviceClassName: goodName,
AllocationMode: resource.DeviceAllocationModeAll,
}
return template
}(),
},
@@ -230,12 +235,12 @@ func TestValidateClaimTemplateUpdate(t *testing.T) {
"invalid-update-class": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
spec := validClaimTemplate.Spec.DeepCopy()
spec.Spec.Devices.Requests[0].DeviceClassName += "2"
spec.Spec.Devices.Requests[0].Exactly.DeviceClassName += "2"
return *spec
}(), "field is immutable")},
oldClaimTemplate: validClaimTemplate,
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
template.Spec.Spec.Devices.Requests[0].DeviceClassName += "2"
template.Spec.Spec.Devices.Requests[0].Exactly.DeviceClassName += "2"
return template
},
},

View File

@@ -27,6 +27,8 @@ import (
"k8s.io/kubernetes/pkg/apis/core"
resourceapi "k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/utils/ptr"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
)
func testAttributes() map[resourceapi.QualifiedName]resourceapi.DeviceAttribute {
@@ -56,7 +58,7 @@ func testResourceSlice(name, nodeName, driverName string, numDevices int) *resou
Name: name,
},
Spec: resourceapi.ResourceSliceSpec{
NodeName: nodeName,
NodeName: &nodeName,
Driver: driverName,
Pool: resourceapi.ResourcePool{
Name: nodeName,
@@ -66,11 +68,9 @@ func testResourceSlice(name, nodeName, driverName string, numDevices int) *resou
}
for i := 0; i < numDevices; i++ {
device := resourceapi.Device{
Name: fmt.Sprintf("device-%d", i),
Basic: &resourceapi.BasicDevice{
Attributes: testAttributes(),
Capacity: testCapacity(),
},
Name: fmt.Sprintf("device-%d", i),
Attributes: testAttributes(),
Capacity: testCapacity(),
}
slice.Spec.Devices = append(slice.Spec.Devices, device)
}
@@ -273,7 +273,7 @@ func TestValidateResourceSlice(t *testing.T) {
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = ""
slice.Spec.NodeName = nil
slice.Spec.NodeSelector = &core.NodeSelector{}
return slice
}(),
@@ -282,7 +282,7 @@ func TestValidateResourceSlice(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), "{`nodeName`, `nodeSelector`}", "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required, but multiple fields are set")},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = "worker"
slice.Spec.NodeName = ptr.To("worker")
slice.Spec.NodeSelector = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{MatchFields: []core.NodeSelectorRequirement{{Key: "metadata.name", Operator: core.NodeSelectorOpIn, Values: []string{"worker"}}}}},
}
@@ -293,8 +293,8 @@ func TestValidateResourceSlice(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), "{`nodeName`, `allNodes`}", "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required, but multiple fields are set")},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = "worker"
slice.Spec.AllNodes = true
slice.Spec.NodeName = ptr.To("worker")
slice.Spec.AllNodes = ptr.To(true)
return slice
}(),
},
@@ -302,7 +302,7 @@ func TestValidateResourceSlice(t *testing.T) {
wantFailures: field.ErrorList{field.Required(field.NewPath("spec"), "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required")},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = ""
slice.Spec.NodeName = nil
return slice
}(),
},
@@ -313,36 +313,34 @@ func TestValidateResourceSlice(t *testing.T) {
"bad-devices": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("name"), 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])?')"),
field.Required(field.NewPath("spec", "devices").Index(2).Child("basic"), ""),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 3)
slice.Spec.Devices[1].Name = badName
slice.Spec.Devices[2].Basic = nil
return slice
}(),
},
"bad-attribute": {
wantFailures: field.ErrorList{
field.TypeInvalid(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(badName), badName, "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
field.Required(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(badName), "exactly one value must be specified"),
field.Invalid(field.NewPath("spec", "devices").Index(2).Child("basic", "attributes").Key(goodName), resourceapi.DeviceAttribute{StringValue: ptr.To("x"), VersionValue: ptr.To("1.2.3")}, "exactly one value must be specified"),
field.Invalid(field.NewPath("spec", "devices").Index(3).Child("basic", "attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), "must be a string compatible with semver.org spec 2.0.0"),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(3).Child("basic", "attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), resourceapi.DeviceAttributeMaxValueLength),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(4).Child("basic", "attributes").Key(goodName).Child("string"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), resourceapi.DeviceAttributeMaxValueLength),
field.TypeInvalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(badName), badName, "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(badName), "exactly one value must be specified"),
field.Invalid(field.NewPath("spec", "devices").Index(2).Child("attributes").Key(goodName), resourceapi.DeviceAttribute{StringValue: ptr.To("x"), VersionValue: ptr.To("1.2.3")}, "exactly one value must be specified"),
field.Invalid(field.NewPath("spec", "devices").Index(3).Child("attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), "must be a string compatible with semver.org spec 2.0.0"),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(3).Child("attributes").Key(goodName).Child("version"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), resourceapi.DeviceAttributeMaxValueLength),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(4).Child("attributes").Key(goodName).Child("string"), strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1), resourceapi.DeviceAttributeMaxValueLength),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 5)
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(badName): {},
}
slice.Spec.Devices[2].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[2].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(goodName): {StringValue: ptr.To("x"), VersionValue: ptr.To("1.2.3")},
}
slice.Spec.Devices[3].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[3].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(goodName): {VersionValue: ptr.To(strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1))},
}
slice.Spec.Devices[4].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[4].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(goodName): {StringValue: ptr.To(strings.Repeat("x", resourceapi.DeviceAttributeMaxValueLength+1))},
}
return slice
@@ -351,7 +349,7 @@ func TestValidateResourceSlice(t *testing.T) {
"good-attribute-names": {
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 2)
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(strings.Repeat("x", resourceapi.DeviceMaxIDLength)): {StringValue: ptr.To("y")},
resourceapi.QualifiedName(strings.Repeat("x", resourceapi.DeviceMaxDomainLength) + "/" + strings.Repeat("y", resourceapi.DeviceMaxIDLength)): {StringValue: ptr.To("z")},
}
@@ -360,12 +358,12 @@ func TestValidateResourceSlice(t *testing.T) {
},
"bad-attribute-c-identifier": {
wantFailures: field.ErrorList{
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength),
field.TypeInvalid(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength),
field.TypeInvalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)), strings.Repeat(".", resourceapi.DeviceMaxIDLength+1), "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 2)
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(strings.Repeat(".", resourceapi.DeviceMaxIDLength+1)): {StringValue: ptr.To("y")},
}
return slice
@@ -373,12 +371,12 @@ func TestValidateResourceSlice(t *testing.T) {
},
"bad-attribute-domain": {
wantFailures: field.ErrorList{
field.TooLong(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength),
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), "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])?)*')"),
field.TooLong(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength),
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1)+"/y"), strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1), "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])?)*')"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 2)
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(strings.Repeat("_", resourceapi.DeviceMaxDomainLength+1) + "/y"): {StringValue: ptr.To("z")},
}
return slice
@@ -386,12 +384,12 @@ func TestValidateResourceSlice(t *testing.T) {
},
"bad-key-too-long": {
wantFailures: field.ErrorList{
field.TooLong(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("y", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength),
field.TooLong(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1), resourceapi.DeviceMaxDomainLength),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"), strings.Repeat("y", resourceapi.DeviceMaxIDLength+1), resourceapi.DeviceMaxIDLength),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 2)
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName(strings.Repeat("x", resourceapi.DeviceMaxDomainLength+1) + "/" + strings.Repeat("y", resourceapi.DeviceMaxIDLength+1)): {StringValue: ptr.To("z")},
}
return slice
@@ -399,38 +397,39 @@ func TestValidateResourceSlice(t *testing.T) {
},
"bad-attribute-empty-domain-and-c-identifier": {
wantFailures: field.ErrorList{
field.Required(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("/"), "the domain must not be empty"),
field.Required(field.NewPath("spec", "devices").Index(1).Child("basic", "attributes").Key("/"), "the name must not be empty"),
field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("/"), "the domain must not be empty"),
field.Required(field.NewPath("spec", "devices").Index(1).Child("attributes").Key("/"), "the name must not be empty"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 2)
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
resourceapi.QualifiedName("/"): {StringValue: ptr.To("z")},
}
return slice
}(),
},
"combined-attributes-and-capacity-length": {
"combined-attributes-capacity-length": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices").Index(2).Child("basic"), resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice+1, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice)),
field.Invalid(field.NewPath("spec", "devices").Index(3), resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice+1, fmt.Sprintf("the total number of attributes and capacities must not exceed %d", resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice)),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 3)
slice.Spec.Devices[0].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
slice.Spec.Devices[0].Basic.Capacity = map[resourceapi.QualifiedName]resourceapi.DeviceCapacity{}
slice := testResourceSlice(goodName, goodName, goodName, 5)
slice.Spec.Devices[0].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
slice.Spec.Devices[0].Capacity = map[resourceapi.QualifiedName]resourceapi.DeviceCapacity{}
for i := 0; i < resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice; i++ {
slice.Spec.Devices[0].Basic.Attributes[resourceapi.QualifiedName(fmt.Sprintf("attr_%d", i))] = resourceapi.DeviceAttribute{StringValue: ptr.To("x")}
slice.Spec.Devices[0].Attributes[resourceapi.QualifiedName(fmt.Sprintf("attr_%d", i))] = resourceapi.DeviceAttribute{StringValue: ptr.To("x")}
}
slice.Spec.Devices[1].Basic.Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
slice.Spec.Devices[1].Basic.Capacity = map[resourceapi.QualifiedName]resourceapi.DeviceCapacity{}
slice.Spec.Devices[1].Attributes = map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{}
slice.Spec.Devices[1].Capacity = map[resourceapi.QualifiedName]resourceapi.DeviceCapacity{}
quantity := resource.MustParse("1Gi")
capacity := resourceapi.DeviceCapacity{Value: quantity}
for i := 0; i < resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice; i++ {
slice.Spec.Devices[1].Basic.Capacity[resourceapi.QualifiedName(fmt.Sprintf("cap_%d", i))] = capacity
slice.Spec.Devices[1].Capacity[resourceapi.QualifiedName(fmt.Sprintf("cap_%d", i))] = capacity
}
// Too large together by one.
slice.Spec.Devices[2].Basic.Attributes = slice.Spec.Devices[0].Basic.Attributes
slice.Spec.Devices[2].Basic.Capacity = map[resourceapi.QualifiedName]resourceapi.DeviceCapacity{
slice.Spec.Devices[3].Attributes = slice.Spec.Devices[0].Attributes
slice.Spec.Devices[3].Capacity = map[resourceapi.QualifiedName]resourceapi.DeviceCapacity{
"cap": capacity,
}
return slice
@@ -440,7 +439,7 @@ func TestValidateResourceSlice(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeSelector", "nodeSelectorTerms").Index(0).Child("matchExpressions").Index(0).Child("values").Index(0), "-1", "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])?')")},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 3)
slice.Spec.NodeName = ""
slice.Spec.NodeName = nil
slice.Spec.NodeSelector = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{
MatchExpressions: []core.NodeSelectorRequirement{{
@@ -455,7 +454,7 @@ func TestValidateResourceSlice(t *testing.T) {
},
"taints": {
wantFailures: func() field.ErrorList {
fldPath := field.NewPath("spec", "devices").Index(0).Child("basic", "taints")
fldPath := field.NewPath("spec", "devices").Index(0).Child("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]')"),
@@ -468,7 +467,7 @@ func TestValidateResourceSlice(t *testing.T) {
}(),
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, goodName, 3)
slice.Spec.Devices[0].Basic.Taints = []resourceapi.DeviceTaint{
slice.Spec.Devices[0].Taints = []resourceapi.DeviceTaint{
{
// Minimal valid taint.
Key: "example.com/taint",
@@ -494,14 +493,29 @@ func TestValidateResourceSlice(t *testing.T) {
return slice
}(),
},
"bad-PerDeviceNodeSelection": {
"too-many-taints": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec"), "{`nodeName`, `perDeviceNodeSelection`}", "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required, but multiple fields are set"),
field.Required(field.NewPath("spec", "devices").Index(0).Child("basic"), "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec"),
field.TooMany(field.NewPath("spec", "devices").Index(0).Child("taints"), resourceapi.DeviceTaintsMaxLength+1, resourceapi.DeviceTaintsMaxLength),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = "worker"
for i := 0; i < resourceapi.DeviceTaintsMaxLength+1; i++ {
slice.Spec.Devices[0].Taints = append(slice.Spec.Devices[0].Taints, resourceapi.DeviceTaint{
Key: "example.com/taint",
Effect: resourceapi.DeviceTaintEffectNoExecute,
})
}
return slice
}(),
},
"bad-PerDeviceNodeSelection": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec"), "{`nodeName`, `perDeviceNodeSelection`}", "exactly one of `nodeName`, `nodeSelector`, `allNodes`, `perDeviceNodeSelection` is required, but multiple fields are set"),
field.Required(field.NewPath("spec", "devices").Index(0), "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = ptr.To("worker")
slice.Spec.PerDeviceNodeSelection = func() *bool {
r := true
return &r
@@ -515,7 +529,7 @@ func TestValidateResourceSlice(t *testing.T) {
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = "worker"
slice.Spec.NodeName = ptr.To("worker")
slice.Spec.PerDeviceNodeSelection = func() *bool {
r := false
return &r
@@ -523,11 +537,39 @@ func TestValidateResourceSlice(t *testing.T) {
return slice
}(),
},
"invalid-false-AllNodes": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "allNodes"), false, "must be either unset or set to true"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = ptr.To("worker")
slice.Spec.AllNodes = func() *bool {
r := false
return &r
}()
return slice
}(),
},
"invalid-empty-NodeName": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "nodeName"), "", "must be either unset or set to a non-empty string"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.NodeName = ptr.To("")
slice.Spec.AllNodes = func() *bool {
r := true
return &r
}()
return slice
}(),
},
"invalid-node-selector-in-basicdevice": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("basic", "nodeName"), "", "must not be empty"),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("basic", "allNodes"), false, "must be either unset or set to true"),
field.Required(field.NewPath("spec", "devices").Index(0).Child("basic"), "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec"),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("nodeName"), "", "must not be empty"),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("allNodes"), false, "must be either unset or set to true"),
field.Required(field.NewPath("spec", "devices").Index(0), "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
@@ -535,12 +577,12 @@ func TestValidateResourceSlice(t *testing.T) {
r := true
return &r
}()
slice.Spec.NodeName = ""
slice.Spec.Devices[0].Basic.NodeName = func() *string {
slice.Spec.NodeName = nil
slice.Spec.Devices[0].NodeName = func() *string {
r := ""
return &r
}()
slice.Spec.Devices[0].Basic.AllNodes = func() *bool {
slice.Spec.Devices[0].AllNodes = func() *bool {
r := false
return &r
}()
@@ -549,7 +591,7 @@ func TestValidateResourceSlice(t *testing.T) {
},
"bad-node-selector-in-basicdevice": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("basic"), "{`nodeName`, `allNodes`}", "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec"),
field.Invalid(field.NewPath("spec", "devices").Index(0), "{`nodeName`, `allNodes`}", "exactly one of `nodeName`, `nodeSelector`, or `allNodes` is required when `perDeviceNodeSelection` is set to true in the ResourceSlice spec"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
@@ -557,12 +599,12 @@ func TestValidateResourceSlice(t *testing.T) {
r := true
return &r
}()
slice.Spec.NodeName = ""
slice.Spec.Devices[0].Basic.NodeName = func() *string {
slice.Spec.NodeName = nil
slice.Spec.Devices[0].NodeName = func() *string {
r := "worker"
return &r
}()
slice.Spec.Devices[0].Basic.AllNodes = func() *bool {
slice.Spec.Devices[0].AllNodes = func() *bool {
r := true
return &r
}()
@@ -636,7 +678,7 @@ func TestValidateResourceSlice(t *testing.T) {
},
"too-large-shared-counters": {
wantFailures: field.ErrorList{
field.TooMany(field.NewPath("spec", "sharedCounters"), resourceapi.ResourceSliceMaxSharedCounters+1, resourceapi.ResourceSliceMaxSharedCounters),
field.Invalid(field.NewPath("spec", "sharedCounters"), resourceapi.ResourceSliceMaxSharedCounters+1, fmt.Sprintf("the total number of shared counters must not exceed %d", resourceapi.ResourceSliceMaxSharedCounters)),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
@@ -644,15 +686,15 @@ func TestValidateResourceSlice(t *testing.T) {
return slice
}(),
},
"missing-sharedcounter-consumes-counter": {
"missing-counterset-consumes-counter": {
wantFailures: field.ErrorList{
field.Required(field.NewPath("spec", "devices").Index(0).Child("basic", "consumesCounter").Index(0).Child("sharedCounter"), ""),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("basic", "consumesCounter").Index(0).Child("sharedCounter"), "", "must reference a counterSet defined in the ResourceSlice sharedCounters"),
field.Required(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counterSet"), ""),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counterSet"), "", "must reference a counterSet defined in the ResourceSlice sharedCounters"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.SharedCounters = createSharedCounters(1)
slice.Spec.Devices[0].Basic.ConsumesCounter = []resourceapi.DeviceCounterConsumption{
slice.Spec.Devices[0].ConsumesCounters = []resourceapi.DeviceCounterConsumption{
{
Counters: testCounters(),
},
@@ -662,14 +704,14 @@ func TestValidateResourceSlice(t *testing.T) {
},
"missing-counter-consumes-counter": {
wantFailures: field.ErrorList{
field.Required(field.NewPath("spec", "devices").Index(0).Child("basic", "consumesCounter").Index(0).Child("counters"), ""),
field.Required(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counters"), ""),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.SharedCounters = createSharedCounters(1)
slice.Spec.Devices[0].Basic.ConsumesCounter = []resourceapi.DeviceCounterConsumption{
slice.Spec.Devices[0].ConsumesCounters = []resourceapi.DeviceCounterConsumption{
{
SharedCounter: "sharedcounters-0",
CounterSet: "counterset-0",
},
}
return slice
@@ -677,17 +719,17 @@ func TestValidateResourceSlice(t *testing.T) {
},
"wrong-counterref-consumes-counter": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("basic", "consumesCounter").Index(0).Child("counters"), "fake", "must reference a counter defined in the ResourceSlice sharedCounters"),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counters"), "fake", "must reference a counter defined in the ResourceSlice sharedCounters"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.SharedCounters = createSharedCounters(1)
slice.Spec.Devices[0].Basic.ConsumesCounter = []resourceapi.DeviceCounterConsumption{
slice.Spec.Devices[0].ConsumesCounters = []resourceapi.DeviceCounterConsumption{
{
Counters: map[string]resourceapi.Counter{
"fake": {Value: resource.MustParse("1Gi")},
},
SharedCounter: "sharedcounters-0",
CounterSet: "counterset-0",
},
}
return slice
@@ -695,15 +737,15 @@ func TestValidateResourceSlice(t *testing.T) {
},
"wrong-sharedcounterref-consumes-counter": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("basic", "consumesCounter").Index(0).Child("sharedCounter"), "fake", "must reference a counterSet defined in the ResourceSlice sharedCounters"),
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("consumesCounters").Index(0).Child("counterSet"), "fake", "must reference a counterSet defined in the ResourceSlice sharedCounters"),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.SharedCounters = createSharedCounters(1)
slice.Spec.Devices[0].Basic.ConsumesCounter = []resourceapi.DeviceCounterConsumption{
slice.Spec.Devices[0].ConsumesCounters = []resourceapi.DeviceCounterConsumption{
{
Counters: testCounters(),
SharedCounter: "fake",
Counters: testCounters(),
CounterSet: "fake",
},
}
return slice
@@ -711,13 +753,29 @@ func TestValidateResourceSlice(t *testing.T) {
},
"too-large-consumes-counter": {
wantFailures: field.ErrorList{
field.TooMany(field.NewPath("spec", "devices").Index(0).Child("basic", "consumesCounter"), resourceapi.ResourceSliceMaxDeviceCounterConsumptions+1, resourceapi.ResourceSliceMaxSharedCounters),
field.TooMany(field.NewPath("spec", "sharedCounters"), resourceapi.ResourceSliceMaxDeviceCounterConsumptions+1, resourceapi.ResourceSliceMaxSharedCounters),
field.Invalid(field.NewPath("spec", "devices").Index(0), resourceapi.ResourceSliceMaxCountersPerDevice+1, fmt.Sprintf("the total number of counters must not exceed %d", resourceapi.ResourceSliceMaxCountersPerDevice)),
field.Invalid(field.NewPath("spec", "sharedCounters"), resourceapi.ResourceSliceMaxSharedCounters+1, fmt.Sprintf("the total number of shared counters must not exceed %d", resourceapi.ResourceSliceMaxSharedCounters)),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 1)
slice.Spec.SharedCounters = createSharedCounters(resourceapi.ResourceSliceMaxDeviceCounterConsumptions + 1)
slice.Spec.Devices[0].Basic.ConsumesCounter = createConsumesCounter(resourceapi.ResourceSliceMaxDeviceCounterConsumptions + 1)
slice.Spec.SharedCounters = createSharedCounters(resourceapi.ResourceSliceMaxSharedCounters + 1)
slice.Spec.Devices[0].Attributes = nil
slice.Spec.Devices[0].Capacity = nil
slice.Spec.Devices[0].ConsumesCounters = createConsumesCounters(resourceapi.ResourceSliceMaxCountersPerDevice + 1)
return slice
}(),
},
"too-many-device-counters-in-slice": {
wantFailures: field.ErrorList{
field.Invalid(field.NewPath("spec", "devices"), resourceapi.ResourceSliceMaxDeviceCountersPerSlice+1, fmt.Sprintf("the total number of counters in devices must not exceed %d", resourceapi.ResourceSliceMaxDeviceCountersPerSlice)),
},
slice: func() *resourceapi.ResourceSlice {
slice := testResourceSlice(goodName, goodName, driverName, 65)
slice.Spec.SharedCounters = createSharedCounters(16)
for i := 0; i < 64; i++ {
slice.Spec.Devices[i].ConsumesCounters = createConsumesCounters(16)
}
slice.Spec.Devices[64].ConsumesCounters = createConsumesCounters(1)
return slice
}(),
},
@@ -753,10 +811,10 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
},
"invalid-update-nodename": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), name+"-updated", "field is immutable")},
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeName"), ptr.To(name+"-updated"), "field is immutable")},
oldResourceSlice: validResourceSlice,
update: func(slice *resourceapi.ResourceSlice) *resourceapi.ResourceSlice {
slice.Spec.NodeName += "-updated"
slice.Spec.NodeName = ptr.To(*slice.Spec.NodeName + "-updated")
return slice
},
},
@@ -780,7 +838,7 @@ func TestValidateResourceSliceUpdate(t *testing.T) {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "nodeSelector", "nodeSelectorTerms").Index(0).Child("matchExpressions").Index(0).Child("values").Index(0), "-1", "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])?')")},
oldResourceSlice: func() *resourceapi.ResourceSlice {
slice := validResourceSlice.DeepCopy()
slice.Spec.NodeName = ""
slice.Spec.NodeName = nil
slice.Spec.NodeSelector = &core.NodeSelector{
NodeSelectorTerms: []core.NodeSelectorTerm{{
MatchExpressions: []core.NodeSelectorRequirement{{
@@ -820,19 +878,19 @@ func createSharedCounters(count int) []resourceapi.CounterSet {
sharedCounters := make([]resourceapi.CounterSet, count)
for i := 0; i < count; i++ {
sharedCounters[i] = resourceapi.CounterSet{
Name: fmt.Sprintf("sharedcounters-%d", i),
Name: fmt.Sprintf("counterset-%d", i),
Counters: testCounters(),
}
}
return sharedCounters
}
func createConsumesCounter(count int) []resourceapi.DeviceCounterConsumption {
func createConsumesCounters(count int) []resourceapi.DeviceCounterConsumption {
consumeCapacity := make([]resourceapi.DeviceCounterConsumption, count)
for i := 0; i < count; i++ {
consumeCapacity[i] = resourceapi.DeviceCounterConsumption{
SharedCounter: fmt.Sprintf("sharedcounters-%d", i),
Counters: testCounters(),
CounterSet: fmt.Sprintf("counterset-%d", i),
Counters: testCounters(),
}
}
return consumeCapacity

View File

@@ -52,6 +52,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
resourcev1alpha3 "k8s.io/api/resource/v1alpha3"
resourcev1beta1 "k8s.io/api/resource/v1beta1"
resourcev1beta2 "k8s.io/api/resource/v1beta2"
schedulingapiv1 "k8s.io/api/scheduling/v1"
storageapiv1 "k8s.io/api/storage/v1"
storageapiv1alpha1 "k8s.io/api/storage/v1alpha1"
@@ -464,6 +465,7 @@ var (
flowcontrolv1beta3.SchemeGroupVersion,
networkingapiv1beta1.SchemeGroupVersion,
resourcev1beta1.SchemeGroupVersion,
resourcev1beta2.SchemeGroupVersion,
}
// alphaAPIGroupVersionsDisabledByDefault holds the alpha APIs we have. They are always disabled by default.

View File

@@ -40,7 +40,7 @@ import (
flowcontrolv1 "k8s.io/api/flowcontrol/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
resourceapi "k8s.io/api/resource/v1beta1"
resourceapi "k8s.io/api/resource/v1beta2"
schedulingv1 "k8s.io/api/scheduling/v1"
storagev1 "k8s.io/api/storage/v1"
storagev1beta1 "k8s.io/api/storage/v1beta1"
@@ -3171,7 +3171,11 @@ func printResourceSlice(obj *resource.ResourceSlice, options printers.GenerateOp
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
row.Cells = append(row.Cells, obj.Name, obj.Spec.NodeName, obj.Spec.Driver, obj.Spec.Pool.Name, translateTimestampSince(obj.CreationTimestamp))
nodeName := ""
if obj.Spec.NodeName != nil {
nodeName = *obj.Spec.NodeName
}
row.Cells = append(row.Cells, obj.Name, nodeName, obj.Spec.Driver, obj.Spec.Pool.Name, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}

View File

@@ -6313,10 +6313,12 @@ func TestPrintResourceClaim(t *testing.T) {
Devices: resourceapis.DeviceClaim{
Requests: []resourceapis.DeviceRequest{
{
Name: "deviceRequest",
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
Name: "deviceRequest",
Exactly: &resourceapis.ExactDeviceRequest{
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
@@ -6337,10 +6339,12 @@ func TestPrintResourceClaim(t *testing.T) {
Devices: resourceapis.DeviceClaim{
Requests: []resourceapis.DeviceRequest{
{
Name: "deviceRequest",
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
Name: "deviceRequest",
Exactly: &resourceapis.ExactDeviceRequest{
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
@@ -6360,10 +6364,12 @@ func TestPrintResourceClaim(t *testing.T) {
Devices: resourceapis.DeviceClaim{
Requests: []resourceapis.DeviceRequest{
{
Name: "deviceRequest",
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
Name: "deviceRequest",
Exactly: &resourceapis.ExactDeviceRequest{
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
@@ -6394,10 +6400,12 @@ func TestPrintResourceClaim(t *testing.T) {
Devices: resourceapis.DeviceClaim{
Requests: []resourceapis.DeviceRequest{
{
Name: "deviceRequest",
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
Name: "deviceRequest",
Exactly: &resourceapis.ExactDeviceRequest{
DeviceClassName: "deviceClass",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
@@ -6438,10 +6446,12 @@ func TestPrintResourceClaimTemplate(t *testing.T) {
Spec: resourceapis.ResourceClaimSpec{
Devices: resourceapis.DeviceClaim{
Requests: []resourceapis.DeviceRequest{{
Name: "test-deviceRequest",
DeviceClassName: "deviceClassName",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
Name: "test-deviceRequest",
Exactly: &resourceapis.ExactDeviceRequest{
DeviceClassName: "deviceClassName",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
},
}},
},
},
@@ -6461,10 +6471,12 @@ func TestPrintResourceClaimTemplate(t *testing.T) {
Spec: resourceapis.ResourceClaimSpec{
Devices: resourceapis.DeviceClaim{
Requests: []resourceapis.DeviceRequest{{
Name: "test-deviceRequest",
DeviceClassName: "deviceClassName",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
Name: "test-deviceRequest",
Exactly: &resourceapis.ExactDeviceRequest{
DeviceClassName: "deviceClassName",
AllocationMode: resourceapis.DeviceAllocationModeExactCount,
Count: 1,
},
}},
},
},
@@ -6502,7 +6514,7 @@ func TestPrintResourceSlice(t *testing.T) {
CreationTimestamp: metav1.Time{Time: time.Now().Add(-3e11)},
},
Spec: resourceapis.ResourceSliceSpec{
NodeName: "nodeName",
NodeName: ptr.To("nodeName"),
Driver: "driverName",
Pool: resourceapis.ResourcePool{
Name: "poolName",
@@ -6520,7 +6532,7 @@ func TestPrintResourceSlice(t *testing.T) {
CreationTimestamp: metav1.Time{},
},
Spec: resourceapis.ResourceSliceSpec{
NodeName: "nodeName",
NodeName: ptr.To("nodeName"),
Driver: "driverName",
Pool: resourceapis.ResourcePool{
Name: "poolName",

View File

@@ -40,7 +40,20 @@ func testResourceClaim(name string, namespace string, spec api.ResourceClaimSpec
func TestResourceClaimEvaluatorUsage(t *testing.T) {
classGpu := "gpu"
classTpu := "tpu"
validClaim := testResourceClaim("foo", "ns", api.ResourceClaimSpec{Devices: api.DeviceClaim{Requests: []api.DeviceRequest{{Name: "req-0", DeviceClassName: classGpu, AllocationMode: api.DeviceAllocationModeExactCount, Count: 1}}}})
validClaim := testResourceClaim("foo", "ns", api.ResourceClaimSpec{
Devices: api.DeviceClaim{
Requests: []api.DeviceRequest{
{
Name: "req-0",
Exactly: &api.ExactDeviceRequest{
DeviceClassName: classGpu,
AllocationMode: api.DeviceAllocationModeExactCount,
Count: 1,
},
},
},
},
})
validClaimWithPrioritizedList := testResourceClaim("foo", "ns", api.ResourceClaimSpec{
Devices: api.DeviceClaim{
Requests: []api.DeviceRequest{
@@ -88,7 +101,7 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
"count": {
claim: func() *api.ResourceClaim {
claim := validClaim.DeepCopy()
claim.Spec.Devices.Requests[0].Count = 5
claim.Spec.Devices.Requests[0].Exactly.Count = 5
return claim
}(),
usage: corev1.ResourceList{
@@ -99,7 +112,7 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
"all": {
claim: func() *api.ResourceClaim {
claim := validClaim.DeepCopy()
claim.Spec.Devices.Requests[0].AllocationMode = api.DeviceAllocationModeAll
claim.Spec.Devices.Requests[0].Exactly.AllocationMode = api.DeviceAllocationModeAll
return claim
}(),
usage: corev1.ResourceList{
@@ -110,7 +123,7 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
"unknown-count-mode": {
claim: func() *api.ResourceClaim {
claim := validClaim.DeepCopy()
claim.Spec.Devices.Requests[0].AllocationMode = "future-mode"
claim.Spec.Devices.Requests[0].Exactly.AllocationMode = "future-mode"
return claim
}(),
usage: corev1.ResourceList{
@@ -122,7 +135,7 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
claim: func() *api.ResourceClaim {
claim := validClaim.DeepCopy()
// Admins are *not* exempt from quota.
claim.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
claim.Spec.Devices.Requests[0].Exactly.AdminAccess = ptr.To(true)
return claim
}(),
usage: corev1.ResourceList{

View File

@@ -71,6 +71,9 @@ func (*resourceclaimStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpa
"resource.k8s.io/v1beta1": fieldpath.NewSet(
fieldpath.MakePathOrDie("status"),
),
"resource.k8s.io/v1beta2": fieldpath.NewSet(
fieldpath.MakePathOrDie("status"),
),
}
return fields
@@ -145,6 +148,9 @@ func (*resourceclaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*f
"resource.k8s.io/v1beta1": fieldpath.NewSet(
fieldpath.MakePathOrDie("spec"),
),
"resource.k8s.io/v1beta2": fieldpath.NewSet(
fieldpath.MakePathOrDie("spec"),
),
}
return fields
@@ -253,7 +259,9 @@ func dropDisabledDRAAdminAccessFields(newClaim, oldClaim *resource.ResourceClaim
}
for i := range newClaim.Spec.Devices.Requests {
newClaim.Spec.Devices.Requests[i].AdminAccess = nil
if newClaim.Spec.Devices.Requests[i].Exactly != nil {
newClaim.Spec.Devices.Requests[i].Exactly.AdminAccess = nil
}
}
if newClaim.Status.Allocation == nil {
@@ -270,7 +278,7 @@ func draAdminAccessFeatureInUse(claim *resource.ResourceClaim) bool {
}
for _, request := range claim.Spec.Devices.Requests {
if request.AdminAccess != nil {
if request.Exactly != nil && request.Exactly.AdminAccess != nil {
return true
}
}

View File

@@ -42,9 +42,11 @@ var obj = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
},
},
},
@@ -60,9 +62,11 @@ var objWithStatus = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
},
},
},
@@ -92,10 +96,12 @@ var objWithAdminAccess = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
},
},
},
},
@@ -111,9 +117,11 @@ var objInNonAdminNamespace = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
},
},
},
@@ -129,10 +137,12 @@ var objWithAdminAccessInNonAdminNamespace = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
},
},
},
},
@@ -148,9 +158,11 @@ var objStatusInNonAdminNamespace = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
},
},
},
@@ -179,9 +191,12 @@ var objWithAdminAccessStatusInNonAdminNamespace = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
},
},
},
},
@@ -236,10 +251,12 @@ var objWithAdminAccessStatus = &resource.ResourceClaim{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
},
},
},
},
@@ -277,7 +294,7 @@ var ns2 = &corev1.Namespace{
var adminAccessError = "Forbidden: admin access to devices requires the `resource.k8s.io/admin-access: true` label"
var fieldImmutableError = "field is immutable"
var metadataError = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters"
var deviceRequestError = "exactly one of `deviceClassName` or `firstAvailable` must be specified"
var deviceRequestError = "exactly one of `exactly` or `firstAvailable` is required"
const (
testRequest = "test-request"
@@ -727,14 +744,14 @@ func TestStatusStrategyUpdate(t *testing.T) {
"keep-fields-admin-access-because-of-status": {
oldObj: func() *resource.ResourceClaim {
oldObj := objWithAdminAccessStatus.DeepCopy()
oldObj.Spec.Devices.Requests[0].AdminAccess = ptr.To(false)
oldObj.Spec.Devices.Requests[0].Exactly.AdminAccess = ptr.To(false)
return oldObj
}(),
newObj: objWithAdminAccessStatus,
adminAccess: false,
expectObj: func() *resource.ResourceClaim {
oldObj := objWithAdminAccessStatus.DeepCopy()
oldObj.Spec.Devices.Requests[0].AdminAccess = ptr.To(false)
oldObj.Spec.Devices.Requests[0].Exactly.AdminAccess = ptr.To(false)
return oldObj
}(),
verify: func(t *testing.T, as []testclient.Action) {

View File

@@ -155,7 +155,9 @@ func dropDisabledDRAAdminAccessFields(newClaimTemplate, oldClaimTemplate *resour
}
for i := range newClaimTemplate.Spec.Spec.Devices.Requests {
newClaimTemplate.Spec.Spec.Devices.Requests[i].AdminAccess = nil
if newClaimTemplate.Spec.Spec.Devices.Requests[i].Exactly != nil {
newClaimTemplate.Spec.Spec.Devices.Requests[i].Exactly.AdminAccess = nil
}
}
}
@@ -165,7 +167,7 @@ func draAdminAccessFeatureInUse(claimTemplate *resource.ResourceClaimTemplate) b
}
for _, request := range claimTemplate.Spec.Spec.Devices.Requests {
if request.AdminAccess != nil {
if request.Exactly != nil && request.Exactly.AdminAccess != nil {
return true
}
}

View File

@@ -43,9 +43,11 @@ var obj = &resource.ResourceClaimTemplate{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
},
},
},
},
@@ -63,10 +65,12 @@ var objWithAdminAccess = &resource.ResourceClaimTemplate{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
},
},
},
},
@@ -84,10 +88,12 @@ var objWithAdminAccessInNonAdminNamespace = &resource.ResourceClaimTemplate{
Devices: resource.DeviceClaim{
Requests: []resource.DeviceRequest{
{
Name: "req-0",
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
Name: "req-0",
Exactly: &resource.ExactDeviceRequest{
DeviceClassName: "class",
AllocationMode: resource.DeviceAllocationModeAll,
AdminAccess: ptr.To(true),
},
},
},
},
@@ -136,7 +142,7 @@ var ns2 = &corev1.Namespace{
var adminAccessError = "Forbidden: admin access to devices requires the `resource.k8s.io/admin-access: true` label on the containing namespace"
var fieldImmutableError = "field is immutable"
var metadataError = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters"
var deviceRequestError = "exactly one of `deviceClassName` or `firstAvailable` must be specified"
var deviceRequestError = "exactly one of `exactly` or `firstAvailable` is required"
func TestClaimTemplateStrategy(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
@@ -339,7 +345,7 @@ func TestClaimTemplateStrategyUpdate(t *testing.T) {
resourceClaimTemplate := obj.DeepCopy()
newClaimTemplate := resourceClaimTemplate.DeepCopy()
newClaimTemplate.ResourceVersion = "4"
newClaimTemplate.Spec.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
newClaimTemplate.Spec.Spec.Devices.Requests[0].Exactly.AdminAccess = ptr.To(true)
strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
errs := strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)

View File

@@ -29,6 +29,7 @@ import (
"k8s.io/kubernetes/pkg/apis/resource"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
"k8s.io/kubernetes/pkg/registry/registrytest"
"k8s.io/utils/ptr"
)
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
@@ -52,7 +53,7 @@ func validNewResourceSlice(name string) *resource.ResourceSlice {
Name: name,
},
Spec: resource.ResourceSliceSpec{
NodeName: name,
NodeName: ptr.To(name),
Driver: "cdi.example.com",
Pool: resource.ResourcePool{
Name: "worker-1",

View File

@@ -102,7 +102,12 @@ var TriggerFunc = map[string]storage.IndexerFunc{
}
func nodeNameTriggerFunc(obj runtime.Object) string {
return obj.(*resource.ResourceSlice).Spec.NodeName
rs := obj.(*resource.ResourceSlice)
if rs.Spec.NodeName == nil {
return ""
} else {
return *rs.Spec.NodeName
}
}
// Indexers returns the indexers for ResourceSlice.
@@ -117,7 +122,10 @@ func nodeNameIndexFunc(obj interface{}) ([]string, error) {
if !ok {
return nil, fmt.Errorf("not a ResourceSlice")
}
return []string{slice.Spec.NodeName}, nil
if slice.Spec.NodeName == nil {
return []string{""}, nil
}
return []string{*slice.Spec.NodeName}, nil
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
@@ -147,7 +155,11 @@ func toSelectableFields(slice *resource.ResourceSlice) fields.Set {
// field here or the number of object-meta related fields changes, this should
// be adjusted.
fields := make(fields.Set, 3)
fields[resource.ResourceSliceSelectorNodeName] = slice.Spec.NodeName
if slice.Spec.NodeName == nil {
fields[resource.ResourceSliceSelectorNodeName] = ""
} else {
fields[resource.ResourceSliceSelectorNodeName] = *slice.Spec.NodeName
}
fields[resource.ResourceSliceSelectorDriver] = slice.Spec.Driver
// Adds one field.
@@ -165,10 +177,8 @@ func dropDisabledDRADeviceTaintsFields(newSlice, oldSlice *resource.ResourceSlic
return
}
for _, device := range newSlice.Spec.Devices {
if device.Basic != nil {
device.Basic.Taints = nil
}
for i := range newSlice.Spec.Devices {
newSlice.Spec.Devices[i].Taints = nil
}
}
@@ -178,7 +188,7 @@ func draDeviceTaintsFeatureInUse(slice *resource.ResourceSlice) bool {
}
for _, device := range slice.Spec.Devices {
if device.Basic != nil && len(device.Basic.Taints) > 0 {
if len(device.Taints) > 0 {
return true
}
}
@@ -192,14 +202,11 @@ func dropDisabledDRAPartitionableDevicesFields(newSlice, oldSlice *resource.Reso
newSlice.Spec.SharedCounters = nil
newSlice.Spec.PerDeviceNodeSelection = nil
for _, device := range newSlice.Spec.Devices {
if device.Basic != nil {
device.Basic.ConsumesCounter = nil
device.Basic.NodeName = nil
device.Basic.NodeSelector = nil
device.Basic.AllNodes = nil
}
for i := range newSlice.Spec.Devices {
newSlice.Spec.Devices[i].ConsumesCounters = nil
newSlice.Spec.Devices[i].NodeName = nil
newSlice.Spec.Devices[i].NodeSelector = nil
newSlice.Spec.Devices[i].AllNodes = nil
}
}
@@ -214,15 +221,12 @@ func draPartitionableDevicesFeatureInUse(slice *resource.ResourceSlice) bool {
}
for _, device := range spec.Devices {
if device.Basic != nil {
if len(device.Basic.ConsumesCounter) > 0 {
return true
}
if device.Basic.NodeName != nil || device.Basic.NodeSelector != nil || device.Basic.AllNodes != nil {
return true
}
if len(device.ConsumesCounters) > 0 {
return true
}
if device.NodeName != nil || device.NodeSelector != nil || device.AllNodes != nil {
return true
}
}
return false
}

View File

@@ -28,6 +28,7 @@ import (
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/apis/resource"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/ptr"
)
var slice = &resource.ResourceSlice{
@@ -35,22 +36,21 @@ var slice = &resource.ResourceSlice{
Name: "valid-resource-slice",
},
Spec: resource.ResourceSliceSpec{
NodeName: "valid-node-name",
NodeName: ptr.To("valid-node-name"),
Driver: "testdriver.example.com",
Pool: resource.ResourcePool{
Name: "valid-pool-name",
ResourceSliceCount: 1,
},
Devices: []resource.Device{{
Name: "device-0",
Basic: &resource.BasicDevice{},
Name: "device-0",
}},
},
}
var sliceWithDeviceTaints = func() *resource.ResourceSlice {
slice := slice.DeepCopy()
slice.Spec.Devices[0].Basic.Taints = []resource.DeviceTaint{{
slice.Spec.Devices[0].Taints = []resource.DeviceTaint{{
Key: "example.com/tainted",
Effect: resource.DeviceTaintEffectNoSchedule,
}}
@@ -85,33 +85,31 @@ var sliceWithPartitionableDevices = &resource.ResourceSlice{
Devices: []resource.Device{
{
Name: "device",
Basic: &resource.BasicDevice{
ConsumesCounter: []resource.DeviceCounterConsumption{
{
SharedCounter: "pool-1",
Counters: map[string]resource.Counter{
"memory": {
Value: k8sresource.MustParse("40Gi"),
},
ConsumesCounters: []resource.DeviceCounterConsumption{
{
CounterSet: "pool-1",
Counters: map[string]resource.Counter{
"memory": {
Value: k8sresource.MustParse("40Gi"),
},
},
},
NodeName: func() *string {
r := "valid-node-name"
return &r
}(),
Attributes: map[resource.QualifiedName]resource.DeviceAttribute{
resource.QualifiedName("version"): {
StringValue: func() *string {
v := "v1"
return &v
}(),
},
},
NodeName: func() *string {
r := "valid-node-name"
return &r
}(),
Attributes: map[resource.QualifiedName]resource.DeviceAttribute{
resource.QualifiedName("version"): {
StringValue: func() *string {
v := "v1"
return &v
}(),
},
Capacity: map[resource.QualifiedName]resource.DeviceCapacity{
resource.QualifiedName("memory"): {
Value: k8sresource.MustParse("40Gi"),
},
},
Capacity: map[resource.QualifiedName]resource.DeviceCapacity{
resource.QualifiedName("memory"): {
Value: k8sresource.MustParse("40Gi"),
},
},
},
@@ -178,7 +176,7 @@ func TestResourceSliceStrategyCreate(t *testing.T) {
r := false
return &r
}()
obj.Spec.NodeName = "valid-node-name"
obj.Spec.NodeName = ptr.To("valid-node-name")
return obj
}(),
partitionableDevices: false,
@@ -187,11 +185,11 @@ func TestResourceSliceStrategyCreate(t *testing.T) {
obj.ObjectMeta.Generation = 1
obj.Spec.SharedCounters = nil
obj.Spec.PerDeviceNodeSelection = nil
obj.Spec.NodeName = "valid-node-name"
obj.Spec.Devices[0].Basic.NodeName = nil
obj.Spec.Devices[0].Basic.NodeSelector = nil
obj.Spec.Devices[0].Basic.AllNodes = nil
obj.Spec.Devices[0].Basic.ConsumesCounter = nil
obj.Spec.NodeName = ptr.To("valid-node-name")
obj.Spec.Devices[0].NodeName = nil
obj.Spec.Devices[0].NodeSelector = nil
obj.Spec.Devices[0].AllNodes = nil
obj.Spec.Devices[0].ConsumesCounters = nil
return obj
}(),
},
@@ -337,7 +335,7 @@ func TestResourceSliceStrategyUpdate(t *testing.T) {
r := false
return &r
}()
obj.Spec.NodeName = "valid-node-name"
obj.Spec.NodeName = ptr.To("valid-node-name")
return obj
}(),
partitionableDevices: false,
@@ -347,9 +345,9 @@ func TestResourceSliceStrategyUpdate(t *testing.T) {
obj.Generation = 1
obj.Spec.SharedCounters = nil
obj.Spec.PerDeviceNodeSelection = nil
obj.Spec.NodeName = "valid-node-name"
obj.Spec.Devices[0].Basic.ConsumesCounter = nil
obj.Spec.Devices[0].Basic.NodeName = nil
obj.Spec.NodeName = ptr.To("valid-node-name")
obj.Spec.Devices[0].ConsumesCounters = nil
obj.Spec.Devices[0].NodeName = nil
return obj
}(),
},
@@ -368,9 +366,9 @@ func TestResourceSliceStrategyUpdate(t *testing.T) {
newObj: func() *resource.ResourceSlice {
obj := sliceWithPartitionableDevices.DeepCopy()
obj.ResourceVersion = "4"
obj.Spec.NodeName = "valid-node-name"
obj.Spec.NodeName = ptr.To("valid-node-name")
obj.Spec.PerDeviceNodeSelection = nil
obj.Spec.Devices[0].Basic.NodeName = nil
obj.Spec.Devices[0].NodeName = nil
return obj
}(),
partitionableDevices: true,
@@ -378,9 +376,9 @@ func TestResourceSliceStrategyUpdate(t *testing.T) {
obj := sliceWithPartitionableDevices.DeepCopy()
obj.ResourceVersion = "4"
obj.Generation = 1
obj.Spec.NodeName = "valid-node-name"
obj.Spec.NodeName = ptr.To("valid-node-name")
obj.Spec.PerDeviceNodeSelection = nil
obj.Spec.Devices[0].Basic.NodeName = nil
obj.Spec.Devices[0].NodeName = nil
return obj
}(),
},

View File

@@ -19,11 +19,12 @@ package rest
import (
resourcev1alpha3 "k8s.io/api/resource/v1alpha3"
resourcev1beta1 "k8s.io/api/resource/v1beta1"
resourcev1beta2 "k8s.io/api/resource/v1beta2"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstorage "k8s.io/apiserver/pkg/server/storage"
"k8s.io/client-go/kubernetes/typed/core/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/resource"
deviceclassstore "k8s.io/kubernetes/pkg/registry/resource/deviceclass/storage"
@@ -58,6 +59,12 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag
apiGroupInfo.VersionedResourcesStorageMap[resourcev1beta1.SchemeGroupVersion.Version] = storageMap
}
if storageMap, err := p.v1beta2Storage(apiResourceConfigSource, restOptionsGetter, p.NamespaceClient); err != nil {
return genericapiserver.APIGroupInfo{}, err
} else if len(storageMap) > 0 {
apiGroupInfo.VersionedResourcesStorageMap[resourcev1beta2.SchemeGroupVersion.Version] = storageMap
}
return apiGroupInfo, nil
}
@@ -147,6 +154,45 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag
return storage, nil
}
func (p RESTStorageProvider) v1beta2Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter, nsClient v1.NamespaceInterface) (map[string]rest.Storage, error) {
storage := map[string]rest.Storage{}
if resource := "deviceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1beta2.SchemeGroupVersion.WithResource(resource)) {
deviceclassStorage, err := deviceclassstore.NewREST(restOptionsGetter)
if err != nil {
return nil, err
}
storage[resource] = deviceclassStorage
}
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1beta2.SchemeGroupVersion.WithResource(resource)) {
resourceClaimStorage, resourceClaimStatusStorage, err := resourceclaimstore.NewREST(restOptionsGetter, nsClient)
if err != nil {
return nil, err
}
storage[resource] = resourceClaimStorage
storage[resource+"/status"] = resourceClaimStatusStorage
}
if resource := "resourceclaimtemplates"; apiResourceConfigSource.ResourceEnabled(resourcev1beta2.SchemeGroupVersion.WithResource(resource)) {
resourceClaimTemplateStorage, err := resourceclaimtemplatestore.NewREST(restOptionsGetter, nsClient)
if err != nil {
return nil, err
}
storage[resource] = resourceClaimTemplateStorage
}
if resource := "resourceslices"; apiResourceConfigSource.ResourceEnabled(resourcev1beta2.SchemeGroupVersion.WithResource(resource)) {
resourceSliceStorage, err := resourceslicestore.NewREST(restOptionsGetter)
if err != nil {
return nil, err
}
storage[resource] = resourceSliceStorage
}
return storage, nil
}
func (p RESTStorageProvider) GroupName() string {
return resource.GroupName
}

View File

@@ -36,7 +36,12 @@ func AuthorizedForAdmin(ctx context.Context, deviceRequests []resource.DeviceReq
// no need to check old request since spec is immutable
for i := range deviceRequests {
value := deviceRequests[i].AdminAccess
// AdminAccess can not be set on subrequests, so it can
// only be used when the Exactly field is set.
if deviceRequests[i].Exactly == nil {
continue
}
value := deviceRequests[i].Exactly.AdminAccess
if value != nil && *value {
adminRequested = true
adminAccessPath = field.NewPath("spec", "devices", "requests").Index(i).Child("adminAccess")

View File

@@ -856,7 +856,7 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
if slice.Spec.NodeName != nodeName {
if slice.Spec.NodeName == nil || *slice.Spec.NodeName != nodeName {
return admission.NewForbidden(a, errors.New("can only create ResourceSlice with the same NodeName as the requesting node"))
}
case admission.Delete:
@@ -865,7 +865,7 @@ func (p *Plugin) admitResourceSlice(nodeName string, a admission.Attributes) err
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject()))
}
if slice.Spec.NodeName != nodeName {
if slice.Spec.NodeName == nil || *slice.Spec.NodeName != nodeName {
return admission.NewForbidden(a, errors.New("can only delete ResourceSlice with the same NodeName as the requesting node"))
}
}

View File

@@ -2154,7 +2154,7 @@ func TestAdmitResourceSlice(t *testing.T) {
Name: "something",
},
Spec: resourceapi.ResourceSliceSpec{
NodeName: nodename,
NodeName: pointer.String(nodename),
},
}
sliceOtherNode := &resourceapi.ResourceSlice{
@@ -2162,7 +2162,7 @@ func TestAdmitResourceSlice(t *testing.T) {
Name: "something",
},
Spec: resourceapi.ResourceSliceSpec{
NodeName: nodename + "-other",
NodeName: pointer.String(nodename + "-other"),
},
}
sliceNoNode := &resourceapi.ResourceSlice{
@@ -2170,7 +2170,7 @@ func TestAdmitResourceSlice(t *testing.T) {
Name: "something",
},
Spec: resourceapi.ResourceSliceSpec{
NodeName: "",
NodeName: nil,
},
}

View File

@@ -178,13 +178,13 @@ type ResourceSliceSpec struct {
// the portion of counters it uses will no longer be available for use
// by other devices.
type CounterSet struct {
// Name defines the name of the counter set.
// It must be a DNS label.
// CounterSet is the name of the set from which the
// counters defined will be consumed.
//
// +required
Name string `json:"name" protobuf:"bytes,1,name=name"`
// Counters defines the set of counters for this CounterSet
// Counters defines the counters that will be consumed by the device.
// The name of each counter must be unique in that set and must be a DNS label.
//
// To ensure this uniqueness, capacities defined by the vendor
@@ -284,20 +284,21 @@ type BasicDevice struct {
// +optional
Capacity map[QualifiedName]resource.Quantity `json:"capacity,omitempty" protobuf:"bytes,2,rep,name=capacity"`
// ConsumesCounter defines a list of references to sharedCounters
// ConsumesCounters defines a list of references to sharedCounters
// and the set of counters that the device will
// consume from those counter sets.
//
// There can only be a single entry per counterSet.
//
// The maximum number of device counter consumption entries
// is 32. This is the same as the maximum number of shared counters
// allowed in a ResourceSlice.
// The total number of device counter consumption entries
// must be <= 32. In addition, the total number in the
// entire ResourceSlice must be <= 1024 (for example,
// 64 devices with 16 counters each).
//
// +optional
// +listType=atomic
// +featureGate=DRAPartitionableDevices
ConsumesCounter []DeviceCounterConsumption `json:"consumesCounter,omitempty" protobuf:"bytes,3,rep,name=consumesCounter"`
ConsumesCounters []DeviceCounterConsumption `json:"consumesCounters,omitempty" protobuf:"bytes,3,rep,name=consumesCounters"`
// NodeName identifies the node where the device is available.
//
@@ -331,7 +332,7 @@ type BasicDevice struct {
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
// The maximum number of taints is 4.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
@@ -345,17 +346,19 @@ type BasicDevice struct {
// DeviceCounterConsumption defines a set of counters that
// a device will consume from a CounterSet.
type DeviceCounterConsumption struct {
// SharedCounter defines the shared counter from which the
// CounterSet defines the set from which the
// counters defined will be consumed.
//
// +required
SharedCounter string `json:"sharedCounter" protobuf:"bytes,1,opt,name=sharedCounter"`
CounterSet string `json:"counterSet" protobuf:"bytes,1,opt,name=counterSet"`
// Counters defines the Counter that will be consumed by
// the device.
//
//
// The maximum number of Counters is 32.
// The maximum number counters in a device is 32.
// In addition, the maximum number of all counters
// in all devices is 1024 (for example, 64 devices with
// 16 counters each).
//
// +required
Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"`
@@ -364,6 +367,14 @@ type DeviceCounterConsumption struct {
// Limit for the sum of the number of entries in both attributes and capacity.
const ResourceSliceMaxAttributesAndCapacitiesPerDevice = 32
// Limit for the total number of counters in each device.
const ResourceSliceMaxCountersPerDevice = 32
// Limit for the total number of counters defined in devices in
// a ResourceSlice. We want to allow up to 64 devices to specify
// up to 16 counters, so the limit for the ResourceSlice will be 1024.
const ResourceSliceMaxDeviceCountersPerSlice = 1024 // 64 * 16
// QualifiedName is the name of a device attribute or capacity.
//
// Attributes and capacities are defined either by the owner of the specific
@@ -427,7 +438,7 @@ type DeviceAttribute struct {
const DeviceAttributeMaxValueLength = 64
// DeviceTaintsMaxLength is the maximum number of taints per device.
const DeviceTaintsMaxLength = 8
const DeviceTaintsMaxLength = 4
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
@@ -784,7 +795,7 @@ type DeviceSubRequest struct {
// Allocation will fail if some devices are already allocated,
// unless adminAccess is requested.
//
// If AlloctionMode is not specified, the default mode is ExactCount. If
// If AllocationMode 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.
//

View File

@@ -279,20 +279,21 @@ type BasicDevice struct {
// +optional
Capacity map[QualifiedName]DeviceCapacity `json:"capacity,omitempty" protobuf:"bytes,2,rep,name=capacity"`
// ConsumesCounter defines a list of references to sharedCounters
// ConsumesCounters defines a list of references to sharedCounters
// and the set of counters that the device will
// consume from those counter sets.
//
// There can only be a single entry per counterSet.
//
// The maximum number of device counter consumption entries
// is 32. This is the same as the maximum number of shared counters
// allowed in a ResourceSlice.
// The total number of device counter consumption entries
// must be <= 32. In addition, the total number in the
// entire ResourceSlice must be <= 1024 (for example,
// 64 devices with 16 counters each).
//
// +optional
// +listType=atomic
// +featureGate=DRAPartitionableDevices
ConsumesCounter []DeviceCounterConsumption `json:"consumesCounter,omitempty" protobuf:"bytes,3,rep,name=consumesCounter"`
ConsumesCounters []DeviceCounterConsumption `json:"consumesCounters,omitempty" protobuf:"bytes,3,rep,name=consumesCounters"`
// NodeName identifies the node where the device is available.
//
@@ -306,6 +307,8 @@ type BasicDevice struct {
// NodeSelector defines the nodes where the device is available.
//
// Must use exactly one term.
//
// Must only be set if Spec.PerDeviceNodeSelection is set to true.
// At most one of NodeName, NodeSelector and AllNodes can be set.
//
@@ -325,7 +328,7 @@ type BasicDevice struct {
// If specified, these are the driver-defined taints.
//
// The maximum number of taints is 8.
// The maximum number of taints is 4.
//
// This is an alpha field and requires enabling the DRADeviceTaints
// feature gate.
@@ -339,17 +342,18 @@ type BasicDevice struct {
// DeviceCounterConsumption defines a set of counters that
// a device will consume from a CounterSet.
type DeviceCounterConsumption struct {
// SharedCounter defines the shared counter from which the
// CounterSet is the name of the set from which the
// counters defined will be consumed.
//
// +required
SharedCounter string `json:"sharedCounter" protobuf:"bytes,1,opt,name=sharedCounter"`
CounterSet string `json:"counterSet" protobuf:"bytes,1,opt,name=counterSet"`
// Counters defines the Counter that will be consumed by
// the device.
// Counters defines the counters that will be consumed by the device.
//
//
// The maximum number of Counters is 32.
// The maximum number counters in a device is 32.
// In addition, the maximum number of all counters
// in all devices is 1024 (for example, 64 devices with
// 16 counters each).
//
// +required
Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,opt,name=counters"`
@@ -369,6 +373,14 @@ type DeviceCapacity struct {
// Limit for the sum of the number of entries in both attributes and capacity.
const ResourceSliceMaxAttributesAndCapacitiesPerDevice = 32
// Limit for the total number of counters in each device.
const ResourceSliceMaxCountersPerDevice = 32
// Limit for the total number of counters defined in devices in
// a ResourceSlice. We want to allow up to 64 devices to specify
// up to 16 counters, so the limit for the ResourceSlice will be 1024.
const ResourceSliceMaxDeviceCountersPerSlice = 1024 // 64 * 16
// QualifiedName is the name of a device attribute or capacity.
//
// Attributes and capacities are defined either by the owner of the specific
@@ -432,7 +444,7 @@ type DeviceAttribute struct {
const DeviceAttributeMaxValueLength = 64
// DeviceTaintsMaxLength is the maximum number of taints per device.
const DeviceTaintsMaxLength = 8
const DeviceTaintsMaxLength = 4
// The device this taint is attached to has the "effect" on
// any claim which does not tolerate the taint and, through the claim,
@@ -790,7 +802,7 @@ type DeviceSubRequest struct {
// Allocation will fail if some devices are already allocated,
// unless adminAccess is requested.
//
// If AlloctionMode is not specified, the default mode is ExactCount. If
// If AllocationMode 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.
//

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 v1beta2
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)
}

View File

@@ -0,0 +1,24 @@
/*
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.
*/
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package
// +k8s:protobuf-gen=package
// +k8s:prerelease-lifecycle-gen=true
// +groupName=resource.k8s.io
// Package v1beta2 is the v1beta2 version of the resource API.
package v1beta2

View File

@@ -0,0 +1,60 @@
/*
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 v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "resource.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta2"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&DeviceClass{},
&DeviceClassList{},
&ResourceClaim{},
&ResourceClaimList{},
&ResourceClaimTemplate{},
&ResourceClaimTemplateList{},
&ResourceSlice{},
&ResourceSliceList{},
)
// Add the watch version that applies
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,7 @@ import (
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
resourcev1alpha3 "k8s.io/api/resource/v1alpha3"
resourcev1beta1 "k8s.io/api/resource/v1beta1"
resourcev1beta2 "k8s.io/api/resource/v1beta2"
schedulingv1 "k8s.io/api/scheduling/v1"
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
schedulingv1beta1 "k8s.io/api/scheduling/v1beta1"
@@ -138,6 +139,7 @@ var groups = []runtime.SchemeBuilder{
rbacv1.SchemeBuilder,
resourcev1alpha3.SchemeBuilder,
resourcev1beta1.SchemeBuilder,
resourcev1beta2.SchemeBuilder,
schedulingv1alpha1.SchemeBuilder,
schedulingv1beta1.SchemeBuilder,
schedulingv1.SchemeBuilder,

View File

@@ -56,18 +56,18 @@ type Device struct {
}
type BasicDevice struct {
Attributes map[QualifiedName]DeviceAttribute
Capacity map[QualifiedName]DeviceCapacity
ConsumesCounter []DeviceCounterConsumption
NodeName *string
NodeSelector *v1.NodeSelector
AllNodes *bool
Taints []resourceapi.DeviceTaint
Attributes map[QualifiedName]DeviceAttribute
Capacity map[QualifiedName]DeviceCapacity
ConsumesCounters []DeviceCounterConsumption
NodeName *string
NodeSelector *v1.NodeSelector
AllNodes *bool
Taints []resourceapi.DeviceTaint
}
type DeviceCounterConsumption struct {
SharedCounter UniqueString
Counters map[string]Counter
CounterSet UniqueString
Counters map[string]Counter
}
type QualifiedName string

View File

@@ -972,8 +972,8 @@ func (alloc *allocator) allocateDevice(r deviceIndices, device deviceWithID, mus
return false, nil, nil
}
// The API validation logic has checked the ConsumesCounter referred should exist inside SharedCounters.
if alloc.features.PartitionableDevices && len(device.basic.ConsumesCounter) > 0 {
// The API validation logic has checked the ConsumesCounters referred should exist inside SharedCounters.
if alloc.features.PartitionableDevices && len(device.basic.ConsumesCounters) > 0 {
// If a device consumes capacity from a capacity pool, verify that
// there is sufficient capacity available.
ok, err := alloc.checkAvailableCapacity(device)
@@ -1075,8 +1075,8 @@ func (alloc *allocator) checkAvailableCapacity(device deviceWithID) (bool, error
slice := device.slice
referencedSharedCounters := sets.New[draapi.UniqueString]()
for _, consumedCounter := range device.basic.ConsumesCounter {
referencedSharedCounters.Insert(consumedCounter.SharedCounter)
for _, consumedCounter := range device.basic.ConsumesCounters {
referencedSharedCounters.Insert(consumedCounter.CounterSet)
}
// Create a structure that captures the initial counter for all sharedCounters
@@ -1104,8 +1104,8 @@ func (alloc *allocator) checkAvailableCapacity(device deviceWithID) (bool, error
if !alloc.allocatedDevices.Has(deviceID) && !alloc.allocatingDevices[deviceID] {
continue
}
for _, consumedCounter := range device.Basic.ConsumesCounter {
counterShared := availableCounters[consumedCounter.SharedCounter]
for _, consumedCounter := range device.Basic.ConsumesCounters {
counterShared := availableCounters[consumedCounter.CounterSet]
for name, cap := range consumedCounter.Counters {
existingCap, ok := counterShared[name]
if !ok {
@@ -1121,8 +1121,8 @@ func (alloc *allocator) checkAvailableCapacity(device deviceWithID) (bool, error
}
// Check if all consumed capacities for the device can be satisfied.
for _, deviceConsumedCounter := range device.basic.ConsumesCounter {
counterShared := availableCounters[deviceConsumedCounter.SharedCounter]
for _, deviceConsumedCounter := range device.basic.ConsumesCounters {
counterShared := availableCounters[deviceConsumedCounter.CounterSet]
for name, cap := range deviceConsumedCounter.Counters {
availableCap, found := counterShared[name]
// If the device requests a capacity that doesn't exist in

View File

@@ -285,7 +285,7 @@ func partitionableDevice(name string, capacity any, attributes map[resourceapi.Q
panic(fmt.Sprintf("unexpected capacity type %T: %+v", capacity, capacity))
}
device.Basic.ConsumesCounter = consumesCapacity
device.Basic.ConsumesCounters = consumesCapacity
return device
}
@@ -328,8 +328,8 @@ func (in wrapDevice) withTaints(taints ...resourceapi.DeviceTaint) wrapDevice {
func deviceCapacityConsumption(capacityPool string, capacity map[resourceapi.QualifiedName]resource.Quantity) resourceapi.DeviceCounterConsumption {
return resourceapi.DeviceCounterConsumption{
SharedCounter: capacityPool,
Counters: toDeviceCounter(capacity),
CounterSet: capacityPool,
Counters: toDeviceCounter(capacity),
}
}

View File

@@ -61,6 +61,7 @@ var resetFieldsStatusData = map[schema.GroupVersionResource]string{
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
gvr("resource.k8s.io", "v1alpha3", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-other-value"]}] }]}}}}`,
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
// standard for []metav1.Condition
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
@@ -157,6 +158,9 @@ var resetFieldsSpecData = map[schema.GroupVersionResource]string{
gvr("resource.k8s.io", "v1beta1", "deviceclasses"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): `{"spec": {"devices": {"requests": [{"name": "req-0", "deviceClassName": "other-class"}]}}}`, // spec is immutable, but that doesn't matter for the test.
gvr("resource.k8s.io", "v1beta1", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
gvr("resource.k8s.io", "v1beta2", "deviceclasses"): `{"metadata": {"labels":{"a":"c"}}}`,
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): `{"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "other-class"}}]}}}`, // spec is immutable, but that doesn't matter for the test.
gvr("resource.k8s.io", "v1beta2", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{}`,
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,

View File

@@ -60,6 +60,7 @@ var statusData = map[schema.GroupVersionResource]string{
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
gvr("resource.k8s.io", "v1alpha3", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1beta1", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): `{"status": {"allocation": {"nodeSelector": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "some-label", "operator": "In", "values": ["some-value"]}] }]}}}}`,
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
// standard for []metav1.Condition
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,

View File

@@ -24,7 +24,7 @@ import (
"testing"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
resourceapi "k8s.io/api/resource/v1beta1"
resourceapi "k8s.io/api/resource/v1beta2"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -168,23 +168,25 @@ func RunAuthzSelectorsLibraryTests(t *testing.T, featureEnabled bool) {
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: []resourceapi.DeviceRequest{{
Name: "req-0",
DeviceClassName: "example-class",
Selectors: []resourceapi.DeviceSelector{{
CEL: &resourceapi.CELDeviceSelector{
Expression: boolFieldSelectorExpression,
},
}},
Name: "req-0",
Exactly: &resourceapi.ExactDeviceRequest{
DeviceClassName: "example-class",
Selectors: []resourceapi.DeviceSelector{{
CEL: &resourceapi.CELDeviceSelector{
Expression: boolFieldSelectorExpression,
},
}},
},
}},
},
},
}
_, err := c.ResourceV1beta1().ResourceClaims("default").Create(context.TODO(), obj, metav1.CreateOptions{})
_, err := c.ResourceV1beta2().ResourceClaims("default").Create(context.TODO(), obj, metav1.CreateOptions{})
return err
},
// authorizer is not available to resource APIs
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`spec\.devices\.requests\[0\]\.selectors\[0\].cel\.expression:.*undeclared reference to 'authorizer'`)},
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`spec\.devices\.requests\[0\]\.selectors\[0\].cel\.expression:.*undeclared reference to 'authorizer'`)},
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`spec\.devices\.requests\[0\]\.exactly\.selectors\[0\].cel\.expression:.*undeclared reference to 'authorizer'`)},
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`spec\.devices\.requests\[0\]\.exactly\.selectors\[0\].cel\.expression:.*undeclared reference to 'authorizer'`)},
},
{
name: "CustomResourceDefinition - rule",

View File

@@ -26,6 +26,7 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/onsi/gomega"
"github.com/onsi/gomega/gstruct"
"github.com/stretchr/testify/assert"
@@ -34,7 +35,9 @@ import (
v1 "k8s.io/api/core/v1"
resourcealphaapi "k8s.io/api/resource/v1alpha3"
resourceapi "k8s.io/api/resource/v1beta1"
resourcev1beta2api "k8s.io/api/resource/v1beta2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@@ -124,7 +127,8 @@ func TestDRA(t *testing.T) {
},
"core": {
apis: map[schema.GroupVersion]bool{
resourceapi.SchemeGroupVersion: true,
resourceapi.SchemeGroupVersion: true,
resourcev1beta2api.SchemeGroupVersion: true,
},
features: map[featuregate.Feature]bool{features.DynamicResourceAllocation: true},
f: func(tCtx ktesting.TContext) {
@@ -136,22 +140,26 @@ func TestDRA(t *testing.T) {
},
"all": {
apis: map[schema.GroupVersion]bool{
resourceapi.SchemeGroupVersion: true,
resourcealphaapi.SchemeGroupVersion: true,
resourceapi.SchemeGroupVersion: true,
resourcev1beta2api.SchemeGroupVersion: true,
resourcealphaapi.SchemeGroupVersion: true,
},
features: map[featuregate.Feature]bool{
features.DynamicResourceAllocation: true,
// Additional DRA feature gates go here,
// in alphabetical order,
// as needed by tests for them.
features.DRAAdminAccess: true,
features.DRAPrioritizedList: true,
features.DRAAdminAccess: true,
features.DRADeviceTaints: true,
features.DRAPartitionableDevices: true,
features.DRAPrioritizedList: true,
},
f: func(tCtx ktesting.TContext) {
tCtx.Run("AdminAccess", func(tCtx ktesting.TContext) { testAdminAccess(tCtx, true) })
tCtx.Run("Convert", testConvert)
tCtx.Run("PrioritizedList", func(tCtx ktesting.TContext) { testPrioritizedList(tCtx, true) })
tCtx.Run("PublishResourceSlices", func(tCtx ktesting.TContext) { testPublishResourceSlices(tCtx) })
tCtx.Run("MaxResourceSlice", testMaxResourceSlice)
},
},
} {
@@ -355,6 +363,8 @@ func testPublishResourceSlices(tCtx ktesting.TContext) {
Name: "device-tainted-default",
Basic: &resourceapi.BasicDevice{
Taints: []resourceapi.DeviceTaint{{
Key: "dra.example.com/taint",
Value: "taint-value",
Effect: resourceapi.DeviceTaintEffectNoExecute,
// TimeAdded is added by apiserver.
}},
@@ -364,6 +374,8 @@ func testPublishResourceSlices(tCtx ktesting.TContext) {
Name: "device-tainted-time-added",
Basic: &resourceapi.BasicDevice{
Taints: []resourceapi.DeviceTaint{{
Key: "dra.example.com/taint",
Value: "taint-value",
Effect: resourceapi.DeviceTaintEffectNoExecute,
TimeAdded: ptr.To(metav1.Now()),
}},
@@ -397,3 +409,26 @@ func testPublishResourceSlices(tCtx ktesting.TContext) {
// No further changes necessary.
ktesting.Consistently(tCtx, getStats).WithTimeout(10 * time.Second).Should(gomega.Equal(expectedStats))
}
// testMaxResourceSlice creates a ResourceSlice that is as large as possible
// and prints some information about it.
func testMaxResourceSlice(tCtx ktesting.TContext) {
slice := NewMaxResourceSlice()
createdSlice, err := tCtx.Client().ResourceV1beta2().ResourceSlices().Create(tCtx, slice, metav1.CreateOptions{})
tCtx.ExpectNoError(err)
totalSize := createdSlice.Size()
var managedFieldsSize int
for _, f := range createdSlice.ManagedFields {
managedFieldsSize += f.Size()
}
specSize := createdSlice.Spec.Size()
tCtx.Logf("\n\nTotal size: %s\nManagedFields size: %s (%.0f%%)\nSpec size: %s (%.0f)%%\n\nManagedFields:\n%s",
resource.NewQuantity(int64(totalSize), resource.BinarySI),
resource.NewQuantity(int64(managedFieldsSize), resource.BinarySI), float64(managedFieldsSize)*100/float64(totalSize),
resource.NewQuantity(int64(specSize), resource.BinarySI), float64(specSize)*100/float64(totalSize),
klog.Format(createdSlice.ManagedFields),
)
if diff := cmp.Diff(slice.Spec, createdSlice.Spec); diff != "" {
tCtx.Errorf("ResourceSliceSpec got modified during Create (- want, + got):\n%s", diff)
}
}

View File

@@ -0,0 +1,160 @@
/*
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 dra
import (
"fmt"
"math"
"strings"
"time"
resourceapi "k8s.io/api/resource/v1beta2"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/utils/ptr"
_ "k8s.io/kubernetes/pkg/apis/resource/install"
)
// NewMaxResourceSlice creates a slice that is as large as possible given the current validation constraints.
func NewMaxResourceSlice() *resourceapi.ResourceSlice {
slice := &resourceapi.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{
Name: maxSubDomain(0),
// Number of labels is not restricted.
Labels: maxKeyValueMap(10),
// Total size of annotations is limited to TotalAnnotationSizeLimitB = 256 KB.
// Let's be a bit more realistic.
Annotations: maxKeyValueMap(10),
},
Spec: resourceapi.ResourceSliceSpec{
Driver: strings.Repeat("x", resourceapi.DriverNameMaxLength),
Pool: resourceapi.ResourcePool{
Name: strings.Repeat("x", resourceapi.PoolNameMaxLength),
Generation: math.MaxInt64,
ResourceSliceCount: math.MaxInt64,
},
// use PerDeviceNodeSelection as it requires setting the node selection on
// every device and therefore will be the most expensive option in terms of
// object size.
PerDeviceNodeSelection: ptr.To(true),
// The validation caps the total number of counters across all CounterSets. So
// the most expensive option is to have a single counter per CounterSet.
SharedCounters: func() []resourceapi.CounterSet {
var counterSets []resourceapi.CounterSet
for i := 0; i < resourceapi.ResourceSliceMaxSharedCounters; i++ {
counterSets = append(counterSets, resourceapi.CounterSet{
Name: maxDNSLabel(i),
Counters: map[string]resourceapi.Counter{
maxDNSLabel(0): {
Value: resource.MustParse("80Gi"),
},
},
})
}
return counterSets
}(),
Devices: func() []resourceapi.Device {
var devices []resourceapi.Device
for i := 0; i < resourceapi.ResourceSliceMaxDevices; i++ {
devices = append(devices, resourceapi.Device{
Name: maxDNSLabel(i),
// Use attributes rather than capacity since it is more expensive.
Attributes: func() map[resourceapi.QualifiedName]resourceapi.DeviceAttribute {
attributes := make(map[resourceapi.QualifiedName]resourceapi.DeviceAttribute)
for i := 0; i < resourceapi.ResourceSliceMaxAttributesAndCapacitiesPerDevice; i++ {
attributes[maxResourceQualifiedName(i)] = resourceapi.DeviceAttribute{
StringValue: ptr.To(maxDNSLabel(i)),
}
}
return attributes
}(),
ConsumesCounters: func() []resourceapi.DeviceCounterConsumption {
var consumesCounters []resourceapi.DeviceCounterConsumption
for i := 0; i < resourceapi.ResourceSliceMaxDeviceCountersPerSlice/resourceapi.ResourceSliceMaxDevices; i++ {
consumesCounters = append(consumesCounters, resourceapi.DeviceCounterConsumption{
CounterSet: maxDNSLabel(i),
Counters: map[string]resourceapi.Counter{
maxDNSLabel(0): {
Value: resource.MustParse("80Gi"),
},
},
})
}
return consumesCounters
}(),
NodeName: ptr.To(maxSubDomain(0)),
Taints: func() []resourceapi.DeviceTaint {
var taints []resourceapi.DeviceTaint
for i := 0; i < resourceapi.DeviceTaintsMaxLength; i++ {
taints = append(taints, resourceapi.DeviceTaint{
Key: maxLabelName(i),
Value: maxLabelValue(i),
Effect: resourceapi.DeviceTaintEffectNoSchedule,
TimeAdded: &metav1.Time{Time: time.Now().Truncate(time.Second)},
})
}
return taints
}(),
})
}
return devices
}(),
},
}
return slice
}
// maxKeyValueMap produces a map for labels or annotations.
func maxKeyValueMap(n int) map[string]string {
m := make(map[string]string)
for i := 0; i < n; i++ {
m[maxQualifiedName(i)] = maxLabelValue(0)
}
return m
}
func maxLabelName(i int) string {
// A "label" is a qualified name.
return maxQualifiedName(i)
}
func maxResourceQualifiedName(i int) resourceapi.QualifiedName {
return resourceapi.QualifiedName(maxString(i, resourceapi.DeviceMaxDomainLength) + "/" + maxString(i, resourceapi.DeviceMaxIDLength))
}
func maxQualifiedName(i int) string {
return maxString(0, validation.DNS1123SubdomainMaxLength-4) + ".com/" + maxString(i, 63 /* qualifiedNameMaxLength */)
}
func maxLabelValue(i int) string {
return maxString(0, validation.LabelValueMaxLength)
}
func maxSubDomain(i int) string {
return maxString(i, validation.DNS1123SubdomainMaxLength)
}
func maxDNSLabel(i int) string {
return maxString(i, validation.DNS1123LabelMaxLength)
}
func maxString(i, l int) string {
return strings.Repeat("x", l-4) + fmt.Sprintf("%04d", i)
}

View File

@@ -625,6 +625,37 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, removeAl
},
// --
// k8s.io/kubernetes/pkg/apis/resource/v1beta2
gvr("resource.k8s.io", "v1beta2", "deviceclasses"): {
Stub: `{"metadata": {"name": "class3name"}}`,
ExpectedEtcdPath: "/registry/deviceclasses/class3name",
ExpectedGVK: gvkP("resource.k8s.io", "v1beta1", "DeviceClass"),
IntroducedVersion: "1.33",
RemovedVersion: "1.39",
},
gvr("resource.k8s.io", "v1beta2", "resourceclaims"): {
Stub: `{"metadata": {"name": "claim3name"}, "spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}}]}}}`,
ExpectedEtcdPath: "/registry/resourceclaims/" + namespace + "/claim3name",
ExpectedGVK: gvkP("resource.k8s.io", "v1beta1", "ResourceClaim"),
IntroducedVersion: "1.33",
RemovedVersion: "1.39",
},
gvr("resource.k8s.io", "v1beta2", "resourceclaimtemplates"): {
Stub: `{"metadata": {"name": "claimtemplate3name"}, "spec": {"spec": {"devices": {"requests": [{"name": "req-0", "exactly": {"deviceClassName": "example-class", "allocationMode": "ExactCount", "count": 1}}]}}}}`,
ExpectedEtcdPath: "/registry/resourceclaimtemplates/" + namespace + "/claimtemplate3name",
ExpectedGVK: gvkP("resource.k8s.io", "v1beta1", "ResourceClaimTemplate"),
IntroducedVersion: "1.33",
RemovedVersion: "1.39",
},
gvr("resource.k8s.io", "v1beta2", "resourceslices"): {
Stub: `{"metadata": {"name": "node3slice"}, "spec": {"nodeName": "worker1", "driver": "dra.example.com", "pool": {"name": "worker1", "resourceSliceCount": 1}}}`,
ExpectedEtcdPath: "/registry/resourceslices/node3slice",
ExpectedGVK: gvkP("resource.k8s.io", "v1beta1", "ResourceSlice"),
IntroducedVersion: "1.33",
RemovedVersion: "1.39",
},
// --
// k8s.io/apiserver/pkg/apis/apiserverinternal/v1alpha1
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): {
Stub: `{"metadata":{"name":"sv1.test"},"spec":{}}`,