-
Notifications
You must be signed in to change notification settings - Fork 277
🐛 Fix panic when OpenStackCluster.Status.Network is nil in HCP scenarios #2635
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
base: main
Are you sure you want to change the base?
Changes from 8 commits
a89107e
b7498dd
523d934
560e5ff
3f7892c
d9c1a62
cc4847f
bf62a91
f7dfd16
0ab4e3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,20 +77,17 @@ func TestOpenStackMachineSpecToOpenStackServerSpec(t *testing.T) { | |
}, | ||
} | ||
portOptsWithAdditionalSecurityGroup := []infrav1.PortOpts{ | ||
{ | ||
Network: &infrav1.NetworkParam{ | ||
ID: ptr.To(openStackCluster.Status.Network.ID), | ||
}, | ||
SecurityGroups: []infrav1.SecurityGroupParam{ | ||
{ | ||
ID: ptr.To(openStackCluster.Status.WorkerSecurityGroup.ID), | ||
}, | ||
{ | ||
ID: ptr.To(extraSecurityGroupUUID), | ||
}, | ||
}, | ||
}, | ||
} | ||
{ | ||
Network: &infrav1.NetworkParam{ | ||
ID: ptr.To(openStackCluster.Status.Network.ID), | ||
}, | ||
SecurityGroups: []infrav1.SecurityGroupParam{ | ||
{ | ||
ID: ptr.To(extraSecurityGroupUUID), | ||
}, | ||
}, | ||
}, | ||
} | ||
image := infrav1.ImageParam{Filter: &infrav1.ImageFilter{Name: ptr.To("my-image")}} | ||
tags := []string{"tag1", "tag2"} | ||
userData := &corev1.LocalObjectReference{Name: "server-data-secret"} | ||
|
@@ -158,27 +155,67 @@ func TestOpenStackMachineSpecToOpenStackServerSpec(t *testing.T) { | |
}, | ||
}, | ||
{ | ||
name: "Test an OpenStackMachineSpec to OpenStackServerSpec conversion with flavorID specified but not flavor", | ||
spec: &infrav1.OpenStackMachineSpec{ | ||
FlavorID: ptr.To(flavorUUID), | ||
Image: image, | ||
SSHKeyName: sshKeyName, | ||
}, | ||
want: &infrav1alpha1.OpenStackServerSpec{ | ||
FlavorID: ptr.To(flavorUUID), | ||
IdentityRef: identityRef, | ||
Image: image, | ||
SSHKeyName: sshKeyName, | ||
Ports: portOpts, | ||
Tags: tags, | ||
UserDataRef: userData, | ||
}, | ||
}, | ||
name: "Test an OpenStackMachineSpec to OpenStackServerSpec conversion with flavorID specified but not flavor", | ||
spec: &infrav1.OpenStackMachineSpec{ | ||
FlavorID: ptr.To(flavorUUID), | ||
Image: image, | ||
SSHKeyName: sshKeyName, | ||
}, | ||
want: &infrav1alpha1.OpenStackServerSpec{ | ||
FlavorID: ptr.To(flavorUUID), | ||
IdentityRef: identityRef, | ||
Image: image, | ||
SSHKeyName: sshKeyName, | ||
Ports: portOpts, | ||
Tags: tags, | ||
UserDataRef: userData, | ||
}, | ||
}, | ||
{ | ||
name: "Cluster network nil, machine defines port network and overrides SG", | ||
spec: &infrav1.OpenStackMachineSpec{ | ||
Ports: []infrav1.PortOpts{{ | ||
Network: &infrav1.NetworkParam{ID: ptr.To(networkUUID)}, | ||
}}, | ||
SecurityGroups: []infrav1.SecurityGroupParam{{ID: ptr.To(extraSecurityGroupUUID)}}, | ||
}, | ||
want: &infrav1alpha1.OpenStackServerSpec{ | ||
IdentityRef: identityRef, | ||
Ports: []infrav1.PortOpts{{ | ||
Network: &infrav1.NetworkParam{ID: ptr.To(networkUUID)}, | ||
SecurityGroups: []infrav1.SecurityGroupParam{{ID: ptr.To(extraSecurityGroupUUID)}}, | ||
}}, | ||
Tags: tags, | ||
UserDataRef: userData, | ||
}, | ||
}, | ||
{ | ||
name: "Cluster network nil, machine defines port network and falls back to cluster SG", | ||
spec: &infrav1.OpenStackMachineSpec{ | ||
Ports: []infrav1.PortOpts{{ | ||
Network: &infrav1.NetworkParam{ID: ptr.To(networkUUID)}, | ||
}}, | ||
}, | ||
want: &infrav1alpha1.OpenStackServerSpec{ | ||
IdentityRef: identityRef, | ||
Ports: []infrav1.PortOpts{{ | ||
Network: &infrav1.NetworkParam{ID: ptr.To(networkUUID)}, | ||
SecurityGroups: []infrav1.SecurityGroupParam{{ID: ptr.To(workerSecurityGroupUUID)}}, | ||
}}, | ||
Tags: tags, | ||
UserDataRef: userData, | ||
}, | ||
}, | ||
} | ||
for i := range tests { | ||
tt := tests[i] | ||
t.Run(tt.name, func(t *testing.T) { | ||
spec := openStackMachineSpecToOpenStackServerSpec(tt.spec, identityRef, tags, "", userData, &openStackCluster.Status.WorkerSecurityGroup.ID, openStackCluster.Status.Network.ID) | ||
defaultNetID := "" | ||
if openStackCluster.Status.Network != nil { | ||
defaultNetID = openStackCluster.Status.Network.ID | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More evidence of what I was saying above: this duplicates part of the functionality in the test. If we did this in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
spec := openStackMachineSpecToOpenStackServerSpec(tt.spec, identityRef, tags, "", userData, &openStackCluster.Status.WorkerSecurityGroup.ID, defaultNetID) | ||
if !reflect.DeepEqual(spec, tt.want) { | ||
t.Errorf("openStackMachineSpecToOpenStackServerSpec() got = %+v, want %+v", spec, tt.want) | ||
} | ||
|
@@ -224,3 +261,153 @@ func TestGetPortIDs(t *testing.T) { | |
}) | ||
} | ||
} | ||
|
||
func TestOpenStackMachineSpecToOpenStackServerSpec_NilNetworkCases(t *testing.T) { | ||
identityRef := infrav1.OpenStackIdentityReference{ | ||
Name: "foo", | ||
CloudName: "my-cloud", | ||
} | ||
image := infrav1.ImageParam{Filter: &infrav1.ImageFilter{Name: ptr.To("my-image")}} | ||
tags := []string{"tag1", "tag2"} | ||
userData := &corev1.LocalObjectReference{Name: "server-data-secret"} | ||
|
||
tests := []struct { | ||
name string | ||
openStackCluster *infrav1.OpenStackCluster | ||
spec *infrav1.OpenStackMachineSpec | ||
expectedDefaultNetID string | ||
expectedPorts []infrav1.PortOpts | ||
description string | ||
}{ | ||
{ | ||
name: "Empty cluster network ID, machine defines explicit ports", | ||
openStackCluster: &infrav1.OpenStackCluster{ | ||
Spec: infrav1.OpenStackClusterSpec{ | ||
ManagedSecurityGroups: &infrav1.ManagedSecurityGroups{}, | ||
}, | ||
Status: infrav1.OpenStackClusterStatus{ | ||
Network: &infrav1.NetworkStatusWithSubnets{ | ||
NetworkStatus: infrav1.NetworkStatus{ | ||
ID: "", // Empty network ID | ||
}, | ||
}, | ||
WorkerSecurityGroup: &infrav1.SecurityGroupStatus{ | ||
ID: workerSecurityGroupUUID, | ||
}, | ||
}, | ||
}, | ||
spec: &infrav1.OpenStackMachineSpec{ | ||
Flavor: ptr.To(flavorName), | ||
Image: image, | ||
Ports: []infrav1.PortOpts{{ | ||
Network: &infrav1.NetworkParam{ID: ptr.To(networkUUID)}, | ||
}}, | ||
}, | ||
expectedDefaultNetID: "", // Empty because cluster network ID is empty | ||
expectedPorts: []infrav1.PortOpts{{ | ||
Network: &infrav1.NetworkParam{ID: ptr.To(networkUUID)}, | ||
SecurityGroups: []infrav1.SecurityGroupParam{{ | ||
ID: ptr.To(workerSecurityGroupUUID), | ||
}}, | ||
}}, | ||
description: "Should work when cluster has empty network ID but machine defines ports", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
// Verify the default network ID extraction logic | ||
var defaultNetworkID string | ||
if tt.openStackCluster.Status.Network != nil { | ||
defaultNetworkID = tt.openStackCluster.Status.Network.ID | ||
} | ||
|
||
if defaultNetworkID != tt.expectedDefaultNetID { | ||
t.Errorf("Expected defaultNetworkID = %q, got %q", tt.expectedDefaultNetID, defaultNetworkID) | ||
} | ||
|
||
// Test the spec conversion | ||
var managedSecurityGroupID *string | ||
if tt.openStackCluster.Status.WorkerSecurityGroup != nil { | ||
managedSecurityGroupID = &tt.openStackCluster.Status.WorkerSecurityGroup.ID | ||
} | ||
|
||
spec := openStackMachineSpecToOpenStackServerSpec( | ||
tt.spec, | ||
identityRef, | ||
tags, | ||
"", // failureDomain | ||
userData, | ||
managedSecurityGroupID, | ||
defaultNetworkID, | ||
) | ||
|
||
if !reflect.DeepEqual(spec.Ports, tt.expectedPorts) { | ||
t.Errorf("Expected ports = %+v, got %+v", tt.expectedPorts, spec.Ports) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestValidateNetworkConfiguration(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
clusterNetworkID string | ||
machinePortsCount int | ||
expectError bool | ||
expectedErrorMsg string | ||
description string | ||
}{ | ||
{ | ||
name: "Valid: cluster has network, machine has no explicit ports", | ||
clusterNetworkID: networkUUID, | ||
machinePortsCount: 0, | ||
expectError: false, | ||
description: "Should succeed when cluster provides default network", | ||
}, | ||
{ | ||
name: "Valid: no cluster network, machine defines explicit ports", | ||
clusterNetworkID: "", | ||
machinePortsCount: 1, | ||
expectError: false, | ||
description: "Should succeed when machine defines its own networking", | ||
}, | ||
{ | ||
name: "Invalid: no cluster network, no machine ports", | ||
clusterNetworkID: "", | ||
machinePortsCount: 0, | ||
expectError: true, | ||
expectedErrorMsg: "no network configured: cluster network is missing and machine spec does not define ports with a network", | ||
description: "Should fail with terminal error when no networking is configured anywhere", | ||
}, | ||
{ | ||
name: "Valid: cluster network and machine ports both defined", | ||
clusterNetworkID: networkUUID, | ||
machinePortsCount: 2, | ||
expectError: false, | ||
description: "Should succeed when both cluster and machine define networking", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
// Simulate the validation logic from the controller | ||
hasClusterNetwork := tt.clusterNetworkID != "" | ||
hasMachinePorts := tt.machinePortsCount > 0 | ||
|
||
shouldFail := !hasClusterNetwork && !hasMachinePorts | ||
|
||
if shouldFail != tt.expectError { | ||
t.Errorf("Expected error: %v, but validation result: %v", tt.expectError, shouldFail) | ||
} | ||
|
||
if tt.expectError && shouldFail { | ||
// In the real controller, this would be a terminal error | ||
actualError := "no network configured: cluster network is missing and machine spec does not define ports with a network" | ||
if actualError != tt.expectedErrorMsg { | ||
t.Errorf("Expected error message: %q, got: %q", tt.expectedErrorMsg, actualError) | ||
} | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I feel like this splits this logic across this function and
openStackMachineSpecToOpenStackServerSpec
. Did you consider putting this logic inopenStackMachineSpecToOpenStackServerSpec
and modifying its signature to return an error?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.