From 39507d911f33d148dd8130468b5f35cfe9fb05bd Mon Sep 17 00:00:00 2001 From: Morten Torkildsen Date: Thu, 20 Mar 2025 07:04:41 +0000 Subject: [PATCH 1/3] Add resource v1beta2 API --- api/api-rules/violation_exceptions.list | 5 + cmd/kube-apiserver/app/aggregator.go | 1 + hack/lib/init.sh | 1 + pkg/api/testing/defaulting_test.go | 6 + pkg/apis/resource/fuzzer/fuzzer.go | 14 +- pkg/apis/resource/install/install.go | 6 +- pkg/apis/resource/types.go | 175 +- pkg/apis/resource/v1alpha3/conversion.go | 253 +++ pkg/apis/resource/v1beta1/conversion.go | 257 ++- pkg/apis/resource/v1beta1/conversion_test.go | 333 ++++ pkg/apis/resource/v1beta2/conversion.go | 40 + pkg/apis/resource/v1beta2/defaults.go | 55 + pkg/apis/resource/v1beta2/defaults_test.go | 179 ++ pkg/apis/resource/v1beta2/doc.go | 23 + pkg/apis/resource/v1beta2/register.go | 46 + pkg/apis/resource/validation/validation.go | 158 +- .../validation_resourceclaim_test.go | 89 +- .../validation_resourceclaimtemplate_test.go | 19 +- .../validation_resourceslice_test.go | 240 ++- pkg/controlplane/instance.go | 2 + pkg/printers/internalversion/printers.go | 8 +- pkg/printers/internalversion/printers_test.go | 64 +- .../v1/evaluator/core/resource_claims_test.go | 23 +- .../resource/resourceclaim/strategy.go | 12 +- .../resource/resourceclaim/strategy_test.go | 77 +- .../resourceclaimtemplate/strategy.go | 6 +- .../resourceclaimtemplate/strategy_test.go | 32 +- .../resourceslice/storage/storage_test.go | 3 +- .../resource/resourceslice/strategy.go | 52 +- .../resource/resourceslice/strategy_test.go | 80 +- .../resource/rest/storage_resource.go | 48 +- pkg/registry/resource/utils.go | 7 +- .../admission/noderestriction/admission.go | 4 +- .../noderestriction/admission_test.go | 6 +- .../src/k8s.io/api/resource/v1alpha3/types.go | 41 +- .../src/k8s.io/api/resource/v1beta1/types.go | 40 +- .../api/resource/v1beta2/devicetaint.go | 35 + .../src/k8s.io/api/resource/v1beta2/doc.go | 24 + .../k8s.io/api/resource/v1beta2/register.go | 60 + .../src/k8s.io/api/resource/v1beta2/types.go | 1552 +++++++++++++++++ staging/src/k8s.io/api/roundtrip_test.go | 2 + .../dynamic-resource-allocation/api/types.go | 18 +- .../structured/allocator.go | 16 +- .../structured/allocator_test.go | 6 +- .../apiserver/apply/reset_fields_test.go | 4 + .../apiserver/apply/status_test.go | 1 + .../cel/authorizerselector/helper.go | 24 +- test/integration/dra/dra_test.go | 45 +- test/integration/dra/objects.go | 160 ++ test/integration/etcd/data.go | 31 + 50 files changed, 3860 insertions(+), 523 deletions(-) create mode 100644 pkg/apis/resource/v1beta1/conversion_test.go create mode 100644 pkg/apis/resource/v1beta2/conversion.go create mode 100644 pkg/apis/resource/v1beta2/defaults.go create mode 100644 pkg/apis/resource/v1beta2/defaults_test.go create mode 100644 pkg/apis/resource/v1beta2/doc.go create mode 100644 pkg/apis/resource/v1beta2/register.go create mode 100644 staging/src/k8s.io/api/resource/v1beta2/devicetaint.go create mode 100644 staging/src/k8s.io/api/resource/v1beta2/doc.go create mode 100644 staging/src/k8s.io/api/resource/v1beta2/register.go create mode 100644 staging/src/k8s.io/api/resource/v1beta2/types.go create mode 100644 test/integration/dra/objects.go diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index 470618222c3..f9614e6f156 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -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 diff --git a/cmd/kube-apiserver/app/aggregator.go b/cmd/kube-apiserver/app/aggregator.go index 74923724ee2..243093d771e 100644 --- a/cmd/kube-apiserver/app/aggregator.go +++ b/cmd/kube-apiserver/app/aggregator.go @@ -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. diff --git a/hack/lib/init.sh b/hack/lib/init.sh index a818dc1c30d..a8e515bbeba 100755 --- a/hack/lib/init.sh +++ b/hack/lib/init.sh @@ -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 \ diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 3ab7111d015..2e04d1dcda1 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -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"}: {}, diff --git a/pkg/apis/resource/fuzzer/fuzzer.go b/pkg/apis/resource/fuzzer/fuzzer.go index 34f12e9847d..e07c71373e1 100644 --- a/pkg/apis/resource/fuzzer/fuzzer.go +++ b/pkg/apis/resource/fuzzer/fuzzer.go @@ -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 + } + }, } } diff --git a/pkg/apis/resource/install/install.go b/pkg/apis/resource/install/install.go index e473b8f5486..5e5f7104a36 100644 --- a/pkg/apis/resource/install/install.go +++ b/pkg/apis/resource/install/install.go @@ -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)) } diff --git a/pkg/apis/resource/types.go b/pkg/apis/resource/types.go index dcc9503f49c..ca5b4999f29 100644 --- a/pkg/apis/resource/types.go +++ b/pkg/apis/resource/types.go @@ -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. // diff --git a/pkg/apis/resource/v1alpha3/conversion.go b/pkg/apis/resource/v1alpha3/conversion.go index 8c37ef0c514..5ffde01cc1b 100644 --- a/pkg/apis/resource/v1alpha3/conversion.go +++ b/pkg/apis/resource/v1alpha3/conversion.go @@ -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 +} diff --git a/pkg/apis/resource/v1beta1/conversion.go b/pkg/apis/resource/v1beta1/conversion.go index 045d6a9cfd7..c1ac24ed0df 100644 --- a/pkg/apis/resource/v1beta1/conversion.go +++ b/pkg/apis/resource/v1beta1/conversion.go @@ -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 +} diff --git a/pkg/apis/resource/v1beta1/conversion_test.go b/pkg/apis/resource/v1beta1/conversion_test.go new file mode 100644 index 00000000000..276c928cedc --- /dev/null +++ b/pkg/apis/resource/v1beta1/conversion_test.go @@ -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)) + } + }) + } + +} diff --git a/pkg/apis/resource/v1beta2/conversion.go b/pkg/apis/resource/v1beta2/conversion.go new file mode 100644 index 00000000000..64d285b9cf7 --- /dev/null +++ b/pkg/apis/resource/v1beta2/conversion.go @@ -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 +} diff --git a/pkg/apis/resource/v1beta2/defaults.go b/pkg/apis/resource/v1beta2/defaults.go new file mode 100644 index 00000000000..4fb093567ae --- /dev/null +++ b/pkg/apis/resource/v1beta2/defaults.go @@ -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)} + } +} diff --git a/pkg/apis/resource/v1beta2/defaults_test.go b/pkg/apis/resource/v1beta2/defaults_test.go new file mode 100644 index 00000000000..3e32a9e2792 --- /dev/null +++ b/pkg/apis/resource/v1beta2/defaults_test.go @@ -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 +} diff --git a/pkg/apis/resource/v1beta2/doc.go b/pkg/apis/resource/v1beta2/doc.go new file mode 100644 index 00000000000..beea4f266b9 --- /dev/null +++ b/pkg/apis/resource/v1beta2/doc.go @@ -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 diff --git a/pkg/apis/resource/v1beta2/register.go b/pkg/apis/resource/v1beta2/register.go new file mode 100644 index 00000000000..b7c2803b2c6 --- /dev/null +++ b/pkg/apis/resource/v1beta2/register.go @@ -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() +} diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index 4d10005a523..2f10ecd5f98 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -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 diff --git a/pkg/apis/resource/validation/validation_resourceclaim_test.go b/pkg/apis/resource/validation/validation_resourceclaim_test.go index 69745829b08..53a6b0db589 100644 --- a/pkg/apis/resource/validation/validation_resourceclaim_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaim_test.go @@ -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: :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: :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 }, }, diff --git a/pkg/apis/resource/validation/validation_resourceclaimtemplate_test.go b/pkg/apis/resource/validation/validation_resourceclaimtemplate_test.go index 2e24d766bbc..07db2676bee 100644 --- a/pkg/apis/resource/validation/validation_resourceclaimtemplate_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaimtemplate_test.go @@ -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 }, }, diff --git a/pkg/apis/resource/validation/validation_resourceslice_test.go b/pkg/apis/resource/validation/validation_resourceslice_test.go index 6a487874f88..99f55d1814d 100644 --- a/pkg/apis/resource/validation/validation_resourceslice_test.go +++ b/pkg/apis/resource/validation/validation_resourceslice_test.go @@ -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 diff --git a/pkg/controlplane/instance.go b/pkg/controlplane/instance.go index dbfb35d7334..83a8cf61db4 100644 --- a/pkg/controlplane/instance.go +++ b/pkg/controlplane/instance.go @@ -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. diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 71ffb3acfa5..7e77db0757d 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -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 } diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index e5e71ae3fca..99c92ecd3b6 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -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", diff --git a/pkg/quota/v1/evaluator/core/resource_claims_test.go b/pkg/quota/v1/evaluator/core/resource_claims_test.go index 2f137e49edc..a1fc47eeaa7 100644 --- a/pkg/quota/v1/evaluator/core/resource_claims_test.go +++ b/pkg/quota/v1/evaluator/core/resource_claims_test.go @@ -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{ diff --git a/pkg/registry/resource/resourceclaim/strategy.go b/pkg/registry/resource/resourceclaim/strategy.go index 6aff3bec488..8576ae4be21 100644 --- a/pkg/registry/resource/resourceclaim/strategy.go +++ b/pkg/registry/resource/resourceclaim/strategy.go @@ -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 } } diff --git a/pkg/registry/resource/resourceclaim/strategy_test.go b/pkg/registry/resource/resourceclaim/strategy_test.go index cefd0716190..ae2c972f129 100644 --- a/pkg/registry/resource/resourceclaim/strategy_test.go +++ b/pkg/registry/resource/resourceclaim/strategy_test.go @@ -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) { diff --git a/pkg/registry/resource/resourceclaimtemplate/strategy.go b/pkg/registry/resource/resourceclaimtemplate/strategy.go index 547d3489c23..57692b463c1 100644 --- a/pkg/registry/resource/resourceclaimtemplate/strategy.go +++ b/pkg/registry/resource/resourceclaimtemplate/strategy.go @@ -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 } } diff --git a/pkg/registry/resource/resourceclaimtemplate/strategy_test.go b/pkg/registry/resource/resourceclaimtemplate/strategy_test.go index 81e260a144d..8048ab30529 100644 --- a/pkg/registry/resource/resourceclaimtemplate/strategy_test.go +++ b/pkg/registry/resource/resourceclaimtemplate/strategy_test.go @@ -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) diff --git a/pkg/registry/resource/resourceslice/storage/storage_test.go b/pkg/registry/resource/resourceslice/storage/storage_test.go index 793a836303b..84a5aa53ab3 100644 --- a/pkg/registry/resource/resourceslice/storage/storage_test.go +++ b/pkg/registry/resource/resourceslice/storage/storage_test.go @@ -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", diff --git a/pkg/registry/resource/resourceslice/strategy.go b/pkg/registry/resource/resourceslice/strategy.go index c6f0a8c2f36..d89d1184cf6 100644 --- a/pkg/registry/resource/resourceslice/strategy.go +++ b/pkg/registry/resource/resourceslice/strategy.go @@ -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 } diff --git a/pkg/registry/resource/resourceslice/strategy_test.go b/pkg/registry/resource/resourceslice/strategy_test.go index e84bb54dbc1..39522081b6c 100644 --- a/pkg/registry/resource/resourceslice/strategy_test.go +++ b/pkg/registry/resource/resourceslice/strategy_test.go @@ -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 }(), }, diff --git a/pkg/registry/resource/rest/storage_resource.go b/pkg/registry/resource/rest/storage_resource.go index 1dff5ad9f6d..8a855e24937 100644 --- a/pkg/registry/resource/rest/storage_resource.go +++ b/pkg/registry/resource/rest/storage_resource.go @@ -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 } diff --git a/pkg/registry/resource/utils.go b/pkg/registry/resource/utils.go index 688f45e3930..fe848efb557 100644 --- a/pkg/registry/resource/utils.go +++ b/pkg/registry/resource/utils.go @@ -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") diff --git a/plugin/pkg/admission/noderestriction/admission.go b/plugin/pkg/admission/noderestriction/admission.go index 45f790913e6..9bfcbb2ea2c 100644 --- a/plugin/pkg/admission/noderestriction/admission.go +++ b/plugin/pkg/admission/noderestriction/admission.go @@ -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")) } } diff --git a/plugin/pkg/admission/noderestriction/admission_test.go b/plugin/pkg/admission/noderestriction/admission_test.go index d3b3da0b2c5..16c84fba777 100644 --- a/plugin/pkg/admission/noderestriction/admission_test.go +++ b/plugin/pkg/admission/noderestriction/admission_test.go @@ -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, }, } diff --git a/staging/src/k8s.io/api/resource/v1alpha3/types.go b/staging/src/k8s.io/api/resource/v1alpha3/types.go index 2fc99f43f8f..29a4b0343f1 100644 --- a/staging/src/k8s.io/api/resource/v1alpha3/types.go +++ b/staging/src/k8s.io/api/resource/v1alpha3/types.go @@ -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. // diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index 41903dc510b..3c64b426d94 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -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. // diff --git a/staging/src/k8s.io/api/resource/v1beta2/devicetaint.go b/staging/src/k8s.io/api/resource/v1beta2/devicetaint.go new file mode 100644 index 00000000000..c9367600225 --- /dev/null +++ b/staging/src/k8s.io/api/resource/v1beta2/devicetaint.go @@ -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 '=:', '=:', ':', or ''. +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) +} diff --git a/staging/src/k8s.io/api/resource/v1beta2/doc.go b/staging/src/k8s.io/api/resource/v1beta2/doc.go new file mode 100644 index 00000000000..365113ae4e1 --- /dev/null +++ b/staging/src/k8s.io/api/resource/v1beta2/doc.go @@ -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 diff --git a/staging/src/k8s.io/api/resource/v1beta2/register.go b/staging/src/k8s.io/api/resource/v1beta2/register.go new file mode 100644 index 00000000000..5e676a05497 --- /dev/null +++ b/staging/src/k8s.io/api/resource/v1beta2/register.go @@ -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 +} diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go new file mode 100644 index 00000000000..0d8d42a0854 --- /dev/null +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -0,0 +1,1552 @@ +/* +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 ( + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" +) + +const ( + // Finalizer is the finalizer that gets set for claims + // which were allocated through a builtin controller. + // Reserved for use by Kubernetes, DRA driver controllers must + // use their own finalizer. + Finalizer = "resource.kubernetes.io/delete-protection" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.33 + +// ResourceSlice represents one or more resources in a pool of similar resources, +// managed by a common driver. A pool may span more than one ResourceSlice, and exactly how many +// ResourceSlices comprise a pool is determined by the driver. +// +// At the moment, the only supported resources are devices with attributes and capacities. +// Each device in a given pool, regardless of how many ResourceSlices, must have a unique name. +// The ResourceSlice in which a device gets published may change over time. The unique identifier +// for a device is the tuple , , . +// +// Whenever a driver needs to update a pool, it increments the pool.Spec.Pool.Generation number +// and updates all ResourceSlices with that new number and new resource definitions. A consumer +// must only use ResourceSlices with the highest generation number and ignore all others. +// +// When allocating all resources in a pool matching certain criteria or when +// looking for the best solution among several different alternatives, a +// consumer should check the number of ResourceSlices in a pool (included in +// each ResourceSlice) to determine whether its view of a pool is complete and +// if not, should wait until the driver has completed updating the pool. +// +// For resources that are not local to a node, the node name is not set. Instead, +// the driver may use a node selector to specify where the devices are available. +// +// This is an alpha type and requires enabling the DynamicResourceAllocation +// feature gate. +type ResourceSlice struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Contains the information published by the driver. + // + // Changing the spec automatically increments the metadata.generation number. + Spec ResourceSliceSpec `json:"spec" protobuf:"bytes,2,name=spec"` +} + +const ( + // ResourceSliceSelectorNodeName can be used in a [metav1.ListOptions] + // field selector to filter based on [ResourceSliceSpec.NodeName]. + ResourceSliceSelectorNodeName = "spec.nodeName" + // ResourceSliceSelectorDriver can be used in a [metav1.ListOptions] + // field selector to filter based on [ResourceSliceSpec.Driver]. + ResourceSliceSelectorDriver = "spec.driver" +) + +// ResourceSliceSpec contains the information published by the driver in one ResourceSlice. +type ResourceSliceSpec struct { + // Driver identifies the DRA driver providing the capacity information. + // A field selector can be used to list only ResourceSlice + // objects with a certain driver name. + // + // Must be a DNS subdomain and should end with a DNS domain owned by the + // vendor of the driver. This field is immutable. + // + // +required + Driver string `json:"driver" protobuf:"bytes,1,name=driver"` + + // Pool describes the pool that this ResourceSlice belongs to. + // + // +required + Pool ResourcePool `json:"pool" protobuf:"bytes,2,name=pool"` + + // NodeName identifies the node which provides the resources in this pool. + // A field selector can be used to list only ResourceSlice + // objects belonging to a certain node. + // + // This field can be used to limit access from nodes to ResourceSlices with + // the same node name. It also indicates to autoscalers that adding + // new nodes of the same type as some old node might also make new + // resources available. + // + // Exactly one of NodeName, NodeSelector, AllNodes, and PerDeviceNodeSelection must be set. + // This field is immutable. + // + // +optional + // +oneOf=NodeSelection + NodeName *string `json:"nodeName,omitempty" protobuf:"bytes,3,opt,name=nodeName"` + + // NodeSelector defines which nodes have access to the resources in the pool, + // when that pool is not limited to a single node. + // + // Must use exactly one term. + // + // Exactly one of NodeName, NodeSelector, AllNodes, and PerDeviceNodeSelection must be set. + // + // +optional + // +oneOf=NodeSelection + NodeSelector *v1.NodeSelector `json:"nodeSelector,omitempty" protobuf:"bytes,4,opt,name=nodeSelector"` + + // AllNodes indicates that all nodes have access to the resources in the pool. + // + // Exactly one of NodeName, NodeSelector, AllNodes, and PerDeviceNodeSelection must be set. + // + // +optional + // +oneOf=NodeSelection + AllNodes *bool `json:"allNodes,omitempty" protobuf:"bytes,5,opt,name=allNodes"` + + // Devices lists some or all of the devices in this pool. + // + // Must not have more than 128 entries. + // + // +optional + // +listType=atomic + Devices []Device `json:"devices" protobuf:"bytes,6,name=devices"` + + // PerDeviceNodeSelection defines whether the access from nodes to + // resources in the pool is set on the ResourceSlice level or on each + // device. If it is set to true, every device defined the ResourceSlice + // must specify this individually. + // + // Exactly one of NodeName, NodeSelector, AllNodes, and PerDeviceNodeSelection must be set. + // + // +optional + // +oneOf=NodeSelection + // +featureGate=DRAPartitionableDevices + PerDeviceNodeSelection *bool `json:"perDeviceNodeSelection,omitempty" protobuf:"bytes,7,name=perDeviceNodeSelection"` + + // SharedCounters defines a list of counter sets, each of which + // has a name and a list of counters available. + // + // The names of the SharedCounters must be unique in the ResourceSlice. + // + // The maximum number of counters in all sets is 32. + // + // +optional + // +listType=atomic + // +featureGate=DRAPartitionableDevices + SharedCounters []CounterSet `json:"sharedCounters,omitempty" protobuf:"bytes,8,name=sharedCounters"` +} + +// CounterSet defines a named set of counters +// that are available to be used by devices defined in the +// ResourceSlice. +// +// The counters are not allocatable by themselves, but +// can be referenced by devices. When a device is allocated, +// 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. + // + // +required + Name string `json:"name" protobuf:"bytes,1,name=name"` + + // 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 in all sets is 32. + // + // +required + Counters map[string]Counter `json:"counters,omitempty" protobuf:"bytes,2,name=counters"` +} + +// DriverNameMaxLength is the maximum valid length of a driver name in the +// ResourceSliceSpec and other places. It's the same as for CSI driver names. +const DriverNameMaxLength = 63 + +// ResourcePool describes the pool that ResourceSlices belong to. +type ResourcePool struct { + // Name is used to identify the pool. For node-local devices, this + // is often the node name, but this is not required. + // + // It must not be longer than 253 characters and must consist of one or more DNS sub-domains + // separated by slashes. This field is immutable. + // + // +required + Name string `json:"name" protobuf:"bytes,1,name=name"` + + // Generation tracks the change in a pool over time. Whenever a driver + // changes something about one or more of the resources in a pool, it + // must change the generation in all ResourceSlices which are part of + // that pool. Consumers of ResourceSlices should only consider + // resources from the pool with the highest generation number. The + // generation may be reset by drivers, which should be fine for + // consumers, assuming that all ResourceSlices in a pool are updated to + // match or deleted. + // + // Combined with ResourceSliceCount, this mechanism enables consumers to + // detect pools which are comprised of multiple ResourceSlices and are + // in an incomplete state. + // + // +required + Generation int64 `json:"generation" protobuf:"bytes,2,name=generation"` + + // ResourceSliceCount is the total number of ResourceSlices in the pool at this + // generation number. Must be greater than zero. + // + // Consumers can use this to check whether they have seen all ResourceSlices + // belonging to the same pool. + // + // +required + ResourceSliceCount int64 `json:"resourceSliceCount" protobuf:"bytes,3,name=resourceSliceCount"` +} + +const ResourceSliceMaxSharedCapacity = 128 +const ResourceSliceMaxDevices = 128 +const PoolNameMaxLength = validation.DNS1123SubdomainMaxLength // Same as for a single node name. + +// 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 + +// 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 { + // Name is unique identifier among all devices managed by + // the driver in the pool. It must be a DNS label. + // + // +required + Name string `json:"name" protobuf:"bytes,1,name=name"` + + // Attributes defines the set of attributes for this device. + // The name of each attribute must be unique in that set. + // + // The maximum number of attributes and capacities combined is 32. + // + // +optional + Attributes map[QualifiedName]DeviceAttribute `json:"attributes,omitempty" protobuf:"bytes,2,rep,name=attributes"` + + // Capacity defines the set of capacities for this device. + // The name of each capacity must be unique in that set. + // + // The maximum number of attributes and capacities combined is 32. + // + // +optional + Capacity map[QualifiedName]DeviceCapacity `json:"capacity,omitempty" protobuf:"bytes,3,rep,name=capacity"` + + // 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 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 + ConsumesCounters []DeviceCounterConsumption `json:"consumesCounters,omitempty" protobuf:"bytes,4,rep,name=consumesCounters"` + + // NodeName identifies the node where the device is available. + // + // Must only be set if Spec.PerDeviceNodeSelection is set to true. + // At most one of NodeName, NodeSelector and AllNodes can be set. + // + // +optional + // +oneOf=DeviceNodeSelection + // +featureGate=DRAPartitionableDevices + NodeName *string `json:"nodeName,omitempty" protobuf:"bytes,5,opt,name=nodeName"` + + // 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. + // + // +optional + // +oneOf=DeviceNodeSelection + // +featureGate=DRAPartitionableDevices + NodeSelector *v1.NodeSelector `json:"nodeSelector,omitempty" protobuf:"bytes,6,opt,name=nodeSelector"` + + // AllNodes indicates that all nodes have access to the device. + // + // Must only be set if Spec.PerDeviceNodeSelection is set to true. + // At most one of NodeName, NodeSelector and AllNodes can be set. + // + // +optional + // +oneOf=DeviceNodeSelection + // +featureGate=DRAPartitionableDevices + AllNodes *bool `json:"allNodes,omitempty" protobuf:"bytes,7,opt,name=allNodes"` + + // If specified, these are the driver-defined taints. + // + // The maximum number of taints is 4. + // + // This is an alpha field and requires enabling the DRADeviceTaints + // feature gate. + // + // +optional + // +listType=atomic + // +featureGate=DRADeviceTaints + Taints []DeviceTaint `json:"taints,omitempty" protobuf:"bytes,8,rep,name=taints"` +} + +// DeviceCounterConsumption defines a set of counters that +// a device will consume from a CounterSet. +type DeviceCounterConsumption struct { + // CounterSet is the name of the set from which the + // counters defined will be consumed. + // + // +required + CounterSet string `json:"counterSet" protobuf:"bytes,1,opt,name=counterSet"` + + // Counters defines the counters that will be consumed by the device. + // + // 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"` +} + +// DeviceCapacity describes a quantity associated with a device. +type DeviceCapacity struct { + // Value defines how much of a certain device capacity is available. + // + // +required + Value resource.Quantity `json:"value" protobuf:"bytes,1,rep,name=value"` + + // potential future addition: fields which define how to "consume" + // capacity (= share a single device between different consumers). +} + +// Counter describes a quantity associated with a device. +type Counter struct { + // Value defines how much of a certain device counter is available. + // + // +required + Value resource.Quantity `json:"value" protobuf:"bytes,1,rep,name=value"` +} + +// 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 +// driver (usually the vendor) or by some 3rd party (e.g. the Kubernetes +// project). Because they are sometimes compared across devices, a given name +// is expected to mean the same thing and have the same type on all devices. +// +// Names must be either a C identifier (e.g. "theName") or a DNS subdomain +// followed by a slash ("/") followed by a C identifier +// (e.g. "dra.example.com/theName"). Names which do not include the +// domain prefix are assumed to be part of the driver's domain. Attributes +// or capacities defined by 3rd parties must include the domain prefix. +// +// The maximum length for the DNS subdomain is 63 characters (same as +// for driver names) and the maximum length of the C identifier +// is 32. +type QualifiedName string + +// FullyQualifiedName is a QualifiedName where the domain is set. +type FullyQualifiedName string + +// DeviceMaxDomainLength is the maximum length of the domain prefix in a fully-qualified name. +const DeviceMaxDomainLength = 63 + +// DeviceMaxIDLength is the maximum length of the identifier in a device attribute or capacity name (`/`). +const DeviceMaxIDLength = 32 + +// DeviceAttribute must have exactly one field set. +type DeviceAttribute struct { + // The Go field names below have a Value suffix to avoid a conflict between the + // field "String" and the corresponding method. That method is required. + // The Kubernetes API is defined without that suffix to keep it more natural. + + // IntValue is a number. + // + // +optional + // +oneOf=ValueType + IntValue *int64 `json:"int,omitempty" protobuf:"varint,2,opt,name=int"` + + // BoolValue is a true/false value. + // + // +optional + // +oneOf=ValueType + BoolValue *bool `json:"bool,omitempty" protobuf:"varint,3,opt,name=bool"` + + // StringValue is a string. Must not be longer than 64 characters. + // + // +optional + // +oneOf=ValueType + StringValue *string `json:"string,omitempty" protobuf:"bytes,4,opt,name=string"` + + // VersionValue is a semantic version according to semver.org spec 2.0.0. + // Must not be longer than 64 characters. + // + // +optional + // +oneOf=ValueType + VersionValue *string `json:"version,omitempty" protobuf:"bytes,5,opt,name=version"` +} + +// DeviceAttributeMaxValueLength is the maximum length of a string or version attribute value. +const DeviceAttributeMaxValueLength = 64 + +// DeviceTaintsMaxLength is the maximum number of taints per device. +const DeviceTaintsMaxLength = 4 + +// The device this taint is attached to has the "effect" on +// any claim which does not tolerate the taint and, through the claim, +// to pods using the claim. +// +// +protobuf.options.(gogoproto.goproto_stringer)=false +type DeviceTaint struct { + // The taint key to be applied to a device. + // Must be a label name. + // + // +required + Key string `json:"key" protobuf:"bytes,1,name=key"` + + // The taint value corresponding to the taint key. + // Must be a label value. + // + // +optional + Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` + + // The effect of the taint on claims that do not tolerate the taint + // and through such claims on the pods using them. + // Valid effects are NoSchedule and NoExecute. PreferNoSchedule as used for + // nodes is not valid here. + // + // +required + Effect DeviceTaintEffect `json:"effect" protobuf:"bytes,3,name=effect,casttype=DeviceTaintEffect"` + + // ^^^^ + // + // Implementing PreferNoSchedule would depend on a scoring solution for DRA. + // It might get added as part of that. + + // TimeAdded represents the time at which the taint was added. + // Added automatically during create or update if not set. + // + // +optional + TimeAdded *metav1.Time `json:"timeAdded,omitempty" protobuf:"bytes,4,opt,name=timeAdded"` + + // ^^^ + // + // This field was defined as "It is only written for NoExecute taints." for node taints. + // But in practice, Kubernetes never did anything with it (no validation, no defaulting, + // ignored during pod eviction in pkg/controller/tainteviction). +} + +// +enum +type DeviceTaintEffect string + +const ( + // Do not allow new pods to schedule which use a tainted device unless they tolerate the taint, + // but allow all pods submitted to Kubelet without going through the scheduler + // to start, and allow all already-running pods to continue running. + DeviceTaintEffectNoSchedule DeviceTaintEffect = "NoSchedule" + + // Evict any already-running pods that do not tolerate the device taint. + DeviceTaintEffectNoExecute DeviceTaintEffect = "NoExecute" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.33 + +// ResourceSliceList is a collection of ResourceSlices. +type ResourceSliceList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is the list of resource ResourceSlices. + Items []ResourceSlice `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:prerelease-lifecycle-gen:introduced=1.33 + +// ResourceClaim describes a request for access to resources in the cluster, +// for use by workloads. For example, if a workload needs an accelerator device +// with specific properties, this is how that request is expressed. The status +// stanza tracks whether this claim has been satisfied and what specific +// resources have been allocated. +// +// This is an alpha type and requires enabling the DynamicResourceAllocation +// feature gate. +type ResourceClaim struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec describes what is being requested and how to configure it. + // The spec is immutable. + Spec ResourceClaimSpec `json:"spec" protobuf:"bytes,2,name=spec"` + + // Status describes whether the claim is ready to use and what has been allocated. + // +optional + Status ResourceClaimStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// ResourceClaimSpec defines what is being requested in a ResourceClaim and how to configure it. +type ResourceClaimSpec struct { + // Devices defines how to request devices. + // + // +optional + Devices DeviceClaim `json:"devices" protobuf:"bytes,1,name=devices"` + + // Controller is tombstoned since Kubernetes 1.32 where + // it got removed. May be reused once decoding v1alpha3 is no longer + // supported. + // Controller string `json:"controller,omitempty" protobuf:"bytes,2,opt,name=controller"` +} + +// DeviceClaim defines how to request devices with a ResourceClaim. +type DeviceClaim struct { + // Requests represent individual requests for distinct devices which + // must all be satisfied. If empty, nothing needs to be allocated. + // + // +optional + // +listType=atomic + Requests []DeviceRequest `json:"requests" protobuf:"bytes,1,name=requests"` + + // These constraints must be satisfied by the set of devices that get + // allocated for the claim. + // + // +optional + // +listType=atomic + Constraints []DeviceConstraint `json:"constraints,omitempty" protobuf:"bytes,2,opt,name=constraints"` + + // This field holds configuration for multiple potential drivers which + // could satisfy requests in this claim. It is ignored while allocating + // the claim. + // + // +optional + // +listType=atomic + Config []DeviceClaimConfiguration `json:"config,omitempty" protobuf:"bytes,3,opt,name=config"` + + // Potential future extension, ignored by older schedulers. This is + // fine because scoring allows users to define a preference, without + // making it a hard requirement. + // + // Score *SomeScoringStruct +} + +const ( + DeviceRequestsMaxSize = AllocationResultsMaxSize + DeviceConstraintsMaxSize = 32 + DeviceConfigMaxSize = 32 +) + +// DRAAdminNamespaceLabelKey is a label key used to grant administrative access +// to certain resource.k8s.io API types within a namespace. When this label is +// set on a namespace with the value "true" (case-sensitive), it allows the use +// of adminAccess: true in any namespaced resource.k8s.io API types. Currently, +// this permission applies to ResourceClaim and ResourceClaimTemplate objects. +const ( + DRAAdminNamespaceLabelKey = "resource.k8s.io/admin-access" +) + +// 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. 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. + // + // 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 `json:"name" protobuf:"bytes,1,name=name"` + + // 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 `json:"exactly,omitempty" protobuf:"bytes,2,name=exactly"` + + // 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 `json:"firstAvailable,omitempty" protobuf:"bytes,3,name=firstAvailable"` +} + +// 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 DeviceClassName is required. + // + // Administrators may use this to restrict which devices may get + // requested by only installing classes with selectors for permitted + // devices. If users are free to request anything without restrictions, + // then administrators can create an empty DeviceClass for users + // to reference. + // + // +required + DeviceClassName string `json:"deviceClassName" protobuf:"bytes,1,name=deviceClassName"` + + // Selectors define criteria which must be satisfied by a specific + // device in order for that device to be considered for this + // request. All selectors must be satisfied for a device to be + // considered. + // + // +optional + // +listType=atomic + Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,2,name=selectors"` + + // AllocationMode and its related fields define how devices are allocated + // to satisfy this request. Supported values are: + // + // - ExactCount: This request is for a specific number of devices. + // This is the default. The exact number is provided in the + // count field. + // + // - All: This request is for all of the matching devices in a pool. + // At least one device must exist on the node for the allocation to succeed. + // Allocation will fail if some devices are already allocated, + // unless adminAccess is requested. + // + // 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. + // + // More modes may get added in the future. Clients must refuse to handle + // requests with unknown modes. + // + // +optional + AllocationMode DeviceAllocationMode `json:"allocationMode,omitempty" protobuf:"bytes,3,opt,name=allocationMode"` + + // 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. + // + // +optional + // +oneOf=AllocationMode + Count int64 `json:"count,omitempty" protobuf:"bytes,4,opt,name=count"` + + // AdminAccess indicates that this is a claim for administrative access + // to the device(s). Claims with AdminAccess are expected to be used for + // monitoring or other management services for a device. They ignore + // all ordinary claims to the device with respect to access modes and + // any resource allocations. + // + // 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. + // + // +optional + // +featureGate=DRAAdminAccess + AdminAccess *bool `json:"adminAccess,omitempty" protobuf:"bytes,5,opt,name=adminAccess"` + + // If specified, the request's tolerations. + // + // Tolerations for NoSchedule are required to allocate a + // device which has a taint with that effect. The same applies + // to NoExecute. + // + // In addition, should any of the allocated devices get tainted + // with NoExecute after allocation and that effect is not tolerated, + // then all pods consuming the ResourceClaim get deleted to evict + // them. The scheduler will not let new pods reserve the claim while + // it has these tainted devices. Once all pods are evicted, the + // claim will get deallocated. + // + // The maximum number of tolerations is 16. + // + // This is an alpha field and requires enabling the DRADeviceTaints + // feature gate. + // + // +optional + // +listType=atomic + // +featureGate=DRADeviceTaints + Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,6,opt,name=tolerations"` +} + +// DeviceSubRequest describes a request for device provided in the +// claim.spec.devices.requests[].firstAvailable array. Each +// is typically a request for a single resource like a device, but can +// also ask for several identical devices. +// +// 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 + // format
/. + // + // Must be a DNS label. + // + // +required + Name string `json:"name" protobuf:"bytes,1,name=name"` + + // DeviceClassName references a specific DeviceClass, which can define + // additional configuration and selectors to be inherited by this + // subrequest. + // + // A class is required. Which classes are available depends on the cluster. + // + // Administrators may use this to restrict which devices may get + // requested by only installing classes with selectors for permitted + // devices. If users are free to request anything without restrictions, + // then administrators can create an empty DeviceClass for users + // to reference. + // + // +required + DeviceClassName string `json:"deviceClassName" protobuf:"bytes,2,name=deviceClassName"` + + // Selectors define criteria which must be satisfied by a specific + // device in order for that device to be considered for this + // subrequest. All selectors must be satisfied for a device to be + // considered. + // + // +optional + // +listType=atomic + Selectors []DeviceSelector `json:"selectors,omitempty" protobuf:"bytes,3,name=selectors"` + + // AllocationMode and its related fields define how devices are allocated + // to satisfy this subrequest. Supported values are: + // + // - ExactCount: This request is for a specific number of devices. + // This is the default. The exact number is provided in the + // count field. + // + // - All: This subrequest is for all of the matching devices in a pool. + // Allocation will fail if some devices are already allocated, + // unless adminAccess is requested. + // + // 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. + // + // More modes may get added in the future. Clients must refuse to handle + // requests with unknown modes. + // + // +optional + AllocationMode DeviceAllocationMode `json:"allocationMode,omitempty" protobuf:"bytes,4,opt,name=allocationMode"` + + // 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. + // + // +optional + // +oneOf=AllocationMode + Count int64 `json:"count,omitempty" protobuf:"bytes,5,opt,name=count"` + + // If specified, the request's tolerations. + // + // Tolerations for NoSchedule are required to allocate a + // device which has a taint with that effect. The same applies + // to NoExecute. + // + // In addition, should any of the allocated devices get tainted + // with NoExecute after allocation and that effect is not tolerated, + // then all pods consuming the ResourceClaim get deleted to evict + // them. The scheduler will not let new pods reserve the claim while + // it has these tainted devices. Once all pods are evicted, the + // claim will get deallocated. + // + // The maximum number of tolerations is 16. + // + // This is an alpha field and requires enabling the DRADeviceTaints + // feature gate. + // + // +optional + // +listType=atomic + // +featureGate=DRADeviceTaints + Tolerations []DeviceToleration `json:"tolerations,omitempty" protobuf:"bytes,6,opt,name=tolerations"` +} + +const ( + DeviceSelectorsMaxSize = 32 + FirstAvailableDeviceRequestMaxSize = 8 + DeviceTolerationsMaxLength = 16 +) + +type DeviceAllocationMode string + +// Valid [DeviceRequest.CountMode] values. +const ( + DeviceAllocationModeExactCount = DeviceAllocationMode("ExactCount") + DeviceAllocationModeAll = DeviceAllocationMode("All") +) + +// DeviceSelector must have exactly one field set. +type DeviceSelector struct { + // CEL contains a CEL expression for selecting a device. + // + // +optional + // +oneOf=SelectorType + CEL *CELDeviceSelector `json:"cel,omitempty" protobuf:"bytes,1,opt,name=cel"` +} + +// CELDeviceSelector contains a CEL expression for selecting a device. +type CELDeviceSelector struct { + // Expression is a CEL expression which evaluates a single device. It + // must evaluate to true when the device under consideration satisfies + // the desired criteria, and false when it does not. Any other result + // is an error and causes allocation of devices to abort. + // + // The expression's input is an object named "device", which carries + // the following properties: + // - driver (string): the name of the driver which defines this device. + // - attributes (map[string]object): the device's attributes, grouped by prefix + // (e.g. device.attributes["dra.example.com"] evaluates to an object with all + // of the attributes which were prefixed by "dra.example.com". + // - capacity (map[string]object): the device's capacities, grouped by prefix. + // + // Example: Consider a device with driver="dra.example.com", which exposes + // two attributes named "model" and "ext.example.com/family" and which + // exposes one capacity named "modules". This input to this expression + // would have the following fields: + // + // device.driver + // device.attributes["dra.example.com"].model + // device.attributes["ext.example.com"].family + // device.capacity["dra.example.com"].modules + // + // The device.driver field can be used to check for a specific driver, + // either as a high-level precondition (i.e. you only want to consider + // devices from this driver) or as part of a multi-clause expression + // that is meant to consider devices from different drivers. + // + // The value type of each attribute is defined by the device + // definition, and users who write these expressions must consult the + // documentation for their specific drivers. The value type of each + // capacity is Quantity. + // + // If an unknown prefix is used as a lookup in either device.attributes + // or device.capacity, an empty map will be returned. Any reference to + // an unknown field will cause an evaluation error and allocation to + // abort. + // + // A robust expression should check for the existence of attributes + // before referencing them. + // + // For ease of use, the cel.bind() function is enabled, and can be used + // to simplify expressions that access multiple attributes with the + // same domain. For example: + // + // cel.bind(dra, device.attributes["dra.example.com"], dra.someBool && dra.anotherBool) + // + // The length of the expression must be smaller or equal to 10 Ki. The + // cost of evaluating it is also limited based on the estimated number + // of logical steps. + // + // +required + Expression string `json:"expression" protobuf:"bytes,1,name=expression"` +} + +// CELSelectorExpressionMaxCost specifies the cost limit for a single CEL selector +// evaluation. +// +// There is no overall budget for selecting a device, so the actual time +// required for that is proportional to the number of CEL selectors and how +// often they need to be evaluated, which can vary depending on several factors +// (number of devices, cluster utilization, additional constraints). +// +// Validation against this limit and [CELSelectorExpressionMaxLength] happens +// only when setting an expression for the first time or when changing it. If +// the limits are changed in a future Kubernetes release, existing users are +// guaranteed that existing expressions will continue to be valid. +// +// However, the kube-scheduler also applies this cost limit at runtime, so it +// could happen that a valid expression fails at runtime after an up- or +// downgrade. This can also happen without version skew when the cost estimate +// underestimated the actual cost. That this might happen is the reason why +// kube-scheduler enforces the runtime limit instead of relying on validation. +// +// According to +// https://github.com/kubernetes/kubernetes/blob/4aeaf1e99e82da8334c0d6dddd848a194cd44b4f/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go#L20-L22, +// this gives roughly 0.1 second for each expression evaluation. +// However, this depends on how fast the machine is. +const CELSelectorExpressionMaxCost = 1000000 + +// CELSelectorExpressionMaxLength is the maximum length of a CEL selector expression string. +const CELSelectorExpressionMaxLength = 10 * 1024 + +// DeviceConstraint must have exactly one field set besides Requests. +type DeviceConstraint struct { + // Requests is a list of the one or more requests in this claim which + // must co-satisfy this constraint. If a request is fulfilled by + // multiple devices, then all of the devices must satisfy the + // constraint. If this is not specified, this constraint applies to all + // requests in this claim. + // + // References to subrequests must include the name of the main request + // and may include the subrequest using the format
[/]. If just + // the main request is given, the constraint applies to all subrequests. + // + // +optional + // +listType=atomic + Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"` + + // MatchAttribute requires that all devices in question have this + // attribute and that its type and value are the same across those + // devices. + // + // For example, if you specified "dra.example.com/numa" (a hypothetical example!), + // then only devices in the same NUMA node will be chosen. A device which + // does not have that attribute will not be chosen. All devices should + // use a value of the same type for this attribute because that is part of + // its specification, but if one device doesn't, then it also will not be + // chosen. + // + // Must include the domain qualifier. + // + // +optional + // +oneOf=ConstraintType + MatchAttribute *FullyQualifiedName `json:"matchAttribute,omitempty" protobuf:"bytes,2,opt,name=matchAttribute"` + + // Potential future extension, not part of the current design: + // A CEL expression which compares different devices and returns + // true if they match. + // + // Because it would be part of a one-of, old schedulers will not + // accidentally ignore this additional, for them unknown match + // criteria. + // + // MatchExpression string +} + +// DeviceClaimConfiguration is used for configuration parameters in DeviceClaim. +type DeviceClaimConfiguration struct { + // Requests lists the names of requests where the configuration applies. + // If empty, it applies to all requests. + // + // References to subrequests must include the name of the main request + // and may include the subrequest using the format
[/]. If just + // the main request is given, the configuration applies to all subrequests. + // + // +optional + // +listType=atomic + Requests []string `json:"requests,omitempty" protobuf:"bytes,1,opt,name=requests"` + + DeviceConfiguration `json:",inline" protobuf:"bytes,2,name=deviceConfiguration"` +} + +// DeviceConfiguration must have exactly one field set. It gets embedded +// inline in some other structs which have other fields, so field names must +// not conflict with those. +type DeviceConfiguration struct { + // Opaque provides driver-specific configuration parameters. + // + // +optional + // +oneOf=ConfigurationType + Opaque *OpaqueDeviceConfiguration `json:"opaque,omitempty" protobuf:"bytes,1,opt,name=opaque"` +} + +// OpaqueDeviceConfiguration contains configuration parameters for a driver +// in a format defined by the driver vendor. +type OpaqueDeviceConfiguration struct { + // Driver is used to determine which kubelet plugin needs + // to be passed these configuration parameters. + // + // An admission policy provided by the driver developer could use this + // to decide whether it needs to validate them. + // + // Must be a DNS subdomain and should end with a DNS domain owned by the + // vendor of the driver. + // + // +required + Driver string `json:"driver" protobuf:"bytes,1,name=driver"` + + // Parameters can contain arbitrary data. It is the responsibility of + // the driver developer to handle validation and versioning. Typically this + // includes self-identification and a version ("kind" + "apiVersion" for + // Kubernetes types), with conversion between different versions. + // + // The length of the raw data must be smaller or equal to 10 Ki. + // + // +required + Parameters runtime.RawExtension `json:"parameters" protobuf:"bytes,2,name=parameters"` +} + +// OpaqueParametersMaxLength is the maximum length of the raw data in an +// [OpaqueDeviceConfiguration.Parameters] field. +const OpaqueParametersMaxLength = 10 * 1024 + +// The ResourceClaim this DeviceToleration is attached to tolerates any taint that matches +// the triple using the matching operator . +type DeviceToleration struct { + // Key is the taint key that the toleration applies to. Empty means match all taint keys. + // If the key is empty, operator must be Exists; this combination means to match all values and all keys. + // Must be a label name. + // + // +optional + Key string `json:"key,omitempty" protobuf:"bytes,1,opt,name=key"` + + // Operator represents a key's relationship to the value. + // Valid operators are Exists and Equal. Defaults to Equal. + // Exists is equivalent to wildcard for value, so that a ResourceClaim can + // tolerate all taints of a particular category. + // + // +optional + // +default="Equal" + Operator DeviceTolerationOperator `json:"operator,omitempty" protobuf:"bytes,2,opt,name=operator,casttype=DeviceTolerationOperator"` + + // Value is the taint value the toleration matches to. + // If the operator is Exists, the value must be empty, otherwise just a regular string. + // Must be a label value. + // + // +optional + Value string `json:"value,omitempty" protobuf:"bytes,3,opt,name=value"` + + // Effect indicates the taint effect to match. Empty means match all taint effects. + // When specified, allowed values are NoSchedule and NoExecute. + // + // +optional + Effect DeviceTaintEffect `json:"effect,omitempty" protobuf:"bytes,4,opt,name=effect,casttype=DeviceTaintEffect"` + + // TolerationSeconds represents the period of time the toleration (which must be + // of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + // it is not set, which means tolerate the taint forever (do not evict). Zero and + // negative values will be treated as 0 (evict immediately) by the system. + // If larger than zero, the time when the pod needs to be evicted is calculated as