fix(controller): make device and gateway class optional (#1775)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2025-12-09 07:54:30 +01:00
committed by GitHub
parent f28ac63398
commit 936a152d39
6 changed files with 161 additions and 38 deletions

View File

@@ -45,10 +45,10 @@ jobs:
fail-fast: false
matrix:
k8s-version:
- '1.30.0'
- '1.31.0'
- '1.32.0'
- '1.33.0'
- 'v1.30.0'
- 'v1.31.0'
- 'v1.32.0'
- 'v1.33.0'
runs-on:
labels: ubuntu-latest-8-cores
steps:
@@ -64,4 +64,4 @@ jobs:
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
- name: e2e (Enterprise)
run: KUBERNETES_SUPPORTED_VERSION=${{ matrix.k8s-version }} sudo make e2e
run: sudo KUBERNETES_SUPPORTED_VERSION=${{ matrix.k8s-version }} make e2e

View File

@@ -5,11 +5,13 @@ 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"
@@ -132,6 +134,13 @@ var _ = Describe("when Tenant handles Device classes", Label("tenant", "classes"
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 {
@@ -146,6 +155,12 @@ var _ = Describe("when Tenant handles Device classes", Label("tenant", "classes"
}).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)
@@ -157,6 +172,12 @@ var _ = Describe("when Tenant handles Device classes", Label("tenant", "classes"
}, 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{}
@@ -303,6 +324,11 @@ var _ = Describe("when Tenant handles Device classes", Label("tenant", "classes"
})
})
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())

View File

@@ -5,6 +5,7 @@ package e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -21,6 +22,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/utils"
)
var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes", "gateway"), func() {
@@ -156,13 +158,21 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
}
JustBeforeEach(func() {
utilruntime.Must(gatewayv1.Install(scheme.Scheme))
for _, tnt := range []*capsulev1beta2.Tenant{tntWithDefault, tntWithoutDefault, tntNoRestrictions} {
tnt.ResourceVersion = ""
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
}
if err := k8sClient.List(context.Background(), &gatewayv1.GatewayClassList{}); err != nil {
if utils.IsUnsupportedAPI(err) {
Skip(fmt.Sprintf("Running test due to unsupported API kind: %s", err.Error()))
}
}
utilruntime.Must(gatewayv1.Install(scheme.Scheme))
for _, crd := range []*gatewayv1.GatewayClass{authorized, unauthorized, exact, exactU} {
Eventually(func() error {
crd.ResourceVersion = ""
@@ -178,6 +188,12 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
}).Should(Succeed())
}
if err := k8sClient.List(context.Background(), &gatewayv1.GatewayClassList{}); 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)
@@ -189,6 +205,12 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("should allow all classes", func() {
if err := k8sClient.List(context.Background(), &gatewayv1.GatewayClassList{}); 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{}
@@ -281,6 +303,12 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
})
It("should block Gateway", func() {
if err := k8sClient.List(context.Background(), &gatewayv1.GatewayClassList{}); 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{}
@@ -391,6 +419,12 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
})
It("should allow Gateway", func() {
if err := k8sClient.List(context.Background(), &gatewayv1.GatewayClassList{}); 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{}
@@ -483,6 +517,12 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes
})
})
It("should fail on invalid configuration", func() {
if err := k8sClient.List(context.Background(), &gatewayv1.GatewayClassList{}); 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{}

View File

@@ -12,11 +12,12 @@ import (
networkingv1 "k8s.io/api/networking/v1"
nodev1 "k8s.io/api/node/v1"
rbacv1 "k8s.io/api/rbac/v1"
resources "k8s.io/api/resource/v1"
resourcesv1 "k8s.io/api/resource/v1"
schedulingv1 "k8s.io/api/scheduling/v1"
storagev1 "k8s.io/api/storage/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/client-go/rest"
@@ -49,10 +50,16 @@ type Manager struct {
Recorder record.EventRecorder
Configuration configuration.Configuration
RESTConfig *rest.Config
classes supportedClasses
}
type supportedClasses struct {
device bool
gateway bool
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
return ctrl.NewControllerManagedBy(mgr).
ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
For(
&capsulev1beta2.Tenant{},
builder.WithPredicates(
@@ -73,15 +80,6 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
&corev1.Namespace{},
handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &capsulev1beta2.Tenant{}),
).
Watches(
&resources.DeviceClass{},
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableDeviceClasses,
"cannot collect device classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&storagev1.StorageClass{},
r.statusOnlyHandlerClasses(
@@ -91,15 +89,6 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&gatewayv1.GatewayClass{},
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableGatewayClasses,
"cannot collect gateway classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
).
Watches(
&schedulingv1.PriorityClass{},
r.statusOnlyHandlerClasses(
@@ -207,8 +196,47 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
},
builder.WithPredicates(utils.PromotedServiceaccountPredicate),
).
WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles}).
Complete(r)
WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles})
// GatewayClass is Optional
r.classes.gateway = utils.HasGVK(mgr.GetRESTMapper(), schema.GroupVersionKind{
Group: "gateway.networking.k8s.io",
Version: "v1",
Kind: "GatewayClass",
})
if r.classes.gateway {
ctrlBuilder = ctrlBuilder.Watches(
&gatewayv1.GatewayClass{},
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableGatewayClasses,
"cannot collect gateway classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
)
}
// DeviceClass is Optional
r.classes.device = utils.HasGVK(mgr.GetRESTMapper(), schema.GroupVersionKind{
Group: "resource.k8s.io",
Version: "v1",
Kind: "DeviceClass",
})
if r.classes.device {
ctrlBuilder = ctrlBuilder.Watches(
&resourcesv1.DeviceClass{},
r.statusOnlyHandlerClasses(
r.reconcileClassStatus,
r.collectAvailableDeviceClasses,
"cannot collect device classes",
),
builder.WithPredicates(utils.UpdatedMetadataPredicate),
)
}
return ctrlBuilder.Complete(r)
}
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {

View File

@@ -73,14 +73,16 @@ func (r Manager) reconcileClassStatus(
func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
log := log.FromContext(ctx)
log.V(5).Info("collecting available deviceclasses")
if r.classes.device {
log.V(5).Info("collecting available deviceclasses")
if err = r.collectAvailableDeviceClasses(ctx, tnt); err != nil {
return err
if err = r.collectAvailableDeviceClasses(ctx, tnt); err != nil {
return err
}
log.V(5).Info("collected available deviceclasses", "size", len(tnt.Status.Classes.DeviceClasses))
}
log.V(5).Info("collected available deviceclasses", "size", len(tnt.Status.Classes.DeviceClasses))
log.V(5).Info("collecting available storageclasses")
if err = r.collectAvailableStorageClasses(ctx, tnt); err != nil {
@@ -93,14 +95,16 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b
return err
}
log.V(5).Info("collected available priorityclasses", "size", len(tnt.Status.Classes.PriorityClasses))
if r.classes.gateway {
log.V(5).Info("collected available priorityclasses", "size", len(tnt.Status.Classes.PriorityClasses))
if err = r.collectAvailableGatewayClasses(ctx, tnt); err != nil {
return err
if err = r.collectAvailableGatewayClasses(ctx, tnt); err != nil {
return err
}
log.V(5).Info("collected available gatewayclasses", "size", len(tnt.Status.Classes.GatewayClasses))
}
log.V(5).Info("collected available gatewayclasses", "size", len(tnt.Status.Classes.GatewayClasses))
if err = r.collectAvailableRuntimeClasses(ctx, tnt); err != nil {
return err
}

View File

@@ -0,0 +1,25 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package utils
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime"
)
func HasGVK(mapper meta.RESTMapper, gvk schema.GroupVersionKind) bool {
_, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
if meta.IsNoMatchError(err) {
return false
}
ctrl.Log.WithName("gvk-check").Error(err, "failed to check RESTMapping", "gvk", gvk.String())
return false
}
return true
}