@@ -1161,6 +1161,110 @@ var _ = Describe("OpenStackCluster controller", func() {
11611161 Expect (conditions .IsTrue (testCluster , infrav1 .NetworkReadyCondition )).To (BeTrue ())
11621162 })
11631163
1164+ It ("should clear ReadyCondition when security group reconciliation fails on a previously ready cluster" , func () {
1165+ const clusterNetworkID = "6c90b532-7ba0-418a-a276-5ae55060b5b0"
1166+ const clusterSubnetID = "cad5a91a-36de-4388-823b-b0cc82cadfdc"
1167+
1168+ testCluster .SetName ("sg-failure-previously-ready" )
1169+ testCluster .Spec = infrav1.OpenStackClusterSpec {
1170+ IdentityRef : infrav1.OpenStackIdentityReference {
1171+ Name : "test-creds" ,
1172+ CloudName : "openstack" ,
1173+ },
1174+ Network : & infrav1.NetworkParam {
1175+ ID : ptr .To (clusterNetworkID ),
1176+ },
1177+ DisableExternalNetwork : ptr .To (true ),
1178+ DisableAPIServerFloatingIP : ptr .To (true ),
1179+ APIServerFixedIP : ptr .To ("192.168.0.10" ),
1180+ ManagedSecurityGroups : & infrav1.ManagedSecurityGroups {
1181+ AllNodesSecurityGroupRules : []infrav1.SecurityGroupRuleSpec {
1182+ {
1183+ Direction : "ingress" ,
1184+ Protocol : ptr .To ("tcp" ),
1185+ RemoteManagedGroups : []infrav1.ManagedSecurityGroupName {
1186+ "worker" ,
1187+ },
1188+ },
1189+ },
1190+ },
1191+ }
1192+ err := k8sClient .Create (ctx , testCluster )
1193+ Expect (err ).To (BeNil ())
1194+ err = k8sClient .Create (ctx , capiCluster )
1195+ Expect (err ).To (BeNil ())
1196+
1197+ // Simulate a previously successful reconcile by pre-setting conditions to True.
1198+ // This is the scenario from https://github.com/kubernetes-sigs/cluster-api-provider-openstack/issues/2993
1199+ // where the cluster was already Ready and a subsequent security group update fails.
1200+ conditions .Set (testCluster , metav1.Condition {
1201+ Type : clusterv1 .ReadyCondition ,
1202+ Status : metav1 .ConditionTrue ,
1203+ Reason : infrav1 .ReadyConditionReason ,
1204+ })
1205+ conditions .Set (testCluster , metav1.Condition {
1206+ Type : infrav1 .SecurityGroupsReadyCondition ,
1207+ Status : metav1 .ConditionTrue ,
1208+ Reason : infrav1 .ReadyConditionReason ,
1209+ })
1210+ conditions .Set (testCluster , metav1.Condition {
1211+ Type : infrav1 .NetworkReadyCondition ,
1212+ Status : metav1 .ConditionTrue ,
1213+ Reason : infrav1 .ReadyConditionReason ,
1214+ })
1215+
1216+ // Verify preconditions: cluster appears Ready
1217+ Expect (conditions .IsTrue (testCluster , clusterv1 .ReadyCondition )).To (BeTrue ())
1218+ Expect (conditions .IsTrue (testCluster , infrav1 .SecurityGroupsReadyCondition )).To (BeTrue ())
1219+
1220+ log := GinkgoLogr
1221+ clientScope , err := mockScopeFactory .NewClientScopeFromObject (ctx , k8sClient , nil , log , testCluster )
1222+ Expect (err ).To (BeNil ())
1223+ scope := scope .NewWithLogger (clientScope , log )
1224+
1225+ networkClientRecorder := mockScopeFactory .NetworkClient .EXPECT ()
1226+
1227+ // Network lookup succeeds
1228+ networkClientRecorder .GetNetwork (clusterNetworkID ).Return (& networks.Network {
1229+ ID : clusterNetworkID ,
1230+ Name : "cluster-network" ,
1231+ }, nil )
1232+
1233+ // Subnet lookup succeeds
1234+ networkClientRecorder .ListSubnet (subnets.ListOpts {
1235+ NetworkID : clusterNetworkID ,
1236+ }).Return ([]subnets.Subnet {
1237+ {
1238+ ID : clusterSubnetID ,
1239+ Name : "cluster-subnet" ,
1240+ CIDR : "192.168.0.0/24" ,
1241+ },
1242+ }, nil )
1243+
1244+ // Security group reconciliation fails (e.g. rule conflict as reported in #2993)
1245+ networkClientRecorder .ListSecGroup (gomock .Any ()).Return ([]groups.SecGroup {}, nil ).AnyTimes ()
1246+ networkClientRecorder .CreateSecGroup (gomock .Any ()).Return (nil , fmt .Errorf ("SecurityGroupRuleExists" )).AnyTimes ()
1247+
1248+ err = reconcileNetworkComponents (scope , capiCluster , testCluster )
1249+ Expect (err ).ToNot (BeNil ())
1250+ Expect (err .Error ()).To (ContainSubstring ("failed to reconcile security groups" ))
1251+
1252+ // Verify ReadyCondition is now False (was True before)
1253+ Expect (conditions .IsFalse (testCluster , clusterv1 .ReadyCondition )).To (BeTrue ())
1254+ readyCondition := conditions .Get (testCluster , clusterv1 .ReadyCondition )
1255+ Expect (readyCondition ).ToNot (BeNil ())
1256+ Expect (readyCondition .Reason ).To (Equal (infrav1 .OpenStackErrorReason ))
1257+
1258+ // Verify SecurityGroupsReadyCondition is now False (was True before)
1259+ Expect (conditions .IsFalse (testCluster , infrav1 .SecurityGroupsReadyCondition )).To (BeTrue ())
1260+ sgCondition := conditions .Get (testCluster , infrav1 .SecurityGroupsReadyCondition )
1261+ Expect (sgCondition ).ToNot (BeNil ())
1262+ Expect (sgCondition .Reason ).To (Equal (infrav1 .SecurityGroupReconcileFailedReason ))
1263+
1264+ // NetworkReadyCondition should remain True since network reconciliation succeeded
1265+ Expect (conditions .IsTrue (testCluster , infrav1 .NetworkReadyCondition )).To (BeTrue ())
1266+ })
1267+
11641268 It ("should set APIEndpointReadyCondition to False when floating IP creation fails" , func () {
11651269 const externalNetworkID = "a42211a2-4d2c-426f-9413-830e4b4abbbc"
11661270 const clusterNetworkID = "6c90b532-7ba0-418a-a276-5ae55060b5b0"
0 commit comments