@@ -27,9 +27,10 @@ import (
2727)
2828
2929const (
30- runcRHCOS10GuardPool = "runc-rhcos10-guard"
31- streamRHEL9 = "rhel-9"
32- streamRHEL10 = "rhel-10"
30+ runcRHCOS10GuardPool = "runc-rhcos10-guard"
31+ crunRHCOS10UpgradePool = "crun-rhcos10-upgrade"
32+ streamRHEL9 = "rhel-9"
33+ streamRHEL10 = "rhel-10"
3334
3435 runcGuardCRCName = "99-runc-rhcos10-guard-runc"
3536 runcCRCDefaultRuntimePath = "/etc/crio/crio.conf.d/01-ctrcfg-defaultRuntime"
@@ -42,20 +43,26 @@ const (
4243
4344var rhelMajorOSImagePattern = regexp .MustCompile (`Linux\s+([0-9]+)` )
4445
45- // When a pool uses runc and targets osImageStream rhel-10, MCO must block RHCOS 9→10
46- // rollout by setting MachineConfigPool Degraded / RenderDegraded. MCO then sets
47- // ClusterOperator Upgradeable=False (DegradedPool), which CVO aggregates on ClusterVersion.
46+ // When a pool targets osImageStream rhel-10, MCO behavior depends on runtime:
47+ // - runc → RenderDegraded blocks rollout; Upgradeable=False (DegradedPool)
48+ // - crun → rollout succeeds to RHCOS 10 without guard errors
4849var _ = g .Describe ("[Suite:openshift/disruptive-longrunning][sig-node][Serial][Disruptive][OCPFeatureGate:OSStreams] runc RHCOS 10 upgrade guard" , func () {
4950 defer g .GinkgoRecover ()
5051
5152 var (
5253 oc = exutil .NewCLI ("runc-rhcos10-guard" )
5354 mcClient * machineconfigclient.Clientset
5455 nodeName string
56+ testPoolName string
57+ cleanupCRC bool
5558 clusterDefaultStream string
5659 )
5760
5861 g .BeforeEach (func (ctx context.Context ) {
62+ nodeName = ""
63+ testPoolName = ""
64+ cleanupCRC = false
65+
5966 var err error
6067 mcClient , err = machineconfigclient .NewForConfig (oc .AdminConfig ())
6168 o .Expect (err ).NotTo (o .HaveOccurred ())
@@ -80,6 +87,9 @@ var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Serial][D
8087 })
8188
8289 g .It ("blocks RHCOS 9 to 10 osImageStream upgrade when ContainerRuntimeConfig sets runc default runtime" , ote .Informing (), func (ctx context.Context ) {
90+ testPoolName = runcRHCOS10GuardPool
91+ cleanupCRC = true
92+
8393 g .By ("Creating custom MachineConfigPool pinned to rhel-9 with runc ContainerRuntimeConfig" )
8494 o .Expect (createRuncGuardPool (ctx , mcClient )).To (o .Succeed ())
8595
@@ -93,7 +103,7 @@ var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Serial][D
93103 "node did not join custom MCP" )
94104
95105 g .By ("Checking default runtime is runc on RHCOS 9" )
96- o .Expect (expectRuncRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed ())
106+ o .Expect (waitForRuncRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed ())
97107 rhelMajor , err := nodeRHELMajorVersion (ctx , oc , nodeName )
98108 o .Expect (err ).NotTo (o .HaveOccurred ())
99109 o .Expect (rhelMajor ).To (o .Equal ("9" ), "pool should be on RHCOS 9 before attempting rhel-10 stream" )
@@ -106,11 +116,11 @@ var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Serial][D
106116 o .Expect (waitForUpgradeBlockedByDegradedPool (ctx , oc )).To (o .Succeed ())
107117
108118 g .By ("Verifying node remains ready, not rolling out, on RHCOS 9 with runc after guard blocks rollout" )
109- o .Expect (verifyNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
119+ o .Expect (assertNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
110120 rhelMajor , err = nodeRHELMajorVersion (ctx , oc , nodeName )
111121 o .Expect (err ).NotTo (o .HaveOccurred ())
112122 o .Expect (rhelMajor ).To (o .Equal ("9" ), "node should remain on RHCOS 9 after guard blocks rollout" )
113- o .Expect (expectRuncRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
123+ o .Expect (waitForRuncRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
114124 "node should keep runc as default runtime after guard blocks rollout" )
115125
116126 g .By ("Recovering pool by setting osImageStream back to rhel-9" )
@@ -122,11 +132,11 @@ var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Serial][D
122132 o .Expect (waitForClusterUpgradeable (ctx , oc , 30 * time .Minute )).To (o .Succeed ())
123133
124134 g .By ("Verifying node remains ready, not rolling out, on RHCOS 9 with runc after recovery" )
125- o .Expect (verifyNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
135+ o .Expect (assertNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
126136 rhelMajor , err = nodeRHELMajorVersion (ctx , oc , nodeName )
127137 o .Expect (err ).NotTo (o .HaveOccurred ())
128138 o .Expect (rhelMajor ).To (o .Equal ("9" ), "node should remain on RHCOS 9 after recovery" )
129- o .Expect (expectRuncRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
139+ o .Expect (waitForRuncRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
130140 "node should keep runc as default runtime after recovery" )
131141
132142 if clusterDefaultStream == streamRHEL10 {
@@ -149,32 +159,75 @@ var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Serial][D
149159 o .Expect (WaitForMCP (ctx , mcClient , runcRHCOS10GuardPool , 30 * time .Minute )).To (o .Succeed ())
150160
151161 g .By ("Verifying node rolled out to RHCOS 10 with crun" )
152- o .Expect (verifyNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
153- o .Expect (assertCrunRuntimeOnNode (ctx , oc , nodeName )).To (o .Succeed (), "node should use crun after runc config is removed" )
162+ o .Expect (assertNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
163+ o .Expect (waitForCrunRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
164+ "node should use crun after runc config is removed" )
154165 }
155166 })
156167
168+ g .It ("allows RHCOS 9 to 10 osImageStream upgrade when default runtime is crun" , ote .Informing (), func (ctx context.Context ) {
169+ testPoolName = crunRHCOS10UpgradePool
170+
171+ g .By ("Creating custom MachineConfigPool pinned to rhel-9 with default runtime crun" )
172+ o .Expect (createCrunUpgradeMCP (ctx , mcClient )).To (o .Succeed ())
173+
174+ g .By ("Labeling one worker into the custom pool" )
175+ var err error
176+ nodeName , err = labelFirstPureWorker (ctx , oc , crunRHCOS10UpgradePool )
177+ o .Expect (err ).NotTo (o .HaveOccurred (), "need a worker node for the custom pool" )
178+
179+ g .By ("Waiting for pool rollout on rhel-9 with crun default runtime" )
180+ o .Expect (waitForMCPWithLabeledNode (ctx , oc , mcClient , crunRHCOS10UpgradePool , nodeName , 30 * time .Minute )).To (o .Succeed (),
181+ "node did not join custom MCP" )
182+
183+ g .By ("Checking node is on RHCOS 9 with crun default runtime" )
184+ rhelMajor , err := nodeRHELMajorVersion (ctx , oc , nodeName )
185+ o .Expect (err ).NotTo (o .HaveOccurred ())
186+ o .Expect (rhelMajor ).To (o .Equal ("9" ), "pool should be on RHCOS 9 before attempting rhel-10 stream" )
187+ o .Expect (waitForCrunRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
188+ "node should use crun as default runtime on RHCOS 9" )
189+
190+ g .By ("Upgrading RHCOS version to RHCOS 10 via osImageStream" )
191+ o .Expect (setPoolOSImageStream (ctx , mcClient , crunRHCOS10UpgradePool , streamRHEL10 )).To (o .Succeed ())
192+ o .Expect (WaitForMCP (ctx , mcClient , crunRHCOS10UpgradePool , 30 * time .Minute )).To (o .Succeed (),
193+ "pool should roll out to rhel-10 without runc guard RenderDegraded" )
194+
195+ g .By ("Verifying node rolled out to RHCOS 10 with crun and pool stayed healthy" )
196+ o .Expect (waitForNodeRHELMajorVersion (ctx , oc , nodeName , "10" , 30 * time .Minute )).To (o .Succeed (),
197+ "node should reboot onto RHCOS 10 after rhel-10 stream change with crun" )
198+ o .Expect (assertNodeReadyAndNotRollingOut (ctx , oc , nodeName )).To (o .Succeed ())
199+ o .Expect (waitForCrunRuntimeOnNode (ctx , oc , nodeName , 2 * time .Minute )).To (o .Succeed (),
200+ "node should keep crun as default runtime on RHCOS 10" )
201+ o .Expect (assertPoolNotRenderDegraded (ctx , mcClient , crunRHCOS10UpgradePool )).To (o .Succeed ())
202+ o .Expect (assertMachineConfigNotBlockedByDegradedPool (ctx , oc )).To (o .Succeed (),
203+ "cluster upgradeability should not be blocked by runc guard when pool uses crun" )
204+ })
205+
157206 g .AfterEach (func (ctx context.Context ) {
158207 // Do not use Expect here; a failed assertion would skip subsequent cleanup steps.
159- if nodeName != "" {
160- roleLabel := poolNodeRoleLabel (runcRHCOS10GuardPool )
208+ if nodeName != "" && testPoolName != "" {
209+ roleLabel := poolNodeRoleLabel (testPoolName )
161210 if err := removeNodeLabel (ctx , oc , nodeName , roleLabel ); err != nil {
162211 framework .Logf ("cleanup: failed to remove node label %s from %s: %v" , roleLabel , nodeName , err )
163212 }
164213 }
165- if err := deleteContainerRuntimeConfig (ctx , mcClient , runcGuardCRCName ); err != nil {
166- framework .Logf ("cleanup: failed to delete ContainerRuntimeConfig %s: %v" , runcGuardCRCName , err )
214+ if cleanupCRC {
215+ if err := deleteContainerRuntimeConfig (ctx , mcClient , runcGuardCRCName ); err != nil {
216+ framework .Logf ("cleanup: failed to delete ContainerRuntimeConfig %s: %v" , runcGuardCRCName , err )
217+ }
167218 }
168- if nodeName != "" {
169- if err := WaitForMCP (ctx , mcClient , runcRHCOS10GuardPool , 10 * time .Minute , WaitMCPWithMachineCount (0 )); err != nil {
170- framework .Logf ("cleanup: failed waiting for MCP %s machine count 0: %v" , runcRHCOS10GuardPool , err )
219+ if nodeName != "" && testPoolName != "" {
220+ if err := WaitForMCP (ctx , mcClient , testPoolName , 10 * time .Minute , WaitMCPWithMachineCount (0 )); err != nil {
221+ framework .Logf ("cleanup: failed waiting for MCP %s machine count 0: %v" , testPoolName , err )
171222 }
172- if err := waitForNodeWorkerConfigRollback (ctx , oc , nodeName , runcRHCOS10GuardPool , 15 * time .Minute ); err != nil {
223+ if err := waitForNodeWorkerConfigRollback (ctx , oc , nodeName , testPoolName , 15 * time .Minute ); err != nil {
173224 framework .Logf ("cleanup: failed waiting for node %s worker config rollback: %v" , nodeName , err )
174225 }
175226 }
176- if err := deleteMachineConfigPool (ctx , mcClient , runcRHCOS10GuardPool ); err != nil {
177- framework .Logf ("cleanup: failed to delete MachineConfigPool %s: %v" , runcRHCOS10GuardPool , err )
227+ if testPoolName != "" {
228+ if err := deleteMachineConfigPool (ctx , mcClient , testPoolName ); err != nil {
229+ framework .Logf ("cleanup: failed to delete MachineConfigPool %s: %v" , testPoolName , err )
230+ }
178231 }
179232 if nodeName != "" {
180233 if err := WaitForMCP (ctx , mcClient , "worker" , 30 * time .Minute ); err != nil {
@@ -184,6 +237,10 @@ var _ = g.Describe("[Suite:openshift/disruptive-longrunning][sig-node][Serial][D
184237 })
185238})
186239
240+ // Helper naming (It blocks use o.Expect(helper(...)).To(o.Succeed())):
241+ // waitFor* — poll until a condition holds or timeout; may retry transient errors
242+ // assert* — immediate check; return an error when the condition is not met
243+
187244// waitForUpgradeBlockedByDegradedPool waits for MCO to propagate an isolated MCP render failure
188245// to ClusterOperator and ClusterVersion Upgradeable=False. CO/CVO Degraded may take ~30 minutes
189246// to flip; this check mirrors MCO extended tests that assert Upgradeable without waiting for Degraded.
@@ -370,7 +427,7 @@ func waitForNodeWorkerConfigRollback(ctx context.Context, oc *exutil.CLI, nodeNa
370427 })
371428}
372429
373- func verifyNodeReadyAndNotRollingOut (ctx context.Context , oc * exutil.CLI , nodeName string ) error {
430+ func assertNodeReadyAndNotRollingOut (ctx context.Context , oc * exutil.CLI , nodeName string ) error {
374431 node , err := oc .AdminKubeClient ().CoreV1 ().Nodes ().Get (ctx , nodeName , metav1.GetOptions {})
375432 if err != nil {
376433 return err
@@ -440,32 +497,7 @@ func createRuncGuardPool(ctx context.Context, mcClient *machineconfigclient.Clie
440497}
441498
442499func createRuncGuardMCP (ctx context.Context , mcClient * machineconfigclient.Clientset ) error {
443- mcp := & machineconfigv1.MachineConfigPool {
444- ObjectMeta : metav1.ObjectMeta {
445- Name : runcRHCOS10GuardPool ,
446- Labels : map [string ]string {
447- poolOperatorLabel (runcRHCOS10GuardPool ): "" ,
448- },
449- },
450- Spec : machineconfigv1.MachineConfigPoolSpec {
451- OSImageStream : machineconfigv1.OSImageStreamReference {Name : streamRHEL9 },
452- MachineConfigSelector : & metav1.LabelSelector {
453- MatchExpressions : []metav1.LabelSelectorRequirement {{
454- Key : machineconfigv1 .MachineConfigRoleLabelKey ,
455- Operator : metav1 .LabelSelectorOpIn ,
456- // worker is required by custom-machine-config-pool-selector VAP for custom pools.
457- Values : []string {"worker" , runcRHCOS10GuardPool },
458- }},
459- },
460- NodeSelector : & metav1.LabelSelector {
461- MatchLabels : map [string ]string {
462- poolNodeRoleLabel (runcRHCOS10GuardPool ): "" ,
463- },
464- },
465- },
466- }
467- _ , err := mcClient .MachineconfigurationV1 ().MachineConfigPools ().Create (ctx , mcp , metav1.CreateOptions {})
468- return err
500+ return createRHEL9UpgradeMCP (ctx , mcClient , runcRHCOS10GuardPool )
469501}
470502
471503func createRuncGuardCRC (ctx context.Context , mcClient * machineconfigclient.Clientset ) error {
@@ -651,7 +683,7 @@ func hasRuncRuntimeOnNode(ctx context.Context, oc *exutil.CLI, nodeName string)
651683 return strings .Contains (out , "runc" ), nil
652684}
653685
654- func expectRuncRuntimeOnNode (ctx context.Context , oc * exutil.CLI , nodeName string , timeout time.Duration ) error {
686+ func waitForRuncRuntimeOnNode (ctx context.Context , oc * exutil.CLI , nodeName string , timeout time.Duration ) error {
655687 err := wait .PollUntilContextTimeout (ctx , 10 * time .Second , timeout , true , func (ctx context.Context ) (bool , error ) {
656688 hasRunc , err := hasRuncRuntimeOnNode (ctx , oc , nodeName )
657689 if err != nil {
@@ -675,6 +707,39 @@ func expectRuncRuntimeOnNode(ctx context.Context, oc *exutil.CLI, nodeName strin
675707 return nil
676708}
677709
710+ func hasCrunRuntimeOnNode (ctx context.Context , oc * exutil.CLI , nodeName string ) (bool , error ) {
711+ hasRunc , err := hasRuncRuntimeOnNode (ctx , oc , nodeName )
712+ if err != nil {
713+ return false , err
714+ }
715+ // RHCOS defaults to crun; absence of the CRC runc drop-in is sufficient.
716+ return ! hasRunc , nil
717+ }
718+
719+ func waitForCrunRuntimeOnNode (ctx context.Context , oc * exutil.CLI , nodeName string , timeout time.Duration ) error {
720+ err := wait .PollUntilContextTimeout (ctx , 10 * time .Second , timeout , true , func (ctx context.Context ) (bool , error ) {
721+ hasCrun , err := hasCrunRuntimeOnNode (ctx , oc , nodeName )
722+ if err != nil {
723+ if isTransientNodeDebugError (err ) {
724+ framework .Logf ("Transient debug error checking crun on node %s: %v" , nodeName , err )
725+ return false , nil
726+ }
727+ return false , err
728+ }
729+ if ! hasCrun {
730+ return false , nil
731+ }
732+ return true , nil
733+ })
734+ if err != nil {
735+ if wait .Interrupted (err ) {
736+ return fmt .Errorf ("node %s does not have crun as default runtime within %s" , nodeName , timeout )
737+ }
738+ return err
739+ }
740+ return nil
741+ }
742+
678743func isTransientNodeDebugError (err error ) bool {
679744 if err == nil {
680745 return false
@@ -806,21 +871,6 @@ func waitForNodeRHELMajorVersion(ctx context.Context, oc *exutil.CLI, nodeName,
806871 })
807872}
808873
809- func assertCrunRuntimeOnNode (ctx context.Context , oc * exutil.CLI , nodeName string ) error {
810- hasRunc , err := hasRuncRuntimeOnNode (ctx , oc , nodeName )
811- if err != nil {
812- if isTransientNodeDebugError (err ) {
813- return fmt .Errorf ("failed to verify runtime on node %s: debug unavailable: %w" , nodeName , err )
814- }
815- return fmt .Errorf ("failed to verify runtime on node %s: %w" , nodeName , err )
816- }
817- if hasRunc {
818- return fmt .Errorf ("node %s still has runc default runtime configured on RHCOS 10" , nodeName )
819- }
820- // RHCOS 10 defaults to crun; absence of the CRC runc drop-in is sufficient.
821- return nil
822- }
823-
824874func nodeRHELMajorVersion (ctx context.Context , oc * exutil.CLI , nodeName string ) (string , error ) {
825875 node , err := oc .AdminKubeClient ().CoreV1 ().Nodes ().Get (ctx , nodeName , metav1.GetOptions {})
826876 if err != nil {
@@ -862,3 +912,66 @@ func deleteMachineConfigPool(ctx context.Context, mcClient *machineconfigclient.
862912 }
863913 return err
864914}
915+
916+ func createCrunUpgradeMCP (ctx context.Context , mcClient * machineconfigclient.Clientset ) error {
917+ return createRHEL9UpgradeMCP (ctx , mcClient , crunRHCOS10UpgradePool )
918+ }
919+
920+ func createRHEL9UpgradeMCP (ctx context.Context , mcClient * machineconfigclient.Clientset , poolName string ) error {
921+ mcp := & machineconfigv1.MachineConfigPool {
922+ ObjectMeta : metav1.ObjectMeta {
923+ Name : poolName ,
924+ Labels : map [string ]string {
925+ poolOperatorLabel (poolName ): "" ,
926+ },
927+ },
928+ Spec : machineconfigv1.MachineConfigPoolSpec {
929+ OSImageStream : machineconfigv1.OSImageStreamReference {Name : streamRHEL9 },
930+ MachineConfigSelector : & metav1.LabelSelector {
931+ MatchExpressions : []metav1.LabelSelectorRequirement {{
932+ Key : machineconfigv1 .MachineConfigRoleLabelKey ,
933+ Operator : metav1 .LabelSelectorOpIn ,
934+ // worker is required by custom-machine-config-pool-selector VAP for custom pools.
935+ Values : []string {"worker" , poolName },
936+ }},
937+ },
938+ NodeSelector : & metav1.LabelSelector {
939+ MatchLabels : map [string ]string {
940+ poolNodeRoleLabel (poolName ): "" ,
941+ },
942+ },
943+ },
944+ }
945+ _ , err := mcClient .MachineconfigurationV1 ().MachineConfigPools ().Create (ctx , mcp , metav1.CreateOptions {})
946+ return err
947+ }
948+
949+ func assertPoolNotRenderDegraded (ctx context.Context , mcClient * machineconfigclient.Clientset , poolName string ) error {
950+ mcp , err := mcClient .MachineconfigurationV1 ().MachineConfigPools ().Get (ctx , poolName , metav1.GetOptions {})
951+ if err != nil {
952+ return err
953+ }
954+ _ , degraded , renderDegraded , _ := mcpPoolConditions (mcp )
955+ if renderDegraded {
956+ return fmt .Errorf ("MachineConfigPool %s has RenderDegraded=True after rhel-10 upgrade" , poolName )
957+ }
958+ if degraded {
959+ return fmt .Errorf ("MachineConfigPool %s is Degraded after rhel-10 upgrade" , poolName )
960+ }
961+ return nil
962+ }
963+
964+ func assertMachineConfigNotBlockedByDegradedPool (ctx context.Context , oc * exutil.CLI ) error {
965+ co , err := oc .AdminConfigClient ().ConfigV1 ().ClusterOperators ().Get (ctx , machineConfigClusterOperator , metav1.GetOptions {})
966+ if err != nil {
967+ return err
968+ }
969+ upgradeable := v1helpers .FindStatusCondition (co .Status .Conditions , configv1 .OperatorUpgradeable )
970+ if upgradeable != nil &&
971+ upgradeable .Status == configv1 .ConditionFalse &&
972+ upgradeable .Reason == degradedPoolUpgradeableReason {
973+ return fmt .Errorf ("ClusterOperator %s Upgradeable=False (reason %s): %q" ,
974+ machineConfigClusterOperator , degradedPoolUpgradeableReason , upgradeable .Message )
975+ }
976+ return nil
977+ }
0 commit comments