mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-21 19:07:13 +00:00
Compare commits
2 Commits
renovate/g
...
copilot/fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
700838e837 | ||
|
|
4431f5934d |
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
102
internal/webhook/defaults/httproute.go
Normal file
102
internal/webhook/defaults/httproute.go
Normal 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
|
||||
}
|
||||
21
internal/webhook/httproute/handler.go
Normal file
21
internal/webhook/httproute/handler.go
Normal 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,
|
||||
}
|
||||
}
|
||||
28
internal/webhook/httproute/utils.go
Normal file
28
internal/webhook/httproute/utils.go
Normal 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
|
||||
}
|
||||
142
internal/webhook/httproute/validate_gateway.go
Normal file
142
internal/webhook/httproute/validate_gateway.go
Normal 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
|
||||
}
|
||||
24
internal/webhook/route/httproute.go
Normal file
24
internal/webhook/route/httproute.go
Normal 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"
|
||||
}
|
||||
@@ -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
83
pkg/api/gateway_rule.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
ReasonMissingGatewayClass string = "MissingGatewayClass"
|
||||
ReasonMissingDeviceClass string = "MissingDeviceClass"
|
||||
ReasonForbiddenDeviceClass string = "ForbiddenDeviceClass"
|
||||
ReasonForbiddenGateway string = "ForbiddenGateway"
|
||||
|
||||
// Pods.
|
||||
ReasonMissingFQCI string = "MissingFQCI"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user