@@ -686,6 +686,345 @@ var _ = Describe("OpenStackOperator controller", func() {
686686
687687 })
688688
689+ // Test that minor updates don't hang when OVN is disabled
690+ When ("Minor update with OVN disabled" , func () {
691+ var (
692+ initialVersion = "old"
693+ updatedVersion = "0.0.1"
694+ targetRabbitMQVersion = ""
695+ targetMariaDBVersion = ""
696+ targetMemcachedVersion = ""
697+ targetKeystoneAPIVersion = ""
698+ testRabbitMQImage = "foo/rabbit:0.0.2"
699+ testMariaDBImage = "foo/maria:0.0.2"
700+ testMemcachedImage = "foo/memcached:0.0.2"
701+ testKeystoneAPIImage = "foo/keystone:0.0.2"
702+ )
703+
704+ BeforeEach (func () {
705+ // Lightweight controlplane spec with OVN DISABLED
706+ spec := GetDefaultOpenStackControlPlaneSpec ()
707+
708+ // a single galera database
709+ galeraTemplate := map [string ]interface {}{
710+ names .DBName .Name : map [string ]interface {}{
711+ "storageRequest" : "500M" ,
712+ },
713+ }
714+ spec ["galera" ] = map [string ]interface {}{
715+ "enabled" : true ,
716+ "templates" : galeraTemplate ,
717+ }
718+
719+ // Disable non-essential services
720+ spec ["horizon" ] = map [string ]interface {}{"enabled" : false }
721+ spec ["glance" ] = map [string ]interface {}{"enabled" : false }
722+ spec ["cinder" ] = map [string ]interface {}{"enabled" : false }
723+ spec ["neutron" ] = map [string ]interface {}{"enabled" : false }
724+ spec ["manila" ] = map [string ]interface {}{"enabled" : false }
725+ spec ["heat" ] = map [string ]interface {}{"enabled" : false }
726+ spec ["telemetry" ] = map [string ]interface {}{"enabled" : false }
727+ spec ["tls" ] = GetTLSPublicSpec ()
728+
729+ // CRITICAL: Disable OVN
730+ spec ["ovn" ] = map [string ]interface {}{
731+ "enabled" : false ,
732+ }
733+
734+ DeferCleanup (
735+ th .DeleteInstance ,
736+ CreateOpenStackVersion (names .OpenStackVersionName , GetDefaultOpenStackVersionSpec ()),
737+ )
738+
739+ // create cert secrets for rabbitmq instances
740+ DeferCleanup (k8sClient .Delete , ctx , th .CreateCertSecret (names .RabbitMQCertName ))
741+ DeferCleanup (k8sClient .Delete , ctx , th .CreateCertSecret (names .RabbitMQCell1CertName ))
742+
743+ // create root CA secrets
744+ DeferCleanup (k8sClient .Delete , ctx , CreateCertSecret (names .RootCAPublicName ))
745+ DeferCleanup (k8sClient .Delete , ctx , CreateCertSecret (names .RootCAInternalName ))
746+ DeferCleanup (k8sClient .Delete , ctx , CreateCertSecret (names .RootCAOvnName ))
747+ DeferCleanup (k8sClient .Delete , ctx , CreateCertSecret (names .RootCALibvirtName ))
748+
749+ // create cert secrets for galera instances
750+ DeferCleanup (k8sClient .Delete , ctx , th .CreateCertSecret (names .DBCertName ))
751+ DeferCleanup (k8sClient .Delete , ctx , th .CreateCertSecret (names .DBCell1CertName ))
752+
753+ // create cert secrets for memcached instance
754+ DeferCleanup (k8sClient .Delete , ctx , th .CreateCertSecret (names .MemcachedCertName ))
755+
756+ // wait for initial version to be created (this gives us version 0.0.1)
757+ Eventually (func (g Gomega ) {
758+ th .ExpectCondition (
759+ names .OpenStackVersionName ,
760+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
761+ corev1 .OpenStackVersionInitialized ,
762+ k8s_corev1 .ConditionTrue ,
763+ )
764+
765+ version := GetOpenStackVersion (names .OpenStackVersionName )
766+ // capture target versions
767+ targetRabbitMQVersion = * version .Status .ContainerImages .RabbitmqImage
768+ targetMariaDBVersion = * version .Status .ContainerImages .MariadbImage
769+ targetMemcachedVersion = * version .Status .ContainerImages .InfraMemcachedImage
770+ targetKeystoneAPIVersion = * version .Status .ContainerImages .KeystoneAPIImage
771+ g .Expect (version ).Should (Not (BeNil ()))
772+
773+ g .Expect (* version .Status .AvailableVersion ).Should (ContainSubstring ("0.0.1" ))
774+ g .Expect (version .Spec .TargetVersion ).Should (ContainSubstring ("0.0.1" ))
775+ updatedVersion = * version .Status .AvailableVersion
776+ }, timeout , interval ).Should (Succeed ())
777+
778+ // inject an "old" version
779+ Eventually (func (g Gomega ) {
780+ version := GetOpenStackVersion (names .OpenStackVersionName )
781+ version .Status .ContainerImageVersionDefaults [initialVersion ] = version .Status .ContainerImageVersionDefaults [updatedVersion ]
782+ version .Status .ContainerImageVersionDefaults [initialVersion ].RabbitmqImage = & testRabbitMQImage
783+ version .Status .ContainerImageVersionDefaults [initialVersion ].MariadbImage = & testMariaDBImage
784+ version .Status .ContainerImageVersionDefaults [initialVersion ].InfraMemcachedImage = & testMemcachedImage
785+ version .Status .ContainerImageVersionDefaults [initialVersion ].KeystoneAPIImage = & testKeystoneAPIImage
786+ g .Expect (th .K8sClient .Status ().Update (th .Ctx , version )).To (Succeed ())
787+ }, timeout , interval ).Should (Succeed ())
788+
789+ Eventually (func (g Gomega ) {
790+ version := GetOpenStackVersion (names .OpenStackVersionName )
791+ version .Spec .TargetVersion = initialVersion
792+ g .Expect (th .K8sClient .Update (th .Ctx , version )).To (Succeed ())
793+ }, timeout , interval ).Should (Succeed ())
794+
795+ Eventually (func (g Gomega ) {
796+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
797+ g .Expect (osversion ).Should (Not (BeNil ()))
798+ g .Expect (osversion .Generation ).Should (Equal (osversion .Status .ObservedGeneration ))
799+
800+ th .ExpectCondition (
801+ names .OpenStackVersionName ,
802+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
803+ corev1 .OpenStackVersionInitialized ,
804+ k8s_corev1 .ConditionTrue ,
805+ )
806+
807+ g .Expect (* osversion .Status .AvailableVersion ).Should (Equal (updatedVersion ))
808+ g .Expect (osversion .Spec .TargetVersion ).Should (Equal (initialVersion ))
809+ g .Expect (osversion .Status .DeployedVersion ).Should (BeNil ())
810+ }, timeout , interval ).Should (Succeed ())
811+
812+ DeferCleanup (
813+ th .DeleteInstance ,
814+ CreateOpenStackControlPlane (names .OpenStackControlplaneName , spec ),
815+ )
816+
817+ DeferCleanup (
818+ th .DeleteInstance ,
819+ CreateDataplaneNodeSet (names .OpenStackVersionName , DefaultDataPlaneNoNodeSetSpec (false )),
820+ )
821+
822+ dataplanenodeset := GetDataplaneNodeset (names .OpenStackVersionName )
823+ dataplanenodeset .Status .DeployedVersion = initialVersion
824+ Expect (th .K8sClient .Status ().Update (th .Ctx , dataplanenodeset )).To (Succeed ())
825+
826+ th .CreateSecret (types.NamespacedName {Name : "openstack-config-secret" , Namespace : namespace }, map [string ][]byte {"secure.yaml" : []byte ("foo" )})
827+ th .CreateConfigMap (types.NamespacedName {Name : "openstack-config" , Namespace : namespace }, map [string ]interface {}{"clouds.yaml" : string ("foo" ), "OS_CLOUD" : "default" })
828+
829+ // Verify OVN is disabled
830+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
831+ Expect (OSCtlplane .Spec .Ovn .Enabled ).Should (BeFalse ())
832+
833+ SimulateControlplaneReady ()
834+
835+ // verify that DeployedVersion is set
836+ Eventually (func (g Gomega ) {
837+ th .ExpectCondition (
838+ names .OpenStackControlplaneName ,
839+ ConditionGetterFunc (OpenStackControlPlaneConditionGetter ),
840+ condition .ReadyCondition ,
841+ k8s_corev1 .ConditionTrue ,
842+ )
843+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
844+ g .Expect (OSCtlplane .Status .DeployedVersion ).Should (Equal (& initialVersion ))
845+ }, timeout , interval ).Should (Succeed ())
846+
847+ // verify DeployedVersion also gets set on the OpenStackVersion resource
848+ Eventually (func (g Gomega ) {
849+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
850+ g .Expect (osversion ).Should (Not (BeNil ()))
851+ g .Expect (osversion .Generation ).Should (Equal (osversion .Status .ObservedGeneration ))
852+ g .Expect (osversion .Status .DeployedVersion ).Should (Equal (& initialVersion ))
853+ }, timeout , interval ).Should (Succeed ())
854+ })
855+
856+ It ("updating targetVersion should not hang on OVN checks" , Serial , func () {
857+ // Trigger minor update by switching to updated version
858+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
859+
860+ // should have a condition which reflects an update is available
861+ th .ExpectCondition (
862+ names .OpenStackVersionName ,
863+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
864+ corev1 .OpenStackVersionMinorUpdateAvailable ,
865+ k8s_corev1 .ConditionTrue ,
866+ )
867+
868+ osversion .Spec .TargetVersion = updatedVersion
869+ Expect (k8sClient .Update (ctx , osversion )).Should (Succeed ())
870+
871+ // Verify the OpenStackVersion gets re-initialized with new version
872+ Eventually (func (g Gomega ) {
873+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
874+ g .Expect (osversion ).Should (Not (BeNil ()))
875+ g .Expect (osversion .Generation ).Should (Equal (osversion .Status .ObservedGeneration ))
876+
877+ th .ExpectCondition (
878+ names .OpenStackVersionName ,
879+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
880+ corev1 .OpenStackVersionInitialized ,
881+ k8s_corev1 .ConditionTrue ,
882+ )
883+
884+ g .Expect (* osversion .Status .AvailableVersion ).Should (Equal (updatedVersion ))
885+ g .Expect (osversion .Spec .TargetVersion ).Should (Equal (updatedVersion ))
886+ // target images should be set
887+ g .Expect (* osversion .Status .ContainerImages .RabbitmqImage ).Should (Equal (targetRabbitMQVersion ))
888+ g .Expect (* osversion .Status .ContainerImages .MariadbImage ).Should (Equal (targetMariaDBVersion ))
889+ g .Expect (* osversion .Status .ContainerImages .InfraMemcachedImage ).Should (Equal (targetMemcachedVersion ))
890+ g .Expect (* osversion .Status .ContainerImages .KeystoneAPIImage ).Should (Equal (targetKeystoneAPIVersion ))
891+ }, timeout , interval ).Should (Succeed ())
892+
893+ // CRITICAL: Verify that OVN controlplane update condition is immediately set to true (not hanging)
894+ // This is the key assertion that proves the bug is fixed
895+ Eventually (func (g Gomega ) {
896+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
897+ g .Expect (osversion ).Should (Not (BeNil ()))
898+
899+ // The OVN update condition should be true because OVN is disabled
900+ th .ExpectCondition (
901+ names .OpenStackVersionName ,
902+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
903+ corev1 .OpenStackVersionMinorUpdateOVNControlplane ,
904+ k8s_corev1 .ConditionTrue ,
905+ )
906+ }, timeout , interval ).Should (Succeed ())
907+
908+ // Verify OVN dataplane update also proceeds
909+ Eventually (func (_ Gomega ) {
910+ th .ExpectCondition (
911+ names .OpenStackVersionName ,
912+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
913+ corev1 .OpenStackVersionMinorUpdateOVNDataplane ,
914+ k8s_corev1 .ConditionTrue ,
915+ )
916+ }, timeout , interval ).Should (Succeed ())
917+
918+ // Continue with infrastructure services
919+ th .ExpectCondition (
920+ names .OpenStackVersionName ,
921+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
922+ corev1 .OpenStackVersionMinorUpdateRabbitMQ ,
923+ k8s_corev1 .ConditionFalse ,
924+ )
925+
926+ SimulateRabbitmqReady ()
927+ Eventually (func (g Gomega ) {
928+ th .ExpectCondition (
929+ names .OpenStackVersionName ,
930+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
931+ corev1 .OpenStackVersionMinorUpdateRabbitMQ ,
932+ k8s_corev1 .ConditionTrue ,
933+ )
934+
935+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
936+ g .Expect (* OSCtlplane .Status .ContainerImages .RabbitmqImage ).Should (Equal (targetRabbitMQVersion ))
937+ }, timeout * 4 , interval ).Should (Succeed ())
938+
939+ SimulateGalaraReady ()
940+ Eventually (func (g Gomega ) {
941+ th .ExpectCondition (
942+ names .OpenStackVersionName ,
943+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
944+ corev1 .OpenStackVersionMinorUpdateMariaDB ,
945+ k8s_corev1 .ConditionTrue ,
946+ )
947+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
948+ g .Expect (* OSCtlplane .Status .ContainerImages .MariadbImage ).Should (Equal (targetMariaDBVersion ))
949+ }, timeout , interval ).Should (Succeed ())
950+
951+ SimulateMemcachedReady ()
952+ Eventually (func (g Gomega ) {
953+ th .ExpectCondition (
954+ names .OpenStackVersionName ,
955+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
956+ corev1 .OpenStackVersionMinorUpdateMemcached ,
957+ k8s_corev1 .ConditionTrue ,
958+ )
959+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
960+ g .Expect (* OSCtlplane .Status .ContainerImages .InfraMemcachedImage ).Should (Equal (targetMemcachedVersion ))
961+ }, timeout , interval ).Should (Succeed ())
962+
963+ keystone .SimulateKeystoneAPIReady (names .KeystoneAPIName )
964+ Eventually (func (g Gomega ) {
965+ th .ExpectCondition (
966+ names .OpenStackVersionName ,
967+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
968+ corev1 .OpenStackVersionMinorUpdateKeystone ,
969+ k8s_corev1 .ConditionTrue ,
970+ )
971+
972+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
973+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
974+ g .Expect (OSCtlplane .Status .ContainerImages .KeystoneAPIImage ).Should (Equal (osversion .Status .ContainerImages .KeystoneAPIImage ))
975+ }, timeout , interval ).Should (Succeed ())
976+
977+ // Simulate controlplane ready
978+ SimulateControlplaneReady ()
979+ Eventually (func (g Gomega ) {
980+ th .ExpectCondition (
981+ names .OpenStackVersionName ,
982+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
983+ corev1 .OpenStackVersionMinorUpdateControlplane ,
984+ k8s_corev1 .ConditionTrue ,
985+ )
986+ th .ExpectCondition (
987+ names .OpenStackControlplaneName ,
988+ ConditionGetterFunc (OpenStackControlPlaneConditionGetter ),
989+ condition .ReadyCondition ,
990+ k8s_corev1 .ConditionTrue ,
991+ )
992+
993+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
994+ OSCtlplane := GetOpenStackControlPlane (names .OpenStackControlplaneName )
995+ // verify images match
996+ g .Expect (OSCtlplane .Status .ContainerImages .RabbitmqImage ).Should (Equal (osversion .Status .ContainerImages .RabbitmqImage ))
997+ g .Expect (OSCtlplane .Status .ContainerImages .MariadbImage ).Should (Equal (osversion .Status .ContainerImages .MariadbImage ))
998+ g .Expect (OSCtlplane .Status .ContainerImages .KeystoneAPIImage ).Should (Equal (osversion .Status .ContainerImages .KeystoneAPIImage ))
999+ g .Expect (OSCtlplane .Status .ContainerImages .InfraMemcachedImage ).Should (Equal (osversion .Status .ContainerImages .InfraMemcachedImage ))
1000+ }, timeout , interval ).Should (Succeed ())
1001+
1002+ // Simulate dataplane deployment complete
1003+ dataplanenodeset := GetDataplaneNodeset (names .OpenStackVersionName )
1004+ dataplanenodeset .Status .ObservedGeneration = dataplanenodeset .Generation
1005+ dataplanenodeset .Status .DeployedVersion = osversion .Spec .TargetVersion
1006+ dataplanenodeset .Status .Conditions .MarkTrue (condition .ReadyCondition , dataplanev1 .NodeSetReadyMessage )
1007+ Expect (th .K8sClient .Status ().Update (th .Ctx , dataplanenodeset )).To (Succeed ())
1008+
1009+ // Verify minor update completes successfully
1010+ Eventually (func (g Gomega ) {
1011+ osversion := GetOpenStackVersion (names .OpenStackVersionName )
1012+ g .Expect (osversion ).Should (Not (BeNil ()))
1013+ g .Expect (osversion .OwnerReferences ).Should (HaveLen (1 ))
1014+ th .ExpectCondition (
1015+ names .OpenStackVersionName ,
1016+ ConditionGetterFunc (OpenStackVersionConditionGetter ),
1017+ condition .ReadyCondition ,
1018+ k8s_corev1 .ConditionTrue ,
1019+ )
1020+ g .Expect (osversion .Status .DeployedVersion ).Should (Equal (& updatedVersion ))
1021+ // no condition which reflects an update is available
1022+ g .Expect (osversion .Status .Conditions .Has (corev1 .OpenStackVersionMinorUpdateAvailable )).To (BeFalse ())
1023+ }, timeout , interval ).Should (Succeed ())
1024+ })
1025+
1026+ })
1027+
6891028 When ("CustomContainerImages are set" , func () {
6901029 var (
6911030 initialVersion = "0.0.1"
0 commit comments