diff --git a/.crd-ref-docs.yaml b/.crd-ref-docs.yaml index 6d75485..67665b2 100644 --- a/.crd-ref-docs.yaml +++ b/.crd-ref-docs.yaml @@ -6,7 +6,6 @@ processor: - "NetworkGatewaySpec" - "NetworkGatewayStatus" - "NetworkRule" - - "NetworkRuleBackend" - "NetworkRuleProtocol" - "NetworkRuleSpec" - "NetworkRuleStatus" diff --git a/api/v1alpha1/rule_types.go b/api/v1alpha1/rule_types.go index 9b7743e..0ff18a3 100644 --- a/api/v1alpha1/rule_types.go +++ b/api/v1alpha1/rule_types.go @@ -32,22 +32,6 @@ const ( AcceptedReasonOwnershipDenied string = "OwnershipDenied" ) -// NetworkRuleBackend is a single backend endpoint that ingress traffic -// matching a NetworkRule's VIP addresses is load-balanced to. -type NetworkRuleBackend struct { - // Address is the backend's IPv4 or IPv6 address. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MaxLength=45 - // +kubebuilder:validation:XValidation:rule="isIP(self)",message="address must be a valid IPv4 or IPv6 address" - Address string `json:"address"` - - // Port is the backend's destination port. - // +kubebuilder:validation:Required - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=65535 - Port int32 `json:"port"` -} - // NetworkRule defines ingress load-balancing for a single tenant // VPC/VPCAttachment, served by every NetworkGateway node identically // (anycast Direct Server Return — see NetworkGateway's doc comment). It is @@ -66,6 +50,12 @@ type NetworkRuleBackend struct { // backend selection never needs a single "owning" node the way Full-NAT's // SNAT-source model did. // +// Backends are not listed. BackendSelector picks the VPCAttachments that +// serve the rule, so the backend set follows attachments as they are +// created, deleted or moved to another node, and galactic-router on each +// node generates the ServiceVIPBinding its backends need from the same +// selection. +// // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced,shortName=netrule @@ -120,12 +110,25 @@ type NetworkRuleSpec struct { // +kubebuilder:validation:Maximum=65535 Port int32 `json:"port"` - // Backends is the list of backend address:port targets that ingress - // traffic matching VIPAddresses/Protocol/Port is load-balanced to. + // BackendSelector selects the VPCAttachments (cloud.datumapis.com/v1alpha1, + // in any namespace) that serve this rule, matched against each + // VPCAttachment object's own labels. Only attachments whose status.vpc + // equals VPCRef are candidates, whatever their labels, so a selector cannot + // reach into another tenant's VPC. Each selected attachment contributes the + // IPv6 addresses in its spec.interface.addresses as backends, on + // BackendPort; the DSR datapath carries IPv6 backends only. An attachment + // with no status.node or no IPv6 address is not a backend until it has + // both. An empty selector is rejected rather than read as "every attachment + // in the VPC". // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=64 - Backends []NetworkRuleBackend `json:"backends"` + // +kubebuilder:validation:XValidation:rule="(has(self.matchLabels) && size(self.matchLabels) > 0) || (has(self.matchExpressions) && size(self.matchExpressions) > 0)",message="backendSelector must not be empty" + BackendSelector metav1.LabelSelector `json:"backendSelector"` + + // BackendPort is the destination port on every selected backend. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + BackendPort int32 `json:"backendPort"` } // NetworkRuleStatus defines the observed state of a NetworkRule. diff --git a/api/v1alpha1/rule_types_test.go b/api/v1alpha1/rule_types_test.go index d3a748f..6c57f0e 100644 --- a/api/v1alpha1/rule_types_test.go +++ b/api/v1alpha1/rule_types_test.go @@ -20,9 +20,10 @@ func newTestRule() *NetworkRule { VIPAddresses: []string{"2001:db8:1::10"}, Protocol: NetworkRuleProtocolTCP, Port: 443, - Backends: []NetworkRuleBackend{ - {Address: "fd00:10:1::1", Port: 8443}, + BackendSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, }, + BackendPort: 8443, }, } } @@ -34,13 +35,13 @@ func TestNetworkRuleDeepCopy(t *testing.T) { dup := orig.DeepCopy() dup.Spec.VIPAddresses[0] = "2001:db8:1::20" - dup.Spec.Backends[0].Address = "fd00:10:1::2" + dup.Spec.BackendSelector.MatchLabels["app"] = "api" if orig.Spec.VIPAddresses[0] != "2001:db8:1::10" { t.Errorf("VIPAddresses[0] mutated: got %q", orig.Spec.VIPAddresses[0]) } - if orig.Spec.Backends[0].Address != "fd00:10:1::1" { - t.Errorf("Backends[0].Address mutated: got %q", orig.Spec.Backends[0].Address) + if orig.Spec.BackendSelector.MatchLabels["app"] != "web" { + t.Errorf("BackendSelector.MatchLabels mutated: got %v", orig.Spec.BackendSelector.MatchLabels) } } @@ -56,7 +57,9 @@ func TestNetworkRuleDeepCopyNil(t *testing.T) { // deserialises through JSON without data loss. func TestNetworkRuleJSONRoundTrip(t *testing.T) { orig := newTestRule() - orig.Spec.Backends = append(orig.Spec.Backends, NetworkRuleBackend{Address: "fd00:10:1::3", Port: 8444}) + orig.Spec.BackendSelector.MatchExpressions = []metav1.LabelSelectorRequirement{ + {Key: "tier", Operator: metav1.LabelSelectorOpIn, Values: []string{"frontend"}}, + } orig.Status.Conditions = []metav1.Condition{ {Type: ConditionTypeAccepted, Status: metav1.ConditionTrue, Reason: AcceptedReasonOwnershipVerified}, } @@ -77,8 +80,11 @@ func TestNetworkRuleJSONRoundTrip(t *testing.T) { if got.Spec.VPCAttachmentRef != orig.Spec.VPCAttachmentRef { t.Errorf("VPCAttachmentRef: got %q, want %q", got.Spec.VPCAttachmentRef, orig.Spec.VPCAttachmentRef) } - if len(got.Spec.Backends) != 2 { - t.Errorf("Backends len: got %d, want 2", len(got.Spec.Backends)) + if got.Spec.BackendSelector.MatchLabels["app"] != "web" || len(got.Spec.BackendSelector.MatchExpressions) != 1 { + t.Errorf("BackendSelector: got %+v", got.Spec.BackendSelector) + } + if got.Spec.BackendPort != orig.Spec.BackendPort { + t.Errorf("BackendPort: got %d, want %d", got.Spec.BackendPort, orig.Spec.BackendPort) } if len(got.Status.Conditions) != 1 || got.Status.Conditions[0].Reason != AcceptedReasonOwnershipVerified { t.Errorf("Conditions: got %v", got.Status.Conditions) @@ -122,11 +128,11 @@ func TestNetworkRuleStatusHasNoPrimaryNode(t *testing.T) { } } -// TestNetworkRuleBackendFieldNames verifies the JSON keys for -// NetworkRuleBackend match the CRD schema ("address", "port"). +// TestNetworkRuleBackendFieldNames verifies the JSON keys for the backend +// selection fields match the CRD schema, and that the static backend list +// they replaced is gone. func TestNetworkRuleBackendFieldNames(t *testing.T) { - b := NetworkRuleBackend{Address: "fd00:10:1::1", Port: 8443} - data, err := json.Marshal(b) + data, err := json.Marshal(newTestRule().Spec) if err != nil { t.Fatalf("Marshal: %v", err) } @@ -134,11 +140,14 @@ func TestNetworkRuleBackendFieldNames(t *testing.T) { if err := json.Unmarshal(data, &m); err != nil { t.Fatalf("Unmarshal: %v", err) } - if v, ok := m["address"]; !ok || v != "fd00:10:1::1" { - t.Errorf("expected JSON key \"address\"=%q, got %v", "fd00:10:1::1", m) + if _, ok := m["backendSelector"]; !ok { + t.Errorf("expected JSON key \"backendSelector\", got %v", m) + } + if v, ok := m["backendPort"]; !ok || v != float64(8443) { + t.Errorf("expected JSON key \"backendPort\"=8443, got %v", m) } - if v, ok := m["port"]; !ok || v != float64(8443) { - t.Errorf("expected JSON key \"port\"=8443, got %v", m) + if _, ok := m["backends"]; ok { + t.Errorf("unexpected \"backends\" key: %v", m) } } diff --git a/api/v1alpha1/vipbinding_types.go b/api/v1alpha1/vipbinding_types.go index 079ba75..08f8541 100644 --- a/api/v1alpha1/vipbinding_types.go +++ b/api/v1alpha1/vipbinding_types.go @@ -8,11 +8,11 @@ import ( // DSR/Maglev load-balancer datapath: it tells the node which service VIP a // specific local backend must be reachable on, so the backend can reply to // clients directly (the "Direct Server Return" this design depends on — -// see NetworkGateway's doc comment). Written by the same controller that -// already resolves a NetworkRule's backends to worker nodes/SRv6 -// information (galactic-gateway's usidresolver.go), one object per -// (node, VIP, backend) triple; consumed by a per-node reconciler running -// inside galactic-router's tenant role. +// see NetworkGateway's doc comment). Generated, never written by users: +// galactic-router on each worker node writes one object per (rule, VIP, +// backend) for every backend a NetworkRule's BackendSelector places on that +// node, owned by the rule, and the same process's per-node reconciler +// consumes it. // // EgressKind decides which of two backend mechanisms this object drives, // mirroring the same veth/tap fork the SRv6 uSID decap datapath already @@ -77,6 +77,14 @@ type ServiceVIPBindingSpec struct { // +kubebuilder:validation:Required TargetRef TargetRef `json:"targetRef"` + // VPCRef is the opaque identifier of the VPC the backend belongs to, + // copied from the owning NetworkRule. It names the tenant VRF the + // binding's translation rows are written for, so two tenants using the + // same backend address on one node never share a row. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + VPCRef string `json:"vpcRef"` + // VIPAddress is the service VIP the backend must be reachable on. // +kubebuilder:validation:Required // +kubebuilder:validation:XValidation:rule="isIP(self)",message="vipAddress must be a valid IP address" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8833b84..d4593c0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1658,21 +1658,6 @@ func (in *NetworkRule) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NetworkRuleBackend) DeepCopyInto(out *NetworkRuleBackend) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkRuleBackend. -func (in *NetworkRuleBackend) DeepCopy() *NetworkRuleBackend { - if in == nil { - return nil - } - out := new(NetworkRuleBackend) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkRuleList) DeepCopyInto(out *NetworkRuleList) { *out = *in @@ -1713,11 +1698,7 @@ func (in *NetworkRuleSpec) DeepCopyInto(out *NetworkRuleSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.Backends != nil { - in, out := &in.Backends, &out.Backends - *out = make([]NetworkRuleBackend, len(*in)) - copy(*out, *in) - } + in.BackendSelector.DeepCopyInto(&out.BackendSelector) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkRuleSpec. diff --git a/config/crd/network.datumapis.com_networkrules.yaml b/config/crd/network.datumapis.com_networkrules.yaml index 8b82b3e..f1cb406 100644 --- a/config/crd/network.datumapis.com_networkrules.yaml +++ b/config/crd/network.datumapis.com_networkrules.yaml @@ -50,6 +50,12 @@ spec: flow (internal/maglev), and forwards without rewriting anything — backend selection never needs a single "owning" node the way Full-NAT's SNAT-source model did. + + Backends are not listed. BackendSelector picks the VPCAttachments that + serve the rule, so the backend set follows attachments as they are + created, deleted or moved to another node, and galactic-router on each + node generates the ServiceVIPBinding its backends need from the same + selection. properties: apiVersion: description: |- @@ -73,35 +79,73 @@ spec: NetworkRuleSpec defines the desired ingress load-balancing state for a tenant VPC/VPCAttachment. properties: - backends: + backendPort: + description: BackendPort is the destination port on every selected + backend. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + backendSelector: description: |- - Backends is the list of backend address:port targets that ingress - traffic matching VIPAddresses/Protocol/Port is load-balanced to. - items: - description: |- - NetworkRuleBackend is a single backend endpoint that ingress traffic - matching a NetworkRule's VIP addresses is load-balanced to. - properties: - address: - description: Address is the backend's IPv4 or IPv6 address. - maxLength: 45 + BackendSelector selects the VPCAttachments (cloud.datumapis.com/v1alpha1, + in any namespace) that serve this rule, matched against each + VPCAttachment object's own labels. Only attachments whose status.vpc + equals VPCRef are candidates, whatever their labels, so a selector cannot + reach into another tenant's VPC. Each selected attachment contributes the + IPv6 addresses in its spec.interface.addresses as backends, on + BackendPort; the DSR datapath carries IPv6 backends only. An attachment + with no status.node or no IPv6 address is not a backend until it has + both. An empty selector is rejected rather than read as "every attachment + in the VPC". + properties: + 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. This array is replaced during a strategic + merge patch. + 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 - x-kubernetes-validations: - - message: address must be a valid IPv4 or IPv6 address - rule: isIP(self) - port: - description: Port is the backend's destination port. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - required: - - address - - port - type: object - maxItems: 64 - minItems: 1 - type: array + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: backendSelector must not be empty + rule: (has(self.matchLabels) && size(self.matchLabels) > 0) || (has(self.matchExpressions) + && size(self.matchExpressions) > 0) port: description: Port is the ingress port on VIPAddresses that this rule load-balances. @@ -146,7 +190,8 @@ spec: minLength: 1 type: string required: - - backends + - backendPort + - backendSelector - port - protocol - vipAddresses diff --git a/config/crd/network.datumapis.com_servicevipbindings.yaml b/config/crd/network.datumapis.com_servicevipbindings.yaml index 3ca467e..79a16a6 100644 --- a/config/crd/network.datumapis.com_servicevipbindings.yaml +++ b/config/crd/network.datumapis.com_servicevipbindings.yaml @@ -40,11 +40,11 @@ spec: DSR/Maglev load-balancer datapath: it tells the node which service VIP a specific local backend must be reachable on, so the backend can reply to clients directly (the "Direct Server Return" this design depends on — - see NetworkGateway's doc comment). Written by the same controller that - already resolves a NetworkRule's backends to worker nodes/SRv6 - information (galactic-gateway's usidresolver.go), one object per - (node, VIP, backend) triple; consumed by a per-node reconciler running - inside galactic-router's tenant role. + see NetworkGateway's doc comment). Generated, never written by users: + galactic-router on each worker node writes one object per (rule, VIP, + backend) for every backend a NetworkRule's BackendSelector places on that + node, owned by the rule, and the same process's per-node reconciler + consumes it. EgressKind decides which of two backend mechanisms this object drives, mirroring the same veth/tap fork the SRv6 uSID decap datapath already @@ -153,12 +153,21 @@ spec: x-kubernetes-validations: - message: vipAddress must be a valid IP address rule: isIP(self) + vpcRef: + description: |- + VPCRef is the opaque identifier of the VPC the backend belongs to, + copied from the owning NetworkRule. It names the tenant VRF the + binding's translation rows are written for, so two tenants using the + same backend address on one node never share a row. + minLength: 1 + type: string required: - egressKind - port - protocol - targetRef - vipAddress + - vpcRef type: object status: description: ServiceVIPBindingStatus defines the observed state of a ServiceVIPBinding. diff --git a/config/samples/network.datumapis.com_v1alpha1_networkrule_tcp.yaml b/config/samples/network.datumapis.com_v1alpha1_networkrule_tcp.yaml index a76824a..bea9747 100644 --- a/config/samples/network.datumapis.com_v1alpha1_networkrule_tcp.yaml +++ b/config/samples/network.datumapis.com_v1alpha1_networkrule_tcp.yaml @@ -10,8 +10,7 @@ spec: - 2001:db8:1::10 protocol: tcp port: 443 - backends: - - address: fd00:10:1::1 - port: 8443 - - address: fd00:10:1::2 - port: 8443 + backendSelector: + matchLabels: + app: web + backendPort: 8443 diff --git a/docs/api/bgp.md b/docs/api/bgp.md index cc6c93f..040b035 100644 --- a/docs/api/bgp.md +++ b/docs/api/bgp.md @@ -1590,11 +1590,11 @@ ServiceVIPBinding drives one worker node's backend-side half of the DSR/Maglev load-balancer datapath: it tells the node which service VIP a specific local backend must be reachable on, so the backend can reply to clients directly (the "Direct Server Return" this design depends on — -see NetworkGateway's doc comment). Written by the same controller that -already resolves a NetworkRule's backends to worker nodes/SRv6 -information (galactic-gateway's usidresolver.go), one object per -(node, VIP, backend) triple; consumed by a per-node reconciler running -inside galactic-router's tenant role. +see NetworkGateway's doc comment). Generated, never written by users: +galactic-router on each worker node writes one object per (rule, VIP, +backend) for every backend a NetworkRule's BackendSelector places on that +node, owned by the rule, and the same process's per-node reconciler +consumes it. EgressKind decides which of two backend mechanisms this object drives, mirroring the same veth/tap fork the SRv6 uSID decap datapath already @@ -1666,6 +1666,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `targetRef` _[TargetRef](#targetref)_ | TargetRef identifies the Node this binding applies to. | | Required: \{\}
| +| `vpcRef` _string_ | VPCRef is the opaque identifier of the VPC the backend belongs to,
copied from the owning NetworkRule. It names the tenant VRF the
binding's translation rows are written for, so two tenants using the
same backend address on one node never share a row. | | MinLength: 1
Required: \{\}
| | `vipAddress` _string_ | VIPAddress is the service VIP the backend must be reachable on. | | Required: \{\}
| | `port` _integer_ | Port is the VIP-facing port traffic arrives on. | | Maximum: 65535
Minimum: 1
Required: \{\}
| | `backendAddress` _string_ | BackendAddress is the backend's own real address (its pod-netns
address for a veth backend, or its actual guest-facing address for a
tap backend) — the VIP-boundary substitution target for both kinds
now (see EgressKind's own doc comment for why veth needs this too,
not just tap). +optional at the API level for the same reason
EgressKind itself carries no matching CEL requirement; the
reconciler validates it's set for either kind before doing anything. | | | diff --git a/docs/api/gateway.md b/docs/api/gateway.md index 179641d..c495d46 100644 --- a/docs/api/gateway.md +++ b/docs/api/gateway.md @@ -374,6 +374,12 @@ flow (internal/maglev), and forwards without rewriting anything — backend selection never needs a single "owning" node the way Full-NAT's SNAT-source model did. +Backends are not listed. BackendSelector picks the VPCAttachments that +serve the rule, so the backend set follows attachments as they are +created, deleted or moved to another node, and galactic-router on each +node generates the ServiceVIPBinding its backends need from the same +selection. + @@ -389,24 +395,6 @@ SNAT-source model did. | `status` _[NetworkRuleStatus](#networkrulestatus)_ | | | | -#### NetworkRuleBackend - - - -NetworkRuleBackend is a single backend endpoint that ingress traffic -matching a NetworkRule's VIP addresses is load-balanced to. - - - -_Appears in:_ -- [NetworkRuleSpec](#networkrulespec) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `address` _string_ | Address is the backend's IPv4 or IPv6 address. | | MaxLength: 45
Required: \{\}
| -| `port` _integer_ | Port is the backend's destination port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| - - #### NetworkRuleProtocol _Underlying type:_ _string_ @@ -448,7 +436,8 @@ _Appears in:_ | `vipAddresses` _string array_ | VIPAddresses is the list of ingress VIP addresses (IPv4 and/or IPv6)
this rule provisions on the assigned gateway node(s). | | MaxItems: 8
MinItems: 1
Required: \{\}
items:MaxLength: 45
| | `protocol` _[NetworkRuleProtocol](#networkruleprotocol)_ | Protocol is the transport protocol matched by VIPAddresses/Port. | | Enum: [tcp udp]
Required: \{\}
| | `port` _integer_ | Port is the ingress port on VIPAddresses that this rule load-balances. | | Maximum: 65535
Minimum: 1
Required: \{\}
| -| `backends` _[NetworkRuleBackend](#networkrulebackend) array_ | Backends is the list of backend address:port targets that ingress
traffic matching VIPAddresses/Protocol/Port is load-balanced to. | | MaxItems: 64
MinItems: 1
Required: \{\}
| +| `backendSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | BackendSelector selects the VPCAttachments (cloud.datumapis.com/v1alpha1,
in any namespace) that serve this rule, matched against each
VPCAttachment object's own labels. Only attachments whose status.vpc
equals VPCRef are candidates, whatever their labels, so a selector cannot
reach into another tenant's VPC. Each selected attachment contributes the
IPv6 addresses in its spec.interface.addresses as backends, on
BackendPort; the DSR datapath carries IPv6 backends only. An attachment
with no status.node or no IPv6 address is not a backend until it has
both. An empty selector is rejected rather than read as "every attachment
in the VPC". | | Required: \{\}
| +| `backendPort` _integer_ | BackendPort is the destination port on every selected backend. | | Maximum: 65535
Minimum: 1
Required: \{\}
| #### NetworkRuleStatus @@ -651,11 +640,11 @@ ServiceVIPBinding drives one worker node's backend-side half of the DSR/Maglev load-balancer datapath: it tells the node which service VIP a specific local backend must be reachable on, so the backend can reply to clients directly (the "Direct Server Return" this design depends on — -see NetworkGateway's doc comment). Written by the same controller that -already resolves a NetworkRule's backends to worker nodes/SRv6 -information (galactic-gateway's usidresolver.go), one object per -(node, VIP, backend) triple; consumed by a per-node reconciler running -inside galactic-router's tenant role. +see NetworkGateway's doc comment). Generated, never written by users: +galactic-router on each worker node writes one object per (rule, VIP, +backend) for every backend a NetworkRule's BackendSelector places on that +node, owned by the rule, and the same process's per-node reconciler +consumes it. EgressKind decides which of two backend mechanisms this object drives, mirroring the same veth/tap fork the SRv6 uSID decap datapath already @@ -727,6 +716,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `targetRef` _[TargetRef](#targetref)_ | TargetRef identifies the Node this binding applies to. | | Required: \{\}
| +| `vpcRef` _string_ | VPCRef is the opaque identifier of the VPC the backend belongs to,
copied from the owning NetworkRule. It names the tenant VRF the
binding's translation rows are written for, so two tenants using the
same backend address on one node never share a row. | | MinLength: 1
Required: \{\}
| | `vipAddress` _string_ | VIPAddress is the service VIP the backend must be reachable on. | | Required: \{\}
| | `port` _integer_ | Port is the VIP-facing port traffic arrives on. | | Maximum: 65535
Minimum: 1
Required: \{\}
| | `protocol` _[NetworkRuleProtocol](#networkruleprotocol)_ | Protocol is the transport protocol this binding applies to. | | Enum: [tcp udp]
Required: \{\}
| diff --git a/test/e2e/tests/networkrule-backend-selector/chainsaw-test.yaml b/test/e2e/tests/networkrule-backend-selector/chainsaw-test.yaml new file mode 100644 index 0000000..ba79c65 --- /dev/null +++ b/test/e2e/tests/networkrule-backend-selector/chainsaw-test.yaml @@ -0,0 +1,144 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: networkrule-backend-selector +spec: + description: > + Verify the schema rules for a NetworkRule's backend selection: a rule with + a non-empty backendSelector and a backendPort is accepted, a rule with an + empty backendSelector is rejected, and a rule that still lists static + backends is rejected, since that field no longer exists. Also verifies that a + ServiceVIPBinding requires vpcRef. Does not require any operator to be + running; every rule under test is enforced by the API server alone. + steps: + - name: accept-rule-with-selector + try: + - apply: + resource: + apiVersion: network.datumapis.com/v1alpha1 + kind: NetworkRule + metadata: + name: e2e-rule-selector + spec: + vpcRef: vpc-a + vpcAttachmentRef: vpc-a-attachment + vipAddresses: + - 2001:db8:1::10 + protocol: tcp + port: 443 + backendSelector: + matchLabels: + app: web + backendPort: 8443 + - assert: + resource: + apiVersion: network.datumapis.com/v1alpha1 + kind: NetworkRule + metadata: + name: e2e-rule-selector + spec: + backendPort: 8443 + backendSelector: + matchLabels: + app: web + + - name: reject-empty-selector + try: + - script: + content: | + set +e + OUTPUT=$(kubectl apply -n "$NAMESPACE" -f - 2>&1 <<'YAML' + apiVersion: network.datumapis.com/v1alpha1 + kind: NetworkRule + metadata: + name: e2e-rule-empty-selector + spec: + vpcRef: vpc-a + vpcAttachmentRef: vpc-a-attachment + vipAddresses: + - 2001:db8:1::11 + protocol: tcp + port: 443 + backendSelector: {} + backendPort: 8443 + YAML + ) + EXIT=$? + set -e + if [ "$EXIT" -eq 0 ]; then + echo "ERROR: expected rejection of an empty backendSelector but the resource was accepted" + exit 1 + fi + case "$OUTPUT" in + *"backendSelector must not be empty"*) ;; + *) echo "ERROR: rejected for the wrong reason: $OUTPUT"; exit 1 ;; + esac + echo "OK: empty backendSelector correctly rejected" + + - name: reject-static-backends + try: + - script: + content: | + set +e + OUTPUT=$(kubectl apply -n "$NAMESPACE" -f - 2>&1 <<'YAML' + apiVersion: network.datumapis.com/v1alpha1 + kind: NetworkRule + metadata: + name: e2e-rule-static-backends + spec: + vpcRef: vpc-a + vpcAttachmentRef: vpc-a-attachment + vipAddresses: + - 2001:db8:1::12 + protocol: tcp + port: 443 + backends: + - address: fd00:10:1::1 + port: 8443 + YAML + ) + EXIT=$? + set -e + if [ "$EXIT" -eq 0 ]; then + echo "ERROR: expected rejection of a rule with static backends but the resource was accepted" + exit 1 + fi + case "$OUTPUT" in + *'unknown field "spec.backends"'*) ;; + *) echo "ERROR: rejected for the wrong reason: $OUTPUT"; exit 1 ;; + esac + echo "OK: rule with static backends correctly rejected" + + - name: reject-binding-without-vpcref + try: + - script: + content: | + set +e + OUTPUT=$(kubectl apply -n "$NAMESPACE" -f - 2>&1 <<'YAML' + apiVersion: network.datumapis.com/v1alpha1 + kind: ServiceVIPBinding + metadata: + name: e2e-binding-no-vpcref + spec: + targetRef: + kind: Node + name: node-a + vipAddress: 2001:db8:1::10 + port: 443 + protocol: tcp + backendAddress: fd00:10:1::1 + backendPort: 8443 + egressKind: veth + YAML + ) + EXIT=$? + set -e + if [ "$EXIT" -eq 0 ]; then + echo "ERROR: expected rejection of a ServiceVIPBinding with no vpcRef but the resource was accepted" + exit 1 + fi + case "$OUTPUT" in + *"vpcRef"*) ;; + *) echo "ERROR: rejected for the wrong reason: $OUTPUT"; exit 1 ;; + esac + echo "OK: ServiceVIPBinding with no vpcRef correctly rejected"