Skip to content

Commit 68e7b13

Browse files
lentzi90mquhuy
andcommittedJan 24, 2025··
Ensure that existing ports also have correct tags and trunks
If port creation fails in the middle, and cleanup also fails, then we may end up with ports with missing tags or trunks. This could happen when hitting rate-limits for example or if there is a network outage. This commit addresses the issue by going through existing ports and ensuring that they have correct tags and trunks. Co-authored-by: Huy Mai <huy.mai@est.tech> Signed-off-by: Lennart Jern <lennart.jern@est.tech>
1 parent d0a76bf commit 68e7b13

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())

‎pkg/cloud/services/networking/service.go

+2-16
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package networking
1818

1919
import (
2020
"fmt"
21-
"sort"
2221

2322
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/attributestags"
2423
"k8s.io/apimachinery/pkg/runtime"
@@ -65,28 +64,15 @@ func (s *Service) replaceAllAttributesTags(eventObject runtime.Object, resourceT
6564
record.Warnf(eventObject, "FailedReplaceAllAttributesTags", "Invalid resourceType argument in function call")
6665
panic(fmt.Errorf("invalid argument: resourceType, %s, does not match allowed arguments: %s or %s", resourceType, trunkResource, portResource))
6766
}
68-
// remove duplicate values from tags
69-
tagsMap := make(map[string]string)
70-
for _, t := range tags {
71-
tagsMap[t] = t
72-
}
73-
74-
uniqueTags := []string{}
75-
for k := range tagsMap {
76-
uniqueTags = append(uniqueTags, k)
77-
}
78-
79-
// Sort the tags so that we always get fixed order of tags to make UT easier
80-
sort.Strings(uniqueTags)
8167

8268
_, err := s.client.ReplaceAllAttributesTags(resourceType, resourceID, attributestags.ReplaceAllOpts{
83-
Tags: uniqueTags,
69+
Tags: tags,
8470
})
8571
if err != nil {
8672
record.Warnf(eventObject, "FailedReplaceAllAttributesTags", "Failed to replace all attributestags, %s: %v", resourceID, err)
8773
return err
8874
}
8975

90-
record.Eventf(eventObject, "SuccessfulReplaceAllAttributeTags", "Replaced all attributestags for %s with tags %s", resourceID, uniqueTags)
76+
record.Eventf(eventObject, "SuccessfulReplaceAllAttributeTags", "Replaced all attributestags for %s with tags %s", resourceID, tags)
9177
return nil
9278
}

‎test/e2e/data/e2e_conf.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
---
21
# E2E test scenario using local dev images and manifests built from the source tree for following providers:
32
# - cluster-api
43
# - bootstrap kubeadm

0 commit comments

Comments
 (0)
Please sign in to comment.