diff --git a/controllers/openstackmachine_controller.go b/controllers/openstackmachine_controller.go index 6a7318911a..9dc8b38f38 100644 --- a/controllers/openstackmachine_controller.go +++ b/controllers/openstackmachine_controller.go @@ -583,7 +583,28 @@ func (r *OpenStackMachineReconciler) getOrCreateMachineServer(ctx context.Contex } return openStackCluster.Spec.IdentityRef }() - machineServerSpec := openStackMachineSpecToOpenStackServerSpec(&openStackMachine.Spec, identityRef, compute.InstanceTags(&openStackMachine.Spec, openStackCluster), failureDomain, userDataRef, getManagedSecurityGroup(openStackCluster, machine), openStackCluster.Status.Network.ID) + // If user has provided .spec.ports (non-empty), skip .Status.Network entirely + // Otherwise, fall back to openStackCluster.Status.Network.ID + // This supports HCP - Hosted Control Plane usage + defaultNetworkID := "" + if len(openStackMachine.Spec.Ports) == 0 { + if openStackCluster.Status.Network == nil || openStackCluster.Status.Network.ID == "" { + return nil, fmt.Errorf( + "no user-defined ports and openStackCluster.Status.Network is nil/empty; cannot create server", + ) + } + defaultNetworkID = openStackCluster.Status.Network.ID + } + + defaultSecGroup := getManagedSecurityGroup(openStackCluster, machine) + var secGroup *string + // If the user explicitly provided security groups in OpenStackMachine, skip the cluster’s managed SG. + if len(openStackMachine.Spec.SecurityGroups) > 0 { + secGroup = nil + } else { + secGroup = defaultSecGroup + } + machineServerSpec := openStackMachineSpecToOpenStackServerSpec(&openStackMachine.Spec, identityRef, compute.InstanceTags(&openStackMachine.Spec, openStackCluster), failureDomain, userDataRef, secGroup, defaultNetworkID) machineServer = &infrav1alpha1.OpenStackServer{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ diff --git a/controllers/openstackmachine_controller_test.go b/controllers/openstackmachine_controller_test.go index 2cace25414..09aec3379a 100644 --- a/controllers/openstackmachine_controller_test.go +++ b/controllers/openstackmachine_controller_test.go @@ -95,12 +95,16 @@ func TestOpenStackMachineSpecToOpenStackServerSpec(t *testing.T) { tags := []string{"tag1", "tag2"} userData := &corev1.LocalObjectReference{Name: "server-data-secret"} tests := []struct { - name string - spec *infrav1.OpenStackMachineSpec - want *infrav1alpha1.OpenStackServerSpec + name string + passWorkerSG bool + passNetID bool + spec *infrav1.OpenStackMachineSpec + want *infrav1alpha1.OpenStackServerSpec }{ { - name: "Test a minimum OpenStackMachineSpec to OpenStackServerSpec conversion", + name: "Test a minimum OpenStackMachineSpec to OpenStackServerSpec conversion", + passWorkerSG: true, + passNetID: true, spec: &infrav1.OpenStackMachineSpec{ Flavor: ptr.To(flavorName), Image: image, @@ -117,7 +121,9 @@ func TestOpenStackMachineSpecToOpenStackServerSpec(t *testing.T) { }, }, { - name: "Test a OpenStackMachineSpec to OpenStackServerSpec conversion with an additional security group", + name: "Test a OpenStackMachineSpec to OpenStackServerSpec conversion with an additional security group", + passWorkerSG: true, + passNetID: true, spec: &infrav1.OpenStackMachineSpec{ Flavor: ptr.To(flavorName), Image: image, @@ -138,13 +144,76 @@ func TestOpenStackMachineSpecToOpenStackServerSpec(t *testing.T) { UserDataRef: userData, }, }, + { + name: "HPC scenario with user-provided ports ignoring cluster network", + passWorkerSG: false, + passNetID: false, + spec: &infrav1.OpenStackMachineSpec{ + Flavor: ptr.To(flavorName), + Image: image, + SSHKeyName: sshKeyName, + Ports: []infrav1.PortOpts{ + { + Network: &infrav1.NetworkParam{ + ID: ptr.To(networkUUID), + }, + }, + }, + SecurityGroups: []infrav1.SecurityGroupParam{ + { + ID: ptr.To(extraSecurityGroupUUID), + }, + }, + }, + want: &infrav1alpha1.OpenStackServerSpec{ + Flavor: ptr.To(flavorName), + IdentityRef: identityRef, + Image: image, + SSHKeyName: sshKeyName, + Ports: []infrav1.PortOpts{ + { + Network: &infrav1.NetworkParam{ + ID: ptr.To(networkUUID), + }, + SecurityGroups: []infrav1.SecurityGroupParam{ + { + ID: ptr.To(extraSecurityGroupUUID), + }, + }, + }, + }, + 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) - if !reflect.DeepEqual(spec, tt.want) { - t.Errorf("openStackMachineSpecToOpenStackServerSpec() got = %+v, want %+v", spec, tt.want) + // Conditionally pass the cluster’s worker SG + var workerSGPtr *string + if tt.passWorkerSG { + workerSGPtr = &openStackCluster.Status.WorkerSecurityGroup.ID + } + + // Conditionally pass the cluster’s network ID + var netID string + if tt.passNetID && openStackCluster.Status.Network != nil { + netID = openStackCluster.Status.Network.ID + } + + got := openStackMachineSpecToOpenStackServerSpec( + tt.spec, + identityRef, + tags, + "", + userData, + workerSGPtr, + netID, + ) + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("openStackMachineSpecToOpenStackServerSpec() got = %+v, want %+v", got, tt.want) } }) }