Files
capsule/internal/webhook/httproute/validate_gateway.go
copilot-swe-agent[bot]andoliverbaehler 700838e837 feat: support allowed gateways in tenant namespace rules (issue #1885)
- Add GatewayNamespacedName, AllowedGatewaySpec, GatewayRuleSpec types to pkg/api/
- Add Gateways field to NamespaceRuleEnforceBody for per-namespace gateway rules
- Add HTTPRoute validation webhook to enforce allowed gateways
- Add HTTPRoute default mutation webhook to inject default Gateway parentRef
- Update Helm chart with validating/mutating webhook configurations for httproutes
- Update CRD schemas for tenants and rulestatuses
- Add GatewayForbiddenError and ReasonForbiddenGateway event reason

Co-authored-by: oliverbaehler <26610571+oliverbaehler@users.noreply.github.com>
2026-03-20 15:26:33 +00:00

143 lines
3.7 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package httproute
import (
"context"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/events"
"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"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
)
type gatewayValidator struct{}
// GatewayValidator returns a TypedHandlerWithTenantWithRuleset that validates
// HTTPRoute parentRefs against the gateway rules configured in namespace rules.
func GatewayValidator() handlers.TypedHandlerWithTenantWithRuleset[*gatewayv1.HTTPRoute] {
return &gatewayValidator{}
}
func (h *gatewayValidator) OnCreate(
c client.Client,
obj *gatewayv1.HTTPRoute,
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
rule *capsulev1beta2.NamespaceRuleBody,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(ctx, c, req, obj, tnt, recorder, rule)
}
}
func (h *gatewayValidator) OnUpdate(
c client.Client,
_ *gatewayv1.HTTPRoute,
obj *gatewayv1.HTTPRoute,
_ admission.Decoder,
recorder events.EventRecorder,
tnt *capsulev1beta2.Tenant,
rule *capsulev1beta2.NamespaceRuleBody,
) handlers.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return h.validate(ctx, c, req, obj, tnt, recorder, rule)
}
}
func (h *gatewayValidator) OnDelete(
_ client.Client,
_ *gatewayv1.HTTPRoute,
_ admission.Decoder,
_ events.EventRecorder,
_ *capsulev1beta2.Tenant,
_ *capsulev1beta2.NamespaceRuleBody,
) handlers.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
}
}
func (h *gatewayValidator) validate(
ctx context.Context,
c client.Client,
req admission.Request,
route *gatewayv1.HTTPRoute,
tnt *capsulev1beta2.Tenant,
recorder events.EventRecorder,
rule *capsulev1beta2.NamespaceRuleBody,
) *admission.Response {
if rule == nil || rule.Enforce.Gateways == nil || rule.Enforce.Gateways.Gateway == nil {
return nil
}
allowed := rule.Enforce.Gateways.Gateway
for _, parentRef := range route.Spec.ParentRefs {
// Only validate parentRefs that point to a Gateway resource.
if parentRef.Kind != nil && *parentRef.Kind != gatewayv1.Kind("Gateway") {
continue
}
if parentRef.Group != nil && string(*parentRef.Group) != gatewayv1.GroupName {
continue
}
gwName := string(parentRef.Name)
gwNamespace := route.Namespace
if parentRef.Namespace != nil {
gwNamespace = string(*parentRef.Namespace)
}
// Try to fetch the Gateway object for label-selector matching.
gw := &gatewayv1.Gateway{}
var gwObj client.Object
if err := c.Get(ctx, types.NamespacedName{Namespace: gwNamespace, Name: gwName}, gw); err != nil {
if !k8serrors.IsNotFound(err) {
return errResponse(err)
}
} else {
gwObj = gw
}
if !allowed.MatchGateway(gwNamespace, gwName, gwObj) {
recorder.Eventf(
tnt,
nil,
corev1.EventTypeWarning,
evt.ReasonForbiddenGateway,
evt.ActionValidationDenied,
"HTTPRoute %s/%s references forbidden Gateway %s/%s",
req.Namespace, req.Name, gwNamespace, gwName,
)
response := admission.Denied(
caperrors.NewGatewayForbidden(gwName, gwNamespace, *allowed).Error(),
)
return &response
}
}
return nil
}
func errResponse(err error) *admission.Response {
resp := admission.Errored(500, err)
return &resp
}