mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 18:09:58 +00:00
562 lines
17 KiB
Go
562 lines
17 KiB
Go
// Copyright 2020-2023 Project Capsule Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api"
|
|
"github.com/projectcapsule/capsule/pkg/utils"
|
|
resources "k8s.io/api/resource/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/selection"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
var _ = Describe("when Tenant handles Device classes", Label("tenant", "classes", "device"), func() {
|
|
erm := "nvidia.com/gpu"
|
|
authorized := &resources.DeviceClass{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "gpu.example.com",
|
|
Labels: map[string]string{
|
|
"env": "authorized",
|
|
},
|
|
},
|
|
Spec: resources.DeviceClassSpec{
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
ExtendedResourceName: &erm,
|
|
},
|
|
}
|
|
authorized2 := &resources.DeviceClass{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "gpu2.example.com",
|
|
Labels: map[string]string{
|
|
"env": "authorized",
|
|
},
|
|
},
|
|
Spec: resources.DeviceClassSpec{
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
ExtendedResourceName: &erm,
|
|
},
|
|
}
|
|
unauthorized := &resources.DeviceClass{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "gpu3.example.com",
|
|
Labels: map[string]string{
|
|
"env": "unauthorized",
|
|
},
|
|
},
|
|
Spec: resources.DeviceClassSpec{
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
ExtendedResourceName: &erm,
|
|
},
|
|
}
|
|
|
|
tntWithAuthorized := &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "e2e-authorized-deviceclass",
|
|
},
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
Owners: []api.OwnerSpec{
|
|
{
|
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
|
UserSpec: api.UserSpec{
|
|
Name: "authorized-deviceclass",
|
|
Kind: "User",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DeviceClasses: &api.SelectorAllowedListSpec{
|
|
LabelSelector: v1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"env": "authorized",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
tntWithUnauthorized := &capsulev1beta2.Tenant{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "e2e-unauthorized-deviceclass",
|
|
},
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
Owners: []api.OwnerSpec{
|
|
{
|
|
CoreOwnerSpec: api.CoreOwnerSpec{
|
|
UserSpec: api.UserSpec{
|
|
Name: "unauthorized-deviceclass",
|
|
Kind: "User",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
DeviceClasses: &api.SelectorAllowedListSpec{
|
|
LabelSelector: v1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"env": "production",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
JustBeforeEach(func() {
|
|
for _, tnt := range []*capsulev1beta2.Tenant{tntWithAuthorized, tntWithUnauthorized} {
|
|
tnt.ResourceVersion = ""
|
|
EventuallyCreation(func() error {
|
|
return k8sClient.Create(context.TODO(), tnt)
|
|
}).Should(Succeed())
|
|
}
|
|
|
|
if err := k8sClient.List(context.Background(), &resources.DeviceClassList{}); err != nil {
|
|
if utils.IsUnsupportedAPI(err) {
|
|
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
|
|
}
|
|
}
|
|
|
|
for _, crd := range []*resources.DeviceClass{authorized, authorized2, unauthorized} {
|
|
crd.ResourceVersion = ""
|
|
EventuallyCreation(func() error {
|
|
return k8sClient.Create(context.TODO(), crd)
|
|
}).Should(Succeed())
|
|
}
|
|
})
|
|
JustAfterEach(func() {
|
|
for _, tnt := range []*capsulev1beta2.Tenant{tntWithAuthorized, tntWithUnauthorized} {
|
|
EventuallyCreation(func() error {
|
|
return ignoreNotFound(k8sClient.Delete(context.TODO(), tnt))
|
|
}).Should(Succeed())
|
|
}
|
|
|
|
if err := k8sClient.List(context.Background(), &resources.DeviceClassList{}); err != nil {
|
|
if utils.IsUnsupportedAPI(err) {
|
|
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
|
|
}
|
|
}
|
|
|
|
Eventually(func() (err error) {
|
|
req, _ := labels.NewRequirement("env", selection.Exists, nil)
|
|
|
|
return k8sClient.DeleteAllOf(context.TODO(), &resources.DeviceClass{}, &client.DeleteAllOfOptions{
|
|
ListOptions: client.ListOptions{
|
|
LabelSelector: labels.NewSelector().Add(*req),
|
|
},
|
|
})
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
})
|
|
It("ResourceClaims", func() {
|
|
if err := k8sClient.List(context.Background(), &resources.DeviceClassList{}); err != nil {
|
|
if utils.IsUnsupportedAPI(err) {
|
|
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
|
|
}
|
|
}
|
|
|
|
By("Verify Status (Creation)", func() {
|
|
Eventually(func() ([]string, error) {
|
|
t := &capsulev1beta2.Tenant{}
|
|
if err := k8sClient.Get(
|
|
context.TODO(),
|
|
types.NamespacedName{Name: tntWithAuthorized.GetName()},
|
|
t,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return t.Status.Classes.DeviceClasses, nil
|
|
}, defaultTimeoutInterval, defaultPollInterval).
|
|
Should(ConsistOf(authorized.GetName(), authorized2.GetName()))
|
|
})
|
|
|
|
ns := NewNamespace("")
|
|
NamespaceCreation(ns, tntWithAuthorized.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
TenantNamespaceList(tntWithAuthorized, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
|
|
|
By("providing authorized device class", func() {
|
|
for _, class := range []*resources.DeviceClass{authorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "authorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
})
|
|
|
|
By("providing unauthorized device class", func() {
|
|
for _, class := range []*resources.DeviceClass{unauthorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "unauthorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu3.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
|
}
|
|
})
|
|
|
|
By("providing non-existent device class", func() {
|
|
for _, class := range []*resources.DeviceClass{unauthorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaim{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "missing-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu53.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
|
}
|
|
})
|
|
|
|
By("Verify Status (Deletion)", func() {
|
|
for _, class := range []*resources.DeviceClass{authorized} {
|
|
Expect(ignoreNotFound(k8sClient.Delete(context.TODO(), class))).To(Succeed())
|
|
}
|
|
|
|
Eventually(func() ([]string, error) {
|
|
t := &capsulev1beta2.Tenant{}
|
|
if err := k8sClient.Get(
|
|
context.TODO(),
|
|
types.NamespacedName{Name: tntWithAuthorized.GetName()},
|
|
t,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return t.Status.Classes.DeviceClasses, nil
|
|
}, defaultTimeoutInterval, defaultPollInterval).
|
|
ShouldNot(ConsistOf(authorized.GetName(), authorized2.GetName()))
|
|
})
|
|
})
|
|
It("ResourceClaimTemplates", func() {
|
|
if err := k8sClient.List(context.Background(), &resources.DeviceClassList{}); err != nil {
|
|
if utils.IsUnsupportedAPI(err) {
|
|
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
|
|
}
|
|
}
|
|
|
|
ns := NewNamespace("")
|
|
NamespaceCreation(ns, tntWithAuthorized.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
|
|
TenantNamespaceList(tntWithAuthorized, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
|
|
|
By("providing authorized device class", func() {
|
|
for _, class := range []*resources.DeviceClass{authorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimTemplateSpec{
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "authorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
|
}
|
|
})
|
|
|
|
By("providing unauthorized device class", func() {
|
|
for _, class := range []*resources.DeviceClass{unauthorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimTemplateSpec{
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "unauthorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu3.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
|
}
|
|
})
|
|
|
|
By("providing both authorized and unauthorized device classes", func() {
|
|
for _, class := range []*resources.DeviceClass{unauthorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimTemplateSpec{
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "unauthorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu3.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
|
}
|
|
})
|
|
|
|
By("providing authorized and missing device classes", func() {
|
|
for _, class := range []*resources.DeviceClass{unauthorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimTemplateSpec{
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "missing-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu63.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
|
}
|
|
})
|
|
|
|
By("providing two authorized device classes", func() {
|
|
for _, class := range []*resources.DeviceClass{unauthorized} {
|
|
Eventually(func() (err error) {
|
|
g := &resources.ResourceClaimTemplate{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: class.GetName() + "-resource-claim",
|
|
Namespace: ns.GetName(),
|
|
},
|
|
Spec: resources.ResourceClaimTemplateSpec{
|
|
Spec: resources.ResourceClaimSpec{
|
|
Devices: resources.DeviceClaim{
|
|
Requests: []resources.DeviceRequest{
|
|
{
|
|
Name: "unauthorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu2.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized-device-class-resource-claim",
|
|
Exactly: &resources.ExactDeviceRequest{
|
|
DeviceClassName: "gpu.example.com",
|
|
Selectors: []resources.DeviceSelector{
|
|
{
|
|
CEL: &resources.CELDeviceSelector{
|
|
Expression: "device.driver == 'gpu.example.com' && device.attributes['gpu.example.com'].type == 'gpu'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err = k8sClient.Create(context.TODO(), g)
|
|
return
|
|
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
|
}
|
|
})
|
|
})
|
|
})
|