diff --git a/deploy/containerlab/resources/galactic-gateway/dfw/networkrules.yaml b/deploy/containerlab/resources/galactic-gateway/dfw/networkrules.yaml index fb0dc9a9..57ab067a 100644 --- a/deploy/containerlab/resources/galactic-gateway/dfw/networkrules.yaml +++ b/deploy/containerlab/resources/galactic-gateway/dfw/networkrules.yaml @@ -6,7 +6,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:1::1 protocol: tcp @@ -24,7 +23,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:1::1 protocol: udp diff --git a/deploy/containerlab/resources/galactic-gateway/iad/networkrules.yaml b/deploy/containerlab/resources/galactic-gateway/iad/networkrules.yaml index d0278ce4..8d02c04f 100644 --- a/deploy/containerlab/resources/galactic-gateway/iad/networkrules.yaml +++ b/deploy/containerlab/resources/galactic-gateway/iad/networkrules.yaml @@ -6,7 +6,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:3::1 protocol: tcp @@ -24,7 +23,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:3::1 protocol: udp diff --git a/deploy/containerlab/resources/galactic-gateway/sjc/networkrules.yaml b/deploy/containerlab/resources/galactic-gateway/sjc/networkrules.yaml index 4f0dc220..90660701 100644 --- a/deploy/containerlab/resources/galactic-gateway/sjc/networkrules.yaml +++ b/deploy/containerlab/resources/galactic-gateway/sjc/networkrules.yaml @@ -6,7 +6,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:2::1 protocol: tcp @@ -24,7 +23,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:2::1 protocol: udp diff --git a/docs/agents/ARCHITECTURE-GATEWAY.md b/docs/agents/ARCHITECTURE-GATEWAY.md index ea9a54f6..c4b3cfdf 100644 --- a/docs/agents/ARCHITECTURE-GATEWAY.md +++ b/docs/agents/ARCHITECTURE-GATEWAY.md @@ -103,10 +103,10 @@ per-VPC gateway design to colocate with the workload's own VRF. ### The two CRDs -| CRD | Scope | Written by | Purpose | -| ---------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `NetworkGateway` | Namespaced, one per gateway node (`spec.targetRef.name` = node name) | Operator, once per gateway node (see [worked example](#worked-containerlab-example) below) | Node-scoped root object, mirroring `BGPRouter`'s pattern. Identifies which nodes participate in the anycast mesh and surfaces each node's engine health via `status.conditions`. Carries **no** self-address field — DSR rewrites nothing, so there is no translation source to publish. | -| `NetworkRule` | Namespaced, tenant-writable | Tenant, via an admission webhook that verifies VPC/VPCAttachment ownership (**webhook not yet deployed in this repo** — see Known Constraints) | Ingress load-balancing spec: `vpcRef`/`vpcAttachmentRef` (opaque tenant identifiers), `vipAddresses` (1–8, IPv4/IPv6), `protocol` (`tcp`/`udp`), `port`, `backends` (1–64 `address:port` pairs). Served by every `NetworkGateway` in the namespace identically. | +| CRD | Scope | Written by | Purpose | +| ---------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NetworkGateway` | Namespaced, one per gateway node (`spec.targetRef.name` = node name) | Operator, once per gateway node (see [worked example](#worked-containerlab-example) below) | Node-scoped root object, mirroring `BGPRouter`'s pattern. Identifies which nodes participate in the anycast mesh and surfaces each node's engine health via `status.conditions`. Carries **no** self-address field — DSR rewrites nothing, so there is no translation source to publish. | +| `NetworkRule` | Namespaced, tenant-writable | Tenant, via an admission webhook that verifies VPC ownership (**webhook not yet deployed in this repo** — see Known Constraints) | Ingress load-balancing spec: `vpcRef` (opaque tenant identifier), `vipAddresses` (1–8, IPv4/IPv6), `protocol` (`tcp`/`udp`), `port`, `backendSelector` (the VPCAttachments in `vpcRef` that serve it), `backendPort`. Served by every `NetworkGateway` in the namespace identically. | Both are defined in `go.datum.net/network`'s `api/v1alpha1` package (`gateway_types.go`, `rule_types.go`) — the same external CRD module the @@ -724,9 +724,8 @@ pod on that node is not a supported configuration. - **No tenant dimension in the datapath.** `vip_table` is keyed by `(proto, VIP port, VIP address)` only — a VIP is globally unique by construction, so no tenant/VPC field is needed to disambiguate ingress traffic. - `DesiredRule.VPCRef`/`VPCAttachmentRef` are carried through purely for - telemetry labeling and admission-webhook auditing, never consulted by the - datapath itself. + `DesiredRule.VPCRef` is carried through only for the per-tenant rule + quota, never consulted by the datapath itself. - **uSID resolution for backends.** There is no exported "IP → uSID" query anywhere else in this codebase (`internal/runtime/gobgp/monitor.go` decodes EVPN Prefix-SID attributes purely internally, for local kernel diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index ae5a7a25..003b702c 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -447,16 +447,15 @@ gateway node, named after that node (see the worked example above). ### `NetworkRule` — tenant-writable ingress load-balancing spec -| Field | Required | Type | Description | -| ----------------------- | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `spec.vpcRef` | Yes | `string` | Opaque VPC identifier (owned by the companion operator, not validated here beyond non-emptiness). Only attachments whose `status.vpc` equals it can be backends. | -| `spec.vpcAttachmentRef` | Yes | `string` | Opaque VPC attachment identifier, paired with `vpcRef`. | -| `spec.vipAddresses` | Yes | `[]string` (1–8) | Ingress VIP addresses this rule provisions. The backend nodes translate IPv6 VIPs only (#705). | -| `spec.protocol` | Yes | `tcp` \| `udp` | Transport protocol matched by `vipAddresses`/`port`. | -| `spec.port` | Yes | `int32` (1–65535) | Ingress port on `vipAddresses` this rule load-balances. | -| `spec.backendSelector` | Yes | `metav1.LabelSelector` | Selects the `VPCAttachment`s (`cloud.datumapis.com`, any namespace) that serve the rule. Each contributes its IPv6 interface addresses as backends. Must not be empty. | -| `spec.backendPort` | Yes | `int32` (1–65535) | Destination port on every selected backend. | -| `status.conditions` | — | — | `Accepted` (set `True` once gateway nodes exist; see the admission-webhook caveat below), `/Programmed` per gateway node, and `/BackendsBound` per node hosting a backend. | +| Field | Required | Type | Description | +| ---------------------- | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `spec.vpcRef` | Yes | `string` | Opaque VPC identifier (owned by the companion operator, not validated here beyond non-emptiness). Only attachments whose `status.vpc` equals it can be backends. | +| `spec.vipAddresses` | Yes | `[]string` (1–8) | Ingress VIP addresses this rule provisions. The backend nodes translate IPv6 VIPs only (#705). | +| `spec.protocol` | Yes | `tcp` \| `udp` | Transport protocol matched by `vipAddresses`/`port`. | +| `spec.port` | Yes | `int32` (1–65535) | Ingress port on `vipAddresses` this rule load-balances. | +| `spec.backendSelector` | Yes | `metav1.LabelSelector` | Selects the `VPCAttachment`s (`cloud.datumapis.com`, any namespace) that serve the rule. Each contributes its IPv6 interface addresses as backends. Must not be empty. | +| `spec.backendPort` | Yes | `int32` (1–65535) | Destination port on every selected backend. | +| `status.conditions` | — | — | `Accepted` (set `True` once gateway nodes exist; see the admission-webhook caveat below), `/Programmed` per gateway node, and `/BackendsBound` per node hosting a backend. | Example, the TCP rule from `deploy/containerlab/resources/galactic-gateway/iad/networkrules.yaml`: @@ -468,7 +467,6 @@ metadata: namespace: galactic-system spec: vpcRef: "60" - vpcAttachmentRef: "60" vipAddresses: - 2001:db8:6060:3::1 protocol: tcp diff --git a/go.mod b/go.mod index 35caa284..71c6c77c 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/vishvananda/netlink v1.3.2-0.20261002201547-1d1c62507e3c go.datum.net/cloud v0.1.1-0.20260918213150-260f797bbc5c - go.datum.net/network v0.3.1-0.20261008204016-fc3154bde4d8 + go.datum.net/network v0.3.1-0.20261009013235-5e00bde12789 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 diff --git a/go.sum b/go.sum index 81446c52..6ebb1ec2 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.datum.net/cloud v0.1.1-0.20260918213150-260f797bbc5c h1:k0P5980+kS90zCa0M4MohyS0woJL3bh/uEarRrW9PmY= go.datum.net/cloud v0.1.1-0.20260918213150-260f797bbc5c/go.mod h1:HTeaepXL0EqiqNsQ13K4/kGEwv+VLJ//BGrUfVIFfEA= -go.datum.net/network v0.3.1-0.20261008204016-fc3154bde4d8 h1:SN5aP+EC7S1JRrW57BQ8d9VzQvE9duXZAyMDgGn1pRg= -go.datum.net/network v0.3.1-0.20261008204016-fc3154bde4d8/go.mod h1:LawI34k+ddtv3kRbmjiss0Lb6QKpSr60YP3fX1IG5RU= +go.datum.net/network v0.3.1-0.20261009013235-5e00bde12789 h1:dPm2P7Ao4yRhuZe67WWXHohEo6IJZgT8lavJRn6hE4Y= +go.datum.net/network v0.3.1-0.20261009013235-5e00bde12789/go.mod h1:LawI34k+ddtv3kRbmjiss0Lb6QKpSr60YP3fX1IG5RU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= diff --git a/internal/controller/networkgateway_controller.go b/internal/controller/networkgateway_controller.go index ff008941..24b438b2 100644 --- a/internal/controller/networkgateway_controller.go +++ b/internal/controller/networkgateway_controller.go @@ -782,11 +782,10 @@ func buildDesiredRule( } return gateway.DesiredRule{ - Key: rule.Namespace + "/" + rule.Name, - VPCRef: rule.Spec.VPCRef, - VPCAttachmentRef: rule.Spec.VPCAttachmentRef, - VIPAddresses: vips, - Protocol: string(rule.Spec.Protocol), + Key: rule.Namespace + "/" + rule.Name, + VPCRef: rule.Spec.VPCRef, + VIPAddresses: vips, + Protocol: string(rule.Spec.Protocol), //nolint:gosec // rule.Spec.Port is CRD-validated to [1,65535] (Minimum/Maximum markers on NetworkRuleSpec.Port) Port: uint16(rule.Spec.Port), Backends: backends, diff --git a/internal/controller/networkrule_controller_test.go b/internal/controller/networkrule_controller_test.go index a77ebf6e..6bb65ba6 100644 --- a/internal/controller/networkrule_controller_test.go +++ b/internal/controller/networkrule_controller_test.go @@ -88,11 +88,10 @@ func newTestRule(name, vpcRef string, vips ...string) *bgpv1alpha1.NetworkRule { return &bgpv1alpha1.NetworkRule{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: name}, Spec: bgpv1alpha1.NetworkRuleSpec{ - VPCRef: vpcRef, - VPCAttachmentRef: "attach-1", - VIPAddresses: vips, - Protocol: bgpv1alpha1.NetworkRuleProtocolTCP, - Port: 443, + VPCRef: vpcRef, + VIPAddresses: vips, + Protocol: bgpv1alpha1.NetworkRuleProtocolTCP, + Port: 443, BackendSelector: metav1.LabelSelector{ MatchLabels: map[string]string{testBackendLabel: testBackendLabelValue}, }, diff --git a/internal/gateway/diff.go b/internal/gateway/diff.go index 915b0591..9f1456ba 100644 --- a/internal/gateway/diff.go +++ b/internal/gateway/diff.go @@ -42,7 +42,6 @@ func diffRuleKeys(active map[string]DesiredRule, desired map[string]DesiredRule) func rulesEqual(a, b DesiredRule) bool { return a.Key == b.Key && a.VPCRef == b.VPCRef && - a.VPCAttachmentRef == b.VPCAttachmentRef && a.Protocol == b.Protocol && a.Port == b.Port && slices.Equal(a.VIPAddresses, b.VIPAddresses) && diff --git a/internal/gateway/diff_test.go b/internal/gateway/diff_test.go index ddb0aace..2e8d0d79 100644 --- a/internal/gateway/diff_test.go +++ b/internal/gateway/diff_test.go @@ -76,7 +76,7 @@ func TestRulesEqual(t *testing.T) { return DesiredBackend{Address: netip.MustParseAddr("2001:db8:1::10"), Port: 8080, USID: usid} } base := DesiredRule{ - Key: testKeyA, VPCRef: "vpc", VPCAttachmentRef: "att", + Key: testKeyA, VPCRef: "vpc", VIPAddresses: []netip.Addr{vip}, Protocol: "udp", Port: 80, Backends: []DesiredBackend{backend(usidA)}, } diff --git a/internal/gateway/types.go b/internal/gateway/types.go index 05d977b7..3b697102 100644 --- a/internal/gateway/types.go +++ b/internal/gateway/types.go @@ -39,12 +39,10 @@ type DesiredRule struct { // NetworkRule), used as the map key in Engine's convergence pass. Key string - // VPCRef and VPCAttachmentRef are opaque tenant identifiers, carried for - // telemetry labels and admission auditing. The datapath never needs them: a - // VIP is globally unique, so no tenant dimension disambiguates ingress - // traffic. - VPCRef string - VPCAttachmentRef string + // VPCRef is the tenant's opaque VPC identifier, used to count rules against + // the per-tenant quota. The datapath never needs it: a VIP is globally + // unique, so no tenant dimension disambiguates ingress traffic. + VPCRef string // VIPAddresses are the ingress VIP addresses this rule provisions. VIPAddresses []netip.Addr diff --git a/internal/webhook/networkrule_webhook.go b/internal/webhook/networkrule_webhook.go index 8f626d8a..d6e87f60 100644 --- a/internal/webhook/networkrule_webhook.go +++ b/internal/webhook/networkrule_webhook.go @@ -16,7 +16,7 @@ import ( // NetworkRuleValidator is the admission validator for NetworkRule, verifying // through the pluggable Authorizer that the requesting identity may act on the -// VPC and attachment the rule names before a create or update is admitted. +// VPC the rule names before a create or update is admitted. type NetworkRuleValidator struct { // Authorizer performs the actual check. Production callers must not leave // this as AllowAllAuthorizer. @@ -26,16 +26,16 @@ type NetworkRuleValidator struct { var _ admission.Validator[*networkv1alpha1.NetworkRule] = &NetworkRuleValidator{} // ValidateCreate verifies the requester is authorized for the NetworkRule's -// vpc/vpcattachment before create is admitted. +// VPC before create is admitted. func (v *NetworkRuleValidator) ValidateCreate( ctx context.Context, rule *networkv1alpha1.NetworkRule, ) (admission.Warnings, error) { return nil, v.authorize(ctx, rule) } -// ValidateUpdate re-verifies authorization on update: a rule's VPC and -// attachment references could otherwise be changed after creation to point at -// another tenant's resources without the create-time check running again. +// ValidateUpdate re-verifies authorization on update: a rule's VPC reference +// could otherwise be changed after creation to point at another tenant's VPC +// without the create-time check running again. func (v *NetworkRuleValidator) ValidateUpdate( ctx context.Context, _, newRule *networkv1alpha1.NetworkRule, ) (admission.Warnings, error) { @@ -74,8 +74,8 @@ func (v *NetworkRuleValidator) authorize(ctx context.Context, rule *networkv1alp } if !ok { return fmt.Errorf( - "%s is not authorized for vpcRef %q / vpcAttachmentRef %q (%s)", - req.UserInfo.Username, rule.Spec.VPCRef, rule.Spec.VPCAttachmentRef, + "%s is not authorized for vpcRef %q (%s)", + req.UserInfo.Username, rule.Spec.VPCRef, networkv1alpha1.AcceptedReasonOwnershipDenied) } return nil diff --git a/internal/webhook/networkrule_webhook_test.go b/internal/webhook/networkrule_webhook_test.go index cf5111d8..8d797b15 100644 --- a/internal/webhook/networkrule_webhook_test.go +++ b/internal/webhook/networkrule_webhook_test.go @@ -36,8 +36,7 @@ func contextWithRequester(username string) context.Context { func testRule() *networkv1alpha1.NetworkRule { return &networkv1alpha1.NetworkRule{ Spec: networkv1alpha1.NetworkRuleSpec{ - VPCRef: "vpc-1", - VPCAttachmentRef: "attach-1", + VPCRef: "vpc-1", }, } }