mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 04:33:26 +00:00
DRA: AdminAccess validate based on namespace label
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
This commit is contained in:
parent
f007012f5f
commit
0301e5a9f8
@ -385,6 +385,15 @@ const (
|
|||||||
DeviceConfigMaxSize = 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.
|
// DeviceRequest is a request for devices required for a claim.
|
||||||
// This is typically a request for a single resource like a device, but can
|
// 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.
|
||||||
|
@ -40,6 +40,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/controller"
|
"k8s.io/kubernetes/pkg/controller"
|
||||||
"k8s.io/kubernetes/pkg/controller/resourceclaim/metrics"
|
"k8s.io/kubernetes/pkg/controller/resourceclaim/metrics"
|
||||||
"k8s.io/kubernetes/test/utils/ktesting"
|
"k8s.io/kubernetes/test/utils/ktesting"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -61,13 +62,15 @@ var (
|
|||||||
testClaimReserved = reserveClaim(testClaimAllocated, testPodWithResource)
|
testClaimReserved = reserveClaim(testClaimAllocated, testPodWithResource)
|
||||||
testClaimReservedTwice = reserveClaim(testClaimReserved, otherTestPod)
|
testClaimReservedTwice = reserveClaim(testClaimReserved, otherTestPod)
|
||||||
|
|
||||||
generatedTestClaim = makeGeneratedClaim(podResourceClaimName, testPodName+"-"+podResourceClaimName+"-", testNamespace, className, 1, makeOwnerReference(testPodWithResource, true))
|
generatedTestClaim = makeGeneratedClaim(podResourceClaimName, testPodName+"-"+podResourceClaimName+"-", testNamespace, className, 1, makeOwnerReference(testPodWithResource, true), nil)
|
||||||
|
generatedTestClaimWithAdmin = makeGeneratedClaim(podResourceClaimName, testPodName+"-"+podResourceClaimName+"-", testNamespace, className, 1, makeOwnerReference(testPodWithResource, true), ptr.To(true))
|
||||||
generatedTestClaimAllocated = allocateClaim(generatedTestClaim)
|
generatedTestClaimAllocated = allocateClaim(generatedTestClaim)
|
||||||
generatedTestClaimReserved = reserveClaim(generatedTestClaimAllocated, testPodWithResource)
|
generatedTestClaimReserved = reserveClaim(generatedTestClaimAllocated, testPodWithResource)
|
||||||
|
|
||||||
conflictingClaim = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, className, nil)
|
conflictingClaim = makeClaim(testPodName+"-"+podResourceClaimName, testNamespace, className, nil)
|
||||||
otherNamespaceClaim = makeClaim(testPodName+"-"+podResourceClaimName, otherNamespace, className, nil)
|
otherNamespaceClaim = makeClaim(testPodName+"-"+podResourceClaimName, otherNamespace, className, nil)
|
||||||
template = makeTemplate(templateName, testNamespace, className)
|
template = makeTemplate(templateName, testNamespace, className, nil)
|
||||||
|
templateWithAdminAccess = makeTemplate(templateName, testNamespace, className, ptr.To(true))
|
||||||
|
|
||||||
testPodWithNodeName = func() *v1.Pod {
|
testPodWithNodeName = func() *v1.Pod {
|
||||||
pod := testPodWithResource.DeepCopy()
|
pod := testPodWithResource.DeepCopy()
|
||||||
@ -78,6 +81,7 @@ var (
|
|||||||
})
|
})
|
||||||
return pod
|
return pod
|
||||||
}()
|
}()
|
||||||
|
adminAccessFeatureOffError = "admin access is requested, but the feature is disabled"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSyncHandler(t *testing.T) {
|
func TestSyncHandler(t *testing.T) {
|
||||||
@ -93,7 +97,7 @@ func TestSyncHandler(t *testing.T) {
|
|||||||
templates []*resourceapi.ResourceClaimTemplate
|
templates []*resourceapi.ResourceClaimTemplate
|
||||||
expectedClaims []resourceapi.ResourceClaim
|
expectedClaims []resourceapi.ResourceClaim
|
||||||
expectedStatuses map[string][]v1.PodResourceClaimStatus
|
expectedStatuses map[string][]v1.PodResourceClaimStatus
|
||||||
expectedError bool
|
expectedError string
|
||||||
expectedMetrics expectedMetrics
|
expectedMetrics expectedMetrics
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -109,6 +113,27 @@ func TestSyncHandler(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedMetrics: expectedMetrics{1, 0},
|
expectedMetrics: expectedMetrics{1, 0},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "create with admin and feature gate off",
|
||||||
|
pods: []*v1.Pod{testPodWithResource},
|
||||||
|
templates: []*resourceapi.ResourceClaimTemplate{templateWithAdminAccess},
|
||||||
|
key: podKey(testPodWithResource),
|
||||||
|
expectedError: adminAccessFeatureOffError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create with admin and feature gate on",
|
||||||
|
pods: []*v1.Pod{testPodWithResource},
|
||||||
|
templates: []*resourceapi.ResourceClaimTemplate{templateWithAdminAccess},
|
||||||
|
key: podKey(testPodWithResource),
|
||||||
|
expectedClaims: []resourceapi.ResourceClaim{*generatedTestClaimWithAdmin},
|
||||||
|
expectedStatuses: map[string][]v1.PodResourceClaimStatus{
|
||||||
|
testPodWithResource.Name: {
|
||||||
|
{Name: testPodWithResource.Spec.ResourceClaims[0].Name, ResourceClaimName: &generatedTestClaimWithAdmin.Name},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminAccessEnabled: true,
|
||||||
|
expectedMetrics: expectedMetrics{1, 0},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "nop",
|
name: "nop",
|
||||||
pods: []*v1.Pod{func() *v1.Pod {
|
pods: []*v1.Pod{func() *v1.Pod {
|
||||||
@ -153,7 +178,7 @@ func TestSyncHandler(t *testing.T) {
|
|||||||
pods: []*v1.Pod{testPodWithResource},
|
pods: []*v1.Pod{testPodWithResource},
|
||||||
templates: nil,
|
templates: nil,
|
||||||
key: podKey(testPodWithResource),
|
key: podKey(testPodWithResource),
|
||||||
expectedError: true,
|
expectedError: "resource claim template \"my-template\": resourceclaimtemplate.resource.k8s.io \"my-template\" not found",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "find-existing-claim-by-label",
|
name: "find-existing-claim-by-label",
|
||||||
@ -219,7 +244,7 @@ func TestSyncHandler(t *testing.T) {
|
|||||||
key: podKey(testPodWithResource),
|
key: podKey(testPodWithResource),
|
||||||
claims: []*resourceapi.ResourceClaim{conflictingClaim},
|
claims: []*resourceapi.ResourceClaim{conflictingClaim},
|
||||||
expectedClaims: []resourceapi.ResourceClaim{*conflictingClaim},
|
expectedClaims: []resourceapi.ResourceClaim{*conflictingClaim},
|
||||||
expectedError: true,
|
expectedError: "resource claim template \"my-template\": resourceclaimtemplate.resource.k8s.io \"my-template\" not found",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create-conflict",
|
name: "create-conflict",
|
||||||
@ -227,7 +252,7 @@ func TestSyncHandler(t *testing.T) {
|
|||||||
templates: []*resourceapi.ResourceClaimTemplate{template},
|
templates: []*resourceapi.ResourceClaimTemplate{template},
|
||||||
key: podKey(testPodWithResource),
|
key: podKey(testPodWithResource),
|
||||||
expectedMetrics: expectedMetrics{1, 1},
|
expectedMetrics: expectedMetrics{1, 1},
|
||||||
expectedError: true,
|
expectedError: "create ResourceClaim : Operation cannot be fulfilled on resourceclaims.resource.k8s.io \"fake name\": fake conflict",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "stay-reserved-seen",
|
name: "stay-reserved-seen",
|
||||||
@ -424,11 +449,12 @@ func TestSyncHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = ec.syncHandler(tCtx, tc.key)
|
err = ec.syncHandler(tCtx, tc.key)
|
||||||
if err != nil && !tc.expectedError {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error while running handler: %v", err)
|
assert.ErrorContains(t, err, tc.expectedError, "the error message should have contained the expected error message")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err == nil && tc.expectedError {
|
if tc.expectedError != "" {
|
||||||
t.Fatalf("unexpected success")
|
t.Fatalf("expected error, got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := fakeKubeClient.ResourceV1beta1().ResourceClaims("").List(tCtx, metav1.ListOptions{})
|
claims, err := fakeKubeClient.ResourceV1beta1().ResourceClaims("").List(tCtx, metav1.ListOptions{})
|
||||||
@ -558,7 +584,7 @@ func makeClaim(name, namespace, classname string, owner *metav1.OwnerReference)
|
|||||||
return claim
|
return claim
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeGeneratedClaim(podClaimName, generateName, namespace, classname string, createCounter int, owner *metav1.OwnerReference) *resourceapi.ResourceClaim {
|
func makeGeneratedClaim(podClaimName, generateName, namespace, classname string, createCounter int, owner *metav1.OwnerReference, adminAccess *bool) *resourceapi.ResourceClaim {
|
||||||
claim := &resourceapi.ResourceClaim{
|
claim := &resourceapi.ResourceClaim{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: fmt.Sprintf("%s-%d", generateName, createCounter),
|
Name: fmt.Sprintf("%s-%d", generateName, createCounter),
|
||||||
@ -570,6 +596,19 @@ func makeGeneratedClaim(podClaimName, generateName, namespace, classname string,
|
|||||||
if owner != nil {
|
if owner != nil {
|
||||||
claim.OwnerReferences = []metav1.OwnerReference{*owner}
|
claim.OwnerReferences = []metav1.OwnerReference{*owner}
|
||||||
}
|
}
|
||||||
|
if adminAccess != nil {
|
||||||
|
claim.Spec = resourceapi.ResourceClaimSpec{
|
||||||
|
Devices: resourceapi.DeviceClaim{
|
||||||
|
Requests: []resourceapi.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AdminAccess: adminAccess,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return claim
|
return claim
|
||||||
}
|
}
|
||||||
@ -618,10 +657,25 @@ func makePod(name, namespace string, uid types.UID, podClaims ...v1.PodResourceC
|
|||||||
return pod
|
return pod
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTemplate(name, namespace, classname string) *resourceapi.ResourceClaimTemplate {
|
func makeTemplate(name, namespace, classname string, adminAccess *bool) *resourceapi.ResourceClaimTemplate {
|
||||||
template := &resourceapi.ResourceClaimTemplate{
|
template := &resourceapi.ResourceClaimTemplate{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||||
}
|
}
|
||||||
|
if adminAccess != nil {
|
||||||
|
template.Spec = resourceapi.ResourceClaimTemplateSpec{
|
||||||
|
Spec: resourceapi.ResourceClaimSpec{
|
||||||
|
Devices: resourceapi.DeviceClaim{
|
||||||
|
Requests: []resourceapi.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AdminAccess: adminAccess,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,6 @@ import (
|
|||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
clientdiscovery "k8s.io/client-go/discovery"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
discoveryclient "k8s.io/client-go/kubernetes/typed/discovery/v1"
|
discoveryclient "k8s.io/client-go/kubernetes/typed/discovery/v1"
|
||||||
@ -328,7 +327,7 @@ func (c CompletedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
restStorageProviders, err := c.StorageProviders(client.Discovery())
|
restStorageProviders, err := c.StorageProviders(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -379,7 +378,7 @@ func (c CompletedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CompletedConfig) StorageProviders(discovery clientdiscovery.DiscoveryInterface) ([]controlplaneapiserver.RESTStorageProvider, error) {
|
func (c CompletedConfig) StorageProviders(client *kubernetes.Clientset) ([]controlplaneapiserver.RESTStorageProvider, error) {
|
||||||
legacyRESTStorageProvider, err := corerest.New(corerest.Config{
|
legacyRESTStorageProvider, err := corerest.New(corerest.Config{
|
||||||
GenericConfig: *c.ControlPlane.NewCoreGenericConfig(),
|
GenericConfig: *c.ControlPlane.NewCoreGenericConfig(),
|
||||||
Proxy: corerest.ProxyConfig{
|
Proxy: corerest.ProxyConfig{
|
||||||
@ -425,9 +424,9 @@ func (c CompletedConfig) StorageProviders(discovery clientdiscovery.DiscoveryInt
|
|||||||
// keep apps after extensions so legacy clients resolve the extensions versions of shared resource names.
|
// keep apps after extensions so legacy clients resolve the extensions versions of shared resource names.
|
||||||
// See https://github.com/kubernetes/kubernetes/issues/42392
|
// See https://github.com/kubernetes/kubernetes/issues/42392
|
||||||
appsrest.StorageProvider{},
|
appsrest.StorageProvider{},
|
||||||
admissionregistrationrest.RESTStorageProvider{Authorizer: c.ControlPlane.Generic.Authorization.Authorizer, DiscoveryClient: discovery},
|
admissionregistrationrest.RESTStorageProvider{Authorizer: c.ControlPlane.Generic.Authorization.Authorizer, DiscoveryClient: client.Discovery()},
|
||||||
eventsrest.RESTStorageProvider{TTL: c.ControlPlane.EventTTL},
|
eventsrest.RESTStorageProvider{TTL: c.ControlPlane.EventTTL},
|
||||||
resourcerest.RESTStorageProvider{},
|
resourcerest.RESTStorageProvider{NamespaceClient: client.CoreV1().Namespaces()},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,7 +490,7 @@ func TestGenericStorageProviders(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
kube, err := completed.StorageProviders(client.Discovery())
|
kube, err := completed.StorageProviders(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,14 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
"k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/kubernetes/pkg/printers"
|
"k8s.io/kubernetes/pkg/printers"
|
||||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
@ -38,7 +40,11 @@ type REST struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewREST returns a RESTStorage object that will work against ResourceClaims.
|
// NewREST returns a RESTStorage object that will work against ResourceClaims.
|
||||||
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) {
|
func NewREST(optsGetter generic.RESTOptionsGetter, nsClient v1.NamespaceInterface) (*REST, *StatusREST, error) {
|
||||||
|
if nsClient == nil {
|
||||||
|
return nil, nil, fmt.Errorf("namespace client is required")
|
||||||
|
}
|
||||||
|
strategy := resourceclaim.NewStrategy(nsClient)
|
||||||
store := &genericregistry.Store{
|
store := &genericregistry.Store{
|
||||||
NewFunc: func() runtime.Object { return &resource.ResourceClaim{} },
|
NewFunc: func() runtime.Object { return &resource.ResourceClaim{} },
|
||||||
NewListFunc: func() runtime.Object { return &resource.ResourceClaimList{} },
|
NewListFunc: func() runtime.Object { return &resource.ResourceClaimList{} },
|
||||||
@ -46,11 +52,11 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) {
|
|||||||
DefaultQualifiedResource: resource.Resource("resourceclaims"),
|
DefaultQualifiedResource: resource.Resource("resourceclaims"),
|
||||||
SingularQualifiedResource: resource.Resource("resourceclaim"),
|
SingularQualifiedResource: resource.Resource("resourceclaim"),
|
||||||
|
|
||||||
CreateStrategy: resourceclaim.Strategy,
|
CreateStrategy: strategy,
|
||||||
UpdateStrategy: resourceclaim.Strategy,
|
UpdateStrategy: strategy,
|
||||||
DeleteStrategy: resourceclaim.Strategy,
|
DeleteStrategy: strategy,
|
||||||
ReturnDeletedObject: true,
|
ReturnDeletedObject: true,
|
||||||
ResetFieldsStrategy: resourceclaim.Strategy,
|
ResetFieldsStrategy: strategy,
|
||||||
|
|
||||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||||
}
|
}
|
||||||
@ -60,8 +66,9 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statusStore := *store
|
statusStore := *store
|
||||||
statusStore.UpdateStrategy = resourceclaim.StatusStrategy
|
statusStrategy := resourceclaim.NewStatusStrategy(strategy)
|
||||||
statusStore.ResetFieldsStrategy = resourceclaim.StatusStrategy
|
statusStore.UpdateStrategy = statusStrategy
|
||||||
|
statusStore.ResetFieldsStrategy = statusStrategy
|
||||||
|
|
||||||
rest := &REST{store}
|
rest := &REST{store}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
@ -43,7 +44,9 @@ func newStorage(t *testing.T) (*REST, *StatusREST, *etcd3testing.EtcdTestServer)
|
|||||||
DeleteCollectionWorkers: 1,
|
DeleteCollectionWorkers: 1,
|
||||||
ResourcePrefix: "resourceclaims",
|
ResourcePrefix: "resourceclaims",
|
||||||
}
|
}
|
||||||
resourceClaimStorage, statusStorage, err := NewREST(restOptions)
|
fakeClient := fake.NewSimpleClientset()
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
resourceClaimStorage, statusStorage, err := NewREST(restOptions, mockNSClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -30,11 +30,13 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
"k8s.io/apiserver/pkg/storage/names"
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"k8s.io/dynamic-resource-allocation/structured"
|
"k8s.io/dynamic-resource-allocation/structured"
|
||||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
|
resourceutils "k8s.io/kubernetes/pkg/registry/resource"
|
||||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,20 +44,26 @@ import (
|
|||||||
type resourceclaimStrategy struct {
|
type resourceclaimStrategy struct {
|
||||||
runtime.ObjectTyper
|
runtime.ObjectTyper
|
||||||
names.NameGenerator
|
names.NameGenerator
|
||||||
|
nsClient v1.NamespaceInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy is the default logic that applies when creating and updating
|
// NewStrategy is the default logic that applies when creating and updating ResourceClaim objects.
|
||||||
// ResourceClaim objects via the REST API.
|
func NewStrategy(nsClient v1.NamespaceInterface) *resourceclaimStrategy {
|
||||||
var Strategy = resourceclaimStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
return &resourceclaimStrategy{
|
||||||
|
legacyscheme.Scheme,
|
||||||
|
names.SimpleNameGenerator,
|
||||||
|
nsClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) NamespaceScoped() bool {
|
func (*resourceclaimStrategy) NamespaceScoped() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetResetFields returns the set of fields that get reset by the strategy and
|
// GetResetFields returns the set of fields that get reset by the strategy and
|
||||||
// should not be modified by the user. For a new ResourceClaim that is the
|
// should not be modified by the user. For a new ResourceClaim that is the
|
||||||
// status.
|
// status.
|
||||||
func (resourceclaimStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
func (*resourceclaimStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||||
"resource.k8s.io/v1alpha3": fieldpath.NewSet(
|
"resource.k8s.io/v1alpha3": fieldpath.NewSet(
|
||||||
fieldpath.MakePathOrDie("status"),
|
fieldpath.MakePathOrDie("status"),
|
||||||
@ -68,7 +76,7 @@ func (resourceclaimStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpat
|
|||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
func (*resourceclaimStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||||
claim := obj.(*resource.ResourceClaim)
|
claim := obj.(*resource.ResourceClaim)
|
||||||
// Status must not be set by user on create.
|
// Status must not be set by user on create.
|
||||||
claim.Status = resource.ResourceClaimStatus{}
|
claim.Status = resource.ResourceClaimStatus{}
|
||||||
@ -76,23 +84,25 @@ func (resourceclaimStrategy) PrepareForCreate(ctx context.Context, obj runtime.O
|
|||||||
dropDisabledFields(claim, nil)
|
dropDisabledFields(claim, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
func (s *resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
claim := obj.(*resource.ResourceClaim)
|
claim := obj.(*resource.ResourceClaim)
|
||||||
return validation.ValidateResourceClaim(claim)
|
|
||||||
|
allErrs := resourceutils.AuthorizedForAdmin(ctx, claim.Spec.Devices.Requests, claim.Namespace, s.nsClient)
|
||||||
|
return append(allErrs, validation.ValidateResourceClaim(claim)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
func (*resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) Canonicalize(obj runtime.Object) {
|
func (*resourceclaimStrategy) Canonicalize(obj runtime.Object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) AllowCreateOnUpdate() bool {
|
func (*resourceclaimStrategy) AllowCreateOnUpdate() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
func (*resourceclaimStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
newClaim := obj.(*resource.ResourceClaim)
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
oldClaim := old.(*resource.ResourceClaim)
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
newClaim.Status = oldClaim.Status
|
newClaim.Status = oldClaim.Status
|
||||||
@ -100,30 +110,34 @@ func (resourceclaimStrategy) PrepareForUpdate(ctx context.Context, obj, old runt
|
|||||||
dropDisabledFields(newClaim, oldClaim)
|
dropDisabledFields(newClaim, oldClaim)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
func (s *resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
newClaim := obj.(*resource.ResourceClaim)
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
oldClaim := old.(*resource.ResourceClaim)
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
|
// AuthorizedForAdmin isn't needed here because the spec is immutable.
|
||||||
errorList := validation.ValidateResourceClaim(newClaim)
|
errorList := validation.ValidateResourceClaim(newClaim)
|
||||||
return append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...)
|
return append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
func (*resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStrategy) AllowUnconditionalUpdate() bool {
|
func (*resourceclaimStrategy) AllowUnconditionalUpdate() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type resourceclaimStatusStrategy struct {
|
type resourceclaimStatusStrategy struct {
|
||||||
resourceclaimStrategy
|
*resourceclaimStrategy
|
||||||
}
|
}
|
||||||
|
|
||||||
var StatusStrategy = resourceclaimStatusStrategy{Strategy}
|
// NewStatusStrategy creates a strategy for operating the status object.
|
||||||
|
func NewStatusStrategy(resourceclaimStrategy *resourceclaimStrategy) *resourceclaimStatusStrategy {
|
||||||
|
return &resourceclaimStatusStrategy{resourceclaimStrategy}
|
||||||
|
}
|
||||||
|
|
||||||
// GetResetFields returns the set of fields that get reset by the strategy and
|
// GetResetFields returns the set of fields that get reset by the strategy and
|
||||||
// should not be modified by the user. For a status update that is the spec.
|
// should not be modified by the user. For a status update that is the spec.
|
||||||
func (resourceclaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
func (*resourceclaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||||
"resource.k8s.io/v1alpha3": fieldpath.NewSet(
|
"resource.k8s.io/v1alpha3": fieldpath.NewSet(
|
||||||
fieldpath.MakePathOrDie("spec"),
|
fieldpath.MakePathOrDie("spec"),
|
||||||
@ -136,7 +150,7 @@ func (resourceclaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fi
|
|||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
func (*resourceclaimStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
newClaim := obj.(*resource.ResourceClaim)
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
oldClaim := old.(*resource.ResourceClaim)
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
newClaim.Spec = oldClaim.Spec
|
newClaim.Spec = oldClaim.Spec
|
||||||
@ -146,14 +160,22 @@ func (resourceclaimStatusStrategy) PrepareForUpdate(ctx context.Context, obj, ol
|
|||||||
dropDisabledFields(newClaim, oldClaim)
|
dropDisabledFields(newClaim, oldClaim)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
func (r *resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
newClaim := obj.(*resource.ResourceClaim)
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
oldClaim := old.(*resource.ResourceClaim)
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
return validation.ValidateResourceClaimStatusUpdate(newClaim, oldClaim)
|
var newAllocationResult, oldAllocationResult []resource.DeviceRequestAllocationResult
|
||||||
|
if newClaim.Status.Allocation != nil {
|
||||||
|
newAllocationResult = newClaim.Status.Allocation.Devices.Results
|
||||||
|
}
|
||||||
|
if oldClaim.Status.Allocation != nil {
|
||||||
|
oldAllocationResult = oldClaim.Status.Allocation.Devices.Results
|
||||||
|
}
|
||||||
|
allErrs := resourceutils.AuthorizedForAdminStatus(ctx, newAllocationResult, oldAllocationResult, newClaim.Namespace, r.nsClient)
|
||||||
|
return append(allErrs, validation.ValidateResourceClaimStatusUpdate(newClaim, oldClaim)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WarningsOnUpdate returns warnings for the given update.
|
// WarningsOnUpdate returns warnings for the given update.
|
||||||
func (resourceclaimStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
func (*resourceclaimStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,9 +21,12 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
testclient "k8s.io/client-go/testing"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
@ -33,7 +36,7 @@ import (
|
|||||||
var obj = &resource.ResourceClaim{
|
var obj = &resource.ResourceClaim{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-claim",
|
Name: "valid-claim",
|
||||||
Namespace: "default",
|
Namespace: "kube-system",
|
||||||
},
|
},
|
||||||
Spec: resource.ResourceClaimSpec{
|
Spec: resource.ResourceClaimSpec{
|
||||||
Devices: resource.DeviceClaim{
|
Devices: resource.DeviceClaim{
|
||||||
@ -51,7 +54,7 @@ var obj = &resource.ResourceClaim{
|
|||||||
var objWithStatus = &resource.ResourceClaim{
|
var objWithStatus = &resource.ResourceClaim{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-claim",
|
Name: "valid-claim",
|
||||||
Namespace: "default",
|
Namespace: "kube-system",
|
||||||
},
|
},
|
||||||
Spec: resource.ResourceClaimSpec{
|
Spec: resource.ResourceClaimSpec{
|
||||||
Devices: resource.DeviceClaim{
|
Devices: resource.DeviceClaim{
|
||||||
@ -81,6 +84,43 @@ var objWithStatus = &resource.ResourceClaim{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var objWithAdminAccess = &resource.ResourceClaim{
|
var objWithAdminAccess = &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim",
|
||||||
|
Namespace: "kube-system",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
Devices: resource.DeviceClaim{
|
||||||
|
Requests: []resource.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AllocationMode: resource.DeviceAllocationModeAll,
|
||||||
|
AdminAccess: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var objInNonAdminNamespace = &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
Devices: resource.DeviceClaim{
|
||||||
|
Requests: []resource.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AllocationMode: resource.DeviceAllocationModeAll,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var objWithAdminAccessInNonAdminNamespace = &resource.ResourceClaim{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-claim",
|
Name: "valid-claim",
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
@ -99,7 +139,38 @@ var objWithAdminAccess = &resource.ResourceClaim{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var objWithAdminAccessStatus = &resource.ResourceClaim{
|
var objStatusInNonAdminNamespace = &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
Devices: resource.DeviceClaim{
|
||||||
|
Requests: []resource.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AllocationMode: resource.DeviceAllocationModeAll,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: resource.ResourceClaimStatus{
|
||||||
|
Allocation: &resource.AllocationResult{
|
||||||
|
Devices: resource.DeviceAllocationResult{
|
||||||
|
Results: []resource.DeviceRequestAllocationResult{
|
||||||
|
{
|
||||||
|
Request: "req-0",
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Pool: "pool-0",
|
||||||
|
Device: "device-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var objWithAdminAccessStatusInNonAdminNamespace = &resource.ResourceClaim{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-claim",
|
Name: "valid-claim",
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
@ -111,7 +182,6 @@ var objWithAdminAccessStatus = &resource.ResourceClaim{
|
|||||||
Name: "req-0",
|
Name: "req-0",
|
||||||
DeviceClassName: "class",
|
DeviceClassName: "class",
|
||||||
AllocationMode: resource.DeviceAllocationModeAll,
|
AllocationMode: resource.DeviceAllocationModeAll,
|
||||||
AdminAccess: ptr.To(true),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -157,6 +227,58 @@ var objWithPrioritizedList = &resource.ResourceClaim{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var objWithAdminAccessStatus = &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim",
|
||||||
|
Namespace: "kube-system",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
Devices: resource.DeviceClaim{
|
||||||
|
Requests: []resource.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AllocationMode: resource.DeviceAllocationModeAll,
|
||||||
|
AdminAccess: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: resource.ResourceClaimStatus{
|
||||||
|
Allocation: &resource.AllocationResult{
|
||||||
|
Devices: resource.DeviceAllocationResult{
|
||||||
|
Results: []resource.DeviceRequestAllocationResult{
|
||||||
|
{
|
||||||
|
Request: "req-0",
|
||||||
|
Driver: "dra.example.com",
|
||||||
|
Pool: "pool-0",
|
||||||
|
Device: "device-0",
|
||||||
|
AdminAccess: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var ns1 = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "default",
|
||||||
|
Labels: map[string]string{"key": "value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var ns2 = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "kube-system",
|
||||||
|
Labels: map[string]string{resource.DRAAdminNamespaceLabelKey: "true"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testRequest = "test-request"
|
testRequest = "test-request"
|
||||||
testDriver = "test-driver"
|
testDriver = "test-driver"
|
||||||
@ -165,27 +287,35 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStrategy(t *testing.T) {
|
func TestStrategy(t *testing.T) {
|
||||||
if !Strategy.NamespaceScoped() {
|
fakeClient := fake.NewSimpleClientset()
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
|
if !strategy.NamespaceScoped() {
|
||||||
t.Errorf("ResourceClaim must be namespace scoped")
|
t.Errorf("ResourceClaim must be namespace scoped")
|
||||||
}
|
}
|
||||||
if Strategy.AllowCreateOnUpdate() {
|
if strategy.AllowCreateOnUpdate() {
|
||||||
t.Errorf("ResourceClaim should not allow create on update")
|
t.Errorf("ResourceClaim should not allow create on update")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStrategyCreate(t *testing.T) {
|
func TestStrategyCreate(t *testing.T) {
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
|
||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
obj *resource.ResourceClaim
|
obj *resource.ResourceClaim
|
||||||
adminAccess bool
|
adminAccess bool
|
||||||
prioritizedList bool
|
prioritizedList bool
|
||||||
expectValidationError bool
|
expectValidationError string
|
||||||
expectObj *resource.ResourceClaim
|
expectObj *resource.ResourceClaim
|
||||||
|
verify func(*testing.T, []testclient.Action)
|
||||||
}{
|
}{
|
||||||
"simple": {
|
"simple": {
|
||||||
obj: obj,
|
obj: obj,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"validation-error": {
|
"validation-error": {
|
||||||
obj: func() *resource.ResourceClaim {
|
obj: func() *resource.ResourceClaim {
|
||||||
@ -193,69 +323,139 @@ func TestStrategyCreate(t *testing.T) {
|
|||||||
obj.Name = "%#@$%$"
|
obj.Name = "%#@$%$"
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
expectValidationError: true,
|
expectValidationError: metadataError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-admin-access": {
|
"drop-fields-admin-access": {
|
||||||
obj: objWithAdminAccess,
|
obj: objWithAdminAccess,
|
||||||
adminAccess: false,
|
adminAccess: false,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-admin-access": {
|
"keep-fields-admin-access": {
|
||||||
obj: objWithAdminAccess,
|
obj: objWithAdminAccess,
|
||||||
adminAccess: true,
|
adminAccess: true,
|
||||||
expectObj: objWithAdminAccess,
|
expectObj: objWithAdminAccess,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "kube-system" {
|
||||||
|
t.Errorf("expected to get the kube-system namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-prioritized-list": {
|
"drop-fields-prioritized-list": {
|
||||||
obj: objWithPrioritizedList,
|
obj: objWithPrioritizedList,
|
||||||
prioritizedList: false,
|
prioritizedList: false,
|
||||||
expectValidationError: true,
|
expectValidationError: deviceRequestError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-prioritized-list": {
|
"keep-fields-prioritized-list": {
|
||||||
obj: objWithPrioritizedList,
|
obj: objWithPrioritizedList,
|
||||||
prioritizedList: true,
|
prioritizedList: true,
|
||||||
expectObj: objWithPrioritizedList,
|
expectObj: objWithPrioritizedList,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"admin-access-admin-namespace": {
|
||||||
|
obj: objWithAdminAccess,
|
||||||
|
adminAccess: true,
|
||||||
|
expectObj: objWithAdminAccess,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "kube-system" {
|
||||||
|
t.Errorf("expected to get the kube-system namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"admin-access-non-admin-namespace": {
|
||||||
|
obj: objWithAdminAccessInNonAdminNamespace,
|
||||||
|
adminAccess: true,
|
||||||
|
expectObj: objWithAdminAccessInNonAdminNamespace,
|
||||||
|
expectValidationError: adminAccessError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "default" {
|
||||||
|
t.Errorf("expected to get the default namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range testcases {
|
for name, tc := range testcases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAPrioritizedList, tc.prioritizedList)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAPrioritizedList, tc.prioritizedList)
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
|
|
||||||
obj := tc.obj.DeepCopy()
|
obj := tc.obj.DeepCopy()
|
||||||
Strategy.PrepareForCreate(ctx, obj)
|
strategy.PrepareForCreate(ctx, obj)
|
||||||
if errs := Strategy.Validate(ctx, obj); len(errs) != 0 {
|
if errs := strategy.Validate(ctx, obj); len(errs) != 0 {
|
||||||
if !tc.expectValidationError {
|
assert.ErrorContains(t, errs[0], tc.expectValidationError, "the error message should have contained the expected error message")
|
||||||
t.Fatalf("unexpected validation errors: %q", errs)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
} else if tc.expectValidationError {
|
}
|
||||||
|
if tc.expectValidationError != "" {
|
||||||
t.Fatal("expected validation error(s), got none")
|
t.Fatal("expected validation error(s), got none")
|
||||||
}
|
}
|
||||||
if warnings := Strategy.WarningsOnCreate(ctx, obj); len(warnings) != 0 {
|
if warnings := strategy.WarningsOnCreate(ctx, obj); len(warnings) != 0 {
|
||||||
t.Fatalf("unexpected warnings: %q", warnings)
|
t.Fatalf("unexpected warnings: %q", warnings)
|
||||||
}
|
}
|
||||||
Strategy.Canonicalize(obj)
|
strategy.Canonicalize(obj)
|
||||||
assert.Equal(t, tc.expectObj, obj)
|
assert.Equal(t, tc.expectObj, obj)
|
||||||
|
tc.verify(t, fakeClient.Actions())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStrategyUpdate(t *testing.T) {
|
func TestStrategyUpdate(t *testing.T) {
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
|
||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
oldObj *resource.ResourceClaim
|
oldObj *resource.ResourceClaim
|
||||||
newObj *resource.ResourceClaim
|
newObj *resource.ResourceClaim
|
||||||
adminAccess bool
|
adminAccess bool
|
||||||
|
expectValidationError string
|
||||||
prioritizedList bool
|
prioritizedList bool
|
||||||
expectValidationError bool
|
|
||||||
expectObj *resource.ResourceClaim
|
expectObj *resource.ResourceClaim
|
||||||
|
verify func(*testing.T, []testclient.Action)
|
||||||
}{
|
}{
|
||||||
"no-changes-okay": {
|
"no-changes-okay": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: obj,
|
newObj: obj,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"name-change-not-allowed": {
|
"name-change-not-allowed": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
@ -264,97 +464,167 @@ func TestStrategyUpdate(t *testing.T) {
|
|||||||
obj.Name += "-2"
|
obj.Name += "-2"
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
expectValidationError: true,
|
expectValidationError: fieldImmutableError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-admin-access": {
|
"drop-fields-admin-access": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: objWithAdminAccess,
|
newObj: objWithAdminAccess,
|
||||||
adminAccess: false,
|
adminAccess: false,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-admin-access": {
|
"keep-fields-admin-access": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: objWithAdminAccess,
|
newObj: objWithAdminAccess,
|
||||||
adminAccess: true,
|
adminAccess: true,
|
||||||
expectValidationError: true, // Spec is immutable.
|
expectValidationError: fieldImmutableError, // Spec is immutable.
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-existing-fields-admin-access": {
|
"keep-existing-fields-admin-access": {
|
||||||
oldObj: objWithAdminAccess,
|
oldObj: objWithAdminAccess,
|
||||||
newObj: objWithAdminAccess,
|
newObj: objWithAdminAccess,
|
||||||
adminAccess: true,
|
adminAccess: true,
|
||||||
expectObj: objWithAdminAccess,
|
expectObj: objWithAdminAccess,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"admin-access-admin-namespace": {
|
||||||
|
oldObj: objWithAdminAccess,
|
||||||
|
newObj: objWithAdminAccess,
|
||||||
|
adminAccess: true,
|
||||||
|
expectObj: objWithAdminAccess,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"admin-access-non-admin-namespace": {
|
||||||
|
oldObj: objInNonAdminNamespace,
|
||||||
|
newObj: objWithAdminAccessInNonAdminNamespace,
|
||||||
|
adminAccess: true,
|
||||||
|
expectValidationError: fieldImmutableError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-prioritized-list": {
|
"drop-fields-prioritized-list": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: objWithPrioritizedList,
|
newObj: objWithPrioritizedList,
|
||||||
prioritizedList: false,
|
prioritizedList: false,
|
||||||
expectValidationError: true,
|
expectValidationError: deviceRequestError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-prioritized-list": {
|
"keep-fields-prioritized-list": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: objWithPrioritizedList,
|
newObj: objWithPrioritizedList,
|
||||||
prioritizedList: true,
|
prioritizedList: true,
|
||||||
expectValidationError: true, // Spec is immutable.
|
expectValidationError: fieldImmutableError, // Spec is immutable.
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-existing-fields-prioritized-list": {
|
"keep-existing-fields-prioritized-list": {
|
||||||
oldObj: objWithPrioritizedList,
|
oldObj: objWithPrioritizedList,
|
||||||
newObj: objWithPrioritizedList,
|
newObj: objWithPrioritizedList,
|
||||||
prioritizedList: true,
|
prioritizedList: true,
|
||||||
expectObj: objWithPrioritizedList,
|
expectObj: objWithPrioritizedList,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-existing-fields-prioritized-list-disabled-feature": {
|
"keep-existing-fields-prioritized-list-disabled-feature": {
|
||||||
oldObj: objWithPrioritizedList,
|
oldObj: objWithPrioritizedList,
|
||||||
newObj: objWithPrioritizedList,
|
newObj: objWithPrioritizedList,
|
||||||
prioritizedList: false,
|
prioritizedList: false,
|
||||||
expectObj: objWithPrioritizedList,
|
expectObj: objWithPrioritizedList,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range testcases {
|
for name, tc := range testcases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAPrioritizedList, tc.prioritizedList)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAPrioritizedList, tc.prioritizedList)
|
||||||
|
|
||||||
oldObj := tc.oldObj.DeepCopy()
|
oldObj := tc.oldObj.DeepCopy()
|
||||||
newObj := tc.newObj.DeepCopy()
|
newObj := tc.newObj.DeepCopy()
|
||||||
newObj.ResourceVersion = "4"
|
newObj.ResourceVersion = "4"
|
||||||
|
|
||||||
Strategy.PrepareForUpdate(ctx, newObj, oldObj)
|
strategy.PrepareForUpdate(ctx, newObj, oldObj)
|
||||||
if errs := Strategy.ValidateUpdate(ctx, newObj, oldObj); len(errs) != 0 {
|
if errs := strategy.ValidateUpdate(ctx, newObj, oldObj); len(errs) != 0 {
|
||||||
if !tc.expectValidationError {
|
assert.ErrorContains(t, errs[0], tc.expectValidationError, "the error message should have contained the expected error message")
|
||||||
t.Fatalf("unexpected validation errors: %q", errs)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
} else if tc.expectValidationError {
|
}
|
||||||
|
if tc.expectValidationError != "" {
|
||||||
t.Fatal("expected validation error(s), got none")
|
t.Fatal("expected validation error(s), got none")
|
||||||
}
|
}
|
||||||
if warnings := Strategy.WarningsOnUpdate(ctx, newObj, oldObj); len(warnings) != 0 {
|
if warnings := strategy.WarningsOnUpdate(ctx, newObj, oldObj); len(warnings) != 0 {
|
||||||
t.Fatalf("unexpected warnings: %q", warnings)
|
t.Fatalf("unexpected warnings: %q", warnings)
|
||||||
}
|
}
|
||||||
Strategy.Canonicalize(newObj)
|
strategy.Canonicalize(newObj)
|
||||||
|
|
||||||
expectObj := tc.expectObj.DeepCopy()
|
expectObj := tc.expectObj.DeepCopy()
|
||||||
expectObj.ResourceVersion = "4"
|
expectObj.ResourceVersion = "4"
|
||||||
assert.Equal(t, expectObj, newObj)
|
assert.Equal(t, expectObj, newObj)
|
||||||
|
tc.verify(t, fakeClient.Actions())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusStrategyUpdate(t *testing.T) {
|
func TestStatusStrategyUpdate(t *testing.T) {
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
|
||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
oldObj *resource.ResourceClaim
|
oldObj *resource.ResourceClaim
|
||||||
newObj *resource.ResourceClaim
|
newObj *resource.ResourceClaim
|
||||||
adminAccess bool
|
adminAccess bool
|
||||||
deviceStatusFeatureGate bool
|
deviceStatusFeatureGate bool
|
||||||
expectValidationError bool
|
expectValidationError string
|
||||||
expectObj *resource.ResourceClaim
|
expectObj *resource.ResourceClaim
|
||||||
|
verify func(*testing.T, []testclient.Action)
|
||||||
}{
|
}{
|
||||||
"no-changes-okay": {
|
"no-changes-okay": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: obj,
|
newObj: obj,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"name-change-not-allowed": {
|
"name-change-not-allowed": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
@ -363,7 +633,12 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
obj.Name += "-2"
|
obj.Name += "-2"
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
expectValidationError: true,
|
expectValidationError: fieldImmutableError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Cannot add finalizers, annotations and labels during status update.
|
// Cannot add finalizers, annotations and labels during status update.
|
||||||
"drop-meta-changes": {
|
"drop-meta-changes": {
|
||||||
@ -376,12 +651,22 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-admin-access": {
|
"drop-fields-admin-access": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
newObj: objWithAdminAccessStatus,
|
newObj: objWithAdminAccessStatus,
|
||||||
adminAccess: false,
|
adminAccess: false,
|
||||||
expectObj: objWithStatus,
|
expectObj: objWithStatus,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-admin-access": {
|
"keep-fields-admin-access": {
|
||||||
oldObj: obj,
|
oldObj: obj,
|
||||||
@ -393,12 +678,48 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
expectObj.Spec = obj.Spec
|
expectObj.Spec = obj.Spec
|
||||||
return expectObj
|
return expectObj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "kube-system" {
|
||||||
|
t.Errorf("expected to get the kube-system namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"keep-fields-admin-access-NonAdminNamespace": {
|
||||||
|
oldObj: objStatusInNonAdminNamespace,
|
||||||
|
newObj: objWithAdminAccessStatusInNonAdminNamespace,
|
||||||
|
adminAccess: true,
|
||||||
|
expectValidationError: adminAccessError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "default" {
|
||||||
|
t.Errorf("expected to get the default namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-admin-access-because-of-spec": {
|
"keep-fields-admin-access-because-of-spec": {
|
||||||
oldObj: objWithAdminAccess,
|
oldObj: objWithAdminAccess,
|
||||||
newObj: objWithAdminAccessStatus,
|
newObj: objWithAdminAccessStatus,
|
||||||
adminAccess: false,
|
adminAccess: false,
|
||||||
expectObj: objWithAdminAccessStatus,
|
expectObj: objWithAdminAccessStatus,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "kube-system" {
|
||||||
|
t.Errorf("expected to get the kube-system namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Normally a claim without admin access in the spec shouldn't
|
// Normally a claim without admin access in the spec shouldn't
|
||||||
// have one in the status either, but it's not invalid and thus
|
// have one in the status either, but it's not invalid and thus
|
||||||
@ -416,6 +737,11 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
oldObj.Spec.Devices.Requests[0].AdminAccess = ptr.To(false)
|
oldObj.Spec.Devices.Requests[0].AdminAccess = ptr.To(false)
|
||||||
return oldObj
|
return oldObj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-devices-status": {
|
"drop-fields-devices-status": {
|
||||||
oldObj: func() *resource.ResourceClaim {
|
oldObj: func() *resource.ResourceClaim {
|
||||||
@ -438,6 +764,11 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
addStatusAllocationDevicesResults(obj, testDriver, testPool, testDevice, testRequest)
|
addStatusAllocationDevicesResults(obj, testDriver, testPool, testDevice, testRequest)
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-devices-status-disable-feature-gate": {
|
"keep-fields-devices-status-disable-feature-gate": {
|
||||||
oldObj: func() *resource.ResourceClaim {
|
oldObj: func() *resource.ResourceClaim {
|
||||||
@ -462,6 +793,11 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
addStatusDevices(obj, testDriver, testPool, testDevice)
|
addStatusDevices(obj, testDriver, testPool, testDevice)
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-devices-status": {
|
"keep-fields-devices-status": {
|
||||||
oldObj: func() *resource.ResourceClaim {
|
oldObj: func() *resource.ResourceClaim {
|
||||||
@ -485,6 +821,11 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
addStatusDevices(obj, testDriver, testPool, testDevice)
|
addStatusDevices(obj, testDriver, testPool, testDevice)
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-status-deallocated-device": {
|
"drop-status-deallocated-device": {
|
||||||
oldObj: func() *resource.ResourceClaim {
|
oldObj: func() *resource.ResourceClaim {
|
||||||
@ -506,6 +847,11 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
addSpecDevicesRequest(obj, testRequest)
|
addSpecDevicesRequest(obj, testRequest)
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-status-deallocated-device-disable-feature-gate": {
|
"drop-status-deallocated-device-disable-feature-gate": {
|
||||||
oldObj: func() *resource.ResourceClaim {
|
oldObj: func() *resource.ResourceClaim {
|
||||||
@ -527,35 +873,45 @@ func TestStatusStrategyUpdate(t *testing.T) {
|
|||||||
addSpecDevicesRequest(obj, testRequest)
|
addSpecDevicesRequest(obj, testRequest)
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range testcases {
|
for name, tc := range testcases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
|
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAResourceClaimDeviceStatus, tc.deviceStatusFeatureGate)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAResourceClaimDeviceStatus, tc.deviceStatusFeatureGate)
|
||||||
|
statusStrategy := NewStatusStrategy(strategy)
|
||||||
|
|
||||||
oldObj := tc.oldObj.DeepCopy()
|
oldObj := tc.oldObj.DeepCopy()
|
||||||
newObj := tc.newObj.DeepCopy()
|
newObj := tc.newObj.DeepCopy()
|
||||||
newObj.ResourceVersion = "4"
|
newObj.ResourceVersion = "4"
|
||||||
|
|
||||||
StatusStrategy.PrepareForUpdate(ctx, newObj, oldObj)
|
statusStrategy.PrepareForUpdate(ctx, newObj, oldObj)
|
||||||
if errs := StatusStrategy.ValidateUpdate(ctx, newObj, oldObj); len(errs) != 0 {
|
if errs := statusStrategy.ValidateUpdate(ctx, newObj, oldObj); len(errs) != 0 {
|
||||||
if !tc.expectValidationError {
|
assert.ErrorContains(t, errs[0], tc.expectValidationError, "the error message should have contained the expected error message")
|
||||||
t.Fatalf("unexpected validation errors: %q", errs)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
} else if tc.expectValidationError {
|
}
|
||||||
|
if tc.expectValidationError != "" {
|
||||||
t.Fatal("expected validation error(s), got none")
|
t.Fatal("expected validation error(s), got none")
|
||||||
}
|
}
|
||||||
if warnings := StatusStrategy.WarningsOnUpdate(ctx, newObj, oldObj); len(warnings) != 0 {
|
if warnings := statusStrategy.WarningsOnUpdate(ctx, newObj, oldObj); len(warnings) != 0 {
|
||||||
t.Fatalf("unexpected warnings: %q", warnings)
|
t.Fatalf("unexpected warnings: %q", warnings)
|
||||||
}
|
}
|
||||||
StatusStrategy.Canonicalize(newObj)
|
statusStrategy.Canonicalize(newObj)
|
||||||
|
|
||||||
expectObj := tc.expectObj.DeepCopy()
|
expectObj := tc.expectObj.DeepCopy()
|
||||||
expectObj.ResourceVersion = "4"
|
expectObj.ResourceVersion = "4"
|
||||||
assert.Equal(t, expectObj, newObj)
|
assert.Equal(t, expectObj, newObj)
|
||||||
|
tc.verify(t, fakeClient.Actions())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,12 @@ limitations under the License.
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
"k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/kubernetes/pkg/printers"
|
"k8s.io/kubernetes/pkg/printers"
|
||||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
@ -33,16 +36,20 @@ type REST struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewREST returns a RESTStorage object that will work against ResourceClass.
|
// NewREST returns a RESTStorage object that will work against ResourceClass.
|
||||||
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
func NewREST(optsGetter generic.RESTOptionsGetter, nsClient v1.NamespaceInterface) (*REST, error) {
|
||||||
|
if nsClient == nil {
|
||||||
|
return nil, fmt.Errorf("namespace client is required")
|
||||||
|
}
|
||||||
|
strategy := resourceclaimtemplate.NewStrategy(nsClient)
|
||||||
store := &genericregistry.Store{
|
store := &genericregistry.Store{
|
||||||
NewFunc: func() runtime.Object { return &resource.ResourceClaimTemplate{} },
|
NewFunc: func() runtime.Object { return &resource.ResourceClaimTemplate{} },
|
||||||
NewListFunc: func() runtime.Object { return &resource.ResourceClaimTemplateList{} },
|
NewListFunc: func() runtime.Object { return &resource.ResourceClaimTemplateList{} },
|
||||||
DefaultQualifiedResource: resource.Resource("resourceclaimtemplates"),
|
DefaultQualifiedResource: resource.Resource("resourceclaimtemplates"),
|
||||||
SingularQualifiedResource: resource.Resource("resourceclaimtemplate"),
|
SingularQualifiedResource: resource.Resource("resourceclaimtemplate"),
|
||||||
|
|
||||||
CreateStrategy: resourceclaimtemplate.Strategy,
|
CreateStrategy: strategy,
|
||||||
UpdateStrategy: resourceclaimtemplate.Strategy,
|
UpdateStrategy: strategy,
|
||||||
DeleteStrategy: resourceclaimtemplate.Strategy,
|
DeleteStrategy: strategy,
|
||||||
ReturnDeletedObject: true,
|
ReturnDeletedObject: true,
|
||||||
|
|
||||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
@ -39,7 +40,9 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
|||||||
DeleteCollectionWorkers: 1,
|
DeleteCollectionWorkers: 1,
|
||||||
ResourcePrefix: "resourceclaimtemplates",
|
ResourcePrefix: "resourceclaimtemplates",
|
||||||
}
|
}
|
||||||
resourceClaimTemplateStorage, err := NewREST(restOptions)
|
fakeClient := fake.NewSimpleClientset()
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
resourceClaimTemplateStorage, err := NewREST(restOptions, mockNSClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -27,60 +27,72 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
"k8s.io/apiserver/pkg/storage/names"
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
|
resourceutils "k8s.io/kubernetes/pkg/registry/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resourceClaimTemplateStrategy implements behavior for ResourceClaimTemplate objects
|
// resourceClaimTemplateStrategy implements behavior for ResourceClaimTemplate objects
|
||||||
type resourceClaimTemplateStrategy struct {
|
type resourceClaimTemplateStrategy struct {
|
||||||
runtime.ObjectTyper
|
runtime.ObjectTyper
|
||||||
names.NameGenerator
|
names.NameGenerator
|
||||||
|
nsClient v1.NamespaceInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
var Strategy = resourceClaimTemplateStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
// NewStrategy is the default logic that applies when creating and updating ResourceClaimTemplate objects.
|
||||||
|
func NewStrategy(nsClient v1.NamespaceInterface) *resourceClaimTemplateStrategy {
|
||||||
|
return &resourceClaimTemplateStrategy{
|
||||||
|
legacyscheme.Scheme,
|
||||||
|
names.SimpleNameGenerator,
|
||||||
|
nsClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) NamespaceScoped() bool {
|
func (*resourceClaimTemplateStrategy) NamespaceScoped() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
func (*resourceClaimTemplateStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||||
claimTemplate := obj.(*resource.ResourceClaimTemplate)
|
claimTemplate := obj.(*resource.ResourceClaimTemplate)
|
||||||
dropDisabledFields(claimTemplate, nil)
|
dropDisabledFields(claimTemplate, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
func (s *resourceClaimTemplateStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
resourceClaimTemplate := obj.(*resource.ResourceClaimTemplate)
|
resourceClaimTemplate := obj.(*resource.ResourceClaimTemplate)
|
||||||
return validation.ValidateResourceClaimTemplate(resourceClaimTemplate)
|
allErrs := resourceutils.AuthorizedForAdmin(ctx, resourceClaimTemplate.Spec.Spec.Devices.Requests, resourceClaimTemplate.Namespace, s.nsClient)
|
||||||
|
return append(allErrs, validation.ValidateResourceClaimTemplate(resourceClaimTemplate)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
func (*resourceClaimTemplateStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) Canonicalize(obj runtime.Object) {
|
func (*resourceClaimTemplateStrategy) Canonicalize(obj runtime.Object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) AllowCreateOnUpdate() bool {
|
func (*resourceClaimTemplateStrategy) AllowCreateOnUpdate() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
func (*resourceClaimTemplateStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
claimTemplate, oldClaimTemplate := obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate)
|
claimTemplate, oldClaimTemplate := obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate)
|
||||||
dropDisabledFields(claimTemplate, oldClaimTemplate)
|
dropDisabledFields(claimTemplate, oldClaimTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
func (s *resourceClaimTemplateStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
// AuthorizedForAdmin isn't needed here because the spec is immutable.
|
||||||
errorList := validation.ValidateResourceClaimTemplate(obj.(*resource.ResourceClaimTemplate))
|
errorList := validation.ValidateResourceClaimTemplate(obj.(*resource.ResourceClaimTemplate))
|
||||||
return append(errorList, validation.ValidateResourceClaimTemplateUpdate(obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate))...)
|
return append(errorList, validation.ValidateResourceClaimTemplateUpdate(obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
func (*resourceClaimTemplateStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resourceClaimTemplateStrategy) AllowUnconditionalUpdate() bool {
|
func (*resourceClaimTemplateStrategy) AllowUnconditionalUpdate() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,9 +21,12 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
testclient "k8s.io/client-go/testing"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
@ -33,7 +36,7 @@ import (
|
|||||||
var obj = &resource.ResourceClaimTemplate{
|
var obj = &resource.ResourceClaimTemplate{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-claim-template",
|
Name: "valid-claim-template",
|
||||||
Namespace: "default",
|
Namespace: "kube-system",
|
||||||
},
|
},
|
||||||
Spec: resource.ResourceClaimTemplateSpec{
|
Spec: resource.ResourceClaimTemplateSpec{
|
||||||
Spec: resource.ResourceClaimSpec{
|
Spec: resource.ResourceClaimSpec{
|
||||||
@ -51,6 +54,27 @@ var obj = &resource.ResourceClaimTemplate{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var objWithAdminAccess = &resource.ResourceClaimTemplate{
|
var objWithAdminAccess = &resource.ResourceClaimTemplate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim-template",
|
||||||
|
Namespace: "kube-system",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimTemplateSpec{
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
Devices: resource.DeviceClaim{
|
||||||
|
Requests: []resource.DeviceRequest{
|
||||||
|
{
|
||||||
|
Name: "req-0",
|
||||||
|
DeviceClassName: "class",
|
||||||
|
AllocationMode: resource.DeviceAllocationModeAll,
|
||||||
|
AdminAccess: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var objWithAdminAccessInNonAdminNamespace = &resource.ResourceClaimTemplate{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-claim-template",
|
Name: "valid-claim-template",
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
@ -97,11 +121,32 @@ var objWithPrioritizedList = &resource.ResourceClaimTemplate{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ns1 = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "default",
|
||||||
|
Labels: map[string]string{"key": "value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var ns2 = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "kube-system",
|
||||||
|
Labels: map[string]string{resource.DRAAdminNamespaceLabelKey: "true"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
|
||||||
func TestClaimTemplateStrategy(t *testing.T) {
|
func TestClaimTemplateStrategy(t *testing.T) {
|
||||||
if !Strategy.NamespaceScoped() {
|
fakeClient := fake.NewSimpleClientset()
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
|
|
||||||
|
if !strategy.NamespaceScoped() {
|
||||||
t.Errorf("ResourceClaimTemplate must be namespace scoped")
|
t.Errorf("ResourceClaimTemplate must be namespace scoped")
|
||||||
}
|
}
|
||||||
if Strategy.AllowCreateOnUpdate() {
|
if strategy.AllowCreateOnUpdate() {
|
||||||
t.Errorf("ResourceClaimTemplate should not allow create on update")
|
t.Errorf("ResourceClaimTemplate should not allow create on update")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,13 +157,19 @@ func TestClaimTemplateStrategyCreate(t *testing.T) {
|
|||||||
testcases := map[string]struct {
|
testcases := map[string]struct {
|
||||||
obj *resource.ResourceClaimTemplate
|
obj *resource.ResourceClaimTemplate
|
||||||
adminAccess bool
|
adminAccess bool
|
||||||
|
expectValidationError string
|
||||||
prioritizedList bool
|
prioritizedList bool
|
||||||
expectValidationError bool
|
|
||||||
expectObj *resource.ResourceClaimTemplate
|
expectObj *resource.ResourceClaimTemplate
|
||||||
|
verify func(*testing.T, []testclient.Action)
|
||||||
}{
|
}{
|
||||||
"simple": {
|
"simple": {
|
||||||
obj: obj,
|
obj: obj,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"validation-error": {
|
"validation-error": {
|
||||||
obj: func() *resource.ResourceClaimTemplate {
|
obj: func() *resource.ResourceClaimTemplate {
|
||||||
@ -126,50 +177,114 @@ func TestClaimTemplateStrategyCreate(t *testing.T) {
|
|||||||
obj.Name = "%#@$%$"
|
obj.Name = "%#@$%$"
|
||||||
return obj
|
return obj
|
||||||
}(),
|
}(),
|
||||||
expectValidationError: true,
|
expectValidationError: metadataError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-admin-access": {
|
"drop-fields-admin-access": {
|
||||||
obj: objWithAdminAccess,
|
obj: objWithAdminAccess,
|
||||||
adminAccess: false,
|
adminAccess: false,
|
||||||
expectObj: obj,
|
expectObj: obj,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-admin-access": {
|
"keep-fields-admin-access": {
|
||||||
obj: objWithAdminAccess,
|
obj: objWithAdminAccess,
|
||||||
adminAccess: true,
|
adminAccess: true,
|
||||||
expectObj: objWithAdminAccess,
|
expectObj: objWithAdminAccess,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "kube-system" {
|
||||||
|
t.Errorf("expected to get the kube-system namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"drop-fields-prioritized-list": {
|
"drop-fields-prioritized-list": {
|
||||||
obj: objWithPrioritizedList,
|
obj: objWithPrioritizedList,
|
||||||
prioritizedList: false,
|
prioritizedList: false,
|
||||||
expectValidationError: true,
|
expectValidationError: deviceRequestError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"keep-fields-prioritized-list": {
|
"keep-fields-prioritized-list": {
|
||||||
obj: objWithPrioritizedList,
|
obj: objWithPrioritizedList,
|
||||||
prioritizedList: true,
|
prioritizedList: true,
|
||||||
expectObj: objWithPrioritizedList,
|
expectObj: objWithPrioritizedList,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"admin-access-admin-namespace": {
|
||||||
|
obj: objWithAdminAccess,
|
||||||
|
adminAccess: true,
|
||||||
|
expectObj: objWithAdminAccess,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "kube-system" {
|
||||||
|
t.Errorf("expected to get the kube-system namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"admin-access-non-admin-namespace": {
|
||||||
|
obj: objWithAdminAccessInNonAdminNamespace,
|
||||||
|
adminAccess: true,
|
||||||
|
expectObj: objWithAdminAccessInNonAdminNamespace,
|
||||||
|
expectValidationError: adminAccessError,
|
||||||
|
verify: func(t *testing.T, as []testclient.Action) {
|
||||||
|
if len(as) != 1 {
|
||||||
|
t.Errorf("expected one action but got %d", len(as))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ns := as[0].(testclient.GetAction).GetName()
|
||||||
|
if ns != "default" {
|
||||||
|
t.Errorf("expected to get the default namespace but got '%s'", ns)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range testcases {
|
for name, tc := range testcases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, tc.adminAccess)
|
||||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAPrioritizedList, tc.prioritizedList)
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAPrioritizedList, tc.prioritizedList)
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
|
|
||||||
obj := tc.obj.DeepCopy()
|
obj := tc.obj.DeepCopy()
|
||||||
Strategy.PrepareForCreate(ctx, obj)
|
strategy.PrepareForCreate(ctx, obj)
|
||||||
if errs := Strategy.Validate(ctx, obj); len(errs) != 0 {
|
if errs := strategy.Validate(ctx, obj); len(errs) != 0 {
|
||||||
if !tc.expectValidationError {
|
assert.ErrorContains(t, errs[0], tc.expectValidationError, "the error message should have contained the expected error message")
|
||||||
t.Fatalf("unexpected validation errors: %q", errs)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
} else if tc.expectValidationError {
|
}
|
||||||
|
if tc.expectValidationError != "" {
|
||||||
t.Fatal("expected validation error(s), got none")
|
t.Fatal("expected validation error(s), got none")
|
||||||
}
|
}
|
||||||
if warnings := Strategy.WarningsOnCreate(ctx, obj); len(warnings) != 0 {
|
if warnings := strategy.WarningsOnCreate(ctx, obj); len(warnings) != 0 {
|
||||||
t.Fatalf("unexpected warnings: %q", warnings)
|
t.Fatalf("unexpected warnings: %q", warnings)
|
||||||
}
|
}
|
||||||
Strategy.Canonicalize(obj)
|
strategy.Canonicalize(obj)
|
||||||
assert.Equal(t, tc.expectObj, obj)
|
assert.Equal(t, tc.expectObj, obj)
|
||||||
|
tc.verify(t, fakeClient.Actions())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,28 +292,66 @@ func TestClaimTemplateStrategyCreate(t *testing.T) {
|
|||||||
func TestClaimTemplateStrategyUpdate(t *testing.T) {
|
func TestClaimTemplateStrategyUpdate(t *testing.T) {
|
||||||
t.Run("no-changes-okay", func(t *testing.T) {
|
t.Run("no-changes-okay", func(t *testing.T) {
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
resourceClaimTemplate := obj.DeepCopy()
|
resourceClaimTemplate := obj.DeepCopy()
|
||||||
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
newClaimTemplate.ResourceVersion = "4"
|
newClaimTemplate.ResourceVersion = "4"
|
||||||
|
|
||||||
Strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
errs := Strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
errs := strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
t.Errorf("unexpected validation errors: %v", errs)
|
t.Errorf("unexpected validation errors: %v", errs)
|
||||||
}
|
}
|
||||||
|
if len(fakeClient.Actions()) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("name-change-not-allowed", func(t *testing.T) {
|
t.Run("name-change-not-allowed", func(t *testing.T) {
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
resourceClaimTemplate := obj.DeepCopy()
|
resourceClaimTemplate := obj.DeepCopy()
|
||||||
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
newClaimTemplate.Name = "valid-class-2"
|
newClaimTemplate.Name = "valid-class-2"
|
||||||
newClaimTemplate.ResourceVersion = "4"
|
newClaimTemplate.ResourceVersion = "4"
|
||||||
|
|
||||||
Strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
errs := Strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
errs := strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
if len(errs) == 0 {
|
if len(errs) == 0 {
|
||||||
t.Errorf("expected a validation error")
|
t.Errorf("expected a validation error")
|
||||||
}
|
}
|
||||||
|
if len(fakeClient.Actions()) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminaccess-update-not-allowed", func(t *testing.T) {
|
||||||
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAAdminAccess, true)
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
fakeClient := fake.NewSimpleClientset(ns1, ns2)
|
||||||
|
mockNSClient := fakeClient.CoreV1().Namespaces()
|
||||||
|
strategy := NewStrategy(mockNSClient)
|
||||||
|
resourceClaimTemplate := obj.DeepCopy()
|
||||||
|
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
|
newClaimTemplate.ResourceVersion = "4"
|
||||||
|
newClaimTemplate.Spec.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
||||||
|
|
||||||
|
strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
|
errs := strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
assert.ErrorContains(t, errs[0], fieldImmutableError, "the error message should have contained the expected error message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Errorf("expected a validation error")
|
||||||
|
}
|
||||||
|
if len(fakeClient.Actions()) != 0 {
|
||||||
|
t.Errorf("expected no action to be taken")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||||
|
"k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
"k8s.io/kubernetes/pkg/apis/resource"
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
deviceclassstore "k8s.io/kubernetes/pkg/registry/resource/deviceclass/storage"
|
deviceclassstore "k8s.io/kubernetes/pkg/registry/resource/deviceclass/storage"
|
||||||
@ -35,20 +36,22 @@ import (
|
|||||||
// feature gate because it might be useful to provide access to these resources
|
// feature gate because it might be useful to provide access to these resources
|
||||||
// while their feature is off to allow cleaning them up.
|
// while their feature is off to allow cleaning them up.
|
||||||
|
|
||||||
type RESTStorageProvider struct{}
|
type RESTStorageProvider struct {
|
||||||
|
NamespaceClient v1.NamespaceInterface
|
||||||
|
}
|
||||||
|
|
||||||
func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error) {
|
func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error) {
|
||||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(resource.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs)
|
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(resource.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs)
|
||||||
// If you add a version here, be sure to add an entry in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go with specific priorities.
|
// If you add a version here, be sure to add an entry in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go with specific priorities.
|
||||||
// TODO refactor the plumbing to provide the information in the APIGroupInfo
|
// TODO refactor the plumbing to provide the information in the APIGroupInfo
|
||||||
|
|
||||||
if storageMap, err := p.v1alpha3Storage(apiResourceConfigSource, restOptionsGetter); err != nil {
|
if storageMap, err := p.v1alpha3Storage(apiResourceConfigSource, restOptionsGetter, p.NamespaceClient); err != nil {
|
||||||
return genericapiserver.APIGroupInfo{}, err
|
return genericapiserver.APIGroupInfo{}, err
|
||||||
} else if len(storageMap) > 0 {
|
} else if len(storageMap) > 0 {
|
||||||
apiGroupInfo.VersionedResourcesStorageMap[resourcev1alpha3.SchemeGroupVersion.Version] = storageMap
|
apiGroupInfo.VersionedResourcesStorageMap[resourcev1alpha3.SchemeGroupVersion.Version] = storageMap
|
||||||
}
|
}
|
||||||
|
|
||||||
if storageMap, err := p.v1beta1Storage(apiResourceConfigSource, restOptionsGetter); err != nil {
|
if storageMap, err := p.v1beta1Storage(apiResourceConfigSource, restOptionsGetter, p.NamespaceClient); err != nil {
|
||||||
return genericapiserver.APIGroupInfo{}, err
|
return genericapiserver.APIGroupInfo{}, err
|
||||||
} else if len(storageMap) > 0 {
|
} else if len(storageMap) > 0 {
|
||||||
apiGroupInfo.VersionedResourcesStorageMap[resourcev1beta1.SchemeGroupVersion.Version] = storageMap
|
apiGroupInfo.VersionedResourcesStorageMap[resourcev1beta1.SchemeGroupVersion.Version] = storageMap
|
||||||
@ -57,7 +60,7 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag
|
|||||||
return apiGroupInfo, nil
|
return apiGroupInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
|
func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter, nsClient v1.NamespaceInterface) (map[string]rest.Storage, error) {
|
||||||
storage := map[string]rest.Storage{}
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
if resource := "deviceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
|
if resource := "deviceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
|
||||||
@ -69,7 +72,7 @@ func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstora
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
|
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
|
||||||
resourceClaimStorage, resourceClaimStatusStorage, err := resourceclaimstore.NewREST(restOptionsGetter)
|
resourceClaimStorage, resourceClaimStatusStorage, err := resourceclaimstore.NewREST(restOptionsGetter, nsClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -78,7 +81,7 @@ func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstora
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resource := "resourceclaimtemplates"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
|
if resource := "resourceclaimtemplates"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha3.SchemeGroupVersion.WithResource(resource)) {
|
||||||
resourceClaimTemplateStorage, err := resourceclaimtemplatestore.NewREST(restOptionsGetter)
|
resourceClaimTemplateStorage, err := resourceclaimtemplatestore.NewREST(restOptionsGetter, nsClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -96,7 +99,7 @@ func (p RESTStorageProvider) v1alpha3Storage(apiResourceConfigSource serverstora
|
|||||||
return storage, nil
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
|
func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter, nsClient v1.NamespaceInterface) (map[string]rest.Storage, error) {
|
||||||
storage := map[string]rest.Storage{}
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
if resource := "deviceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1beta1.SchemeGroupVersion.WithResource(resource)) {
|
if resource := "deviceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1beta1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
@ -108,7 +111,7 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1beta1.SchemeGroupVersion.WithResource(resource)) {
|
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1beta1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
resourceClaimStorage, resourceClaimStatusStorage, err := resourceclaimstore.NewREST(restOptionsGetter)
|
resourceClaimStorage, resourceClaimStatusStorage, err := resourceclaimstore.NewREST(restOptionsGetter, nsClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -117,7 +120,7 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resource := "resourceclaimtemplates"; apiResourceConfigSource.ResourceEnabled(resourcev1beta1.SchemeGroupVersion.WithResource(resource)) {
|
if resource := "resourceclaimtemplates"; apiResourceConfigSource.ResourceEnabled(resourcev1beta1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
resourceClaimTemplateStorage, err := resourceclaimtemplatestore.NewREST(restOptionsGetter)
|
resourceClaimTemplateStorage, err := resourceclaimtemplatestore.NewREST(restOptionsGetter, nsClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
99
pkg/registry/resource/utils.go
Normal file
99
pkg/registry/resource/utils.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
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 resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthorizedForAdmin checks if the request is authorized to get admin access to devices
|
||||||
|
// based on namespace label
|
||||||
|
func AuthorizedForAdmin(ctx context.Context, deviceRequests []resource.DeviceRequest, namespaceName string, nsClient v1.NamespaceInterface) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
adminRequested := false
|
||||||
|
var adminAccessPath *field.Path
|
||||||
|
|
||||||
|
// no need to check old request since spec is immutable
|
||||||
|
|
||||||
|
for i := range deviceRequests {
|
||||||
|
value := deviceRequests[i].AdminAccess
|
||||||
|
if value != nil && *value {
|
||||||
|
adminRequested = true
|
||||||
|
adminAccessPath = field.NewPath("spec", "devices", "requests").Index(i).Child("adminAccess")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !adminRequested {
|
||||||
|
// No need to validate unless admin access is requested
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the namespace object from the store
|
||||||
|
ns, err := nsClient.Get(ctx, namespaceName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return append(allErrs, field.InternalError(adminAccessPath, fmt.Errorf("could not retrieve namespace to verify admin access: %w", err)))
|
||||||
|
}
|
||||||
|
if ns.Labels[resource.DRAAdminNamespaceLabelKey] != "true" {
|
||||||
|
return append(allErrs, field.Forbidden(adminAccessPath, fmt.Sprintf("admin access to devices requires the `%s: true` label on the containing namespace", resource.DRAAdminNamespaceLabelKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizedForAdminStatus checks if the request status is authorized to get admin access to devices
|
||||||
|
// based on namespace label
|
||||||
|
func AuthorizedForAdminStatus(ctx context.Context, newAllocationResult, oldAllocationResult []resource.DeviceRequestAllocationResult, namespaceName string, nsClient v1.NamespaceInterface) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
var adminAccessPath *field.Path
|
||||||
|
|
||||||
|
if wasGranted, _ := adminRequested(oldAllocationResult); wasGranted {
|
||||||
|
// No need to validate if old status has admin access granted, since status.Allocation is immutable
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
isRequested, adminAccessPath := adminRequested(newAllocationResult)
|
||||||
|
if !isRequested {
|
||||||
|
// No need to validate unless admin access is requested
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the namespace object from the store
|
||||||
|
ns, err := nsClient.Get(ctx, namespaceName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return append(allErrs, field.InternalError(adminAccessPath, fmt.Errorf("could not retrieve namespace to verify admin access: %w", err)))
|
||||||
|
}
|
||||||
|
if ns.Labels[resource.DRAAdminNamespaceLabelKey] != "true" {
|
||||||
|
return append(allErrs, field.Forbidden(adminAccessPath, fmt.Sprintf("admin access to devices requires the `%s: true` label on the containing namespace", resource.DRAAdminNamespaceLabelKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminRequested(deviceRequestResults []resource.DeviceRequestAllocationResult) (bool, *field.Path) {
|
||||||
|
for i := range deviceRequestResults {
|
||||||
|
value := deviceRequestResults[i].AdminAccess
|
||||||
|
if value != nil && *value {
|
||||||
|
return true, field.NewPath("status", "allocation", "devices", "results").Index(i).Child("adminAccess")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
@ -383,6 +383,15 @@ const (
|
|||||||
DeviceConfigMaxSize = 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.
|
// DeviceRequest is a request for devices required for a claim.
|
||||||
// This is typically a request for a single resource like a device, but can
|
// 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.
|
||||||
|
@ -391,6 +391,15 @@ const (
|
|||||||
DeviceConfigMaxSize = 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.
|
// DeviceRequest is a request for devices required for a claim.
|
||||||
// This is typically a request for a single resource like a device, but can
|
// 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.
|
||||||
|
@ -32,7 +32,6 @@ import (
|
|||||||
"github.com/onsi/gomega/gstruct"
|
"github.com/onsi/gomega/gstruct"
|
||||||
"github.com/onsi/gomega/types"
|
"github.com/onsi/gomega/types"
|
||||||
|
|
||||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
resourceapi "k8s.io/api/resource/v1beta1"
|
resourceapi "k8s.io/api/resource/v1beta1"
|
||||||
@ -59,9 +58,6 @@ const (
|
|||||||
podStartTimeout = 5 * time.Minute
|
podStartTimeout = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed test-driver/deploy/example/admin-access-policy.yaml
|
|
||||||
var adminAccessPolicyYAML string
|
|
||||||
|
|
||||||
// networkResources can be passed to NewDriver directly.
|
// networkResources can be passed to NewDriver directly.
|
||||||
func networkResources() Resources {
|
func networkResources() Resources {
|
||||||
return Resources{}
|
return Resources{}
|
||||||
@ -1379,30 +1375,13 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation,
|
|||||||
driver := NewDriver(f, nodes, networkResources)
|
driver := NewDriver(f, nodes, networkResources)
|
||||||
b := newBuilder(f, driver)
|
b := newBuilder(f, driver)
|
||||||
|
|
||||||
f.It("support validating admission policy for admin access", feature.DRAAdminAccess, framework.WithFeatureGate(features.DRAAdminAccess), framework.WithFeatureGate(features.DynamicResourceAllocation), func(ctx context.Context) {
|
f.It("validate ResourceClaimTemplate and ResourceClaim for admin access", feature.DRAAdminAccess, framework.WithFeatureGate(features.DRAAdminAccess), framework.WithFeatureGate(features.DynamicResourceAllocation), func(ctx context.Context) {
|
||||||
// Create VAP, after making it unique to the current test.
|
|
||||||
adminAccessPolicyYAML := strings.ReplaceAll(adminAccessPolicyYAML, "dra.example.com", b.f.UniqueName)
|
|
||||||
driver.createFromYAML(ctx, []byte(adminAccessPolicyYAML), "")
|
|
||||||
|
|
||||||
// Wait for both VAPs to be processed. This ensures that there are no check errors in the status.
|
|
||||||
matchStatus := gomega.Equal(admissionregistrationv1.ValidatingAdmissionPolicyStatus{ObservedGeneration: 1, TypeChecking: &admissionregistrationv1.TypeChecking{}})
|
|
||||||
gomega.Eventually(ctx, framework.ListObjects(b.f.ClientSet.AdmissionregistrationV1().ValidatingAdmissionPolicies().List, metav1.ListOptions{})).Should(gomega.HaveField("Items", gomega.ContainElements(
|
|
||||||
gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
|
|
||||||
"ObjectMeta": gomega.HaveField("Name", "resourceclaim-policy."+b.f.UniqueName),
|
|
||||||
"Status": matchStatus,
|
|
||||||
}),
|
|
||||||
gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
|
|
||||||
"ObjectMeta": gomega.HaveField("Name", "resourceclaimtemplate-policy."+b.f.UniqueName),
|
|
||||||
"Status": matchStatus,
|
|
||||||
}),
|
|
||||||
)))
|
|
||||||
|
|
||||||
// Attempt to create claim and claim template with admin access. Must fail eventually.
|
// Attempt to create claim and claim template with admin access. Must fail eventually.
|
||||||
claim := b.externalClaim()
|
claim := b.externalClaim()
|
||||||
claim.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
claim.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
||||||
_, claimTemplate := b.podInline()
|
_, claimTemplate := b.podInline()
|
||||||
claimTemplate.Spec.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
claimTemplate.Spec.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
||||||
matchVAPError := gomega.MatchError(gomega.ContainSubstring("admin access to devices not enabled in namespace " + b.f.Namespace.Name))
|
matchValidationError := gomega.MatchError(gomega.ContainSubstring("admin access to devices requires the `resource.k8s.io/admin-access: true` label on the containing namespace"))
|
||||||
gomega.Eventually(ctx, func(ctx context.Context) error {
|
gomega.Eventually(ctx, func(ctx context.Context) error {
|
||||||
// First delete, in case that it succeeded earlier.
|
// First delete, in case that it succeeded earlier.
|
||||||
if err := b.f.ClientSet.ResourceV1beta1().ResourceClaims(b.f.Namespace.Name).Delete(ctx, claim.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
if err := b.f.ClientSet.ResourceV1beta1().ResourceClaims(b.f.Namespace.Name).Delete(ctx, claim.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||||
@ -1410,7 +1389,7 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation,
|
|||||||
}
|
}
|
||||||
_, err := b.f.ClientSet.ResourceV1beta1().ResourceClaims(b.f.Namespace.Name).Create(ctx, claim, metav1.CreateOptions{})
|
_, err := b.f.ClientSet.ResourceV1beta1().ResourceClaims(b.f.Namespace.Name).Create(ctx, claim, metav1.CreateOptions{})
|
||||||
return err
|
return err
|
||||||
}).Should(matchVAPError)
|
}).Should(matchValidationError)
|
||||||
|
|
||||||
gomega.Eventually(ctx, func(ctx context.Context) error {
|
gomega.Eventually(ctx, func(ctx context.Context) error {
|
||||||
// First delete, in case that it succeeded earlier.
|
// First delete, in case that it succeeded earlier.
|
||||||
@ -1419,11 +1398,11 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation,
|
|||||||
}
|
}
|
||||||
_, err := b.f.ClientSet.ResourceV1beta1().ResourceClaimTemplates(b.f.Namespace.Name).Create(ctx, claimTemplate, metav1.CreateOptions{})
|
_, err := b.f.ClientSet.ResourceV1beta1().ResourceClaimTemplates(b.f.Namespace.Name).Create(ctx, claimTemplate, metav1.CreateOptions{})
|
||||||
return err
|
return err
|
||||||
}).Should(matchVAPError)
|
}).Should(matchValidationError)
|
||||||
|
|
||||||
// After labeling the namespace, creation must (eventually...) succeed.
|
// After labeling the namespace, creation must (eventually...) succeed.
|
||||||
_, err := b.f.ClientSet.CoreV1().Namespaces().Apply(ctx,
|
_, err := b.f.ClientSet.CoreV1().Namespaces().Apply(ctx,
|
||||||
applyv1.Namespace(b.f.Namespace.Name).WithLabels(map[string]string{"admin-access." + b.f.UniqueName: "on"}),
|
applyv1.Namespace(b.f.Namespace.Name).WithLabels(map[string]string{"resource.k8s.io/admin-access": "true"}),
|
||||||
metav1.ApplyOptions{FieldManager: b.f.UniqueName})
|
metav1.ApplyOptions{FieldManager: b.f.UniqueName})
|
||||||
framework.ExpectNoError(err)
|
framework.ExpectNoError(err)
|
||||||
gomega.Eventually(ctx, func(ctx context.Context) error {
|
gomega.Eventually(ctx, func(ctx context.Context) error {
|
||||||
@ -1499,6 +1478,12 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation,
|
|||||||
})
|
})
|
||||||
|
|
||||||
f.It("DaemonSet with admin access", feature.DRAAdminAccess, framework.WithFeatureGate(features.DRAAdminAccess), framework.WithFeatureGate(features.DynamicResourceAllocation), func(ctx context.Context) {
|
f.It("DaemonSet with admin access", feature.DRAAdminAccess, framework.WithFeatureGate(features.DRAAdminAccess), framework.WithFeatureGate(features.DynamicResourceAllocation), func(ctx context.Context) {
|
||||||
|
// Ensure namespace has the dra admin label.
|
||||||
|
_, err := b.f.ClientSet.CoreV1().Namespaces().Apply(ctx,
|
||||||
|
applyv1.Namespace(b.f.Namespace.Name).WithLabels(map[string]string{"resource.k8s.io/admin-access": "true"}),
|
||||||
|
metav1.ApplyOptions{FieldManager: b.f.UniqueName})
|
||||||
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
pod, template := b.podInline()
|
pod, template := b.podInline()
|
||||||
template.Spec.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
template.Spec.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
||||||
// Limit the daemon set to the one node where we have the driver.
|
// Limit the daemon set to the one node where we have the driver.
|
||||||
@ -1507,7 +1492,8 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation,
|
|||||||
pod.Spec.RestartPolicy = v1.RestartPolicyAlways
|
pod.Spec.RestartPolicy = v1.RestartPolicyAlways
|
||||||
daemonSet := &appsv1.DaemonSet{
|
daemonSet := &appsv1.DaemonSet{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "monitoring-ds",
|
Name: "monitoring-ds",
|
||||||
|
Namespace: b.f.Namespace.Name,
|
||||||
},
|
},
|
||||||
Spec: appsv1.DaemonSetSpec{
|
Spec: appsv1.DaemonSetSpec{
|
||||||
Selector: &metav1.LabelSelector{
|
Selector: &metav1.LabelSelector{
|
||||||
|
@ -87,7 +87,7 @@ var (
|
|||||||
// - Non-alpha-numeric characters replaced by hyphen.
|
// - Non-alpha-numeric characters replaced by hyphen.
|
||||||
// - Truncated in the middle to make it short enough for GenerateName.
|
// - Truncated in the middle to make it short enough for GenerateName.
|
||||||
// - Hyphen plus random suffix added by the apiserver.
|
// - Hyphen plus random suffix added by the apiserver.
|
||||||
func createTestNamespace(tCtx ktesting.TContext) string {
|
func createTestNamespace(tCtx ktesting.TContext, labels map[string]string) string {
|
||||||
tCtx.Helper()
|
tCtx.Helper()
|
||||||
name := regexp.MustCompile(`[^[:alnum:]_-]`).ReplaceAllString(tCtx.Name(), "-")
|
name := regexp.MustCompile(`[^[:alnum:]_-]`).ReplaceAllString(tCtx.Name(), "-")
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
@ -95,6 +95,7 @@ func createTestNamespace(tCtx ktesting.TContext) string {
|
|||||||
name = name[:30] + "--" + name[len(name)-30:]
|
name = name[:30] + "--" + name[len(name)-30:]
|
||||||
}
|
}
|
||||||
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: name + "-"}}
|
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: name + "-"}}
|
||||||
|
ns.Labels = labels
|
||||||
ns, err := tCtx.Client().CoreV1().Namespaces().Create(tCtx, ns, metav1.CreateOptions{})
|
ns, err := tCtx.Client().CoreV1().Namespaces().Create(tCtx, ns, metav1.CreateOptions{})
|
||||||
tCtx.ExpectNoError(err, "create test namespace")
|
tCtx.ExpectNoError(err, "create test namespace")
|
||||||
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
|
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
|
||||||
@ -211,7 +212,7 @@ func newDefaultSchedulerComponentConfig(tCtx ktesting.TContext) *config.KubeSche
|
|||||||
// whether that field is or isn't getting dropped.
|
// whether that field is or isn't getting dropped.
|
||||||
func testPod(tCtx ktesting.TContext, draEnabled bool) {
|
func testPod(tCtx ktesting.TContext, draEnabled bool) {
|
||||||
tCtx.Parallel()
|
tCtx.Parallel()
|
||||||
namespace := createTestNamespace(tCtx)
|
namespace := createTestNamespace(tCtx, nil)
|
||||||
podWithClaimName := podWithClaimName.DeepCopy()
|
podWithClaimName := podWithClaimName.DeepCopy()
|
||||||
podWithClaimName.Namespace = namespace
|
podWithClaimName.Namespace = namespace
|
||||||
pod, err := tCtx.Client().CoreV1().Pods(namespace).Create(tCtx, podWithClaimName, metav1.CreateOptions{})
|
pod, err := tCtx.Client().CoreV1().Pods(namespace).Create(tCtx, podWithClaimName, metav1.CreateOptions{})
|
||||||
@ -235,7 +236,7 @@ func testAPIDisabled(tCtx ktesting.TContext) {
|
|||||||
// testConvert creates a claim using a one API version and reads it with another.
|
// testConvert creates a claim using a one API version and reads it with another.
|
||||||
func testConvert(tCtx ktesting.TContext) {
|
func testConvert(tCtx ktesting.TContext) {
|
||||||
tCtx.Parallel()
|
tCtx.Parallel()
|
||||||
namespace := createTestNamespace(tCtx)
|
namespace := createTestNamespace(tCtx, nil)
|
||||||
claim := claim.DeepCopy()
|
claim := claim.DeepCopy()
|
||||||
claim.Namespace = namespace
|
claim.Namespace = namespace
|
||||||
claim, err := tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
|
claim, err := tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
|
||||||
@ -248,17 +249,34 @@ func testConvert(tCtx ktesting.TContext) {
|
|||||||
|
|
||||||
// testAdminAccess creates a claim with AdminAccess and then checks
|
// testAdminAccess creates a claim with AdminAccess and then checks
|
||||||
// whether that field is or isn't getting dropped.
|
// whether that field is or isn't getting dropped.
|
||||||
|
// when the AdminAccess feature is enabled, it also checks that the field
|
||||||
|
// is only allowed to be used in namespace with the Resource Admin Access label
|
||||||
func testAdminAccess(tCtx ktesting.TContext, adminAccessEnabled bool) {
|
func testAdminAccess(tCtx ktesting.TContext, adminAccessEnabled bool) {
|
||||||
tCtx.Parallel()
|
namespace := createTestNamespace(tCtx, nil)
|
||||||
namespace := createTestNamespace(tCtx)
|
claim1 := claim.DeepCopy()
|
||||||
claim := claim.DeepCopy()
|
claim1.Namespace = namespace
|
||||||
claim.Namespace = namespace
|
claim1.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
||||||
claim.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
// create claim with AdminAccess in non-admin namespace
|
||||||
claim, err := tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
|
_, err := tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim1, metav1.CreateOptions{})
|
||||||
tCtx.ExpectNoError(err, "create claim")
|
|
||||||
if adminAccessEnabled {
|
if adminAccessEnabled {
|
||||||
if !ptr.Deref(claim.Spec.Devices.Requests[0].AdminAccess, false) {
|
if err != nil {
|
||||||
tCtx.Fatal("should store AdminAccess in ResourceClaim")
|
// should result in validation error
|
||||||
|
assert.ErrorContains(tCtx, err, "admin access to devices requires the `resource.k8s.io/admin-access: true` label on the containing namespace", "the error message should have contained the expected error message")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
tCtx.Fatal("expected validation error(s), got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create claim with AdminAccess in admin namespace
|
||||||
|
adminNS := createTestNamespace(tCtx, map[string]string{"resource.k8s.io/admin-access": "true"})
|
||||||
|
claim2 := claim.DeepCopy()
|
||||||
|
claim2.Namespace = adminNS
|
||||||
|
claim2.Name = "claim2"
|
||||||
|
claim2.Spec.Devices.Requests[0].AdminAccess = ptr.To(true)
|
||||||
|
claim2, err := tCtx.Client().ResourceV1beta1().ResourceClaims(adminNS).Create(tCtx, claim2, metav1.CreateOptions{})
|
||||||
|
tCtx.ExpectNoError(err, "create claim")
|
||||||
|
if !ptr.Deref(claim2.Spec.Devices.Requests[0].AdminAccess, true) {
|
||||||
|
tCtx.Fatalf("should store AdminAccess in ResourceClaim %v", claim2)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if claim.Spec.Devices.Requests[0].AdminAccess != nil {
|
if claim.Spec.Devices.Requests[0].AdminAccess != nil {
|
||||||
@ -271,7 +289,7 @@ func testPrioritizedList(tCtx ktesting.TContext, enabled bool) {
|
|||||||
tCtx.Parallel()
|
tCtx.Parallel()
|
||||||
_, err := tCtx.Client().ResourceV1beta1().DeviceClasses().Create(tCtx, class, metav1.CreateOptions{})
|
_, err := tCtx.Client().ResourceV1beta1().DeviceClasses().Create(tCtx, class, metav1.CreateOptions{})
|
||||||
tCtx.ExpectNoError(err, "create class")
|
tCtx.ExpectNoError(err, "create class")
|
||||||
namespace := createTestNamespace(tCtx)
|
namespace := createTestNamespace(tCtx, nil)
|
||||||
claim := claimPrioritizedList.DeepCopy()
|
claim := claimPrioritizedList.DeepCopy()
|
||||||
claim.Namespace = namespace
|
claim.Namespace = namespace
|
||||||
claim, err = tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
|
claim, err = tCtx.Client().ResourceV1beta1().ResourceClaims(namespace).Create(tCtx, claim, metav1.CreateOptions{})
|
||||||
|
Loading…
Reference in New Issue
Block a user