Skip to content

Commit 43df21f

Browse files
committed
Add unit tests for getOrCreatePort func
The getOrCreatePort func is quite complicated, taking many arguments and options and with conditional calls to OpenStack. This change adds unit tests for the function, which should make it clearer what OpenStack API calls are made under which conditions, and shows off all the supported features.
1 parent eb83033 commit 43df21f

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package networking
18+
19+
import (
20+
"testing"
21+
22+
"github.com/go-logr/logr"
23+
"github.com/golang/mock/gomock"
24+
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/attributestags"
25+
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding"
26+
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsecurity"
27+
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/trunks"
28+
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
29+
. "github.com/onsi/gomega"
30+
31+
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4"
32+
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking/mock_networking"
33+
)
34+
35+
func Test_GetOrCreatePort(t *testing.T) {
36+
mockCtrl := gomock.NewController(t)
37+
defer mockCtrl.Finish()
38+
39+
// Arbitrary GUIDs used in the tests
40+
netID := "7fd24ceb-788a-441f-ad0a-d8e2f5d31a1d"
41+
subnetID := "d9c88a6d-0b8c-48ff-8f0e-8d85a078c194"
42+
portID1 := "50214c48-c09e-4a54-914f-97b40fd22802"
43+
portID2 := "4c096384-f0a5-466d-9534-06a7ed281a79"
44+
hostID := "825c1b11-3dca-4bfe-a2d8-a3cc1964c8d5"
45+
tenantID := "62b523a7-f838-45fd-904f-d2db2bb58e04"
46+
projectID := "063171b1-0595-4882-98cd-3ee79676ff87"
47+
trunkID := "eb7541fa-5e2a-4cca-b2c3-dfa409b917ce"
48+
49+
// Other arbitrary variables passed in to the tests
50+
instanceSecurityGroups := []string{"instance-secgroup"}
51+
portSecurityGroups := []string{"port-secgroup"}
52+
53+
pointerToTrue := pointerTo(true)
54+
pointerToFalse := pointerTo(false)
55+
56+
tests := []struct {
57+
name string
58+
portName string
59+
net infrav1.Network
60+
instanceSecurityGroups *[]string
61+
tags []string
62+
expect func(m *mock_networking.MockNetworkClientMockRecorder)
63+
// Note the 'wanted' port isn't so important, since it will be whatever we tell ListPort or CreatePort to return.
64+
// Mostly in this test suite, we're checking that ListPort/CreatePort is called with the expected port opts.
65+
want *ports.Port
66+
wantErr bool
67+
}{
68+
{
69+
"gets and returns existing port if name matches",
70+
"foo-port-1",
71+
infrav1.Network{
72+
ID: netID,
73+
Subnet: &infrav1.Subnet{},
74+
},
75+
nil,
76+
[]string{},
77+
func(m *mock_networking.MockNetworkClientMockRecorder) {
78+
m.
79+
ListPort(ports.ListOpts{
80+
Name: "foo-port-1",
81+
NetworkID: netID,
82+
}).Return([]ports.Port{{
83+
ID: portID1,
84+
}}, nil)
85+
},
86+
&ports.Port{
87+
ID: portID1,
88+
},
89+
false,
90+
},
91+
{
92+
"errors if multiple matching ports are found",
93+
"foo-port-1",
94+
infrav1.Network{
95+
ID: netID,
96+
Subnet: &infrav1.Subnet{},
97+
},
98+
nil,
99+
[]string{},
100+
func(m *mock_networking.MockNetworkClientMockRecorder) {
101+
m.
102+
ListPort(ports.ListOpts{
103+
Name: "foo-port-1",
104+
NetworkID: netID,
105+
}).Return([]ports.Port{
106+
{
107+
ID: portID1,
108+
NetworkID: netID,
109+
Name: "foo-port-1",
110+
},
111+
{
112+
ID: portID2,
113+
NetworkID: netID,
114+
Name: "foo-port-2",
115+
},
116+
}, nil)
117+
},
118+
nil,
119+
true,
120+
},
121+
{
122+
"creates port with defaults (description and secgroups) if not specified in portOpts",
123+
"foo-port-1",
124+
infrav1.Network{
125+
ID: netID,
126+
PortOpts: &infrav1.PortOpts{},
127+
},
128+
&instanceSecurityGroups,
129+
[]string{},
130+
func(m *mock_networking.MockNetworkClientMockRecorder) {
131+
// No ports found
132+
m.
133+
ListPort(ports.ListOpts{
134+
Name: "foo-port-1",
135+
NetworkID: netID,
136+
}).Return([]ports.Port{}, nil)
137+
m.
138+
CreatePort(portsbinding.CreateOptsExt{
139+
CreateOptsBuilder: ports.CreateOpts{
140+
Name: "foo-port-1",
141+
Description: "Created by cluster-api-provider-openstack cluster test-cluster",
142+
SecurityGroups: &instanceSecurityGroups,
143+
NetworkID: netID,
144+
AllowedAddressPairs: []ports.AddressPair{},
145+
},
146+
}).Return(&ports.Port{ID: portID1}, nil)
147+
},
148+
&ports.Port{ID: portID1},
149+
false,
150+
},
151+
{
152+
"creates port with specified portOpts if no matching port exists",
153+
"foo-port-bar",
154+
infrav1.Network{
155+
ID: netID,
156+
Subnet: &infrav1.Subnet{},
157+
PortOpts: &infrav1.PortOpts{
158+
NameSuffix: "bar",
159+
Description: "this is a test port",
160+
MACAddress: "fe:fe:fe:fe:fe:fe",
161+
AdminStateUp: pointerToTrue,
162+
FixedIPs: []infrav1.FixedIP{{
163+
SubnetID: subnetID,
164+
IPAddress: "192.168.0.50",
165+
}, {IPAddress: "192.168.1.50"}},
166+
TenantID: tenantID,
167+
ProjectID: projectID,
168+
SecurityGroups: &portSecurityGroups,
169+
AllowedAddressPairs: []infrav1.AddressPair{{
170+
IPAddress: "10.10.10.10",
171+
MACAddress: "f1:f1:f1:f1:f1:f1",
172+
}},
173+
HostID: hostID,
174+
VNICType: "direct",
175+
Profile: map[string]string{"interface_name": "eno1"},
176+
DisablePortSecurity: pointerToFalse,
177+
},
178+
},
179+
nil,
180+
nil,
181+
func(m *mock_networking.MockNetworkClientMockRecorder) {
182+
portCreateOpts := ports.CreateOpts{
183+
NetworkID: netID,
184+
Name: "foo-port-bar",
185+
Description: "this is a test port",
186+
AdminStateUp: pointerToTrue,
187+
MACAddress: "fe:fe:fe:fe:fe:fe",
188+
FixedIPs: []ports.IP{
189+
{
190+
SubnetID: subnetID,
191+
IPAddress: "192.168.0.50",
192+
}, {
193+
IPAddress: "192.168.1.50",
194+
},
195+
},
196+
TenantID: tenantID,
197+
ProjectID: projectID,
198+
SecurityGroups: &portSecurityGroups,
199+
AllowedAddressPairs: []ports.AddressPair{{
200+
IPAddress: "10.10.10.10",
201+
MACAddress: "f1:f1:f1:f1:f1:f1",
202+
}},
203+
}
204+
portsecurityCreateOptsExt := portsecurity.PortCreateOptsExt{
205+
CreateOptsBuilder: portCreateOpts,
206+
PortSecurityEnabled: pointerToTrue,
207+
}
208+
portbindingCreateOptsExt := portsbinding.CreateOptsExt{
209+
// Note for the test matching, the order in which the builders are composed
210+
// must be the same as in the function we are testing.
211+
CreateOptsBuilder: portsecurityCreateOptsExt,
212+
HostID: hostID,
213+
VNICType: "direct",
214+
Profile: map[string]interface{}{"interface_name": "eno1"},
215+
}
216+
m.
217+
ListPort(ports.ListOpts{
218+
Name: "foo-port-bar",
219+
NetworkID: netID,
220+
}).Return([]ports.Port{}, nil)
221+
m.
222+
CreatePort(portbindingCreateOptsExt).
223+
Return(&ports.Port{
224+
ID: portID1,
225+
}, nil)
226+
},
227+
&ports.Port{
228+
ID: portID1,
229+
},
230+
false,
231+
},
232+
{
233+
"overrides default (instance) security groups if port security groups are specified",
234+
"foo-port-1",
235+
infrav1.Network{
236+
ID: netID,
237+
PortOpts: &infrav1.PortOpts{
238+
SecurityGroups: &portSecurityGroups,
239+
},
240+
},
241+
&instanceSecurityGroups,
242+
[]string{},
243+
func(m *mock_networking.MockNetworkClientMockRecorder) {
244+
// No ports found
245+
m.
246+
ListPort(ports.ListOpts{
247+
Name: "foo-port-1",
248+
NetworkID: netID,
249+
}).Return([]ports.Port{}, nil)
250+
m.
251+
CreatePort(portsbinding.CreateOptsExt{
252+
CreateOptsBuilder: ports.CreateOpts{
253+
Name: "foo-port-1",
254+
Description: "Created by cluster-api-provider-openstack cluster test-cluster",
255+
SecurityGroups: &portSecurityGroups,
256+
NetworkID: netID,
257+
AllowedAddressPairs: []ports.AddressPair{},
258+
},
259+
},
260+
).Return(&ports.Port{ID: portID1}, nil)
261+
},
262+
&ports.Port{ID: portID1},
263+
false,
264+
},
265+
{
266+
"creates port with tags passed to function",
267+
"foo-port-1",
268+
infrav1.Network{
269+
ID: netID,
270+
PortOpts: &infrav1.PortOpts{},
271+
},
272+
nil,
273+
[]string{"my-tag"},
274+
func(m *mock_networking.MockNetworkClientMockRecorder) {
275+
// No ports found
276+
m.
277+
ListPort(ports.ListOpts{
278+
Name: "foo-port-1",
279+
NetworkID: netID,
280+
}).Return([]ports.Port{}, nil)
281+
m.CreatePort(portsbinding.CreateOptsExt{
282+
CreateOptsBuilder: ports.CreateOpts{
283+
Name: "foo-port-1",
284+
Description: "Created by cluster-api-provider-openstack cluster test-cluster",
285+
NetworkID: netID,
286+
AllowedAddressPairs: []ports.AddressPair{},
287+
},
288+
}).Return(&ports.Port{ID: portID1}, nil)
289+
m.ReplaceAllAttributesTags("ports", portID1, attributestags.ReplaceAllOpts{Tags: []string{"my-tag"}}).Return([]string{"my-tag"}, nil)
290+
},
291+
&ports.Port{ID: portID1},
292+
false,
293+
},
294+
{
295+
"creates port and trunk (with tags) if they aren't found",
296+
"foo-port-1",
297+
infrav1.Network{
298+
ID: netID,
299+
PortOpts: &infrav1.PortOpts{
300+
Trunk: pointerToTrue,
301+
},
302+
},
303+
nil,
304+
[]string{"my-tag"},
305+
func(m *mock_networking.MockNetworkClientMockRecorder) {
306+
// No ports found
307+
m.
308+
ListPort(ports.ListOpts{
309+
Name: "foo-port-1",
310+
NetworkID: netID,
311+
}).Return([]ports.Port{}, nil)
312+
m.
313+
CreatePort(portsbinding.CreateOptsExt{
314+
CreateOptsBuilder: ports.CreateOpts{
315+
Name: "foo-port-1",
316+
Description: "Created by cluster-api-provider-openstack cluster test-cluster",
317+
NetworkID: netID,
318+
AllowedAddressPairs: []ports.AddressPair{},
319+
},
320+
}).Return(&ports.Port{Name: "foo-port-1", ID: portID1}, nil)
321+
m.
322+
ListTrunk(trunks.ListOpts{
323+
Name: "foo-port-1",
324+
PortID: portID1,
325+
}).Return([]trunks.Trunk{}, nil)
326+
m.
327+
CreateTrunk(trunks.CreateOpts{
328+
Name: "foo-port-1",
329+
PortID: portID1,
330+
Description: "Created by cluster-api-provider-openstack cluster test-cluster",
331+
}).Return(&trunks.Trunk{ID: trunkID}, nil)
332+
333+
m.ReplaceAllAttributesTags("ports", portID1, attributestags.ReplaceAllOpts{Tags: []string{"my-tag"}}).Return([]string{"my-tag"}, nil)
334+
m.ReplaceAllAttributesTags("trunks", trunkID, attributestags.ReplaceAllOpts{Tags: []string{"my-tag"}}).Return([]string{"my-tag"}, nil)
335+
},
336+
&ports.Port{Name: "foo-port-1", ID: portID1},
337+
false,
338+
},
339+
}
340+
341+
eventObject := &infrav1.OpenStackMachine{}
342+
for _, tt := range tests {
343+
t.Run(tt.name, func(t *testing.T) {
344+
g := NewWithT(t)
345+
mockClient := mock_networking.NewMockNetworkClient(mockCtrl)
346+
tt.expect(mockClient.EXPECT())
347+
s := Service{
348+
client: mockClient,
349+
logger: logr.DiscardLogger{},
350+
}
351+
got, err := s.GetOrCreatePort(
352+
eventObject,
353+
"test-cluster",
354+
tt.portName,
355+
tt.net,
356+
tt.instanceSecurityGroups,
357+
tt.tags,
358+
)
359+
if tt.wantErr {
360+
g.Expect(err).To(HaveOccurred())
361+
} else {
362+
g.Expect(err).NotTo(HaveOccurred())
363+
}
364+
g.Expect(got).To(Equal(tt.want))
365+
})
366+
}
367+
}
368+
369+
func pointerTo(b bool) *bool {
370+
return &b
371+
}

0 commit comments

Comments
 (0)