Skip to content

Commit fcf6f1f

Browse files
authored
Merge pull request #2356 from Nordix/lentzi90/ensure-ports-tags-release-0.10
🐛 Ensure that existing ports also have correct tags and trunks
2 parents d0a76bf + 68e7b13 commit fcf6f1f

File tree

7 files changed

+207
-59
lines changed

7 files changed

+207
-59
lines changed

controllers/openstackcluster_controller.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -616,11 +616,7 @@ func getOrCreateBastionPorts(openStackCluster *infrav1.OpenStackCluster, network
616616
return errors.New("bastion resources are nil")
617617
}
618618

619-
if len(desiredPorts) == len(resources.Ports) {
620-
return nil
621-
}
622-
623-
err := networkingService.CreatePorts(openStackCluster, desiredPorts, resources)
619+
err := networkingService.EnsurePorts(openStackCluster, desiredPorts, resources)
624620
if err != nil {
625621
return fmt.Errorf("failed to create ports for bastion %s: %w", bastionName(openStackCluster.Name), err)
626622
}

controllers/openstackcluster_controller_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ var _ = Describe("OpenStackCluster controller", func() {
277277
server.Status = "ACTIVE"
278278

279279
networkClientRecorder := mockScopeFactory.NetworkClient.EXPECT()
280+
// One list for adopting and one for ensuring the ports and tags are correct
281+
networkClientRecorder.ListPort(gomock.Any()).Return([]ports.Port{{ID: "portID1"}}, nil)
280282
networkClientRecorder.ListPort(gomock.Any()).Return([]ports.Port{{ID: "portID1"}}, nil)
281283

282284
computeClientRecorder := mockScopeFactory.ComputeClient.EXPECT()
@@ -362,6 +364,7 @@ var _ = Describe("OpenStackCluster controller", func() {
362364

363365
networkClientRecorder := mockScopeFactory.NetworkClient.EXPECT()
364366
networkClientRecorder.ListPort(gomock.Any()).Return([]ports.Port{{ID: "portID1"}}, nil)
367+
networkClientRecorder.ListPort(gomock.Any()).Return([]ports.Port{{ID: "portID1"}}, nil)
365368

366369
computeClientRecorder := mockScopeFactory.ComputeClient.EXPECT()
367370
computeClientRecorder.GetServer("adopted-fip-bastion-uuid").Return(&server, nil)
@@ -445,6 +448,9 @@ var _ = Describe("OpenStackCluster controller", func() {
445448
computeClientRecorder := mockScopeFactory.ComputeClient.EXPECT()
446449
computeClientRecorder.GetServer("requeue-bastion-uuid").Return(&server, nil)
447450

451+
networkClientRecorder := mockScopeFactory.NetworkClient.EXPECT()
452+
networkClientRecorder.ListPort(gomock.Any()).Return([]ports.Port{{ID: "portID1"}}, nil)
453+
448454
res, err := reconcileBastion(scope, capiCluster, testCluster)
449455
Expect(testCluster.Status.Bastion).To(Equal(&infrav1.BastionStatus{
450456
ID: "requeue-bastion-uuid",

controllers/openstackmachine_controller.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -752,11 +752,7 @@ func getOrCreateMachinePorts(openStackMachine *infrav1.OpenStackMachine, network
752752
}
753753
desiredPorts := resolved.Ports
754754

755-
if len(desiredPorts) == len(resources.Ports) {
756-
return nil
757-
}
758-
759-
if err := networkingService.CreatePorts(openStackMachine, desiredPorts, resources); err != nil {
755+
if err := networkingService.EnsurePorts(openStackMachine, desiredPorts, resources); err != nil {
760756
return fmt.Errorf("creating ports: %w", err)
761757
}
762758

pkg/cloud/services/networking/port.go

+90-27
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,61 @@ func (s *Service) GetPortForExternalNetwork(instanceID string, externalNetworkID
123123
return nil, nil
124124
}
125125

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

199-
if len(portSpec.Tags) > 0 {
200-
if err = s.replaceAllAttributesTags(eventObject, portResource, port.ID, portSpec.Tags); err != nil {
201-
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace port tags %s: %v", portSpec.Name, err)
202-
return nil, err
203-
}
253+
if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
254+
return nil, err
204255
}
205256
record.Eventf(eventObject, "SuccessfulCreatePort", "Created port %s with id %s", port.Name, port.ID)
206-
if ptr.Deref(portSpec.Trunk, false) {
207-
trunk, err := s.getOrCreateTrunkForPort(eventObject, port)
208-
if err != nil {
209-
record.Warnf(eventObject, "FailedCreateTrunk", "Failed to create trunk for port %s: %v", port.Name, err)
210-
return nil, err
211-
}
212-
if err = s.replaceAllAttributesTags(eventObject, trunkResource, trunk.ID, portSpec.Tags); err != nil {
213-
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace trunk tags %s: %v", port.Name, err)
214-
return nil, err
215-
}
216-
}
217257

218258
return port, nil
219259
}
@@ -324,23 +364,30 @@ func getPortName(baseName string, portSpec *infrav1.PortOpts, netIndex int) stri
324364
return fmt.Sprintf("%s-%d", baseName, netIndex)
325365
}
326366

327-
func (s *Service) CreatePorts(eventObject runtime.Object, desiredPorts []infrav1.ResolvedPortSpec, resources *infrav1.MachineResources) error {
367+
// EnsurePorts ensures that every one of desiredPorts is created and has
368+
// expected trunk and tags.
369+
func (s *Service) EnsurePorts(eventObject runtime.Object, desiredPorts []infrav1.ResolvedPortSpec, resources *infrav1.MachineResources) error {
328370
for i := range desiredPorts {
329-
// Skip creation of ports which already exist
371+
// If we already created the port, make use of the status
372+
portStatus := infrav1.PortStatus{}
330373
if i < len(resources.Ports) {
331-
continue
374+
portStatus = resources.Ports[i]
332375
}
333-
334-
portSpec := &desiredPorts[i]
335-
// Events are recorded in CreatePort
336-
port, err := s.CreatePort(eventObject, portSpec)
376+
// Events are recorded in EnsurePort
377+
port, err := s.EnsurePort(eventObject, &desiredPorts[i], portStatus)
337378
if err != nil {
338379
return err
339380
}
340381

341-
resources.Ports = append(resources.Ports, infrav1.PortStatus{
342-
ID: port.ID,
343-
})
382+
// If we already have the status, replace it,
383+
// otherwise append it.
384+
if i < len(resources.Ports) {
385+
resources.Ports[i] = portStatus
386+
} else {
387+
resources.Ports = append(resources.Ports, infrav1.PortStatus{
388+
ID: port.ID,
389+
})
390+
}
344391
}
345392

346393
return nil
@@ -609,3 +656,19 @@ func (s *Service) AdoptPorts(scope *scope.WithLogger, desiredPorts []infrav1.Res
609656

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

pkg/cloud/services/networking/port_test.go

+107-5
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import (
4040
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
4141
)
4242

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

159+
m.ListPort(ports.ListOpts{
160+
Name: "foo-port-1",
161+
NetworkID: netID,
162+
}).Return(nil, nil)
159163
// The following allows us to use gomega to
160164
// compare the argument instead of gomock.
161165
// Gomock's output in the case of a mismatch is
@@ -183,6 +187,10 @@ func Test_CreatePort(t *testing.T) {
183187
expectedCreateOpts = portsbinding.CreateOptsExt{
184188
CreateOptsBuilder: expectedCreateOpts,
185189
}
190+
m.ListPort(ports.ListOpts{
191+
Name: "test-port",
192+
NetworkID: netID,
193+
}).Return(nil, nil)
186194
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
187195
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
188196
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
@@ -219,6 +227,10 @@ func Test_CreatePort(t *testing.T) {
219227
expectedCreateOpts = portsbinding.CreateOptsExt{
220228
CreateOptsBuilder: expectedCreateOpts,
221229
}
230+
m.ListPort(ports.ListOpts{
231+
Name: "test-port",
232+
NetworkID: netID,
233+
}).Return(nil, nil)
222234
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
223235
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
224236
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
@@ -261,6 +273,10 @@ func Test_CreatePort(t *testing.T) {
261273
expectedCreateOpts = portsbinding.CreateOptsExt{
262274
CreateOptsBuilder: expectedCreateOpts,
263275
}
276+
m.ListPort(ports.ListOpts{
277+
Name: "test-port",
278+
NetworkID: netID,
279+
}).Return(nil, nil)
264280
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
265281
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
266282
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
@@ -270,7 +286,7 @@ func Test_CreatePort(t *testing.T) {
270286
want: &ports.Port{ID: portID},
271287
},
272288
{
273-
name: "tags and trunk",
289+
name: "create port with tags and trunk",
274290
port: infrav1.ResolvedPortSpec{
275291
Name: "test-port",
276292
NetworkID: netID,
@@ -287,6 +303,10 @@ func Test_CreatePort(t *testing.T) {
287303
CreateOptsBuilder: expectedCreateOpts,
288304
}
289305

306+
m.ListPort(ports.ListOpts{
307+
Name: "test-port",
308+
NetworkID: netID,
309+
}).Return(nil, nil)
290310
// Create the port
291311
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
292312
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
@@ -318,6 +338,87 @@ func Test_CreatePort(t *testing.T) {
318338
},
319339
want: &ports.Port{ID: portID, Name: "test-port"},
320340
},
341+
{
342+
name: "port with tags and trunk already exists",
343+
port: infrav1.ResolvedPortSpec{
344+
Name: "test-port",
345+
NetworkID: netID,
346+
Tags: []string{"tag1", "tag2"},
347+
Trunk: ptr.To(true),
348+
},
349+
expect: func(m *mock.MockNetworkClientMockRecorder, _ types.Gomega) {
350+
m.ListPort(ports.ListOpts{
351+
Name: "test-port",
352+
NetworkID: netID,
353+
}).Return([]ports.Port{{
354+
ID: portID,
355+
Name: "test-port",
356+
NetworkID: netID,
357+
Tags: []string{"tag1", "tag2"},
358+
}}, nil)
359+
360+
// Look for existing trunk
361+
m.ListTrunk(trunks.ListOpts{
362+
PortID: portID,
363+
Name: "test-port",
364+
}).Return([]trunks.Trunk{{
365+
ID: trunkID,
366+
Tags: []string{"tag1", "tag2"},
367+
}}, nil)
368+
},
369+
want: &ports.Port{
370+
ID: portID,
371+
Name: "test-port",
372+
NetworkID: netID,
373+
Tags: []string{"tag1", "tag2"},
374+
},
375+
},
376+
{
377+
name: "partial port missing tags and trunk",
378+
port: infrav1.ResolvedPortSpec{
379+
Name: "test-port",
380+
NetworkID: netID,
381+
Tags: []string{"tag1", "tag2"},
382+
Trunk: ptr.To(true),
383+
},
384+
expect: func(m *mock.MockNetworkClientMockRecorder, _ types.Gomega) {
385+
m.ListPort(ports.ListOpts{
386+
Name: "test-port",
387+
NetworkID: netID,
388+
}).Return([]ports.Port{{
389+
ID: portID,
390+
Name: "test-port",
391+
NetworkID: netID,
392+
}}, nil)
393+
394+
// Tag the port
395+
m.ReplaceAllAttributesTags("ports", portID, attributestags.ReplaceAllOpts{
396+
Tags: []string{"tag1", "tag2"},
397+
})
398+
399+
// Look for existing trunk
400+
m.ListTrunk(trunks.ListOpts{
401+
PortID: portID,
402+
Name: "test-port",
403+
}).Return([]trunks.Trunk{}, nil)
404+
405+
// Create the trunk
406+
m.CreateTrunk(trunks.CreateOpts{
407+
PortID: portID,
408+
Name: "test-port",
409+
}).Return(&trunks.Trunk{ID: trunkID}, nil)
410+
411+
// Tag the trunk
412+
m.ReplaceAllAttributesTags("trunks", trunkID, attributestags.ReplaceAllOpts{
413+
Tags: []string{"tag1", "tag2"},
414+
})
415+
},
416+
want: &ports.Port{
417+
ID: portID,
418+
Name: "test-port",
419+
NetworkID: netID,
420+
},
421+
},
321422
}
322423

323424
eventObject := &infrav1.OpenStackMachine{}
@@ -333,9 +434,10 @@ func Test_CreatePort(t *testing.T) {
333434
s := Service{
334435
client: mockClient,
335436
}
336-
got, err := s.CreatePort(
437+
got, err := s.EnsurePort(
337438
eventObject,
338439
&tt.port,
440+
infrav1.PortStatus{},
339441
)
340442
if tt.wantErr {
341443
g.Expect(err).To(HaveOccurred())

0 commit comments

Comments
 (0)