@@ -1070,6 +1070,185 @@ func Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadline(t *testing.T) {
10701070 }
10711071}
10721072
1073+ // Test_ClusterObjectSetReconciler_Reconcile_ArchivalAfterProgressDeadlineExceeded verifies that
1074+ // a COS with ProgressDeadlineExceeded can still be archived. It simulates the real scenario:
1075+ // the COS starts as Active with ProgressDeadlineExceeded, then a spec patch sets
1076+ // lifecycleState to Archived (as a succeeding revision would do), and a subsequent
1077+ // reconcile processes the archival.
1078+ func Test_ClusterObjectSetReconciler_Reconcile_ArchivalAfterProgressDeadlineExceeded (t * testing.T ) {
1079+ const clusterObjectSetName = "test-ext-1"
1080+
1081+ testScheme := newScheme (t )
1082+ require .NoError (t , corev1 .AddToScheme (testScheme ))
1083+
1084+ ext := newTestClusterExtension ()
1085+ rev1 := newTestClusterObjectSet (t , clusterObjectSetName , ext , testScheme )
1086+ rev1 .Finalizers = []string {"olm.operatorframework.io/teardown" }
1087+ meta .SetStatusCondition (& rev1 .Status .Conditions , metav1.Condition {
1088+ Type : ocv1 .ClusterObjectSetTypeProgressing ,
1089+ Status : metav1 .ConditionFalse ,
1090+ Reason : ocv1 .ReasonProgressDeadlineExceeded ,
1091+ Message : "Revision has not rolled out for 1 minute(s)." ,
1092+ ObservedGeneration : rev1 .Generation ,
1093+ })
1094+
1095+ testClient := fake .NewClientBuilder ().
1096+ WithScheme (testScheme ).
1097+ WithStatusSubresource (& ocv1.ClusterObjectSet {}).
1098+ WithObjects (rev1 , ext ).
1099+ Build ()
1100+
1101+ // Simulate the patch that a succeeding revision would apply.
1102+ patch := []byte (`{"spec":{"lifecycleState":"Archived"}}` )
1103+ require .NoError (t , testClient .Patch (t .Context (),
1104+ & ocv1.ClusterObjectSet {ObjectMeta : metav1.ObjectMeta {Name : clusterObjectSetName }},
1105+ client .RawPatch (types .MergePatchType , patch ),
1106+ ))
1107+
1108+ mockEngine := & mockRevisionEngine {
1109+ teardown : func (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionTeardownOption ) (machinery.RevisionTeardownResult , error ) {
1110+ return & mockRevisionTeardownResult {isComplete : true }, nil
1111+ },
1112+ }
1113+
1114+ result , err := (& controllers.ClusterObjectSetReconciler {
1115+ Client : testClient ,
1116+ RevisionEngineFactory : & mockRevisionEngineFactory {engine : mockEngine },
1117+ TrackingCache : & mockTrackingCache {client : testClient },
1118+ }).Reconcile (t .Context (), ctrl.Request {
1119+ NamespacedName : types.NamespacedName {Name : clusterObjectSetName },
1120+ })
1121+ require .NoError (t , err )
1122+ require .Equal (t , ctrl.Result {}, result )
1123+
1124+ rev := & ocv1.ClusterObjectSet {}
1125+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : clusterObjectSetName }, rev ))
1126+
1127+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
1128+ require .NotNil (t , cond )
1129+ require .Equal (t , metav1 .ConditionFalse , cond .Status )
1130+ require .Equal (t , ocv1 .ClusterObjectSetReasonArchived , cond .Reason )
1131+
1132+ cond = meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeAvailable )
1133+ require .NotNil (t , cond )
1134+ require .Equal (t , metav1 .ConditionUnknown , cond .Status )
1135+ require .Equal (t , ocv1 .ClusterObjectSetReasonArchived , cond .Reason )
1136+ }
1137+
1138+ // Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_StaysSticky verifies that
1139+ // once ProgressDeadlineExceeded is set, it is not overwritten when the reconciler runs again
1140+ // and sees objects still in transition. The Progressing condition should stay at
1141+ // ProgressDeadlineExceeded instead of being set back to RollingOut.
1142+ func Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_StaysSticky (t * testing.T ) {
1143+ const clusterObjectSetName = "test-ext-1"
1144+
1145+ testScheme := newScheme (t )
1146+ require .NoError (t , corev1 .AddToScheme (testScheme ))
1147+
1148+ ext := newTestClusterExtension ()
1149+ rev1 := newTestClusterObjectSet (t , clusterObjectSetName , ext , testScheme )
1150+ rev1 .Spec .ProgressDeadlineMinutes = 1
1151+ rev1 .CreationTimestamp = metav1 .NewTime (time .Now ().Add (- 5 * time .Minute ))
1152+ meta .SetStatusCondition (& rev1 .Status .Conditions , metav1.Condition {
1153+ Type : ocv1 .ClusterObjectSetTypeProgressing ,
1154+ Status : metav1 .ConditionFalse ,
1155+ Reason : ocv1 .ReasonProgressDeadlineExceeded ,
1156+ Message : "Revision has not rolled out for 1 minute(s)." ,
1157+ ObservedGeneration : rev1 .Generation ,
1158+ })
1159+
1160+ testClient := fake .NewClientBuilder ().
1161+ WithScheme (testScheme ).
1162+ WithStatusSubresource (& ocv1.ClusterObjectSet {}).
1163+ WithObjects (rev1 , ext ).
1164+ Build ()
1165+
1166+ mockEngine := & mockRevisionEngine {
1167+ reconcile : func (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionReconcileOption ) (machinery.RevisionResult , error ) {
1168+ return & mockRevisionResult {inTransition : true }, nil
1169+ },
1170+ }
1171+
1172+ result , err := (& controllers.ClusterObjectSetReconciler {
1173+ Client : testClient ,
1174+ RevisionEngineFactory : & mockRevisionEngineFactory {engine : mockEngine },
1175+ TrackingCache : & mockTrackingCache {client : testClient },
1176+ }).Reconcile (t .Context (), ctrl.Request {
1177+ NamespacedName : types.NamespacedName {Name : clusterObjectSetName },
1178+ })
1179+ require .NoError (t , err )
1180+ require .Equal (t , ctrl.Result {}, result )
1181+
1182+ rev := & ocv1.ClusterObjectSet {}
1183+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : clusterObjectSetName }, rev ))
1184+
1185+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
1186+ require .NotNil (t , cond )
1187+ require .Equal (t , metav1 .ConditionFalse , cond .Status , "Progressing should stay False" )
1188+ require .Equal (t , ocv1 .ReasonProgressDeadlineExceeded , cond .Reason , "Reason should stay ProgressDeadlineExceeded" )
1189+ }
1190+
1191+ // Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_SucceededOverrides verifies
1192+ // that if a revision's objects eventually complete rolling out after the deadline was exceeded,
1193+ // the COS can still transition to Succeeded. ProgressDeadlineExceeded should not prevent a
1194+ // revision from completing when its objects become ready.
1195+ func Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_SucceededOverrides (t * testing.T ) {
1196+ const clusterObjectSetName = "test-ext-1"
1197+
1198+ testScheme := newScheme (t )
1199+ require .NoError (t , corev1 .AddToScheme (testScheme ))
1200+
1201+ ext := newTestClusterExtension ()
1202+ rev1 := newTestClusterObjectSet (t , clusterObjectSetName , ext , testScheme )
1203+ rev1 .Spec .ProgressDeadlineMinutes = 1
1204+ rev1 .CreationTimestamp = metav1 .NewTime (time .Now ().Add (- 5 * time .Minute ))
1205+ meta .SetStatusCondition (& rev1 .Status .Conditions , metav1.Condition {
1206+ Type : ocv1 .ClusterObjectSetTypeProgressing ,
1207+ Status : metav1 .ConditionFalse ,
1208+ Reason : ocv1 .ReasonProgressDeadlineExceeded ,
1209+ Message : "Revision has not rolled out for 1 minute(s)." ,
1210+ ObservedGeneration : rev1 .Generation ,
1211+ })
1212+
1213+ testClient := fake .NewClientBuilder ().
1214+ WithScheme (testScheme ).
1215+ WithStatusSubresource (& ocv1.ClusterObjectSet {}).
1216+ WithObjects (rev1 , ext ).
1217+ Build ()
1218+
1219+ mockEngine := & mockRevisionEngine {
1220+ reconcile : func (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionReconcileOption ) (machinery.RevisionResult , error ) {
1221+ return & mockRevisionResult {isComplete : true }, nil
1222+ },
1223+ }
1224+
1225+ result , err := (& controllers.ClusterObjectSetReconciler {
1226+ Client : testClient ,
1227+ RevisionEngineFactory : & mockRevisionEngineFactory {engine : mockEngine },
1228+ TrackingCache : & mockTrackingCache {client : testClient },
1229+ }).Reconcile (t .Context (), ctrl.Request {
1230+ NamespacedName : types.NamespacedName {Name : clusterObjectSetName },
1231+ })
1232+ require .NoError (t , err )
1233+ require .Equal (t , ctrl.Result {}, result )
1234+
1235+ rev := & ocv1.ClusterObjectSet {}
1236+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : clusterObjectSetName }, rev ))
1237+
1238+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
1239+ require .NotNil (t , cond )
1240+ require .Equal (t , metav1 .ConditionTrue , cond .Status , "Progressing should transition to True" )
1241+ require .Equal (t , ocv1 .ReasonSucceeded , cond .Reason , "Reason should be Succeeded" )
1242+
1243+ cond = meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeAvailable )
1244+ require .NotNil (t , cond )
1245+ require .Equal (t , metav1 .ConditionTrue , cond .Status )
1246+
1247+ cond = meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeSucceeded )
1248+ require .NotNil (t , cond )
1249+ require .Equal (t , metav1 .ConditionTrue , cond .Status )
1250+ }
1251+
10731252func newTestClusterExtension () * ocv1.ClusterExtension {
10741253 return & ocv1.ClusterExtension {
10751254 ObjectMeta : metav1.ObjectMeta {
0 commit comments