Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .crd-ref-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ processor:
- "NetworkGatewaySpec"
- "NetworkGatewayStatus"
- "NetworkRule"
- "NetworkRuleBackend"
- "NetworkRuleProtocol"
- "NetworkRuleSpec"
- "NetworkRuleStatus"
Expand Down
45 changes: 24 additions & 21 deletions api/v1alpha1/rule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
41 changes: 25 additions & 16 deletions api/v1alpha1/rule_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
}
Expand All @@ -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)
}
}

Expand All @@ -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},
}
Expand All @@ -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)
Expand Down Expand Up @@ -122,23 +128,26 @@ 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)
}
var m map[string]any
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)
}
}

Expand Down
18 changes: 13 additions & 5 deletions api/v1alpha1/vipbinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
21 changes: 1 addition & 20 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 73 additions & 28 deletions config/crd/network.datumapis.com_networkrules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |-
Expand All @@ -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.
Expand Down Expand Up @@ -146,7 +190,8 @@ spec:
minLength: 1
type: string
required:
- backends
- backendPort
- backendSelector
- port
- protocol
- vipAddresses
Expand Down
19 changes: 14 additions & 5 deletions config/crd/network.datumapis.com_servicevipbindings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading