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
13 changes: 13 additions & 0 deletions api/v1alpha1/advertisement_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ type BGPAdvertisementSpec struct {
// +optional
// +kubebuilder:validation:Minimum=0
LocalPreference *int32 `json:"localPreference,omitempty"`

// VRFID is the 16-bit PoP-local VRF identifier this advertisement's SRv6
// SID resolves into, per RFC 9800 uSID Argument addressing. Required when
// Function is set.
// +optional
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
VRFID *int32 `json:"vrfID,omitempty"`

// Function is the RFC 8986 SRv6 endpoint behavior applied when this
// advertisement originates a compressed uSID. Required when VRFID is set.
// +optional
Function *SRv6Function `json:"function,omitempty"`
}

// BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
Expand Down
107 changes: 107 additions & 0 deletions api/v1alpha1/advertisement_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,113 @@ func TestBGPAdvertisementListDeepCopy(t *testing.T) {
}
}

// TestBGPAdvertisementSRv6JSONRoundTrip verifies VRFID and Function serialise
// and deserialise correctly.
func TestBGPAdvertisementSRv6JSONRoundTrip(t *testing.T) {
orig := newTestAdvertisement()
orig.Spec.VRFID = ptr(int32(100))
orig.Spec.Function = ptr(SRv6FunctionEndDT6)

data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("Marshal: %v", err)
}

var got BGPAdvertisement
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("Unmarshal: %v", err)
}

if got.Spec.VRFID == nil || *got.Spec.VRFID != 100 {
t.Errorf("VRFID: got %v, want 100", got.Spec.VRFID)
}
if got.Spec.Function == nil || *got.Spec.Function != SRv6FunctionEndDT6 {
t.Errorf("Function: got %v, want %q", got.Spec.Function, SRv6FunctionEndDT6)
}
}

// TestBGPAdvertisementSRv6FieldNames verifies the JSON keys are "vrfID" and "function".
func TestBGPAdvertisementSRv6FieldNames(t *testing.T) {
orig := newTestAdvertisement()
orig.Spec.VRFID = ptr(int32(100))
orig.Spec.Function = ptr(SRv6FunctionEndDT46)

data, err := json.Marshal(orig.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 raw, ok := m["vrfID"]; !ok || raw != float64(100) {
t.Errorf("unexpected vrfID value: %v", raw)
}
if raw, ok := m["function"]; !ok || raw != "End.DT46" {
t.Errorf("unexpected function value: %v", raw)
}
}

// TestBGPAdvertisementSRv6OmitEmpty verifies that VRFID and Function are
// omitted from JSON output when unset.
func TestBGPAdvertisementSRv6OmitEmpty(t *testing.T) {
orig := newTestAdvertisement()

data, err := json.Marshal(orig.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 _, ok := m["vrfID"]; ok {
t.Error("expected \"vrfID\" key to be absent when empty")
}
if _, ok := m["function"]; ok {
t.Error("expected \"function\" key to be absent when empty")
}
}

// TestBGPAdvertisementSRv6DeepCopy verifies DeepCopy isolates VRFID and Function.
func TestBGPAdvertisementSRv6DeepCopy(t *testing.T) {
orig := newTestAdvertisement()
orig.Spec.VRFID = ptr(int32(100))
orig.Spec.Function = ptr(SRv6FunctionEndDT4)

dup := orig.DeepCopy()
*dup.Spec.VRFID = 999
*dup.Spec.Function = SRv6FunctionEndDT6

if *orig.Spec.VRFID != 100 {
t.Errorf("VRFID mutated: got %d", *orig.Spec.VRFID)
}
if *orig.Spec.Function != SRv6FunctionEndDT4 {
t.Errorf("Function mutated: got %q", *orig.Spec.Function)
}
}

// TestSRv6FunctionConstants verifies enum values match expected strings.
func TestSRv6FunctionConstants(t *testing.T) {
cases := []struct {
fn SRv6Function
want string
}{
{SRv6FunctionEndDT4, "End.DT4"},
{SRv6FunctionEndDT6, "End.DT6"},
{SRv6FunctionEndDT46, "End.DT46"},
}
for _, tt := range cases {
if string(tt.fn) != tt.want {
t.Errorf("SRv6Function %q: got %q, want %q", tt.fn, string(tt.fn), tt.want)
}
}
}

// TestRedistributeSourceConstants verifies enum values match expected strings.
func TestRedistributeSourceConstants(t *testing.T) {
cases := []struct {
Expand Down
6 changes: 0 additions & 6 deletions api/v1alpha1/policy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,6 @@ type PolicySetActions struct {
// +optional
// +kubebuilder:validation:Minimum=0
Color *int32 `json:"color,omitempty"`

// Srv6EndpointBehavior sets the SRv6 endpoint behavior on a route.
// Common values: End, End.X, End.DT6, End.B6, End.M.
// +optional
// +kubebuilder:validation:MaxLength=64
Srv6EndpointBehavior *string `json:"srv6EndpointBehavior,omitempty"`
}

// BGPOrigin is the BGP origin attribute per RFC 4271.
Expand Down
28 changes: 7 additions & 21 deletions api/v1alpha1/policy_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ func TestPolicySetActionsFieldNamesJSON(t *testing.T) {
self := true
metric := int32(100)
color := int32(42)
ep := "End.DT6"
origin := BGPOriginIGP
prepend := uint32(2)
asn := int64(65000)
Expand All @@ -239,9 +238,8 @@ func TestPolicySetActionsFieldNamesJSON(t *testing.T) {
Add: []string{"65000:100"},
Remove: []string{"65000:200"},
},
Metric: &metric,
Color: &color,
Srv6EndpointBehavior: &ep,
Metric: &metric,
Color: &color,
}

data, err := json.Marshal(set)
Expand All @@ -256,7 +254,7 @@ func TestPolicySetActionsFieldNamesJSON(t *testing.T) {

expectedKeys := []string{
"origin", "asPath", "nextHop", "extCommunities",
"metric", "color", "srv6EndpointBehavior",
"metric", "color",
}
for _, key := range expectedKeys {
if _, ok := m[key]; !ok {
Expand Down Expand Up @@ -320,7 +318,6 @@ func TestPolicySetActionsDeepCopy(t *testing.T) {
self := true
metric := int32(100)
color := int32(42)
ep := "End.DT6"
origin := BGPOriginIGP
prepend := uint32(2)
asn := int64(65000)
Expand All @@ -338,18 +335,15 @@ func TestPolicySetActionsDeepCopy(t *testing.T) {
Add: []string{"65000:100"},
Remove: []string{"65000:200"},
},
Metric: &metric,
Color: &color,
Srv6EndpointBehavior: &ep,
Metric: &metric,
Color: &color,
}

dup := orig.DeepCopy()

// Mutate duplicate and verify original unchanged.
*dup.Metric = 999
*dup.Color = 999
newEP := "End.X"
dup.Srv6EndpointBehavior = &newEP
dup.ExtCommunities.Add[0] = "99999:99999"

if *orig.Metric != 100 {
Expand All @@ -358,9 +352,6 @@ func TestPolicySetActionsDeepCopy(t *testing.T) {
if *orig.Color != 42 {
t.Errorf("Color mutated: got %d", *orig.Color)
}
if *orig.Srv6EndpointBehavior != "End.DT6" {
t.Errorf("Srv6EndpointBehavior mutated: got %q", *orig.Srv6EndpointBehavior)
}
if orig.ExtCommunities.Add[0] != "65000:100" {
t.Errorf("ExtCommunities.Add[0] mutated: got %q", orig.ExtCommunities.Add[0])
}
Expand Down Expand Up @@ -427,7 +418,6 @@ func TestPolicySetActionsJSONRoundTrip(t *testing.T) {
addr := "2001:db8::1"
metric := int32(200)
color := int32(10)
ep := "End.B6"
origin := BGPOriginEGP
prepend := uint32(3)
asn := int64(65000)
Expand All @@ -446,9 +436,8 @@ func TestPolicySetActionsJSONRoundTrip(t *testing.T) {
Add: []string{"65000:100", "65000:200"},
Remove: []string{"65000:999"},
},
Metric: &metric,
Color: &color,
Srv6EndpointBehavior: &ep,
Metric: &metric,
Color: &color,
}

data, err := json.Marshal(orig)
Expand Down Expand Up @@ -476,9 +465,6 @@ func TestPolicySetActionsJSONRoundTrip(t *testing.T) {
if *got.Color != color {
t.Errorf("Color: got %d, want %d", *got.Color, color)
}
if *got.Srv6EndpointBehavior != ep {
t.Errorf("Srv6EndpointBehavior: got %q, want %q", *got.Srv6EndpointBehavior, ep)
}
if len(got.ExtCommunities.Add) != 2 {
t.Errorf("ExtCommunities.Add count: got %d, want 2", len(got.ExtCommunities.Add))
}
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/router_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ type BGPRouterSpec struct {
// +kubebuilder:validation:XValidation:rule="self.contains(':')",message="srv6Locator must be an IPv6 CIDR"
// +optional
SRv6Locator string `json:"srv6Locator,omitempty"`

// NodeID is this router's 8-bit slot within its PoP's SRv6Locator block,
// used for RFC 9800 NEXT-CSID compression. Unique within the PoP.
// Values 0 and 255 are reserved.
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=254
// +optional
NodeID int32 `json:"nodeID,omitempty"`
}

// BGPRouterStatus defines the observed state of a BGPRouter.
Expand Down
61 changes: 61 additions & 0 deletions api/v1alpha1/router_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,64 @@ func TestBGPRouterSRv6LocatorOmitEmpty(t *testing.T) {
t.Error("expected \"srv6Locator\" key to be absent when empty")
}
}

// TestBGPRouterJSONRoundTripNodeID verifies that NodeID survives JSON
// marshal/unmarshal.
func TestBGPRouterJSONRoundTripNodeID(t *testing.T) {
orig := newTestRouter(RouterRoleFabric)
orig.Spec.NodeID = 42

data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("Marshal: %v", err)
}

var got BGPRouter
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("Unmarshal: %v", err)
}

if got.Spec.NodeID != orig.Spec.NodeID {
t.Errorf("NodeID: got %d, want %d", got.Spec.NodeID, orig.Spec.NodeID)
}
}

// TestBGPRouterNodeIDFieldName verifies the JSON key is "nodeID".
func TestBGPRouterNodeIDFieldName(t *testing.T) {
orig := newTestRouter(RouterRoleFabric)
orig.Spec.NodeID = 42

data, err := json.Marshal(orig.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 raw, ok := m["nodeID"]; !ok || raw != float64(42) {
t.Errorf("unexpected nodeID value: %v", raw)
}
}

// TestBGPRouterNodeIDOmitEmpty verifies that a router without a NodeID omits
// the "nodeID" key from JSON output.
func TestBGPRouterNodeIDOmitEmpty(t *testing.T) {
orig := newTestRouter(RouterRoleFabric)

data, err := json.Marshal(orig.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 _, ok := m["nodeID"]; ok {
t.Error("expected \"nodeID\" key to be absent when empty")
}
}
18 changes: 18 additions & 0 deletions api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ type AddressFamily struct {
SAFI SAFI `json:"safi"`
}

// SRv6Function is the RFC 8986 endpoint behavior applied to a decapsulated
// SRv6 packet, addressed via the uSID Argument space per RFC 9800.
//
// +kubebuilder:validation:Enum=End.DT4;End.DT6;End.DT46
type SRv6Function string

const (
// SRv6FunctionEndDT4 decapsulates and looks up the packet in an IPv4 VRF.
SRv6FunctionEndDT4 SRv6Function = "End.DT4"

// SRv6FunctionEndDT6 decapsulates and looks up the packet in an IPv6 VRF.
SRv6FunctionEndDT6 SRv6Function = "End.DT6"

// SRv6FunctionEndDT46 decapsulates and looks up the packet in an IPv4 or
// IPv6 VRF based on the inner packet's address family.
SRv6FunctionEndDT46 SRv6Function = "End.DT46"
)

// RouterRole defines the functional role of a BGPRouter within the network.
//
// +kubebuilder:validation:Enum=fabric;tenant;transit
Expand Down
17 changes: 9 additions & 8 deletions api/v1alpha1/vrf_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced,shortName=bgpvrf
// +kubebuilder:printcolumn:name="RD",type="string",JSONPath=".spec.routeDistinguisher"
// +kubebuilder:printcolumn:name="VRF-ID",type="integer",JSONPath=".spec.vrfID"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type BGPVRFInstance struct {
metav1.TypeMeta `json:",inline"`
Expand All @@ -21,17 +21,18 @@ type BGPVRFInstance struct {
}

// BGPVRFInstanceSpec defines the desired VRF configuration.
//
// +kubebuilder:validation:XValidation:rule="self.routeDistinguisher.matches('^([0-9]{1,9}[.][0-9]{1,9}[.][0-9]{1,9}[.][0-9]{1,9}|[0-9]{1,9}):[0-9]{1,9}$')",message="routeDistinguisher must be in ASN:NN or IP:NN format"
type BGPVRFInstanceSpec struct {
RouterTarget `json:",inline"`

// RouteDistinguisher uniquely identifies this VRF in the BGP control plane.
// Format: "ASN:NN" (e.g. "65000:100") or "IP:NN" (e.g. "192.0.2.1:100").
// VRFID is the 16-bit PoP-local VRF identifier used for RFC 9800 uSID
// Argument addressing and to derive the RFC 4364 Type 1 Route
// Distinguisher ("routerID:vrfID"). Unique per (VPC, PoP). Value 0 is
// reserved.
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=21
RouteDistinguisher string `json:"routeDistinguisher"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
VRFID int32 `json:"vrfID"`

// ImportRouteTargets is the list of BGP extended community route targets
// used to import routes into this VRF.
Expand Down
Loading
Loading