Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
986e6bea14 fix(deps): update module github.com/onsi/ginkgo/v2 to v2.28.1 2026-03-13 10:42:21 +00:00
23 changed files with 20 additions and 791 deletions

View File

@@ -30,7 +30,7 @@ jobs:
timeout-minutes: 5
continue-on-error: true
- uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0
- uses: anchore/sbom-action/download-syft@a0a65128ee20bfc2cba8a1e7fc6ca46a88149706
- uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Run GoReleaser

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Close stale pull requests
uses: actions/stale@db5d06a4c82d5e94513c09c406638111df61f63e
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.'
stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 30 days. Please update this pull request or it will be automatically closed in 30 days.'

View File

@@ -30,9 +30,4 @@ 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,11 +464,6 @@ 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,86 +51,6 @@ 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,86 +2492,6 @@ 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,43 +157,6 @@ 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,43 +153,6 @@ 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,27 +623,6 @@ 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,7 +49,6 @@ 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"
@@ -281,11 +280,6 @@ 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(

20
go.mod
View File

@@ -3,8 +3,12 @@ module github.com/projectcapsule/capsule
go 1.25.4
require (
github.com/BurntSushi/toml v1.6.0
github.com/fluxcd/pkg/apis/kustomize v1.15.0
github.com/fluxcd/pkg/ssa v0.64.0
github.com/go-logr/logr v1.4.3
github.com/onsi/ginkgo/v2 v2.27.5
github.com/go-sprout/sprout v1.0.3
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
@@ -14,6 +18,7 @@ require (
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.1
golang.org/x/sync v0.19.0
gomodules.xyz/jsonpatch/v2 v2.5.0
k8s.io/api v0.35.0
k8s.io/apiextensions-apiserver v0.35.0
k8s.io/apimachinery v0.35.0
@@ -23,11 +28,11 @@ require (
sigs.k8s.io/cluster-api v1.12.2
sigs.k8s.io/controller-runtime v0.23.0
sigs.k8s.io/gateway-api v1.4.1
sigs.k8s.io/yaml v1.6.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
@@ -36,8 +41,6 @@ require (
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fluxcd/cli-utils v0.37.1-flux.1 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.15.0 // indirect
github.com/fluxcd/pkg/ssa v0.64.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
@@ -56,13 +59,12 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-sprout/sprout v1.0.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobuffalo/flect v1.0.3 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -89,15 +91,14 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
@@ -110,5 +111,4 @@ require (
sigs.k8s.io/kustomize/kyaml v0.21.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

8
go.sum
View File

@@ -153,6 +153,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
@@ -195,6 +197,8 @@ github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/ginkgo/v2 v2.27.5 h1:ZeVgZMx2PDMdJm/+w5fE/OyG6ILo1Y3e+QX4zSR0zTE=
github.com/onsi/ginkgo/v2 v2.27.5/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
@@ -303,6 +307,8 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
@@ -331,6 +337,8 @@ golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=

View File

@@ -58,8 +58,6 @@ 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

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

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

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

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

@@ -1,24 +0,0 @@
// 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,7 +6,6 @@ package errors
import (
"fmt"
"reflect"
"strings"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -77,41 +76,3 @@ 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, " ")
}

View File

@@ -1,83 +0,0 @@
// 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,32 +111,6 @@ 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
@@ -264,41 +238,6 @@ 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,7 +33,6 @@ const (
ReasonMissingGatewayClass string = "MissingGatewayClass"
ReasonMissingDeviceClass string = "MissingDeviceClass"
ReasonForbiddenDeviceClass string = "ForbiddenDeviceClass"
ReasonForbiddenGateway string = "ForbiddenGateway"
// Pods.
ReasonMissingFQCI string = "MissingFQCI"

View File

@@ -58,11 +58,6 @@ 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