Skip to content

Commit 647c28e

Browse files
committed
e2e test case for RHCOS 9 → 10
1 parent 3cae547 commit 647c28e

2 files changed

Lines changed: 204 additions & 74 deletions

File tree

test/extended/node/runc_upgrade_cases.go

Lines changed: 179 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ import (
2727
)
2828

2929
const (
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

4344
var 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
4849
var _ = 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

442499
func 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

471503
func 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+
678743
func 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-
824874
func 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

Comments
 (0)