Skip to content

Commit 1b07941

Browse files
edipascaleFrostman
authored andcommitted
chore: gw validation unit tests
Signed-off-by: Emanuele Di Pascale <[email protected]>
1 parent 7fbc961 commit 1b07941

File tree

2 files changed

+254
-25
lines changed

2 files changed

+254
-25
lines changed

api/gateway/v1alpha1/gateway_types.go

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package v1alpha1
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"net"
1011
"net/netip"
@@ -13,6 +14,8 @@ import (
1314
kclient "sigs.k8s.io/controller-runtime/pkg/client"
1415
)
1516

17+
var ErrInvalidGW = errors.New("invalid gateway")
18+
1619
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
1720

1821
// GatewaySpec defines the desired state of Gateway.
@@ -88,92 +91,92 @@ func (gw *Gateway) Default() {
8891
func (gw *Gateway) Validate(ctx context.Context, kube kclient.Reader) error {
8992
protoIP, err := netip.ParsePrefix(gw.Spec.ProtocolIP)
9093
if err != nil {
91-
return fmt.Errorf("invalid ProtocolIP %s: %w", gw.Spec.ProtocolIP, err)
94+
return fmt.Errorf("invalid ProtocolIP %s: %w", gw.Spec.ProtocolIP, errors.Join(err, ErrInvalidGW))
9295
}
9396
if protoIP.Bits() != 32 {
94-
return fmt.Errorf("ProtocolIP %s must be a /32 prefix", gw.Spec.ProtocolIP) //nolint:goerr113
97+
return fmt.Errorf("ProtocolIP %s must be a /32 prefix: %w", gw.Spec.ProtocolIP, ErrInvalidGW)
9598
}
9699
if !protoIP.Addr().Is4() {
97-
return fmt.Errorf("ProtocolIP %s must be an IPv4 address", gw.Spec.ProtocolIP) //nolint:goerr113
100+
return fmt.Errorf("ProtocolIP %s must be an IPv4 address: %w", gw.Spec.ProtocolIP, ErrInvalidGW)
98101
}
99102

100103
vtepIP, err := netip.ParsePrefix(gw.Spec.VTEPIP)
101104
if err != nil {
102-
return fmt.Errorf("invalid VTEPIP %s: %w", gw.Spec.VTEPIP, err)
105+
return fmt.Errorf("invalid VTEPIP %s: %w", gw.Spec.VTEPIP, errors.Join(err, ErrInvalidGW))
103106
}
104107
if vtepIP.Bits() != 32 {
105-
return fmt.Errorf("VTEPIP %s must be a /32 prefix", gw.Spec.VTEPIP) //nolint:goerr113
108+
return fmt.Errorf("VTEPIP %s must be a /32 prefix: %w", gw.Spec.VTEPIP, ErrInvalidGW)
106109
}
107110
if !vtepIP.Addr().Is4() {
108-
return fmt.Errorf("VTEPIP %s must be an IPv4 address", gw.Spec.VTEPIP) //nolint:goerr113
111+
return fmt.Errorf("VTEPIP %s must be an IPv4 address: %w", gw.Spec.VTEPIP, ErrInvalidGW)
109112
}
110113
if vtepIP.Addr().IsMulticast() || vtepIP.Addr().IsLoopback() || vtepIP.Addr().IsUnspecified() {
111-
return fmt.Errorf("VTEPIP %s must be a unicast IPv4 address", gw.Spec.VTEPIP) //nolint:goerr113
114+
return fmt.Errorf("VTEPIP %s must be a unicast IPv4 address: %w", gw.Spec.VTEPIP, ErrInvalidGW)
112115
}
113116
localhostNet, err := netip.ParsePrefix("127.0.0.0/8")
114117
if err != nil {
115118
return fmt.Errorf("internal error: cannot parse localhost network: %w", err)
116119
}
117120
if localhostNet.Contains(vtepIP.Addr()) {
118-
return fmt.Errorf("VTEPIP %s must not be in the localhost range", gw.Spec.VTEPIP) //nolint:goerr113
121+
return fmt.Errorf("VTEPIP %s must not be in the localhost range: %w", gw.Spec.VTEPIP, ErrInvalidGW)
119122
}
120123

121124
if gw.Spec.VTEPMAC == "" {
122-
return fmt.Errorf("VTEPMAC must be set") //nolint:goerr113
125+
return fmt.Errorf("VTEPMAC must be set: %w", ErrInvalidGW)
123126
}
124127
vtepMAC, err := net.ParseMAC(gw.Spec.VTEPMAC)
125128
if err != nil {
126-
return fmt.Errorf("invalid VTEPMAC %s: %w", gw.Spec.VTEPMAC, err)
129+
return fmt.Errorf("invalid VTEPMAC %s: %w", gw.Spec.VTEPMAC, errors.Join(err, ErrInvalidGW))
127130
}
128131
if vtepMAC.String() == "00:00:00:00:00:00" {
129-
return fmt.Errorf("VTEPMAC must not be all zeros") //nolint:goerr113
132+
return fmt.Errorf("VTEPMAC must not be all zeros: %w", ErrInvalidGW)
130133
}
131134
if (vtepMAC[0] & 1) == 1 {
132-
return fmt.Errorf("VTEPMAC %s must be a unicast MAC address", gw.Spec.VTEPMAC) //nolint:goerr113
135+
return fmt.Errorf("VTEPMAC %s must be a unicast MAC address: %w", gw.Spec.VTEPMAC, ErrInvalidGW)
133136
}
134137

135138
if gw.Spec.ASN == 0 {
136-
return fmt.Errorf("ASN must be set") //nolint:goerr113
139+
return fmt.Errorf("ASN must be set: %w", ErrInvalidGW)
137140
}
138141

139142
if len(gw.Spec.Interfaces) == 0 {
140-
return fmt.Errorf("at least one interface must be defined") //nolint:goerr113
143+
return fmt.Errorf("at least one interface must be defined: %w", ErrInvalidGW)
141144
}
142145
for name, iface := range gw.Spec.Interfaces {
143146
if len(iface.IPs) == 0 {
144-
return fmt.Errorf("interface %s must have at least one IP address", name) //nolint:goerr113
147+
return fmt.Errorf("interface %s must have at least one IP address: %w", name, ErrInvalidGW)
145148
}
146149
for _, ifaceIP := range iface.IPs {
147150
ifaceIP, err := netip.ParsePrefix(ifaceIP)
148151
if err != nil {
149-
return fmt.Errorf("invalid interface %s IP %s: %w", name, ifaceIP, err)
152+
return fmt.Errorf("invalid interface %s IP %s: %w", name, ifaceIP, errors.Join(err, ErrInvalidGW))
150153
}
151154
if !ifaceIP.Addr().Is4() {
152-
return fmt.Errorf("interface %s IP %s must be an IPv4 address", name, ifaceIP) //nolint:goerr113
155+
return fmt.Errorf("interface %s IP %s must be an IPv4 address: %w", name, ifaceIP, ErrInvalidGW)
153156
}
154157
}
155158
}
156159

157160
if len(gw.Spec.Neighbors) == 0 {
158-
return fmt.Errorf("at least one BGP neighbor must be defined") //nolint:goerr113
161+
return fmt.Errorf("at least one BGP neighbor must be defined: %w", ErrInvalidGW)
159162
}
160163
for _, neigh := range gw.Spec.Neighbors {
161164
if neigh.IP == "" {
162-
return fmt.Errorf("BGP neighbor must have an IP address") //nolint:goerr113
165+
return fmt.Errorf("BGP neighbor must have an IP address: %w", ErrInvalidGW)
163166
}
164167
neighIP, err := netip.ParseAddr(neigh.IP)
165168
if err != nil {
166-
return fmt.Errorf("invalid neighbor IP %s: %w", neigh.IP, err)
169+
return fmt.Errorf("invalid neighbor IP %s: %w", neigh.IP, errors.Join(err, ErrInvalidGW))
167170
}
168171
if !neighIP.Is4() {
169-
return fmt.Errorf("BGP neighbor IP %s must be an IPv4 address", neigh.IP) //nolint:goerr113
172+
return fmt.Errorf("BGP neighbor IP %s must be an IPv4 address: %w", neigh.IP, ErrInvalidGW)
170173
}
171174
if neighIP.IsMulticast() || neighIP.IsUnspecified() {
172-
return fmt.Errorf("BGP neighbor IP %s must be a unicast IPv4 address", neigh.IP) //nolint:goerr113
175+
return fmt.Errorf("BGP neighbor IP %s must be a unicast IPv4 address: %w", neigh.IP, ErrInvalidGW)
173176
}
174177

175178
if neigh.ASN == 0 {
176-
return fmt.Errorf("BGP neighbor %s must have an ASN", neigh.IP) //nolint:goerr113
179+
return fmt.Errorf("BGP neighbor %s must have an ASN: %w", neigh.IP, ErrInvalidGW)
177180
}
178181
}
179182

@@ -202,10 +205,10 @@ func (gw *Gateway) Validate(ctx context.Context, kube kclient.Reader) error {
202205
}
203206
}
204207
if _, exist := protocolIPs[protoIP.Addr()]; exist {
205-
return fmt.Errorf("gateway %s protocol IP %s is already in use", gw.Name, protoIP) //nolint:goerr113
208+
return fmt.Errorf("gateway %s protocol IP %s is already in use: %w", gw.Name, protoIP, ErrInvalidGW)
206209
}
207210
if _, exist := vtepIPs[vtepIP.Addr()]; exist {
208-
return fmt.Errorf("gateway %s VTEP IP %s is already in use", gw.Name, vtepIP) //nolint:goerr113
211+
return fmt.Errorf("gateway %s VTEP IP %s is already in use: %w", gw.Name, vtepIP, ErrInvalidGW)
209212
}
210213
}
211214

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// Copyright 2025 Hedgehog
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1alpha1_test
5+
6+
import (
7+
"slices"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"go.githedgehog.com/gateway/api/gateway/v1alpha1"
13+
runtime "k8s.io/apimachinery/pkg/runtime"
14+
kclient "sigs.k8s.io/controller-runtime/pkg/client"
15+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
16+
)
17+
18+
func withName[T kclient.Object](name string, obj T) T {
19+
obj.SetName(name)
20+
obj.SetNamespace("fab") // FIXME: do not hardcode
21+
22+
return obj
23+
}
24+
25+
func gwa(name string, f ...func(gw *v1alpha1.Gateway)) *v1alpha1.Gateway {
26+
gw := withName(name, &v1alpha1.Gateway{
27+
Spec: v1alpha1.GatewaySpec{
28+
ProtocolIP: "172.30.8.3/32",
29+
VTEPIP: "172.30.12.1/32",
30+
ASN: 65101,
31+
VTEPMAC: "ca:fe:ba:be:00:01",
32+
VTEPMTU: 1500,
33+
Interfaces: map[string]v1alpha1.GatewayInterface{
34+
"eth0": {
35+
IPs: []string{"172.30.128.3/31"},
36+
MTU: 1500,
37+
},
38+
},
39+
Neighbors: []v1alpha1.GatewayBGPNeighbor{
40+
{
41+
Source: "eth0",
42+
IP: "172.30.128.1",
43+
ASN: 65100,
44+
},
45+
},
46+
},
47+
})
48+
49+
for _, fn := range f {
50+
fn(gw)
51+
}
52+
53+
return gw
54+
}
55+
56+
func withObjs(base []kclient.Object, objs ...kclient.Object) []kclient.Object {
57+
return append(slices.Clone(base), objs...)
58+
}
59+
60+
func TestGatewayValidate(t *testing.T) {
61+
base := []kclient.Object{
62+
withName("gw-2", &v1alpha1.Gateway{
63+
Spec: v1alpha1.GatewaySpec{
64+
ProtocolIP: "172.30.8.2/32",
65+
VTEPIP: "172.30.12.0/32",
66+
ASN: 65101,
67+
VTEPMAC: "ca:fe:ba:be:00:01",
68+
VTEPMTU: 1500,
69+
Interfaces: map[string]v1alpha1.GatewayInterface{
70+
"eth0": {
71+
IPs: []string{"172.30.128.1/31"},
72+
MTU: 1500,
73+
},
74+
},
75+
Neighbors: []v1alpha1.GatewayBGPNeighbor{
76+
{
77+
Source: "eth0",
78+
IP: "172.30.128.0",
79+
ASN: 65100,
80+
},
81+
},
82+
},
83+
}),
84+
}
85+
86+
tests := []struct {
87+
name string
88+
gw v1alpha1.Gateway
89+
objs []kclient.Object
90+
err error
91+
}{
92+
{
93+
name: "test-no-overlap",
94+
gw: *gwa("gw-1"),
95+
objs: base,
96+
},
97+
{
98+
name: "test-proto-ip-overlap",
99+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.ProtocolIP = "172.30.8.2/32" }),
100+
objs: base,
101+
err: v1alpha1.ErrInvalidGW,
102+
},
103+
{
104+
name: "test-vtep-ip-overlap",
105+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPIP = "172.30.12.0/32" }),
106+
objs: base,
107+
err: v1alpha1.ErrInvalidGW,
108+
},
109+
{
110+
name: "test-invalid-proto-ip",
111+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.ProtocolIP = "172.30.12.0.1/32" }),
112+
objs: base,
113+
err: v1alpha1.ErrInvalidGW,
114+
},
115+
{
116+
name: "test-non-32-proto-ip",
117+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.ProtocolIP = "172.30.12.0/24" }),
118+
objs: base,
119+
err: v1alpha1.ErrInvalidGW,
120+
},
121+
{
122+
name: "test-non-v4-proto-ip",
123+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.ProtocolIP = "2001:db8::1/32" }),
124+
objs: base,
125+
err: v1alpha1.ErrInvalidGW,
126+
},
127+
{
128+
name: "test-invalid-vtep-ip",
129+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPIP = "172.30.12.0.1/32" }),
130+
objs: base,
131+
err: v1alpha1.ErrInvalidGW,
132+
},
133+
{
134+
name: "test-non-32-vtep-ip",
135+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPIP = "172.30.12.0/24" }),
136+
objs: base,
137+
err: v1alpha1.ErrInvalidGW,
138+
},
139+
{
140+
name: "test-non-v4-vtep-ip",
141+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPIP = "2001:db8::1/32" }),
142+
objs: base,
143+
err: v1alpha1.ErrInvalidGW,
144+
},
145+
{
146+
name: "test-localhost-vtep-ip",
147+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPIP = "127.0.1.2/32" }),
148+
objs: base,
149+
err: v1alpha1.ErrInvalidGW,
150+
},
151+
{
152+
name: "test-invalid-mac",
153+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPMAC = "00:11:22:33:44:55:66" }),
154+
objs: base,
155+
err: v1alpha1.ErrInvalidGW,
156+
},
157+
{
158+
name: "test-all-zeros-mac",
159+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPMAC = "00:00:00:00:00:00" }),
160+
objs: base,
161+
err: v1alpha1.ErrInvalidGW,
162+
},
163+
{
164+
name: "test-multicast-mac",
165+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.VTEPMAC = "01:00:5E:00:00:00" }),
166+
objs: base,
167+
err: v1alpha1.ErrInvalidGW,
168+
},
169+
{
170+
name: "test-no-asn",
171+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.ASN = 0 }),
172+
objs: base,
173+
err: v1alpha1.ErrInvalidGW,
174+
},
175+
{
176+
name: "test-no-interfaces",
177+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.Interfaces = map[string]v1alpha1.GatewayInterface{} }),
178+
objs: base,
179+
err: v1alpha1.ErrInvalidGW,
180+
},
181+
{
182+
name: "test-interface-invalid-ip",
183+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) {
184+
gw.Spec.Interfaces["eth0"] = v1alpha1.GatewayInterface{IPs: []string{"172.30.128.256/31"}, MTU: 1500}
185+
}),
186+
objs: base,
187+
err: v1alpha1.ErrInvalidGW,
188+
},
189+
{
190+
name: "test-no-neighbors",
191+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.Neighbors = []v1alpha1.GatewayBGPNeighbor{} }),
192+
objs: base,
193+
err: v1alpha1.ErrInvalidGW,
194+
},
195+
{
196+
name: "test-neighbor-invalid-ip",
197+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.Neighbors[0].IP = "172.30.128.256" }),
198+
objs: base,
199+
err: v1alpha1.ErrInvalidGW,
200+
},
201+
{
202+
name: "test-neighbor-no-asn",
203+
gw: *gwa("gw-1", func(gw *v1alpha1.Gateway) { gw.Spec.Neighbors[0].ASN = 0 }),
204+
objs: base,
205+
err: v1alpha1.ErrInvalidGW,
206+
},
207+
}
208+
209+
scheme := runtime.NewScheme()
210+
require.NoError(t, v1alpha1.AddToScheme(scheme), "should add gateway API to scheme")
211+
212+
for _, tt := range tests {
213+
t.Run(tt.name, func(t *testing.T) {
214+
ctx := t.Context()
215+
216+
kube := fake.NewClientBuilder().
217+
WithScheme(scheme).
218+
WithObjects(tt.objs...).
219+
Build()
220+
221+
tt.gw.Default()
222+
actual := tt.gw.Validate(ctx, kube)
223+
assert.ErrorIs(t, actual, tt.err, "validate should return expected error")
224+
})
225+
}
226+
}

0 commit comments

Comments
 (0)