Skip to content

Commit b4d268d

Browse files
Reconcile OpenStack port additional security groups
The OpenStack Port task tracks kOps-managed SecurityGroups and user-supplied AdditionalSecurityGroups (by name). Two bugs prevented AdditionalSecurityGroups from being reconciled on existing ports: - newPortTaskFromCloud copied the desired AdditionalSecurityGroups into the actual state without checking the port, so drift was never detected. - RenderOpenstack had no path to apply security-group changes to an existing port, so changes were only picked up when an instance was recreated. Record an additional group in the actual state only when its ID is actually attached to the port, and update the port in place via UpdatePort with the merged managed + additional security-group IDs when the actual and desired sets differ. Add unit tests for both the read (drift detection) and write (in-place UpdatePort) paths. Fixes #18731 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rounak Debnath <r.debnath@x-ion.de>
1 parent 241fb97 commit b4d268d

2 files changed

Lines changed: 184 additions & 12 deletions

File tree

upup/pkg/fi/cloudup/openstacktasks/port.go

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package openstacktasks
1818

1919
import (
2020
"fmt"
21+
"slices"
2122
"sort"
2223
"strings"
2324

@@ -119,12 +120,31 @@ func getActualAllowedAddressPairs(port *ports.Port, find *Port) []ports.AddressP
119120
return allowedAddressPairs
120121
}
121122

123+
// securityGroupIDsEqual compares two ID-sorted lists of SecurityGroup tasks by ID.
124+
func securityGroupIDsEqual(a, e []*SecurityGroup) bool {
125+
if len(a) != len(e) {
126+
return false
127+
}
128+
for i := range a {
129+
if fi.ValueOf(a[i].ID) != fi.ValueOf(e[i].ID) {
130+
return false
131+
}
132+
}
133+
return true
134+
}
135+
122136
func newPortTaskFromCloud(cloud openstack.OpenstackCloud, lifecycle fi.Lifecycle, port *ports.Port, find *Port) (*Port, error) {
137+
portSecurityGroupIDs := map[string]struct{}{}
138+
for _, sgid := range port.SecurityGroups {
139+
portSecurityGroupIDs[sgid] = struct{}{}
140+
}
141+
123142
additionalSecurityGroupIDs := map[string]struct{}{}
143+
var actualAdditionalSecurityGroups []string
124144
if find != nil {
125-
for _, sg := range find.AdditionalSecurityGroups {
145+
for _, sgName := range find.AdditionalSecurityGroups {
126146
opt := secgroup.ListOpts{
127-
Name: sg,
147+
Name: sgName,
128148
}
129149
gs, err := cloud.ListSecurityGroups(opt)
130150
if err != nil {
@@ -133,7 +153,12 @@ func newPortTaskFromCloud(cloud openstack.OpenstackCloud, lifecycle fi.Lifecycle
133153
if len(gs) == 0 {
134154
continue
135155
}
136-
additionalSecurityGroupIDs[gs[0].ID] = struct{}{}
156+
sgID := gs[0].ID
157+
if _, ok := portSecurityGroupIDs[sgID]; !ok {
158+
continue
159+
}
160+
additionalSecurityGroupIDs[sgID] = struct{}{}
161+
actualAdditionalSecurityGroups = append(actualAdditionalSecurityGroups, sgName)
137162
}
138163
}
139164
sgs := []*SecurityGroup{}
@@ -197,7 +222,7 @@ func newPortTaskFromCloud(cloud openstack.OpenstackCloud, lifecycle fi.Lifecycle
197222
if find != nil {
198223
find.ID = actual.ID
199224
actual.InstanceGroupName = find.InstanceGroupName
200-
actual.AdditionalSecurityGroups = find.AdditionalSecurityGroups
225+
actual.AdditionalSecurityGroups = actualAdditionalSecurityGroups
201226
actual.WellKnownServices = find.WellKnownServices
202227
}
203228
return actual, nil
@@ -292,6 +317,31 @@ func (*Port) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, changes *Por
292317
return fmt.Errorf("error updating port: %v", err)
293318
}
294319
}
320+
// Compare against the actual state rather than inspecting changes: BuildChanges
321+
// copies the expected value into changes, so a list the user emptied out is
322+
// indistinguishable there from one that did not change at all.
323+
if !slices.Equal(a.AdditionalSecurityGroups, e.AdditionalSecurityGroups) || !securityGroupIDsEqual(a.SecurityGroups, e.SecurityGroups) {
324+
klog.V(2).Infof("Updating security groups for Port with name: %q", fi.ValueOf(e.Name))
325+
sgs := make([]string, 0, len(e.SecurityGroups)+len(e.AdditionalSecurityGroups))
326+
for _, sg := range e.SecurityGroups {
327+
sgs = append(sgs, fi.ValueOf(sg.ID))
328+
}
329+
for _, sgName := range e.AdditionalSecurityGroups {
330+
gs, err := t.Cloud.ListSecurityGroups(secgroup.ListOpts{Name: sgName})
331+
if err != nil {
332+
return fmt.Errorf("error looking up additional security group %q: %v", sgName, err)
333+
}
334+
if len(gs) == 0 {
335+
return fmt.Errorf("additional SecurityGroup not found for name %s", sgName)
336+
}
337+
sgs = append(sgs, gs[0].ID)
338+
}
339+
if _, err := t.Cloud.UpdatePort(fi.ValueOf(a.ID), ports.UpdateOpts{
340+
SecurityGroups: &sgs,
341+
}); err != nil {
342+
return fmt.Errorf("error updating port security groups: %v", err)
343+
}
344+
}
295345
}
296346
e.ID = a.ID
297347
klog.V(2).Infof("Using an existing Openstack port, id=%s", fi.ValueOf(e.ID))

upup/pkg/fi/cloudup/openstacktasks/port_test.go

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,61 @@ func Test_NewPortTaskFromCloud(t *testing.T) {
508508
},
509509
expectedError: nil,
510510
},
511+
{
512+
desc: "cloud port found port not nil drops additional security groups not on the port",
513+
lifecycle: fi.LifecycleSync,
514+
cloud: &portCloud{
515+
listSecurityGroups: map[string][]sg.SecGroup{
516+
"add-1": {
517+
{ID: "add-1-id", Name: "add-1"},
518+
},
519+
"add-2": {
520+
{ID: "add-2-id", Name: "add-2"},
521+
},
522+
},
523+
},
524+
cloudPort: &ports.Port{
525+
ID: "id",
526+
Name: "name",
527+
NetworkID: "networkID",
528+
FixedIPs: []ports.IP{
529+
{SubnetID: "subnet-a"},
530+
},
531+
SecurityGroups: []string{
532+
"sg-1",
533+
"add-1-id",
534+
},
535+
},
536+
foundPort: &Port{
537+
AdditionalSecurityGroups: []string{
538+
"add-1",
539+
"add-2",
540+
},
541+
},
542+
modifiedFoundPort: &Port{
543+
ID: new("id"),
544+
AdditionalSecurityGroups: []string{
545+
"add-1",
546+
"add-2",
547+
},
548+
},
549+
expectedPortTask: &Port{
550+
ID: new("id"),
551+
Name: new("name"),
552+
Network: &Network{ID: new("networkID")},
553+
SecurityGroups: []*SecurityGroup{
554+
{ID: new("sg-1"), Lifecycle: fi.LifecycleSync},
555+
},
556+
AdditionalSecurityGroups: []string{
557+
"add-1",
558+
},
559+
Subnets: []*Subnet{
560+
{ID: new("subnet-a"), Lifecycle: fi.LifecycleSync},
561+
},
562+
Lifecycle: fi.LifecycleSync,
563+
},
564+
expectedError: nil,
565+
},
511566
{
512567
desc: "cloud port found port not nil honors allowed address pairs",
513568
lifecycle: fi.LifecycleSync,
@@ -996,14 +1051,15 @@ func Test_Port_CheckChanges(t *testing.T) {
9961051

9971052
func Test_Port_RenderOpenstack(t *testing.T) {
9981053
tests := []struct {
999-
desc string
1000-
target *openstack.OpenstackAPITarget
1001-
actual *Port
1002-
expected *Port
1003-
changes *Port
1004-
expectedCloudPort *ports.Port
1005-
expectedAfter *Port
1006-
expectedError error
1054+
desc string
1055+
target *openstack.OpenstackAPITarget
1056+
actual *Port
1057+
expected *Port
1058+
changes *Port
1059+
expectedCloudPort *ports.Port
1060+
expectedAfter *Port
1061+
expectedError error
1062+
expectedUpdatePortCalls []updatePortCall
10071063
}{
10081064
{
10091065
desc: "actual not nil",
@@ -1200,6 +1256,58 @@ func Test_Port_RenderOpenstack(t *testing.T) {
12001256
expectedCloudPort: nil,
12011257
expectedError: nil,
12021258
},
1259+
{
1260+
desc: "changes in additional security groups merges with managed security groups",
1261+
target: &openstack.OpenstackAPITarget{
1262+
Cloud: &portCloud{
1263+
updatePort: &ports.Port{ID: "cloud-id"},
1264+
listSecurityGroups: map[string][]sg.SecGroup{
1265+
"add-1": {
1266+
{ID: "add-1-id", Name: "add-1"},
1267+
},
1268+
},
1269+
},
1270+
},
1271+
actual: &Port{
1272+
ID: new("cloud-id"),
1273+
Name: new("name"),
1274+
Network: &Network{ID: new("networkID")},
1275+
SecurityGroups: []*SecurityGroup{
1276+
{ID: new("sg-1")},
1277+
},
1278+
},
1279+
expected: &Port{
1280+
ID: new("expected-id"),
1281+
Name: new("name"),
1282+
Network: &Network{ID: new("networkID")},
1283+
SecurityGroups: []*SecurityGroup{
1284+
{ID: new("sg-1")},
1285+
},
1286+
AdditionalSecurityGroups: []string{"add-1"},
1287+
},
1288+
changes: &Port{
1289+
AdditionalSecurityGroups: []string{"add-1"},
1290+
},
1291+
expectedAfter: &Port{
1292+
ID: new("cloud-id"),
1293+
Name: new("name"),
1294+
Network: &Network{ID: new("networkID")},
1295+
SecurityGroups: []*SecurityGroup{
1296+
{ID: new("sg-1")},
1297+
},
1298+
AdditionalSecurityGroups: []string{"add-1"},
1299+
},
1300+
expectedUpdatePortCalls: []updatePortCall{
1301+
{
1302+
id: "cloud-id",
1303+
opts: ports.UpdateOpts{
1304+
SecurityGroups: &[]string{"sg-1", "add-1-id"},
1305+
},
1306+
},
1307+
},
1308+
expectedCloudPort: nil,
1309+
expectedError: nil,
1310+
},
12031311
}
12041312

12051313
for _, testCase := range tests {
@@ -1212,6 +1320,13 @@ func Test_Port_RenderOpenstack(t *testing.T) {
12121320
if !reflect.DeepEqual(testCase.expected, testCase.expectedAfter) {
12131321
t.Errorf("Expected Port task differs:\n%v\n\tinstead of\n%v", testCase.expected, testCase.expectedAfter)
12141322
}
1323+
1324+
if testCase.expectedUpdatePortCalls != nil {
1325+
cloud := testCase.target.Cloud.(*portCloud)
1326+
if !reflect.DeepEqual(cloud.updatePortCalls, testCase.expectedUpdatePortCalls) {
1327+
t.Errorf("UpdatePort calls differ:\n%+v\n\tinstead of\n%+v", cloud.updatePortCalls, testCase.expectedUpdatePortCalls)
1328+
}
1329+
}
12151330
})
12161331
}
12171332
}
@@ -1342,10 +1457,16 @@ type portCloud struct {
13421457
createPortError error
13431458
updatePort *ports.Port
13441459
updatePortError error
1460+
updatePortCalls []updatePortCall
13451461
listSecurityGroups map[string][]sg.SecGroup
13461462
listSecurityGroupsError error
13471463
}
13481464

1465+
type updatePortCall struct {
1466+
id string
1467+
opts ports.UpdateOptsBuilder
1468+
}
1469+
13491470
func (p *portCloud) ListPorts(opt ports.ListOptsBuilder) ([]ports.Port, error) {
13501471
return p.listPorts, p.listPortsError
13511472
}
@@ -1359,6 +1480,7 @@ func (p *portCloud) ListSecurityGroups(opt sg.ListOpts) ([]sg.SecGroup, error) {
13591480
}
13601481

13611482
func (p *portCloud) UpdatePort(id string, opt ports.UpdateOptsBuilder) (*ports.Port, error) {
1483+
p.updatePortCalls = append(p.updatePortCalls, updatePortCall{id: id, opts: opt})
13621484
return p.updatePort, p.updatePortError
13631485
}
13641486

0 commit comments

Comments
 (0)