mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-05-15 22:06:52 +00:00
feat(tenant): support gateway/class (#1463)
* feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> Co-authored-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> * feat(tenant): support gateway/class Signed-off-by: Hristo Hristov <me@hhristov.info> --------- Signed-off-by: Hristo Hristov <me@hhristov.info> Co-authored-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
@@ -5,6 +5,8 @@ package defaults
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
type StorageClassError struct {
|
||||
@@ -39,6 +41,39 @@ func (e IngressClassError) Error() string {
|
||||
return fmt.Sprintf("Failed to resolve Ingress Class %s: %s", e.ingressClass, e.msg)
|
||||
}
|
||||
|
||||
type GatewayClassError struct {
|
||||
gatewayClass string
|
||||
msg error
|
||||
}
|
||||
|
||||
func NewGatewayClassError(class string, msg error) error {
|
||||
return &GatewayClassError{
|
||||
gatewayClass: class,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e GatewayClassError) Error() string {
|
||||
return fmt.Sprintf("Failed to resolve Gateway Class %s: %s", e.gatewayClass, e.msg)
|
||||
}
|
||||
|
||||
type GatewayError struct {
|
||||
gateway string
|
||||
msg error
|
||||
}
|
||||
|
||||
func NewGatewayError(gateway gatewayv1.ObjectName, msg error) error {
|
||||
|
||||
return &GatewayError{
|
||||
gateway: reflect.ValueOf(gateway).String(),
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (e GatewayError) Error() string {
|
||||
return fmt.Sprintf("Failed to resolve Gateway %s: %s", e.gateway, e.msg)
|
||||
}
|
||||
|
||||
type PriorityClassError struct {
|
||||
priorityClass string
|
||||
msg error
|
||||
|
||||
84
pkg/webhook/defaults/gateway.go
Normal file
84
pkg/webhook/defaults/gateway.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"net/http"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
|
||||
capsulegateway "github.com/projectcapsule/capsule/pkg/webhook/gateway"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
func mutateGatewayDefaults(ctx context.Context, req admission.Request, c client.Client, decoder admission.Decoder, recorder record.EventRecorder, namespce string) *admission.Response {
|
||||
gatewayObj := &gatewayv1.Gateway{}
|
||||
if err := decoder.Decode(req, gatewayObj); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
gatewayObj.SetNamespace(namespce)
|
||||
|
||||
tnt, err := capsulegateway.TenantFromGateway(ctx, c, gatewayObj)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.GatewayOptions.AllowedClasses
|
||||
|
||||
if allowed == nil || allowed.Default == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var mutate bool
|
||||
gatewayClass, err := utils.GetGatewayClassClassByObjectName(ctx, c, gatewayObj.Spec.GatewayClassName)
|
||||
|
||||
if gatewayClass == nil {
|
||||
if gatewayObj.Spec.GatewayClassName == ("") {
|
||||
mutate = true
|
||||
} else {
|
||||
response := admission.Denied(NewGatewayError(gatewayObj.Spec.GatewayClassName, err).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
if gatewayClass != nil && gatewayClass.Name != allowed.Default {
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
response := admission.Denied(NewGatewayClassError(gatewayClass.Name, err).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
} else {
|
||||
mutate = true
|
||||
}
|
||||
if mutate = mutate || (gatewayClass.Name == allowed.Default); !mutate {
|
||||
return nil
|
||||
}
|
||||
|
||||
gatewayObj.Spec.GatewayClassName = gatewayv1.ObjectName(allowed.Default)
|
||||
|
||||
marshaled, err := json.Marshal(gatewayObj)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
recorder.Eventf(tnt, corev1.EventTypeNormal, "TenantDefault", "Assigned Tenant default Gateway Class %s to %s/%s", allowed.Default, gatewayObj.Name, gatewayObj.Namespace)
|
||||
|
||||
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -56,6 +56,8 @@ func (h *handler) mutate(ctx context.Context, req admission.Request, c client.Cl
|
||||
response = mutatePVCDefaults(ctx, req, c, decoder, recorder, req.Namespace)
|
||||
case metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}, metav1.GroupVersionResource{Group: "networking.k8s.io", Version: "v1beta1", Resource: "ingresses"}:
|
||||
response = mutateIngressDefaults(ctx, req, h.version, c, decoder, recorder, req.Namespace)
|
||||
case metav1.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1", Resource: "gateways"}:
|
||||
response = mutateGatewayDefaults(ctx, req, c, decoder, recorder, req.Namespace)
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
|
||||
43
pkg/webhook/gateway/errors.go
Normal file
43
pkg/webhook/gateway/errors.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type gatewayClassForbiddenError struct {
|
||||
gatewayClassName string
|
||||
spec api.SelectionListWithDefaultSpec
|
||||
}
|
||||
|
||||
func NewGatewayClassForbidden(class string, spec api.SelectionListWithDefaultSpec) error {
|
||||
return &gatewayClassForbiddenError{
|
||||
gatewayClassName: class,
|
||||
spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func (i gatewayClassForbiddenError) Error() string {
|
||||
err := fmt.Sprintf("Gateway Class %s is forbidden for the current Tenant: ", i.gatewayClassName)
|
||||
|
||||
return utils.SelectionListWithDefaultErrorMessage(i.spec, err)
|
||||
}
|
||||
|
||||
type gatewayClassUndefinedError struct {
|
||||
spec api.SelectionListWithDefaultSpec
|
||||
}
|
||||
|
||||
func NewGatewayClassUndefined(spec api.SelectionListWithDefaultSpec) error {
|
||||
return &gatewayClassUndefinedError{
|
||||
spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
func (i gatewayClassUndefinedError) Error() string {
|
||||
return utils.SelectionListWithDefaultErrorMessage(i.spec, "No gateway Class is forbidden for the current Tenant. Specify a gateway Class which is allowed within the Tenant: ")
|
||||
}
|
||||
29
pkg/webhook/gateway/utils.go
Normal file
29
pkg/webhook/gateway/utils.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
v1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
func TenantFromGateway(ctx context.Context, c client.Client, gateway *v1.Gateway) (*capsulev1beta2.Tenant, error) {
|
||||
tenantList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", gateway.Namespace),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tenantList.Items) == 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
return &tenantList.Items[0], nil
|
||||
}
|
||||
115
pkg/webhook/gateway/validate_class.go
Normal file
115
pkg/webhook/gateway/validate_class.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type class struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func Class(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &class{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) OnCreate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.validate(ctx, client, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) OnUpdate(client client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.validate(ctx, client, req, decoder, recorder)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) OnDelete(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *class) validate(ctx context.Context, client client.Client, req admission.Request, decoder admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
gatewayObj := &gatewayv1.Gateway{}
|
||||
if err := decoder.Decode(req, gatewayObj); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
var tnt *capsulev1beta2.Tenant
|
||||
|
||||
tnt, err := TenantFromGateway(ctx, client, gatewayObj)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
allowed := tnt.Spec.GatewayOptions.AllowedClasses
|
||||
|
||||
if allowed == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gatewayClass, err := utils.GetGatewayClassClassByObjectName(ctx, client, gatewayObj.Spec.GatewayClassName)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if gatewayClass == nil {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "MissingGatewayClass", "Gateway %s/%s is missing GatewayClass", req.Namespace, req.Name)
|
||||
|
||||
response := admission.Denied(NewGatewayClassUndefined(*allowed).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
selector := false
|
||||
// Verify if the GatewayClass exists and matches the label selector/expression
|
||||
if len(allowed.MatchExpressions) > 0 || len(allowed.MatchLabels) > 0 {
|
||||
gatewayClassObj, err := utils.GetGatewayClassClassByObjectName(ctx, client, gatewayObj.Spec.GatewayClassName)
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
// Gateway Class is present, check if it matches the selector
|
||||
if gatewayClassObj != nil {
|
||||
selector = allowed.SelectorMatch(gatewayClassObj)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case allowed.MatchDefault(gatewayClass.Name):
|
||||
return nil
|
||||
case selector:
|
||||
return nil
|
||||
default:
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenGatewaClass", "Gateway %s/%s GatewayClass %s is forbidden for the current Tenant", req.Namespace, req.Name, &gatewayClass)
|
||||
|
||||
response := admission.Denied(NewGatewayClassForbidden(gatewayObj.Name, *allowed).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
24
pkg/webhook/route/gateway.go
Normal file
24
pkg/webhook/route/gateway.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
)
|
||||
|
||||
type gateway struct {
|
||||
handlers []capsulewebhook.Handler
|
||||
}
|
||||
|
||||
func Gateway(handler ...capsulewebhook.Handler) capsulewebhook.Webhook {
|
||||
return &gateway{handlers: handler}
|
||||
}
|
||||
|
||||
func (w *gateway) GetHandlers() []capsulewebhook.Handler {
|
||||
return w.handlers
|
||||
}
|
||||
|
||||
func (w *gateway) GetPath() string {
|
||||
return "/gateways"
|
||||
}
|
||||
@@ -37,3 +37,14 @@ func DefaultAllowedValuesErrorMessage(allowed api.DefaultAllowedListSpec, err st
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func SelectionListWithDefaultErrorMessage(allowed api.SelectionListWithDefaultSpec, err string) string {
|
||||
var extra []string
|
||||
if len(allowed.MatchLabels) > 0 || len(allowed.MatchExpressions) > 0 {
|
||||
extra = append(extra, "matching the label selector defined in the Tenant")
|
||||
}
|
||||
|
||||
err += strings.Join(extra, " or ")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
const TRUE string = "true"
|
||||
@@ -63,6 +65,18 @@ func GetIngressClassByName(ctx context.Context, version *version.Version, c clie
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// Get GatewayClassClass by name (Does not return error if not found).
|
||||
func GetGatewayClassClassByObjectName(ctx context.Context, c client.Client, gatewayClassName gatewayv1.ObjectName) (*gatewayv1.GatewayClass, error) {
|
||||
objName := reflect.ValueOf(gatewayClassName).String()
|
||||
gatewayClass := &gatewayv1.GatewayClass{}
|
||||
|
||||
if err := c.Get(ctx, types.NamespacedName{Name: objName}, gatewayClass); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gatewayClass, nil
|
||||
}
|
||||
|
||||
// IsDefaultPriorityClass checks if the given PriorityClass is cluster default.
|
||||
func IsDefaultPriorityClass(class *schedulev1.PriorityClass) bool {
|
||||
if class != nil {
|
||||
|
||||
Reference in New Issue
Block a user