diff --git a/pkg/apis/resource/types.go b/pkg/apis/resource/types.go index 95377a45452..ff7b704cb87 100644 --- a/pkg/apis/resource/types.go +++ b/pkg/apis/resource/types.go @@ -703,6 +703,18 @@ type ResourceClaimStatus struct { // it got removed. May be reused once decoding v1alpha3 is no longer // supported. // DeallocationRequested bool + + // Devices contains the status of each device allocated for this + // claim, as reported by the driver. This can include driver-specific + // information. Entries are owned by their respective drivers. + // + // +optional + // +listType=map + // +listMapKey=driver + // +listMapKey=device + // +listMapKey=pool + // +featureGate=DRAResourceClaimDeviceStatus + Devices []AllocatedDeviceStatus } // ReservedForMaxSize is the maximum number of entries in @@ -975,3 +987,77 @@ type ResourceClaimTemplateList struct { // Items is the list of resource claim templates. Items []ResourceClaimTemplate } + +// AllocatedDeviceStatus contains the status of an allocated device, if the +// driver chooses to report it. This may include driver-specific information. +type AllocatedDeviceStatus struct { + // Driver specifies the name of the DRA driver whose kubelet + // plugin should be invoked to process the allocation once the claim is + // needed on a node. + // + // Must be a DNS subdomain and should end with a DNS domain owned by the + // vendor of the driver. + // + // +required + Driver string + + // This name together with the driver name and the device name field + // identify which device was allocated (`//`). + // + // Must not be longer than 253 characters and may contain one or more + // DNS sub-domains separated by slashes. + // + // +required + Pool string + + // Device references one device instance via its name in the driver's + // resource pool. It must be a DNS label. + // + // +required + Device string + + // Conditions contains the latest observation of the device's state. + // If the device has been configured according to the class and claim + // config references, the `Ready` condition should be True. + // + // +optional + // +listType=atomic + Conditions []metav1.Condition + + // Data contains arbitrary driver-specific data. + // + // +optional + Data *runtime.RawExtension + + // NetworkData contains network-related information specific to the device. + // + // +optional + NetworkData *NetworkDeviceData +} + +// NetworkDeviceData provides network-related details for the allocated device. +// This information may be filled by drivers or other components to configure +// or identify the device within a network context. +type NetworkDeviceData struct { + // InterfaceName specifies the name of the network interface associated with + // the allocated device. This might be the name of a physical or virtual + // network interface. + // + // +optional + InterfaceName *string + + // Addresses lists the network addresses assigned to the device's network interface. + // This can include both IPv4 and IPv6 addresses. + // The addresses are in the CIDR notation, which includes both the address and the + // associated subnet mask. + // e.g.: "192.0.2.5/24" for IPv4 and "2001:db8::5/64" for IPv6. + // + // +optional + // +listType=atomic + Addresses []string + + // HWAddress represents the hardware address (e.g. MAC Address) of the device's network interface. + // + // +optional + HWAddress *string +} diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index a12abf5a5c3..866ed2fffe3 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -25,15 +25,20 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/environment" + utilfeature "k8s.io/apiserver/pkg/util/feature" dracel "k8s.io/dynamic-resource-allocation/cel" + "k8s.io/dynamic-resource-allocation/structured" corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/apis/resource" + "k8s.io/kubernetes/pkg/features" ) var ( @@ -123,6 +128,15 @@ func gatherRequestNames(deviceClaim *resource.DeviceClaim) sets.Set[string] { return requestNames } +func gatherAllocatedDevices(allocationResult *resource.DeviceAllocationResult) sets.Set[structured.DeviceID] { + allocatedDevices := sets.New[structured.DeviceID]() + for _, result := range allocationResult.Results { + deviceID := structured.DeviceID{Driver: result.Driver, Pool: result.Pool, Device: result.Device} + allocatedDevices.Insert(deviceID) + } + return allocatedDevices +} + func validateDeviceRequest(request resource.DeviceRequest, fldPath *field.Path, stored bool) field.ErrorList { allErrs := validateRequestName(request.Name, fldPath.Child("name")) if request.DeviceClassName == "" { @@ -271,6 +285,21 @@ func validateResourceClaimStatusUpdate(status, oldStatus *resource.ResourceClaim func(consumer resource.ResourceClaimConsumerReference) (types.UID, string) { return consumer.UID, "uid" }, fldPath.Child("reservedFor"))...) + if utilfeature.DefaultFeatureGate.Enabled(features.DRAResourceClaimDeviceStatus) { + var allocatedDevices sets.Set[structured.DeviceID] + if status.Allocation != nil { + allocatedDevices = gatherAllocatedDevices(&status.Allocation.Devices) + } + allErrs = append(allErrs, validateSet(status.Devices, -1, + func(device resource.AllocatedDeviceStatus, fldPath *field.Path) field.ErrorList { + return validateDeviceStatus(device, fldPath, allocatedDevices) + }, + func(device resource.AllocatedDeviceStatus) (structured.DeviceID, string) { + return structured.DeviceID{Driver: device.Driver, Pool: device.Pool, Device: device.Device}, "deviceID" + }, + fldPath.Child("devices"))...) + } + // Now check for invariants that must be valid for a ResourceClaim. if len(status.ReservedFor) > 0 { if status.Allocation == nil { @@ -729,3 +758,47 @@ func truncateIfTooLong(str string, maxLen int) string { remaining := maxLen - len(ellipsis) return str[0:(remaining+1)/2] + ellipsis + str[len(str)-remaining/2:] } + +func validateDeviceStatus(device resource.AllocatedDeviceStatus, fldPath *field.Path, allocatedDevices sets.Set[structured.DeviceID]) field.ErrorList { + var allErrs field.ErrorList + deviceID := structured.DeviceID{Driver: device.Driver, Pool: device.Pool, Device: device.Device} + if !allocatedDevices.Has(deviceID) { + allErrs = append(allErrs, field.Invalid(fldPath, deviceID, "must be an allocated device in the claim")) + } + allErrs = append(allErrs, validateSlice(device.Conditions, -1, + func(condition metav1.Condition, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + return allErrs + }, fldPath.Child("conditions"))...) + allErrs = append(allErrs, validateRawExtension(device.Data, fldPath.Child("data"))...) + allErrs = append(allErrs, validateNetworkDeviceData(device.NetworkData, fldPath.Child("networkData"))...) + return allErrs +} + +func validateRawExtension(rawExtension *runtime.RawExtension, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if rawExtension == nil { + return allErrs + } + var v any + if err := json.Unmarshal(rawExtension.Raw, &v); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("error parsing data: %v", err.Error()))) + } else if _, isObject := v.(map[string]any); !isObject { + allErrs = append(allErrs, field.Invalid(fldPath, "", "parameters must be a valid JSON object")) + } + return allErrs +} + +func validateNetworkDeviceData(networkDeviceData *resource.NetworkDeviceData, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if networkDeviceData == nil { + return allErrs + } + allErrs = append(allErrs, validateSlice(networkDeviceData.Addresses, -1, + func(address string, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + allErrs = append(allErrs, validation.IsValidCIDR(fldPath, address)...) + return allErrs + }, fldPath.Child("addresses"))...) + return allErrs +} diff --git a/pkg/apis/resource/validation/validation_resourceclaim_test.go b/pkg/apis/resource/validation/validation_resourceclaim_test.go index ce29a0106a7..060b701a2c9 100644 --- a/pkg/apis/resource/validation/validation_resourceclaim_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaim_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/dynamic-resource-allocation/structured" "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/resource" "k8s.io/kubernetes/pkg/features" @@ -597,6 +598,8 @@ func TestValidateClaimUpdate(t *testing.T) { } func TestValidateClaimStatusUpdate(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAResourceClaimDeviceStatus, true) + validAllocatedClaim := validClaim.DeepCopy() validAllocatedClaim.Status = resource.ResourceClaimStatus{ Allocation: &resource.AllocationResult{ @@ -983,6 +986,118 @@ func TestValidateClaimStatusUpdate(t *testing.T) { return claim }, }, + "valid-network-device-status": { + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim = claim.DeepCopy() + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: goodName, + Pool: goodName, + Device: goodName, + Conditions: []metav1.Condition{ + { + Type: "test", + Status: metav1.ConditionTrue, + }, + }, + Data: &runtime.RawExtension{ + Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`), + }, + NetworkData: &resource.NetworkDeviceData{ + InterfaceName: ptr.To("net-1"), + Addresses: []string{ + "10.9.8.0/24", + "2001:db8::/64", + }, + HWAddress: ptr.To("ea:9f:cb:40:b1:7b"), + }, + }, + } + return claim + }, + }, + "invalid-device-status-duplicate": { + wantFailures: field.ErrorList{ + field.Duplicate(field.NewPath("status", "devices").Index(1).Child("deviceID"), structured.DeviceID{Driver: goodName, Pool: goodName, Device: goodName}), + }, + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim = claim.DeepCopy() + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: goodName, + Pool: goodName, + Device: goodName, + }, + { + Driver: goodName, + Pool: goodName, + Device: goodName, + }, + } + return claim + }, + }, + "invalid-network-device-status": { + wantFailures: field.ErrorList{ + field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "addresses").Index(0), "300.9.8.0/24", "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"), + }, + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim = claim.DeepCopy() + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: goodName, + Pool: goodName, + Device: goodName, + NetworkData: &resource.NetworkDeviceData{ + Addresses: []string{ + "300.9.8.0/24", + }, + }, + }, + } + return claim + }, + }, + "invalid-data-device-status": { + wantFailures: field.ErrorList{ + field.Invalid(field.NewPath("status", "devices").Index(0).Child("data"), "", "error parsing data: invalid character 'o' in literal false (expecting 'a')"), + }, + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim = claim.DeepCopy() + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: goodName, + Pool: goodName, + Device: goodName, + Data: &runtime.RawExtension{ + Raw: []byte(`foo`), + }, + }, + } + return claim + }, + }, + "invalid-device-status-no-device": { + wantFailures: field.ErrorList{ + field.Invalid(field.NewPath("status", "devices").Index(0), structured.DeviceID{Driver: "b", Pool: "a", Device: "r"}, "must be an allocated device in the claim"), + }, + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim = claim.DeepCopy() + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: "b", + Pool: "a", + Device: "r", + }, + } + return claim + }, + }, } for name, scenario := range scenarios { diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index aca2150f4ad..b6dea880dce 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -223,6 +223,14 @@ const ( // based on "structured parameters". DynamicResourceAllocation featuregate.Feature = "DynamicResourceAllocation" + // owner: @LionelJouin + // kep: http://kep.k8s.io/4817 + // alpha: v1.32 + // + // Enables support the ResourceClaim.status.devices field and for setting this + // status from DRA drivers. + DRAResourceClaimDeviceStatus featuregate.Feature = "DRAResourceClaimDeviceStatus" + // owner: @harche // kep: http://kep.k8s.io/3386 // diff --git a/pkg/features/versioned_kube_features.go b/pkg/features/versioned_kube_features.go index 4e306502e96..80588b79741 100644 --- a/pkg/features/versioned_kube_features.go +++ b/pkg/features/versioned_kube_features.go @@ -182,6 +182,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Beta}, }, + DRAResourceClaimDeviceStatus: { + {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, + }, + ElasticIndexedJob: { {Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.31, remove in 1.32 diff --git a/pkg/registry/resource/resourceclaim/strategy.go b/pkg/registry/resource/resourceclaim/strategy.go index 4313577f97b..e8b783f4c7d 100644 --- a/pkg/registry/resource/resourceclaim/strategy.go +++ b/pkg/registry/resource/resourceclaim/strategy.go @@ -181,6 +181,7 @@ func toSelectableFields(claim *resource.ResourceClaim) fields.Set { // dropDisabledFields removes fields which are covered by a feature gate. func dropDisabledFields(newClaim, oldClaim *resource.ResourceClaim) { dropDisabledDRAAdminAccessFields(newClaim, oldClaim) + dropDisabledDRAResourceClaimDeviceStatusFields(newClaim, oldClaim) } func dropDisabledDRAAdminAccessFields(newClaim, oldClaim *resource.ResourceClaim) { @@ -231,3 +232,9 @@ func draAdminAccessFeatureInUse(claim *resource.ResourceClaim) bool { return false } + +func dropDisabledDRAResourceClaimDeviceStatusFields(newClaim, oldClaim *resource.ResourceClaim) { + if !utilfeature.DefaultFeatureGate.Enabled(features.DRAResourceClaimDeviceStatus) { + newClaim.Status.Devices = nil + } +} diff --git a/staging/src/k8s.io/api/resource/v1alpha3/types.go b/staging/src/k8s.io/api/resource/v1alpha3/types.go index bae302e6b84..cfe018d2709 100644 --- a/staging/src/k8s.io/api/resource/v1alpha3/types.go +++ b/staging/src/k8s.io/api/resource/v1alpha3/types.go @@ -701,6 +701,18 @@ type ResourceClaimStatus struct { // it got removed. May be reused once decoding v1alpha3 is no longer // supported. // DeallocationRequested bool `json:"deallocationRequested,omitempty" protobuf:"bytes,3,opt,name=deallocationRequested"` + + // Devices contains the status of each device allocated for this + // claim, as reported by the driver. This can include driver-specific + // information. Entries are owned by their respective drivers. + // + // +optional + // +listType=map + // +listMapKey=driver + // +listMapKey=device + // +listMapKey=pool + // +featureGate=DRAResourceClaimDeviceStatus + Devices []AllocatedDeviceStatus `json:"devices,omitempty" protobuf:"bytes,4,opt,name=devices"` } // ReservedForMaxSize is the maximum number of entries in @@ -986,3 +998,77 @@ type ResourceClaimTemplateList struct { // Items is the list of resource claim templates. Items []ResourceClaimTemplate `json:"items" protobuf:"bytes,2,rep,name=items"` } + +// AllocatedDeviceStatus contains the status of an allocated device, if the +// driver chooses to report it. This may include driver-specific information. +type AllocatedDeviceStatus struct { + // Driver specifies the name of the DRA driver whose kubelet + // plugin should be invoked to process the allocation once the claim is + // needed on a node. + // + // 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,rep,name=driver"` + + // This name together with the driver name and the device name field + // identify which device was allocated (`//`). + // + // Must not be longer than 253 characters and may contain one or more + // DNS sub-domains separated by slashes. + // + // +required + Pool string `json:"pool" protobuf:"bytes,2,rep,name=pool"` + + // Device references one device instance via its name in the driver's + // resource pool. It must be a DNS label. + // + // +required + Device string `json:"device" protobuf:"bytes,3,rep,name=device"` + + // Conditions contains the latest observation of the device's state. + // If the device has been configured according to the class and claim + // config references, the `Ready` condition should be True. + // + // +optional + // +listType=atomic + Conditions []metav1.Condition `json:"conditions" protobuf:"bytes,4,opt,name=conditions"` + + // Data contains arbitrary driver-specific data. + // + // +optional + Data *runtime.RawExtension `json:"data,omitempty" protobuf:"bytes,5,opt,name=data"` + + // NetworkData contains network-related information specific to the device. + // + // +optional + NetworkData *NetworkDeviceData `json:"networkData,omitempty" protobuf:"bytes,6,opt,name=networkData"` +} + +// NetworkDeviceData provides network-related details for the allocated device. +// This information may be filled by drivers or other components to configure +// or identify the device within a network context. +type NetworkDeviceData struct { + // InterfaceName specifies the name of the network interface associated with + // the allocated device. This might be the name of a physical or virtual + // network interface. + // + // +optional + InterfaceName *string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"` + + // Addresses lists the network addresses assigned to the device's network interface. + // This can include both IPv4 and IPv6 addresses. + // The addresses are in the CIDR notation, which includes both the address and the + // associated subnet mask. + // e.g.: "192.0.2.5/24" for IPv4 and "2001:db8::5/64" for IPv6. + // + // +optional + // +listType=atomic + Addresses []string `json:"addresses,omitempty" protobuf:"bytes,2,opt,name=addresses"` + + // HWAddress represents the hardware address (e.g. MAC Address) of the device's network interface. + // + // +optional + HWAddress *string `json:"hwAddress,omitempty" protobuf:"bytes,3,opt,name=hwAddress"` +} diff --git a/test/integration/resourceclaim/feature_enable_disable_test.go b/test/integration/resourceclaim/feature_enable_disable_test.go new file mode 100644 index 00000000000..b980195d033 --- /dev/null +++ b/test/integration/resourceclaim/feature_enable_disable_test.go @@ -0,0 +1,187 @@ +/* +Copyright 2024 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 resourceclaim + +import ( + "context" + "fmt" + "testing" + + "k8s.io/api/resource/v1alpha3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientset "k8s.io/client-go/kubernetes" + kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/pkg/features" + "k8s.io/kubernetes/test/integration/framework" +) + +// TestEnableDisableDRAResourceClaimDeviceStatus first test the feature gate disabled +// by creating a ResourceClaim with an invalid device (not allocated device) and checks +// the object is not validated. +// Then the feature gate is created, and an attempt to create similar invalid ResourceClaim +// is done with no success. +func TestEnableDisableDRAResourceClaimDeviceStatus(t *testing.T) { + // start etcd instance + etcdOptions := framework.SharedEtcd() + apiServerOptions := kubeapiservertesting.NewDefaultTestServerOptions() + // apiserver with the feature disabled + server1 := kubeapiservertesting.StartTestServerOrDie(t, apiServerOptions, + []string{ + fmt.Sprintf("--feature-gates=%s=true,%s=false", features.DynamicResourceAllocation, features.DRAResourceClaimDeviceStatus), + }, + etcdOptions) + client1, err := clientset.NewForConfig(server1.ClientConfig) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + ns := framework.CreateNamespaceOrDie(client1, "test-enable-dra-resourceclaim-device-status", t) + + rcDisabledName := "test-enable-dra-resourceclaim-device-status-rc-disabled" + rcDisabled := &v1alpha3.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: rcDisabledName, + }, + Spec: v1alpha3.ResourceClaimSpec{ + Devices: v1alpha3.DeviceClaim{ + Requests: []v1alpha3.DeviceRequest{ + { + Name: "foo", + DeviceClassName: "foo", + Count: 1, + AllocationMode: v1alpha3.DeviceAllocationModeExactCount, + }, + }, + }, + }, + } + + if _, err := client1.ResourceV1alpha3().ResourceClaims(ns.Name).Create(context.TODO(), rcDisabled, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + rcDisabled.Status = v1alpha3.ResourceClaimStatus{ + Devices: []v1alpha3.AllocatedDeviceStatus{ + { + Driver: "foo", + Pool: "foo", + Device: "foo", + }, + }, + } + if _, err := client1.ResourceV1alpha3().ResourceClaims(ns.Name).UpdateStatus(context.TODO(), rcDisabled, metav1.UpdateOptions{}); err != nil { + t.Fatal(err) + } + + rcDisabled, err = client1.ResourceV1alpha3().ResourceClaims(ns.Name).Get(context.TODO(), rcDisabledName, metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + // No devices as the Kubernetes api-server dropped these fields since the feature is disabled. + if len(rcDisabled.Status.Devices) != 0 { + t.Fatalf("expected 0 Device in status got %d", len(rcDisabled.Status.Devices)) + } + + // shutdown apiserver with the feature disabled + server1.TearDownFn() + + // apiserver with the feature enabled + server2 := kubeapiservertesting.StartTestServerOrDie(t, apiServerOptions, + []string{ + fmt.Sprintf("--feature-gates=%s=true,%s=true", features.DynamicResourceAllocation, features.DRAResourceClaimDeviceStatus), + }, + etcdOptions) + client2, err := clientset.NewForConfig(server2.ClientConfig) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + rcEnabledName := "test-enable-dra-resourceclaim-device-status-rc-enabled" + rcEnabled := &v1alpha3.ResourceClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: rcEnabledName, + }, + Spec: v1alpha3.ResourceClaimSpec{ + Devices: v1alpha3.DeviceClaim{ + Requests: []v1alpha3.DeviceRequest{ + { + Name: "bar", + DeviceClassName: "bar", + Count: 1, + AllocationMode: v1alpha3.DeviceAllocationModeExactCount, + }, + }, + }, + }, + } + + if _, err := client2.ResourceV1alpha3().ResourceClaims(ns.Name).Create(context.TODO(), rcEnabled, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + // Tests the validation is enabled. + // validation will refuse this update as the device is not allocated. + rcEnabled.Status = v1alpha3.ResourceClaimStatus{ + Devices: []v1alpha3.AllocatedDeviceStatus{ + { + Driver: "bar", + Pool: "bar", + Device: "bar", + }, + }, + } + if _, err := client2.ResourceV1alpha3().ResourceClaims(ns.Name).UpdateStatus(context.TODO(), rcEnabled, metav1.UpdateOptions{}); err == nil { + t.Fatalf("Expected error (must be an allocated device in the claim)") + } + + rcEnabled.Status = v1alpha3.ResourceClaimStatus{ + Allocation: &v1alpha3.AllocationResult{ + Devices: v1alpha3.DeviceAllocationResult{ + Results: []v1alpha3.DeviceRequestAllocationResult{ + { + Request: "bar", + Driver: "bar", + Pool: "bar", + Device: "bar", + }, + }, + }, + }, + Devices: []v1alpha3.AllocatedDeviceStatus{ + { + Driver: "bar", + Pool: "bar", + Device: "bar", + }, + }, + } + if _, err := client2.ResourceV1alpha3().ResourceClaims(ns.Name).UpdateStatus(context.TODO(), rcEnabled, metav1.UpdateOptions{}); err != nil { + t.Fatal(err) + } + + // Tests the field is enabled. + rcEnabled, err = client2.ResourceV1alpha3().ResourceClaims(ns.Name).Get(context.TODO(), rcEnabledName, metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if len(rcEnabled.Status.Devices) != 1 { + t.Fatalf("expected 1 Device in status got %d", len(rcEnabled.Status.Devices)) + } + + // shutdown apiserver with the feature enabled + server2.TearDownFn() +} diff --git a/test/integration/resourceclaim/main_test.go b/test/integration/resourceclaim/main_test.go new file mode 100644 index 00000000000..7b48de60741 --- /dev/null +++ b/test/integration/resourceclaim/main_test.go @@ -0,0 +1,27 @@ +/* +Copyright 2023 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 resourceclaim + +import ( + "testing" + + "k8s.io/kubernetes/test/integration/framework" +) + +func TestMain(m *testing.M) { + framework.EtcdMain(m.Run) +}