[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:
Lionel Jouin 2024-10-02 11:02:33 +02:00
parent 530278b1de
commit 3e595db0af
9 changed files with 593 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()
}

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