@@ -784,7 +784,7 @@ var _ = Describe("ScalityUIComponent Controller", func() {
784784 })
785785
786786 Context ("Validation failure handling" , func () {
787- It ("should not retry fetch after validation failure for same image " , func () {
787+ It ("should retry fetch after validation failure to handle rolling update race conditions " , func () {
788788 ctx := context .Background ()
789789
790790 scalityUIComponent := & uiv1alpha1.ScalityUIComponent {
@@ -838,21 +838,100 @@ var _ = Describe("ScalityUIComponent Controller", func() {
838838 Expect (err ).NotTo (HaveOccurred ())
839839 Expect (mockFetcher .ReceivedCalls ).To (HaveLen (1 ), "Should fetch once" )
840840
841- By ("Verify LastFetchedImage is set despite validation failure" )
841+ By ("Verify LastFetchedImage is NOT set on validation failure" )
842842 Expect (k8sClient .Get (ctx , typeNamespacedName , scalityUIComponent )).To (Succeed ())
843- Expect (scalityUIComponent .Status .LastFetchedImage ).To (Equal ( "scality/invalid-config:v1.0.0" ))
843+ Expect (scalityUIComponent .Status .LastFetchedImage ).To (BeEmpty ( ))
844844
845- By ("Third reconcile - should NOT fetch again (same image )" )
845+ By ("Third reconcile - should retry fetch (lastFetchedImage not stamped )" )
846846 _ , err = controllerReconciler .Reconcile (ctx , reconcile.Request {NamespacedName : typeNamespacedName })
847847 Expect (err ).NotTo (HaveOccurred ())
848- Expect (mockFetcher .ReceivedCalls ).To (HaveLen (1 ), "Should still be 1 - no refetch " )
848+ Expect (mockFetcher .ReceivedCalls ).To (HaveLen (2 ), "Should retry fetch after validation failure " )
849849
850850 By ("Verify ConfigurationRetrieved condition is False" )
851+ Expect (k8sClient .Get (ctx , typeNamespacedName , scalityUIComponent )).To (Succeed ())
851852 condition := meta .FindStatusCondition (scalityUIComponent .Status .Conditions , "ConfigurationRetrieved" )
852853 Expect (condition ).NotTo (BeNil ())
853854 Expect (condition .Status ).To (Equal (metav1 .ConditionFalse ))
854855 Expect (condition .Reason ).To (Equal ("ValidationFailed" ))
855856 })
857+
858+ It ("should eventually succeed after validation failure when config becomes valid" , func () {
859+ ctx := context .Background ()
860+
861+ scalityUIComponent := & uiv1alpha1.ScalityUIComponent {
862+ ObjectMeta : metav1.ObjectMeta {
863+ Name : "test-validation-recovery" ,
864+ Namespace : "default" ,
865+ },
866+ Spec : uiv1alpha1.ScalityUIComponentSpec {
867+ Image : "scality/ui-component:v2.0.0" ,
868+ MountPath : "/usr/share/ui-config" ,
869+ },
870+ }
871+ Expect (k8sClient .Create (ctx , scalityUIComponent )).To (Succeed ())
872+
873+ mockFetcher := & MockConfigFetcher {
874+ ConfigContent : `{
875+ "metadata": {},
876+ "spec": {
877+ "publicPath": "/test/"
878+ }
879+ }` ,
880+ }
881+ controllerReconciler := & ScalityUIComponentReconciler {
882+ Client : k8sClient ,
883+ Scheme : k8sClient .Scheme (),
884+ ConfigFetcher : mockFetcher ,
885+ }
886+
887+ typeNamespacedName := types.NamespacedName {
888+ Name : "test-validation-recovery" ,
889+ Namespace : "default" ,
890+ }
891+
892+ By ("First reconcile - creates deployment" )
893+ _ , err := controllerReconciler .Reconcile (ctx , reconcile.Request {NamespacedName : typeNamespacedName })
894+ Expect (err ).NotTo (HaveOccurred ())
895+
896+ By ("Wait for deployment to be ready" )
897+ deployment := & appsv1.Deployment {}
898+ Eventually (func () error {
899+ return k8sClient .Get (ctx , typeNamespacedName , deployment )
900+ }, time .Second * 5 , time .Millisecond * 250 ).Should (Succeed ())
901+
902+ deployment .Status .ReadyReplicas = 1
903+ deployment .Status .Replicas = 1
904+ deployment .Status .UpdatedReplicas = 1
905+ Expect (k8sClient .Status ().Update (ctx , deployment )).To (Succeed ())
906+
907+ By ("Second reconcile - fetches config missing metadata.kind, fails validation" )
908+ _ , err = controllerReconciler .Reconcile (ctx , reconcile.Request {NamespacedName : typeNamespacedName })
909+ Expect (err ).NotTo (HaveOccurred ())
910+ Expect (mockFetcher .ReceivedCalls ).To (HaveLen (1 ))
911+
912+ By ("Simulate rolling update completing - new pod now returns valid config" )
913+ mockFetcher .ConfigContent = `{
914+ "metadata": {"kind": "TestKind"},
915+ "spec": {
916+ "publicPath": "/test/",
917+ "remoteEntryPath": "/remoteEntry.js",
918+ "version": "2.0.0"
919+ }
920+ }`
921+
922+ By ("Third reconcile - should retry and succeed with valid config" )
923+ _ , err = controllerReconciler .Reconcile (ctx , reconcile.Request {NamespacedName : typeNamespacedName })
924+ Expect (err ).NotTo (HaveOccurred ())
925+ Expect (mockFetcher .ReceivedCalls ).To (HaveLen (2 ))
926+
927+ By ("Verify ConfigurationRetrieved is now True" )
928+ Expect (k8sClient .Get (ctx , typeNamespacedName , scalityUIComponent )).To (Succeed ())
929+ condition := meta .FindStatusCondition (scalityUIComponent .Status .Conditions , "ConfigurationRetrieved" )
930+ Expect (condition ).NotTo (BeNil ())
931+ Expect (condition .Status ).To (Equal (metav1 .ConditionTrue ))
932+ Expect (scalityUIComponent .Status .PublicPath ).To (Equal ("/test/" ))
933+ Expect (scalityUIComponent .Status .LastFetchedImage ).To (Equal ("scality/ui-component:v2.0.0" ))
934+ })
856935 })
857936
858937 Context ("Rolling update handling" , func () {
0 commit comments