Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
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
copilot-swe-agent[bot]
4431f5934d Initial plan 2026-03-20 14:51:33 +00:00
19 changed files with 779 additions and 0 deletions

View File

@@ -30,4 +30,9 @@ type NamespaceRuleEnforceBody struct {
// Define registries which are allowed to be used within this tenant
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
Registries []api.OCIRegistry `json:"registries,omitempty"`
// Define gateway rules for this namespace, restricting which Gateway resources
// Routes may reference and optionally injecting a default Gateway parentRef.
// +optional
Gateways *api.GatewayRuleSpec `json:"gateways,omitempty"`
}

View File

@@ -464,6 +464,11 @@ func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody)
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Gateways != nil {
in, out := &in.Gateways, &out.Gateways
*out = new(api.GatewayRuleSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.

View File

@@ -51,6 +51,86 @@ spec:
enforce:
description: Enforcement Rules applied
properties:
gateways:
description: Define gateway rules for this namespace, restricting
which Gateway resources Routes may reference and optionally
injecting a default Gateway parentRef.
properties:
gateway:
description: Gateway restricts which Gateways Routes may
reference and optionally specifies a default Gateway injected
when no parentRef is provided.
properties:
allowed:
description: Allowed is an explicit list of Gateways
(by name and optional namespace) that Routes in this
namespace may reference.
items:
properties:
name:
description: Name of the Gateway.
type: string
namespace:
description: Namespace of the Gateway. When empty,
the Route's namespace is assumed.
type: string
required:
- name
type: object
type: array
default:
description: Default is the Gateway injected as parentRef
when no parentRef is specified in a Route resource.
properties:
name:
description: Name of the Gateway.
type: string
namespace:
description: Namespace of the Gateway. When empty,
the Route's namespace is assumed.
type: string
required:
- name
type: object
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs.
type: object
type: object
type: object
registries:
description: |-
Define registries which are allowed to be used within this tenant

View File

@@ -2492,6 +2492,86 @@ spec:
enforce:
description: Enforcement Rules applied
properties:
gateways:
description: Define gateway rules for this namespace, restricting
which Gateway resources Routes may reference and optionally
injecting a default Gateway parentRef.
properties:
gateway:
description: Gateway restricts which Gateways Routes may
reference and optionally specifies a default Gateway
injected when no parentRef is provided.
properties:
allowed:
description: Allowed is an explicit list of Gateways
(by name and optional namespace) that Routes in this
namespace may reference.
items:
properties:
name:
description: Name of the Gateway.
type: string
namespace:
description: Namespace of the Gateway. When empty,
the Route's namespace is assumed.
type: string
required:
- name
type: object
type: array
default:
description: Default is the Gateway injected as parentRef
when no parentRef is specified in a Route resource.
properties:
name:
description: Name of the Gateway.
type: string
namespace:
description: Namespace of the Gateway. When empty,
the Route's namespace is assumed.
type: string
required:
- name
type: object
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs.
type: object
type: object
type: object
registries:
description: |-
Define registries which are allowed to be used within this tenant

View File

@@ -157,6 +157,43 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.httproutes }}
{{- if .enabled }}
- name: httproute.defaults.projectcapsule.dev
admissionReviewVersions:
- v1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/defaults" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
reinvocationPolicy: {{ .reinvocationPolicy }}
{{- with .namespaceSelector }}
namespaceSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .objectSelector }}
objectSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .matchConditions }}
matchConditions:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- gateway.networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- httproutes
scope: "Namespaced"
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with (mergeOverwrite .Values.webhooks.hooks.namespaces .Values.webhooks.hooks.namespaceOwnerReference) }}
{{- if .enabled }}
- name: namespaces.tenants.projectcapsule.dev

View File

@@ -153,6 +153,43 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.httproutes }}
{{- if .enabled }}
- name: httproute.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/httproutes/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
namespaceSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .objectSelector }}
objectSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .matchConditions }}
matchConditions:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- gateway.networking.k8s.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- httproutes
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.ingresses }}
{{- if .enabled }}
- name: ingress.projectcapsule.dev

View File

@@ -623,6 +623,27 @@ webhooks:
operator: Exists
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
httproutes:
# -- Enable the Hook
enabled: true
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
failurePolicy: Fail
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchPolicy: Equivalent
# -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)
objectSelector: {}
# -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
ingresses:
# -- Enable the Hook

View File

@@ -49,6 +49,7 @@ import (
"github.com/projectcapsule/capsule/internal/webhook/defaults"
"github.com/projectcapsule/capsule/internal/webhook/dra"
"github.com/projectcapsule/capsule/internal/webhook/gateway"
"github.com/projectcapsule/capsule/internal/webhook/httproute"
"github.com/projectcapsule/capsule/internal/webhook/ingress"
"github.com/projectcapsule/capsule/internal/webhook/misc"
namespacemutation "github.com/projectcapsule/capsule/internal/webhook/namespace/mutation"
@@ -280,6 +281,11 @@ func main() {
),
route.MiscCustomResources(misc.ResourceCounterHandler(manager.GetClient())),
route.Gateway(gateway.Class(cfg)),
route.HTTPRoute(
httproute.Handler(
httproute.GatewayValidator(),
),
),
route.DeviceClass(dra.DeviceClass()),
route.Defaults(defaults.Handler(cfg, kubeVersion)),
route.TenantMutation(

View File

@@ -58,6 +58,8 @@ func (h *handler) mutate(ctx context.Context, req admission.Request, c client.Cl
response = mutateIngressDefaults(ctx, req, h.version, c, decoder, req.Namespace)
case metav1.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1", Resource: "gateways"}:
response = mutateGatewayDefaults(ctx, req, c, decoder, req.Namespace)
case metav1.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1", Resource: "httproutes"}:
response = mutateHTTPRouteDefaults(ctx, req, c, decoder, req.Namespace)
}
if response == nil {

View File

@@ -0,0 +1,102 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package defaults
import (
"context"
"encoding/json"
"net/http"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
capsulehttproute "github.com/projectcapsule/capsule/internal/webhook/httproute"
"github.com/projectcapsule/capsule/internal/webhook/utils"
"github.com/projectcapsule/capsule/pkg/tenant"
)
func mutateHTTPRouteDefaults(ctx context.Context, req admission.Request, c client.Client, decoder admission.Decoder, namespace string) *admission.Response {
routeObj := &gatewayv1.HTTPRoute{}
if err := decoder.Decode(req, routeObj); err != nil {
return utils.ErroredResponse(err)
}
routeObj.SetNamespace(namespace)
tnt, err := capsulehttproute.TenantFromHTTPRoute(ctx, c, routeObj)
if err != nil {
return utils.ErroredResponse(err)
}
if tnt == nil {
return nil
}
// Resolve namespace-level rules to find the gateway default.
ns := &corev1.Namespace{}
if err = c.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
return utils.ErroredResponse(err)
}
ruleBody, err := tenant.BuildNamespaceRuleBodyForNamespace(ns, tnt)
if err != nil {
return utils.ErroredResponse(err)
}
if ruleBody == nil || ruleBody.Enforce.Gateways == nil || ruleBody.Enforce.Gateways.Gateway == nil {
return nil
}
gwDefault := ruleBody.Enforce.Gateways.Gateway.Default
if gwDefault == nil {
return nil
}
// Only inject the default when the HTTPRoute has no parentRefs.
if len(routeObj.Spec.ParentRefs) > 0 {
return nil
}
defaultNamespace := gwDefault.Namespace
if defaultNamespace == "" {
defaultNamespace = namespace
}
ns16 := gatewayv1.Namespace(defaultNamespace)
routeObj.Spec.ParentRefs = []gatewayv1.ParentReference{
{
Group: groupPtr(gatewayv1.GroupName),
Kind: kindPtr("Gateway"),
Name: gatewayv1.ObjectName(gwDefault.Name),
Namespace: &ns16,
},
}
marshaled, err := json.Marshal(routeObj)
if err != nil {
response := admission.Errored(http.StatusInternalServerError, err)
return &response
}
response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
return &response
}
func groupPtr(g string) *gatewayv1.Group {
v := gatewayv1.Group(g)
return &v
}
func kindPtr(k string) *gatewayv1.Kind {
v := gatewayv1.Kind(k)
return &v
}

View File

@@ -0,0 +1,21 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package httproute
import (
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
)
// Handler builds a handlers.Handler that wraps all provided
// TypedHandlerWithTenantWithRuleset[*gatewayv1.HTTPRoute] implementations.
func Handler(handler ...handlers.TypedHandlerWithTenantWithRuleset[*gatewayv1.HTTPRoute]) handlers.Handler {
return &handlers.TypedTenantWithRulesetHandler[*gatewayv1.HTTPRoute]{
Factory: func() *gatewayv1.HTTPRoute {
return &gatewayv1.HTTPRoute{}
},
Handlers: handler,
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package httproute
import (
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
)
// TenantFromHTTPRoute returns the Tenant owning the namespace of the given HTTPRoute,
// or nil if the namespace does not belong to any Tenant.
func TenantFromHTTPRoute(ctx context.Context, c client.Client, route *gatewayv1.HTTPRoute) (*capsulev1beta2.Tenant, error) {
tenantList := &capsulev1beta2.TenantList{}
if err := c.List(ctx, tenantList, client.MatchingFields{".status.namespaces": route.Namespace}); err != nil {
return nil, err
}
if len(tenantList.Items) == 0 {
return nil, nil //nolint:nilnil
}
return &tenantList.Items[0], nil
}

View File

@@ -0,0 +1,142 @@
// 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
}

View File

@@ -0,0 +1,24 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package route
import "github.com/projectcapsule/capsule/pkg/runtime/handlers"
type httproute struct {
handlers []handlers.Handler
}
// HTTPRoute returns a Webhook that handles admission requests for
// gateway.networking.k8s.io/v1 HTTPRoute resources.
func HTTPRoute(handler ...handlers.Handler) handlers.Webhook {
return &httproute{handlers: handler}
}
func (w *httproute) GetHandlers() []handlers.Handler {
return w.handlers
}
func (w *httproute) GetPath() string {
return "/httproutes/validating"
}

View File

@@ -6,6 +6,7 @@ package errors
import (
"fmt"
"reflect"
"strings"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -76,3 +77,41 @@ func NewGatewayClassUndefined(spec api.DefaultAllowedListSpec) error {
func (i GatewayClassUndefinedError) Error() string {
return utils.DefaultAllowedValuesErrorMessage(i.spec, "No gateway Class is forbidden for the current Tenant. Specify a gateway Class which is allowed within the Tenant: ")
}
// GatewayForbiddenError is returned when an HTTPRoute references a Gateway that
// is not in the allowed list for the Tenant's namespace rules.
type GatewayForbiddenError struct {
gatewayNamespace string
gatewayName string
spec api.AllowedGatewaySpec
}
func NewGatewayForbidden(name, namespace string, spec api.AllowedGatewaySpec) error {
return &GatewayForbiddenError{
gatewayNamespace: namespace,
gatewayName: name,
spec: spec,
}
}
func (e GatewayForbiddenError) Error() string {
msg := fmt.Sprintf("Gateway %s/%s is forbidden for the current Tenant: ", e.gatewayNamespace, e.gatewayName)
parts := []string{msg}
if e.spec.Default != nil {
parts = append(parts, fmt.Sprintf("default: %s/%s", e.spec.Default.Namespace, e.spec.Default.Name))
}
if len(e.spec.Allowed) > 0 {
names := make([]string, 0, len(e.spec.Allowed))
for _, a := range e.spec.Allowed {
names = append(names, fmt.Sprintf("%s/%s", a.Namespace, a.Name))
}
parts = append(parts, fmt.Sprintf("allowed: [%s]", strings.Join(names, ", ")))
}
return strings.Join(parts, " ")
}

83
pkg/api/gateway_rule.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package api
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// GatewayNamespacedName is a namespaced reference to a Gateway resource.
// +kubebuilder:object:generate=true
type GatewayNamespacedName struct {
// Name of the Gateway.
Name string `json:"name"`
// Namespace of the Gateway. When empty, the Route's namespace is assumed.
// +optional
Namespace string `json:"namespace,omitempty"`
}
// AllowedGatewaySpec defines which Gateway resources are allowed to be
// referenced by Routes in a namespace, and an optional default Gateway.
// +kubebuilder:object:generate=true
type AllowedGatewaySpec struct {
// Default is the Gateway injected as parentRef when no parentRef is specified
// in a Route resource. The Gateway must also be allowed via Allowed or the
// LabelSelector.
// +optional
Default *GatewayNamespacedName `json:"default,omitempty"`
// LabelSelector matches Gateway resources by labels.
// +optional
metav1.LabelSelector `json:",inline"`
// Allowed is an explicit list of Gateways (by name and optional namespace)
// that Routes in this namespace may reference.
// +optional
Allowed []GatewayNamespacedName `json:"allowed,omitempty"`
}
// MatchDefault returns true when the given namespace/name matches the configured
// default Gateway.
func (in *AllowedGatewaySpec) MatchDefault(gwNamespace, gwName string) bool {
if in.Default == nil {
return false
}
return in.Default.Name == gwName && (in.Default.Namespace == "" || in.Default.Namespace == gwNamespace)
}
// MatchGateway returns true when the given Gateway is allowed by this spec.
// A Gateway is allowed if it is the default, appears in the Allowed list, or
// its labels match the LabelSelector.
func (in *AllowedGatewaySpec) MatchGateway(gwNamespace, gwName string, obj client.Object) bool {
if in.MatchDefault(gwNamespace, gwName) {
return true
}
for _, a := range in.Allowed {
if a.Name == gwName && (a.Namespace == "" || a.Namespace == gwNamespace) {
return true
}
}
if obj != nil && (len(in.MatchLabels) > 0 || len(in.MatchExpressions) > 0) {
selector, err := metav1.LabelSelectorAsSelector(&in.LabelSelector)
if err == nil && selector.Matches(labels.Set(obj.GetLabels())) {
return true
}
}
return false
}
// GatewayRuleSpec defines gateway-related enforcement rules for a namespace.
// +kubebuilder:object:generate=true
type GatewayRuleSpec struct {
// Gateway restricts which Gateways Routes may reference and optionally
// specifies a default Gateway injected when no parentRef is provided.
// +optional
Gateway *AllowedGatewaySpec `json:"gateway,omitempty"`
}

View File

@@ -111,6 +111,32 @@ func (in *AdditionalRoleBindingsSpec) DeepCopy() *AdditionalRoleBindingsSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedGatewaySpec) DeepCopyInto(out *AllowedGatewaySpec) {
*out = *in
if in.Default != nil {
in, out := &in.Default, &out.Default
*out = new(GatewayNamespacedName)
**out = **in
}
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make([]GatewayNamespacedName, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedGatewaySpec.
func (in *AllowedGatewaySpec) DeepCopy() *AllowedGatewaySpec {
if in == nil {
return nil
}
out := new(AllowedGatewaySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AllowedListSpec) DeepCopyInto(out *AllowedListSpec) {
*out = *in
@@ -238,6 +264,41 @@ func (in *ForbiddenListSpec) DeepCopy() *ForbiddenListSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewayNamespacedName) DeepCopyInto(out *GatewayNamespacedName) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayNamespacedName.
func (in *GatewayNamespacedName) DeepCopy() *GatewayNamespacedName {
if in == nil {
return nil
}
out := new(GatewayNamespacedName)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewayRuleSpec) DeepCopyInto(out *GatewayRuleSpec) {
*out = *in
if in.Gateway != nil {
in, out := &in.Gateway, &out.Gateway
*out = new(AllowedGatewaySpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayRuleSpec.
func (in *GatewayRuleSpec) DeepCopy() *GatewayRuleSpec {
if in == nil {
return nil
}
out := new(GatewayRuleSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LimitRangesSpec) DeepCopyInto(out *LimitRangesSpec) {
*out = *in

View File

@@ -33,6 +33,7 @@ const (
ReasonMissingGatewayClass string = "MissingGatewayClass"
ReasonMissingDeviceClass string = "MissingDeviceClass"
ReasonForbiddenDeviceClass string = "ForbiddenDeviceClass"
ReasonForbiddenGateway string = "ForbiddenGateway"
// Pods.
ReasonMissingFQCI string = "MissingFQCI"

View File

@@ -58,6 +58,11 @@ func BuildNamespaceRuleBodyForNamespace(
if len(rule.Enforce.Registries) > 0 {
out.Enforce.Registries = append(out.Enforce.Registries, rule.Enforce.Registries...)
}
// Merge gateway rules: last non-nil gateway rule wins.
if rule.Enforce.Gateways != nil {
out.Enforce.Gateways = rule.Enforce.Gateways.DeepCopy()
}
}
return out, nil