mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
[KEP-4817] API, validation and feature-gate
* Add status * Add validation to check if fields are correct (Network field, device has been allocated)) * Add feature-gate * Drop field if feature-gate not set Signed-off-by: Lionel Jouin <lionel.jouin@est.tech>
This commit is contained in:
parent
530278b1de
commit
3e595db0af
@ -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 (`<driver name>/<pool name>/<device name>`).
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
@ -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, "<value omitted>", fmt.Sprintf("error parsing data: %v", err.Error())))
|
||||
} else if _, isObject := v.(map[string]any); !isObject {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, "<value omitted>", "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
|
||||
}
|
||||
|
@ -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"), "<value omitted>", "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 {
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 (`<driver name>/<pool name>/<device name>`).
|
||||
//
|
||||
// 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"`
|
||||
}
|
||||
|
187
test/integration/resourceclaim/feature_enable_disable_test.go
Normal file
187
test/integration/resourceclaim/feature_enable_disable_test.go
Normal file
@ -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()
|
||||
}
|
27
test/integration/resourceclaim/main_test.go
Normal file
27
test/integration/resourceclaim/main_test.go
Normal file
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user