Skip to content

Commit 811167b

Browse files
authored
Merge pull request #2256 from Nordix/mquhuy/ensure-port-tags-and-trunks
🐛 Ensure that existing ports also have correct tags and trunks
2 parents 7f6b872 + 0dbf13b commit 811167b

File tree

6 files changed

+218
-54
lines changed

6 files changed

+218
-54
lines changed

controllers/openstackserver_controller.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,7 @@ func getOrCreateServerPorts(openStackServer *infrav1alpha1.OpenStackServer, netw
419419
}
420420
desiredPorts := resolved.Ports
421421

422-
if len(desiredPorts) == len(resources.Ports) {
423-
return nil
424-
}
425-
426-
if err := networkingService.CreatePorts(openStackServer, desiredPorts, resources); err != nil {
422+
if err := networkingService.EnsurePorts(openStackServer, desiredPorts, resources); err != nil {
427423
return fmt.Errorf("creating ports: %w", err)
428424
}
429425

controllers/openstackserver_controller_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ var listDefaultPorts = func(r *recorders) {
114114
}, nil)
115115
}
116116

117+
var listDefaultPortsWithID = func(r *recorders) {
118+
r.network.ListPort(ports.ListOpts{
119+
Name: openStackServerName + "-0",
120+
ID: portUUID,
121+
NetworkID: networkUUID,
122+
}).Return([]ports.Port{
123+
{
124+
ID: portUUID,
125+
},
126+
}, nil)
127+
}
128+
117129
var listDefaultPortsNotFound = func(r *recorders) {
118130
r.network.ListPort(ports.ListOpts{
119131
Name: openStackServerName + "-0",
@@ -479,6 +491,7 @@ func Test_OpenStackServerReconcileCreate(t *testing.T) {
479491
listDefaultPortsNotFound(r)
480492
createDefaultPort(r)
481493
listDefaultServerNotFound(r)
494+
listDefaultPortsNotFound(r)
482495
createDefaultServer(r)
483496
},
484497
},
@@ -500,6 +513,7 @@ func Test_OpenStackServerReconcileCreate(t *testing.T) {
500513
},
501514
expect: func(r *recorders) {
502515
listDefaultPorts(r)
516+
listDefaultPortsWithID(r)
503517
listDefaultServerFound(r)
504518
},
505519
},

pkg/cloud/services/networking/port.go

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,61 @@ func (s *Service) GetPortForExternalNetwork(instanceID string, externalNetworkID
124124
return nil, nil
125125
}
126126

127-
func (s *Service) CreatePort(eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec) (*ports.Port, error) {
127+
// ensurePortTagsAndTrunk ensures that the provided port has the tags and trunk defined in portSpec.
128+
func (s *Service) ensurePortTagsAndTrunk(port *ports.Port, eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec) error {
129+
wantedTags := uniqueSortedTags(portSpec.Tags)
130+
actualTags := uniqueSortedTags(port.Tags)
131+
// Only replace tags if there is a difference
132+
if !slices.Equal(wantedTags, actualTags) && len(wantedTags) > 0 {
133+
if err := s.replaceAllAttributesTags(eventObject, portResource, port.ID, wantedTags); err != nil {
134+
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace port tags %s: %v", port.Name, err)
135+
return err
136+
}
137+
}
138+
if ptr.Deref(portSpec.Trunk, false) {
139+
trunk, err := s.getOrCreateTrunkForPort(eventObject, port)
140+
if err != nil {
141+
record.Warnf(eventObject, "FailedCreateTrunk", "Failed to create trunk for port %s: %v", port.Name, err)
142+
return err
143+
}
144+
145+
if !slices.Equal(wantedTags, trunk.Tags) {
146+
if err = s.replaceAllAttributesTags(eventObject, trunkResource, trunk.ID, wantedTags); err != nil {
147+
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace trunk tags %s: %v", port.Name, err)
148+
return err
149+
}
150+
}
151+
}
152+
return nil
153+
}
154+
155+
// EnsurePort ensure that a port defined with portSpec Name and NetworkID exists,
156+
// and that the port has suitable tags and trunk. If the PortStatus is already known,
157+
// use the ID when filtering for existing ports.
158+
func (s *Service) EnsurePort(eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec, portStatus infrav1.PortStatus) (*ports.Port, error) {
159+
opts := ports.ListOpts{
160+
Name: portSpec.Name,
161+
NetworkID: portSpec.NetworkID,
162+
}
163+
if portStatus.ID != "" {
164+
opts.ID = portStatus.ID
165+
}
166+
167+
existingPorts, err := s.client.ListPort(opts)
168+
if err != nil {
169+
return nil, fmt.Errorf("searching for existing port for server: %v", err)
170+
}
171+
if len(existingPorts) > 1 {
172+
return nil, fmt.Errorf("multiple ports found with name \"%s\"", portSpec.Name)
173+
}
174+
175+
if len(existingPorts) == 1 {
176+
port := &existingPorts[0]
177+
if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
178+
return nil, err
179+
}
180+
return port, nil
181+
}
128182
var addressPairs []ports.AddressPair
129183
if !ptr.Deref(portSpec.DisablePortSecurity, false) {
130184
for _, ap := range portSpec.AllowedAddressPairs {
@@ -200,24 +254,10 @@ func (s *Service) CreatePort(eventObject runtime.Object, portSpec *infrav1.Resol
200254
return nil, err
201255
}
202256

203-
if len(portSpec.Tags) > 0 {
204-
if err = s.replaceAllAttributesTags(eventObject, portResource, port.ID, portSpec.Tags); err != nil {
205-
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace port tags %s: %v", portSpec.Name, err)
206-
return nil, err
207-
}
257+
if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
258+
return nil, err
208259
}
209260
record.Eventf(eventObject, "SuccessfulCreatePort", "Created port %s with id %s", port.Name, port.ID)
210-
if ptr.Deref(portSpec.Trunk, false) {
211-
trunk, err := s.getOrCreateTrunkForPort(eventObject, port)
212-
if err != nil {
213-
record.Warnf(eventObject, "FailedCreateTrunk", "Failed to create trunk for port %s: %v", port.Name, err)
214-
return nil, err
215-
}
216-
if err = s.replaceAllAttributesTags(eventObject, trunkResource, trunk.ID, portSpec.Tags); err != nil {
217-
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace trunk tags %s: %v", port.Name, err)
218-
return nil, err
219-
}
220-
}
221261

222262
return port, nil
223263
}
@@ -328,23 +368,30 @@ func getPortName(baseName string, portSpec *infrav1.PortOpts, netIndex int) stri
328368
return fmt.Sprintf("%s-%d", baseName, netIndex)
329369
}
330370

331-
func (s *Service) CreatePorts(eventObject runtime.Object, desiredPorts []infrav1.ResolvedPortSpec, resources *infrav1alpha1.ServerResources) error {
371+
// EnsurePorts ensures that every one of desiredPorts is created and has
372+
// expected trunk and tags.
373+
func (s *Service) EnsurePorts(eventObject runtime.Object, desiredPorts []infrav1.ResolvedPortSpec, resources *infrav1alpha1.ServerResources) error {
332374
for i := range desiredPorts {
333-
// Skip creation of ports which already exist
375+
// If we already created the port, make use of the status
376+
portStatus := infrav1.PortStatus{}
334377
if i < len(resources.Ports) {
335-
continue
378+
portStatus = resources.Ports[i]
336379
}
337-
338-
portSpec := &desiredPorts[i]
339-
// Events are recorded in CreatePort
340-
port, err := s.CreatePort(eventObject, portSpec)
380+
// Events are recorded in EnsurePort
381+
port, err := s.EnsurePort(eventObject, &desiredPorts[i], portStatus)
341382
if err != nil {
342383
return err
343384
}
344385

345-
resources.Ports = append(resources.Ports, infrav1.PortStatus{
346-
ID: port.ID,
347-
})
386+
// If we already have the status, replace it,
387+
// otherwise append it.
388+
if i < len(resources.Ports) {
389+
resources.Ports[i] = portStatus
390+
} else {
391+
resources.Ports = append(resources.Ports, infrav1.PortStatus{
392+
ID: port.ID,
393+
})
394+
}
348395
}
349396

350397
return nil
@@ -604,3 +651,19 @@ func (s *Service) AdoptPortsServer(scope *scope.WithLogger, desiredPorts []infra
604651

605652
return nil
606653
}
654+
655+
// uniqueSortedTags returns a new, sorted slice where any duplicates have been removed.
656+
func uniqueSortedTags(tags []string) []string {
657+
// remove duplicate values from tags
658+
tagsMap := make(map[string]string)
659+
for _, t := range tags {
660+
tagsMap[t] = t
661+
}
662+
663+
uniqueTags := []string{}
664+
for k := range tagsMap {
665+
uniqueTags = append(uniqueTags, k)
666+
}
667+
slices.Sort(uniqueTags)
668+
return uniqueTags
669+
}

pkg/cloud/services/networking/port_test.go

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import (
4141
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
4242
)
4343

44-
func Test_CreatePort(t *testing.T) {
44+
func Test_EnsurePort(t *testing.T) {
4545
// Arbitrary values used in the tests
4646
const (
4747
netID = "7fd24ceb-788a-441f-ad0a-d8e2f5d31a1d"
@@ -60,8 +60,8 @@ func Test_CreatePort(t *testing.T) {
6060
name string
6161
port infrav1.ResolvedPortSpec
6262
expect func(m *mock.MockNetworkClientMockRecorder, g Gomega)
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 CreatePort is called with the expected port opts.
63+
// Note the 'wanted' port isn't so important, since it will be whatever we tell ListPort or EnsurePort to return.
64+
// Mostly in this test suite, we're checking that EnsurePort is called with the expected port opts.
6565
want *ports.Port
6666
wantErr bool
6767
}{
@@ -157,6 +157,10 @@ func Test_CreatePort(t *testing.T) {
157157
},
158158
}
159159

160+
m.ListPort(ports.ListOpts{
161+
Name: "foo-port-1",
162+
NetworkID: netID,
163+
}).Return(nil, nil)
160164
// The following allows us to use gomega to
161165
// compare the argument instead of gomock.
162166
// Gomock's output in the case of a mismatch is
@@ -184,6 +188,10 @@ func Test_CreatePort(t *testing.T) {
184188
expectedCreateOpts = portsbinding.CreateOptsExt{
185189
CreateOptsBuilder: expectedCreateOpts,
186190
}
191+
m.ListPort(ports.ListOpts{
192+
Name: "test-port",
193+
NetworkID: netID,
194+
}).Return(nil, nil)
187195
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
188196
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
189197
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
@@ -203,6 +211,10 @@ func Test_CreatePort(t *testing.T) {
203211
SecurityGroups: []string{portSecurityGroupID},
204212
},
205213
expect: func(m *mock.MockNetworkClientMockRecorder, _ Gomega) {
214+
m.ListPort(ports.ListOpts{
215+
Name: "test-port",
216+
NetworkID: netID,
217+
}).Return(nil, nil)
206218
m.CreatePort(gomock.Any()).Times(0)
207219
},
208220
wantErr: true,
@@ -235,6 +247,10 @@ func Test_CreatePort(t *testing.T) {
235247
expectedCreateOpts = portsbinding.CreateOptsExt{
236248
CreateOptsBuilder: expectedCreateOpts,
237249
}
250+
m.ListPort(ports.ListOpts{
251+
Name: "test-port",
252+
NetworkID: netID,
253+
}).Return(nil, nil)
238254
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
239255
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
240256
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
@@ -277,6 +293,10 @@ func Test_CreatePort(t *testing.T) {
277293
expectedCreateOpts = portsbinding.CreateOptsExt{
278294
CreateOptsBuilder: expectedCreateOpts,
279295
}
296+
m.ListPort(ports.ListOpts{
297+
Name: "test-port",
298+
NetworkID: netID,
299+
}).Return(nil, nil)
280300
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
281301
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
282302
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
@@ -286,7 +306,7 @@ func Test_CreatePort(t *testing.T) {
286306
want: &ports.Port{ID: portID},
287307
},
288308
{
289-
name: "tags and trunk",
309+
name: "create port with tags and trunk",
290310
port: infrav1.ResolvedPortSpec{
291311
Name: "test-port",
292312
NetworkID: netID,
@@ -303,6 +323,10 @@ func Test_CreatePort(t *testing.T) {
303323
CreateOptsBuilder: expectedCreateOpts,
304324
}
305325

326+
m.ListPort(ports.ListOpts{
327+
Name: "test-port",
328+
NetworkID: netID,
329+
}).Return(nil, nil)
306330
// Create the port
307331
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
308332
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
@@ -334,6 +358,87 @@ func Test_CreatePort(t *testing.T) {
334358
},
335359
want: &ports.Port{ID: portID, Name: "test-port"},
336360
},
361+
{
362+
name: "port with tags and trunk already exists",
363+
port: infrav1.ResolvedPortSpec{
364+
Name: "test-port",
365+
NetworkID: netID,
366+
Tags: []string{"tag1", "tag2"},
367+
Trunk: ptr.To(true),
368+
},
369+
expect: func(m *mock.MockNetworkClientMockRecorder, _ types.Gomega) {
370+
m.ListPort(ports.ListOpts{
371+
Name: "test-port",
372+
NetworkID: netID,
373+
}).Return([]ports.Port{{
374+
ID: portID,
375+
Name: "test-port",
376+
NetworkID: netID,
377+
Tags: []string{"tag1", "tag2"},
378+
}}, nil)
379+
380+
// Look for existing trunk
381+
m.ListTrunk(trunks.ListOpts{
382+
PortID: portID,
383+
Name: "test-port",
384+
}).Return([]trunks.Trunk{{
385+
ID: trunkID,
386+
Tags: []string{"tag1", "tag2"},
387+
}}, nil)
388+
},
389+
want: &ports.Port{
390+
ID: portID,
391+
Name: "test-port",
392+
NetworkID: netID,
393+
Tags: []string{"tag1", "tag2"},
394+
},
395+
},
396+
{
397+
name: "partial port missing tags and trunk",
398+
port: infrav1.ResolvedPortSpec{
399+
Name: "test-port",
400+
NetworkID: netID,
401+
Tags: []string{"tag1", "tag2"},
402+
Trunk: ptr.To(true),
403+
},
404+
expect: func(m *mock.MockNetworkClientMockRecorder, _ types.Gomega) {
405+
m.ListPort(ports.ListOpts{
406+
Name: "test-port",
407+
NetworkID: netID,
408+
}).Return([]ports.Port{{
409+
ID: portID,
410+
Name: "test-port",
411+
NetworkID: netID,
412+
}}, nil)
413+
414+
// Tag the port
415+
m.ReplaceAllAttributesTags("ports", portID, attributestags.ReplaceAllOpts{
416+
Tags: []string{"tag1", "tag2"},
417+
})
418+
419+
// Look for existing trunk
420+
m.ListTrunk(trunks.ListOpts{
421+
PortID: portID,
422+
Name: "test-port",
423+
}).Return([]trunks.Trunk{}, nil)
424+
425+
// Create the trunk
426+
m.CreateTrunk(trunks.CreateOpts{
427+
PortID: portID,
428+
Name: "test-port",
429+
}).Return(&trunks.Trunk{ID: trunkID}, nil)
430+
431+
// Tag the trunk
432+
m.ReplaceAllAttributesTags("trunks", trunkID, attributestags.ReplaceAllOpts{
433+
Tags: []string{"tag1", "tag2"},
434+
})
435+
},
436+
want: &ports.Port{
437+
ID: portID,
438+
Name: "test-port",
439+
NetworkID: netID,
440+
},
441+
},
337442
}
338443

339444
eventObject := &infrav1.OpenStackMachine{}
@@ -349,9 +454,10 @@ func Test_CreatePort(t *testing.T) {
349454
s := Service{
350455
client: mockClient,
351456
}
352-
got, err := s.CreatePort(
457+
got, err := s.EnsurePort(
353458
eventObject,
354459
&tt.port,
460+
infrav1.PortStatus{},
355461
)
356462
if tt.wantErr {
357463
g.Expect(err).To(HaveOccurred())

0 commit comments

Comments
 (0)