mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-25 04:49:15 +00:00
Compare commits
1 Commits
copilot/fe
...
renovate/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f335e95f24 |
2
.github/workflows/releaser.yml
vendored
2
.github/workflows/releaser.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -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.'
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
dependencies:
|
||||
- name: capsule-proxy
|
||||
repository: oci://ghcr.io/projectcapsule/charts
|
||||
version: 0.10.0
|
||||
digest: sha256:b268fe0a87e4fa4d0196e5dac82c7e8ae20e96053f5ca860b1f7c44e3a357406
|
||||
generated: "2025-12-09T15:58:45.796317945Z"
|
||||
version: 0.11.2
|
||||
digest: sha256:d4a756b7abb7ed928fd1d18e3870793bdc900eef81d2530a04e42606e273985c
|
||||
generated: "2026-03-13T10:42:09.898265739Z"
|
||||
|
||||
@@ -6,7 +6,7 @@ home: https://projectcapsule.dev/
|
||||
icon: https://github.com/projectcapsule/capsule/raw/main/assets/logo/capsule_small.png
|
||||
dependencies:
|
||||
- name: capsule-proxy
|
||||
version: 0.10.0
|
||||
version: 0.11.2
|
||||
repository: "oci://ghcr.io/projectcapsule/charts"
|
||||
condition: proxy.enabled
|
||||
alias: proxy
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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, " ")
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -33,7 +33,6 @@ const (
|
||||
ReasonMissingGatewayClass string = "MissingGatewayClass"
|
||||
ReasonMissingDeviceClass string = "MissingDeviceClass"
|
||||
ReasonForbiddenDeviceClass string = "ForbiddenDeviceClass"
|
||||
ReasonForbiddenGateway string = "ForbiddenGateway"
|
||||
|
||||
// Pods.
|
||||
ReasonMissingFQCI string = "MissingFQCI"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user