Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Ensure that existing ports also have correct tags and trunks #2256

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions controllers/openstackserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,11 +418,7 @@ func getOrCreateServerPorts(openStackServer *infrav1alpha1.OpenStackServer, netw
}
desiredPorts := resolved.Ports

if len(desiredPorts) == len(resources.Ports) {
return nil
}

if err := networkingService.CreatePorts(openStackServer, desiredPorts, resources); err != nil {
if err := networkingService.EnsurePorts(openStackServer, desiredPorts, resources); err != nil {
return fmt.Errorf("creating ports: %w", err)
}

Expand Down
14 changes: 14 additions & 0 deletions controllers/openstackserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ var listDefaultPorts = func(r *recorders) {
}, nil)
}

var listDefaultPortsWithID = func(r *recorders) {
r.network.ListPort(ports.ListOpts{
Name: openStackServerName + "-0",
ID: portUUID,
NetworkID: networkUUID,
}).Return([]ports.Port{
{
ID: portUUID,
},
}, nil)
}

var listDefaultPortsNotFound = func(r *recorders) {
r.network.ListPort(ports.ListOpts{
Name: openStackServerName + "-0",
Expand Down Expand Up @@ -479,6 +491,7 @@ func Test_OpenStackServerReconcileCreate(t *testing.T) {
listDefaultPortsNotFound(r)
createDefaultPort(r)
listDefaultServerNotFound(r)
listDefaultPortsNotFound(r)
createDefaultServer(r)
},
},
Expand All @@ -500,6 +513,7 @@ func Test_OpenStackServerReconcileCreate(t *testing.T) {
},
expect: func(r *recorders) {
listDefaultPorts(r)
lentzi90 marked this conversation as resolved.
Show resolved Hide resolved
listDefaultPortsWithID(r)
listDefaultServerFound(r)
},
},
Expand Down
117 changes: 90 additions & 27 deletions pkg/cloud/services/networking/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,61 @@ func (s *Service) GetPortForExternalNetwork(instanceID string, externalNetworkID
return nil, nil
}

func (s *Service) CreatePort(eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec) (*ports.Port, error) {
// ensurePortTagsAndTrunk ensures that the provided port has the tags and trunk defined in portSpec.
func (s *Service) ensurePortTagsAndTrunk(port *ports.Port, eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec) error {
wantedTags := uniqueSortedTags(portSpec.Tags)
actualTags := uniqueSortedTags(port.Tags)
// Only replace tags if there is a difference
if !slices.Equal(wantedTags, actualTags) && len(wantedTags) > 0 {
if err := s.replaceAllAttributesTags(eventObject, portResource, port.ID, wantedTags); err != nil {
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace port tags %s: %v", port.Name, err)
return err
}
}
if ptr.Deref(portSpec.Trunk, false) {
trunk, err := s.getOrCreateTrunkForPort(eventObject, port)
if err != nil {
record.Warnf(eventObject, "FailedCreateTrunk", "Failed to create trunk for port %s: %v", port.Name, err)
return err
}

if !slices.Equal(wantedTags, trunk.Tags) {
if err = s.replaceAllAttributesTags(eventObject, trunkResource, trunk.ID, wantedTags); err != nil {
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace trunk tags %s: %v", port.Name, err)
return err
}
}
}
return nil
}

// EnsurePort ensure that a port defined with portSpec Name and NetworkID exists,
// and that the port has suitable tags and trunk. If the PortStatus is already known,
// use the ID when filtering for existing ports.
func (s *Service) EnsurePort(eventObject runtime.Object, portSpec *infrav1.ResolvedPortSpec, portStatus infrav1.PortStatus) (*ports.Port, error) {
opts := ports.ListOpts{
Name: portSpec.Name,
NetworkID: portSpec.NetworkID,
}
if portStatus.ID != "" {
opts.ID = portStatus.ID
}

existingPorts, err := s.client.ListPort(opts)
if err != nil {
return nil, fmt.Errorf("searching for existing port for server: %v", err)
}
if len(existingPorts) > 1 {
return nil, fmt.Errorf("multiple ports found with name \"%s\"", portSpec.Name)
}

if len(existingPorts) == 1 {
port := &existingPorts[0]
if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
return nil, err
}
return port, nil
}
var addressPairs []ports.AddressPair
if !ptr.Deref(portSpec.DisablePortSecurity, false) {
for _, ap := range portSpec.AllowedAddressPairs {
Expand Down Expand Up @@ -200,24 +254,10 @@ func (s *Service) CreatePort(eventObject runtime.Object, portSpec *infrav1.Resol
return nil, err
}

if len(portSpec.Tags) > 0 {
if err = s.replaceAllAttributesTags(eventObject, portResource, port.ID, portSpec.Tags); err != nil {
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace port tags %s: %v", portSpec.Name, err)
return nil, err
}
if err = s.ensurePortTagsAndTrunk(port, eventObject, portSpec); err != nil {
return nil, err
}
record.Eventf(eventObject, "SuccessfulCreatePort", "Created port %s with id %s", port.Name, port.ID)
if ptr.Deref(portSpec.Trunk, false) {
trunk, err := s.getOrCreateTrunkForPort(eventObject, port)
if err != nil {
record.Warnf(eventObject, "FailedCreateTrunk", "Failed to create trunk for port %s: %v", port.Name, err)
return nil, err
}
if err = s.replaceAllAttributesTags(eventObject, trunkResource, trunk.ID, portSpec.Tags); err != nil {
record.Warnf(eventObject, "FailedReplaceTags", "Failed to replace trunk tags %s: %v", port.Name, err)
return nil, err
}
}

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

func (s *Service) CreatePorts(eventObject runtime.Object, desiredPorts []infrav1.ResolvedPortSpec, resources *infrav1alpha1.ServerResources) error {
// EnsurePorts ensures that every one of desiredPorts is created and has
// expected trunk and tags.
func (s *Service) EnsurePorts(eventObject runtime.Object, desiredPorts []infrav1.ResolvedPortSpec, resources *infrav1alpha1.ServerResources) error {
for i := range desiredPorts {
// Skip creation of ports which already exist
// If we already created the port, make use of the status
portStatus := infrav1.PortStatus{}
if i < len(resources.Ports) {
continue
portStatus = resources.Ports[i]
}

portSpec := &desiredPorts[i]
// Events are recorded in CreatePort
port, err := s.CreatePort(eventObject, portSpec)
// Events are recorded in EnsurePort
port, err := s.EnsurePort(eventObject, &desiredPorts[i], portStatus)
if err != nil {
return err
}

resources.Ports = append(resources.Ports, infrav1.PortStatus{
ID: port.ID,
})
// If we already have the status, replace it,
// otherwise append it.
if i < len(resources.Ports) {
resources.Ports[i] = portStatus
} else {
resources.Ports = append(resources.Ports, infrav1.PortStatus{
ID: port.ID,
})
}
}

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

return nil
}

// uniqueSortedTags returns a new, sorted slice where any duplicates have been removed.
func uniqueSortedTags(tags []string) []string {
// remove duplicate values from tags
tagsMap := make(map[string]string)
for _, t := range tags {
tagsMap[t] = t
}

uniqueTags := []string{}
for k := range tagsMap {
uniqueTags = append(uniqueTags, k)
}
slices.Sort(uniqueTags)
return uniqueTags
}
116 changes: 111 additions & 5 deletions pkg/cloud/services/networking/port_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import (
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
)

func Test_CreatePort(t *testing.T) {
func Test_EnsurePort(t *testing.T) {
// Arbitrary values used in the tests
const (
netID = "7fd24ceb-788a-441f-ad0a-d8e2f5d31a1d"
Expand All @@ -60,8 +60,8 @@ func Test_CreatePort(t *testing.T) {
name string
port infrav1.ResolvedPortSpec
expect func(m *mock.MockNetworkClientMockRecorder, g Gomega)
// Note the 'wanted' port isn't so important, since it will be whatever we tell ListPort or CreatePort to return.
// Mostly in this test suite, we're checking that CreatePort is called with the expected port opts.
// Note the 'wanted' port isn't so important, since it will be whatever we tell ListPort or EnsurePort to return.
// Mostly in this test suite, we're checking that EnsurePort is called with the expected port opts.
want *ports.Port
wantErr bool
}{
Expand Down Expand Up @@ -157,6 +157,10 @@ func Test_CreatePort(t *testing.T) {
},
}

m.ListPort(ports.ListOpts{
Name: "foo-port-1",
NetworkID: netID,
}).Return(nil, nil)
// The following allows us to use gomega to
// compare the argument instead of gomock.
// Gomock's output in the case of a mismatch is
Expand Down Expand Up @@ -184,6 +188,10 @@ func Test_CreatePort(t *testing.T) {
expectedCreateOpts = portsbinding.CreateOptsExt{
CreateOptsBuilder: expectedCreateOpts,
}
m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return(nil, nil)
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
Expand All @@ -203,6 +211,10 @@ func Test_CreatePort(t *testing.T) {
SecurityGroups: []string{portSecurityGroupID},
},
expect: func(m *mock.MockNetworkClientMockRecorder, _ Gomega) {
m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return(nil, nil)
m.CreatePort(gomock.Any()).Times(0)
},
wantErr: true,
Expand Down Expand Up @@ -235,6 +247,10 @@ func Test_CreatePort(t *testing.T) {
expectedCreateOpts = portsbinding.CreateOptsExt{
CreateOptsBuilder: expectedCreateOpts,
}
m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return(nil, nil)
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
Expand Down Expand Up @@ -277,6 +293,10 @@ func Test_CreatePort(t *testing.T) {
expectedCreateOpts = portsbinding.CreateOptsExt{
CreateOptsBuilder: expectedCreateOpts,
}
m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return(nil, nil)
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
g.Expect(gotCreateOpts).To(Equal(expectedCreateOpts), cmp.Diff(gotCreateOpts, expectedCreateOpts))
Expand All @@ -286,7 +306,7 @@ func Test_CreatePort(t *testing.T) {
want: &ports.Port{ID: portID},
},
{
name: "tags and trunk",
name: "create port with tags and trunk",
port: infrav1.ResolvedPortSpec{
Name: "test-port",
NetworkID: netID,
Expand All @@ -303,6 +323,10 @@ func Test_CreatePort(t *testing.T) {
CreateOptsBuilder: expectedCreateOpts,
}

m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return(nil, nil)
// Create the port
m.CreatePort(gomock.Any()).DoAndReturn(func(builder ports.CreateOptsBuilder) (*ports.Port, error) {
gotCreateOpts := builder.(portsbinding.CreateOptsExt)
Expand Down Expand Up @@ -334,6 +358,87 @@ func Test_CreatePort(t *testing.T) {
},
want: &ports.Port{ID: portID, Name: "test-port"},
},
{
name: "port with tags and trunk already exists",
port: infrav1.ResolvedPortSpec{
Name: "test-port",
NetworkID: netID,
Tags: []string{"tag1", "tag2"},
Trunk: ptr.To(true),
},
expect: func(m *mock.MockNetworkClientMockRecorder, _ types.Gomega) {
m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return([]ports.Port{{
ID: portID,
Name: "test-port",
NetworkID: netID,
Tags: []string{"tag1", "tag2"},
}}, nil)

// Look for existing trunk
m.ListTrunk(trunks.ListOpts{
PortID: portID,
Name: "test-port",
}).Return([]trunks.Trunk{{
ID: trunkID,
Tags: []string{"tag1", "tag2"},
}}, nil)
},
want: &ports.Port{
ID: portID,
Name: "test-port",
NetworkID: netID,
Tags: []string{"tag1", "tag2"},
},
},
{
name: "partial port missing tags and trunk",
port: infrav1.ResolvedPortSpec{
Name: "test-port",
NetworkID: netID,
Tags: []string{"tag1", "tag2"},
Trunk: ptr.To(true),
},
expect: func(m *mock.MockNetworkClientMockRecorder, _ types.Gomega) {
m.ListPort(ports.ListOpts{
Name: "test-port",
NetworkID: netID,
}).Return([]ports.Port{{
ID: portID,
Name: "test-port",
NetworkID: netID,
}}, nil)

// Tag the port
m.ReplaceAllAttributesTags("ports", portID, attributestags.ReplaceAllOpts{
Tags: []string{"tag1", "tag2"},
})

// Look for existing trunk
m.ListTrunk(trunks.ListOpts{
PortID: portID,
Name: "test-port",
}).Return([]trunks.Trunk{}, nil)

// Create the trunk
m.CreateTrunk(trunks.CreateOpts{
PortID: portID,
Name: "test-port",
}).Return(&trunks.Trunk{ID: trunkID}, nil)

// Tag the trunk
m.ReplaceAllAttributesTags("trunks", trunkID, attributestags.ReplaceAllOpts{
Tags: []string{"tag1", "tag2"},
})
},
want: &ports.Port{
ID: portID,
Name: "test-port",
NetworkID: netID,
},
},
}

eventObject := &infrav1.OpenStackMachine{}
Expand All @@ -349,9 +454,10 @@ func Test_CreatePort(t *testing.T) {
s := Service{
client: mockClient,
}
got, err := s.CreatePort(
got, err := s.EnsurePort(
eventObject,
&tt.port,
infrav1.PortStatus{},
)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
Expand Down
Loading