@@ -1070,6 +1070,188 @@ 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+ Clock : clock.RealClock {},
1119+ }).Reconcile (t .Context (), ctrl.Request {
1120+ NamespacedName : types.NamespacedName {Name : clusterObjectSetName },
1121+ })
1122+ require .NoError (t , err )
1123+ require .Equal (t , ctrl.Result {}, result )
1124+
1125+ rev := & ocv1.ClusterObjectSet {}
1126+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : clusterObjectSetName }, rev ))
1127+
1128+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
1129+ require .NotNil (t , cond )
1130+ require .Equal (t , metav1 .ConditionFalse , cond .Status )
1131+ require .Equal (t , ocv1 .ClusterObjectSetReasonArchived , cond .Reason )
1132+
1133+ cond = meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeAvailable )
1134+ require .NotNil (t , cond )
1135+ require .Equal (t , metav1 .ConditionUnknown , cond .Status )
1136+ require .Equal (t , ocv1 .ClusterObjectSetReasonArchived , cond .Reason )
1137+ }
1138+
1139+ // Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_StaysSticky verifies that
1140+ // once ProgressDeadlineExceeded is set, it is not overwritten when the reconciler runs again
1141+ // and sees objects still in transition. The Progressing condition should stay at
1142+ // ProgressDeadlineExceeded instead of being set back to RollingOut.
1143+ func Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_StaysSticky (t * testing.T ) {
1144+ const clusterObjectSetName = "test-ext-1"
1145+
1146+ testScheme := newScheme (t )
1147+ require .NoError (t , corev1 .AddToScheme (testScheme ))
1148+
1149+ ext := newTestClusterExtension ()
1150+ rev1 := newTestClusterObjectSet (t , clusterObjectSetName , ext , testScheme )
1151+ rev1 .Spec .ProgressDeadlineMinutes = 1
1152+ rev1 .CreationTimestamp = metav1 .NewTime (time .Now ().Add (- 5 * time .Minute ))
1153+ meta .SetStatusCondition (& rev1 .Status .Conditions , metav1.Condition {
1154+ Type : ocv1 .ClusterObjectSetTypeProgressing ,
1155+ Status : metav1 .ConditionFalse ,
1156+ Reason : ocv1 .ReasonProgressDeadlineExceeded ,
1157+ Message : "Revision has not rolled out for 1 minute(s)." ,
1158+ ObservedGeneration : rev1 .Generation ,
1159+ })
1160+
1161+ testClient := fake .NewClientBuilder ().
1162+ WithScheme (testScheme ).
1163+ WithStatusSubresource (& ocv1.ClusterObjectSet {}).
1164+ WithObjects (rev1 , ext ).
1165+ Build ()
1166+
1167+ mockEngine := & mockRevisionEngine {
1168+ reconcile : func (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionReconcileOption ) (machinery.RevisionResult , error ) {
1169+ return & mockRevisionResult {inTransition : true }, nil
1170+ },
1171+ }
1172+
1173+ result , err := (& controllers.ClusterObjectSetReconciler {
1174+ Client : testClient ,
1175+ RevisionEngineFactory : & mockRevisionEngineFactory {engine : mockEngine },
1176+ TrackingCache : & mockTrackingCache {client : testClient },
1177+ Clock : clock.RealClock {},
1178+ }).Reconcile (t .Context (), ctrl.Request {
1179+ NamespacedName : types.NamespacedName {Name : clusterObjectSetName },
1180+ })
1181+ require .NoError (t , err )
1182+ require .Equal (t , ctrl.Result {}, result )
1183+
1184+ rev := & ocv1.ClusterObjectSet {}
1185+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : clusterObjectSetName }, rev ))
1186+
1187+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
1188+ require .NotNil (t , cond )
1189+ require .Equal (t , metav1 .ConditionFalse , cond .Status , "Progressing should stay False" )
1190+ require .Equal (t , ocv1 .ReasonProgressDeadlineExceeded , cond .Reason , "Reason should stay ProgressDeadlineExceeded" )
1191+ }
1192+
1193+ // Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_SucceededOverrides verifies
1194+ // that if a revision's objects eventually complete rolling out after the deadline was exceeded,
1195+ // the COS can still transition to Succeeded. ProgressDeadlineExceeded should not prevent a
1196+ // revision from completing when its objects become ready.
1197+ func Test_ClusterObjectSetReconciler_Reconcile_ProgressDeadlineExceeded_SucceededOverrides (t * testing.T ) {
1198+ const clusterObjectSetName = "test-ext-1"
1199+
1200+ testScheme := newScheme (t )
1201+ require .NoError (t , corev1 .AddToScheme (testScheme ))
1202+
1203+ ext := newTestClusterExtension ()
1204+ rev1 := newTestClusterObjectSet (t , clusterObjectSetName , ext , testScheme )
1205+ rev1 .Spec .ProgressDeadlineMinutes = 1
1206+ rev1 .CreationTimestamp = metav1 .NewTime (time .Now ().Add (- 5 * time .Minute ))
1207+ meta .SetStatusCondition (& rev1 .Status .Conditions , metav1.Condition {
1208+ Type : ocv1 .ClusterObjectSetTypeProgressing ,
1209+ Status : metav1 .ConditionFalse ,
1210+ Reason : ocv1 .ReasonProgressDeadlineExceeded ,
1211+ Message : "Revision has not rolled out for 1 minute(s)." ,
1212+ ObservedGeneration : rev1 .Generation ,
1213+ })
1214+
1215+ testClient := fake .NewClientBuilder ().
1216+ WithScheme (testScheme ).
1217+ WithStatusSubresource (& ocv1.ClusterObjectSet {}).
1218+ WithObjects (rev1 , ext ).
1219+ Build ()
1220+
1221+ mockEngine := & mockRevisionEngine {
1222+ reconcile : func (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionReconcileOption ) (machinery.RevisionResult , error ) {
1223+ return & mockRevisionResult {isComplete : true }, nil
1224+ },
1225+ }
1226+
1227+ result , err := (& controllers.ClusterObjectSetReconciler {
1228+ Client : testClient ,
1229+ RevisionEngineFactory : & mockRevisionEngineFactory {engine : mockEngine },
1230+ TrackingCache : & mockTrackingCache {client : testClient },
1231+ Clock : clock.RealClock {},
1232+ }).Reconcile (t .Context (), ctrl.Request {
1233+ NamespacedName : types.NamespacedName {Name : clusterObjectSetName },
1234+ })
1235+ require .NoError (t , err )
1236+ require .Equal (t , ctrl.Result {}, result )
1237+
1238+ rev := & ocv1.ClusterObjectSet {}
1239+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : clusterObjectSetName }, rev ))
1240+
1241+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
1242+ require .NotNil (t , cond )
1243+ require .Equal (t , metav1 .ConditionTrue , cond .Status , "Progressing should transition to True" )
1244+ require .Equal (t , ocv1 .ReasonSucceeded , cond .Reason , "Reason should be Succeeded" )
1245+
1246+ cond = meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeAvailable )
1247+ require .NotNil (t , cond )
1248+ require .Equal (t , metav1 .ConditionTrue , cond .Status )
1249+
1250+ cond = meta .FindStatusCondition (rev .Status .Conditions , ocv1 .ClusterObjectSetTypeSucceeded )
1251+ require .NotNil (t , cond )
1252+ require .Equal (t , metav1 .ConditionTrue , cond .Status )
1253+ }
1254+
10731255func newTestClusterExtension () * ocv1.ClusterExtension {
10741256 return & ocv1.ClusterExtension {
10751257 ObjectMeta : metav1.ObjectMeta {
0 commit comments