Skip to content

Commit f939493

Browse files
fix: replace hardcoded sleeps with waitUntil polling
Setup verification improvements: - Replace all `sleep N && oc wait` patterns with cy.waitUntil-based polling via shared wait-utils.ts helpers (waitForPodsReady, waitForPodsReadyOrAbsent, waitForResourceCondition) - Make UI verification in waitForCOOReady conditional on COO_UI_INSTALL since CLI readiness check is sufficient for bundle installs - Eliminates ~195s of unconditional sleeping in a full COO setup Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d0e2e26 commit f939493

File tree

4 files changed

+156
-101
lines changed

4 files changed

+156
-101
lines changed

web/cypress/support/commands/coo-install-commands.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'cypress-wait-until';
12
import { operatorHubPage } from '../../views/operator-hub-page';
23
import { nav } from '../../views/nav';
34

@@ -76,38 +77,53 @@ export const cooInstallUtils = {
7677

7778
waitForCOOReady(MCP: { namespace: string }): void {
7879
cy.log('Check Cluster Observability Operator status');
80+
const kubeconfig = Cypress.env('KUBECONFIG_PATH');
7981

80-
cy.exec(`oc project ${MCP.namespace} --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`);
82+
cy.exec(`oc project ${MCP.namespace} --kubeconfig ${kubeconfig}`);
83+
84+
cy.waitUntil(
85+
() =>
86+
cy
87+
.exec(
88+
`oc get pods -n ${MCP.namespace} -o name --kubeconfig ${kubeconfig} | grep observability-operator | grep -v bundle`,
89+
{ failOnNonZeroExit: false },
90+
)
91+
.then((result) => result.code === 0 && result.stdout.trim().length > 0),
92+
{
93+
timeout: readyTimeoutMilliseconds,
94+
interval: 10000,
95+
errorMsg: `Observability operator pod not found in namespace ${MCP.namespace}`,
96+
},
97+
);
8198

8299
cy.exec(
83-
`sleep 60 && oc get pods -n ${MCP.namespace} | grep observability-operator | grep -v bundle | awk '{print $1}'`,
84-
{ timeout: readyTimeoutMilliseconds, failOnNonZeroExit: true },
100+
`oc get pods -n ${MCP.namespace} -o name --kubeconfig ${kubeconfig} | grep observability-operator | grep -v bundle`,
85101
)
86102
.its('stdout')
87-
.then((podName) => {
88-
const COO_POD_NAME = podName.trim();
89-
cy.log(`Successfully retrieved Pod Name: ${COO_POD_NAME}`);
103+
.then((podOutput) => {
104+
const podName = podOutput.trim();
105+
cy.log(`Found COO pod: ${podName}`);
106+
90107
cy.exec(
91-
`sleep 15 && oc wait --for=condition=Ready pods ${COO_POD_NAME} -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
92-
{
93-
timeout: readyTimeoutMilliseconds,
94-
failOnNonZeroExit: true,
95-
},
108+
`oc wait --for=condition=Ready ${podName} -n ${MCP.namespace} --timeout=120s --kubeconfig ${kubeconfig}`,
109+
{ timeout: readyTimeoutMilliseconds, failOnNonZeroExit: true },
96110
).then((result) => {
97111
expect(result.code).to.eq(0);
98112
cy.log(`Observability-operator pod is now running in namespace: ${MCP.namespace}`);
99113
});
100114
});
101115

102-
cy.get('#page-sidebar').then(($sidebar) => {
103-
const section = $sidebar.text().includes('Ecosystem') ? 'Ecosystem' : 'Operators';
104-
nav.sidenav.clickNavLink([section, 'Installed Operators']);
105-
});
116+
if (Cypress.env('COO_UI_INSTALL')) {
117+
cy.get('#page-sidebar').then(($sidebar) => {
118+
const section = $sidebar.text().includes('Ecosystem') ? 'Ecosystem' : 'Operators';
119+
nav.sidenav.clickNavLink([section, 'Installed Operators']);
120+
});
106121

107-
cy.byTestID('name-filter-input').should('be.visible').type('Observability{enter}');
108-
cy.get('[data-test="status-text"]', { timeout: installTimeoutMilliseconds })
109-
.eq(0)
110-
.should('contain.text', 'Succeeded', { timeout: installTimeoutMilliseconds });
122+
cy.byTestID('name-filter-input').should('be.visible').type('Observability{enter}');
123+
cy.get('[data-test="status-text"]', { timeout: installTimeoutMilliseconds })
124+
.eq(0)
125+
.should('contain.text', 'Succeeded', { timeout: installTimeoutMilliseconds });
126+
}
111127
},
112128

113129
cleanupCOONamespace(MCP: { namespace: string }): void {

web/cypress/support/commands/dashboards-commands.ts

Lines changed: 18 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DataTestIDs, LegacyTestIDs } from '../../../src/components/data-test';
2+
import { waitForPodsReady, waitForResourceCondition } from './wait-utils';
23

34
export { };
45

@@ -9,16 +10,8 @@ export const dashboardsUtils = {
910
setupMonitoringUIPlugin(MCP: { namespace: string }): void {
1011
cy.log('Create Monitoring UI Plugin instance.');
1112
cy.exec(`oc apply -f ./cypress/fixtures/coo/monitoring-ui-plugin.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`);
12-
cy.exec(
13-
`sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=monitoring -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
14-
{
15-
timeout: readyTimeoutMilliseconds,
16-
failOnNonZeroExit: true,
17-
},
18-
).then((result) => {
19-
expect(result.code).to.eq(0);
20-
cy.log(`Monitoring plugin pod is now running in namespace: ${MCP.namespace}`);
21-
});
13+
waitForPodsReady('app.kubernetes.io/instance=monitoring', MCP.namespace, readyTimeoutMilliseconds);
14+
cy.log(`Monitoring plugin pod is now running in namespace: ${MCP.namespace}`);
2215
},
2316

2417
setupDashboardsAndPlugins(MCP: { namespace: string }): void {
@@ -68,27 +61,16 @@ export const dashboardsUtils = {
6861
`oc label namespace ${MCP.namespace} openshift.io/cluster-monitoring=true --overwrite=true --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
6962
);
7063

71-
cy.exec(
72-
`sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=perses -n ${MCP.namespace} --timeout=600s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
73-
{
74-
timeout: installTimeoutMilliseconds,
75-
failOnNonZeroExit: true,
76-
},
77-
).then((result) => {
78-
expect(result.code).to.eq(0);
79-
cy.log(`Perses-0 pod is now running in namespace: ${MCP.namespace}`);
80-
});
64+
waitForPodsReady('app.kubernetes.io/instance=perses', MCP.namespace, installTimeoutMilliseconds);
65+
cy.log(`Perses-0 pod is now running in namespace: ${MCP.namespace}`);
8166

82-
cy.exec(
83-
`sleep 15 && oc wait --for=jsonpath='{.metadata.name}'=health-analyzer --timeout=60s servicemonitor/health-analyzer -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
84-
{
85-
timeout: readyTimeoutMilliseconds,
86-
failOnNonZeroExit: true,
87-
},
88-
).then((result) => {
89-
expect(result.code).to.eq(0);
90-
cy.log(`Health-analyzer service monitor is now running in namespace: ${MCP.namespace}`);
91-
});
67+
waitForResourceCondition(
68+
'servicemonitor/health-analyzer',
69+
"jsonpath='{.metadata.name}'=health-analyzer",
70+
MCP.namespace,
71+
readyTimeoutMilliseconds,
72+
);
73+
cy.log(`Health-analyzer service monitor is now running in namespace: ${MCP.namespace}`);
9274

9375
cy.reload(true);
9476
cy.visit('/monitoring/v2/dashboards');
@@ -100,34 +82,14 @@ export const dashboardsUtils = {
10082
cy.exec(`oc apply -f ./cypress/fixtures/coo/troubleshooting-panel-ui-plugin.yaml --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`);
10183

10284
cy.log('Troubleshooting panel instance created. Waiting for pods to be ready.');
103-
cy.exec(
104-
`sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=troubleshooting-panel -n ${MCP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
105-
{
106-
timeout: readyTimeoutMilliseconds,
107-
failOnNonZeroExit: true,
108-
},
109-
).then((result) => {
110-
expect(result.code).to.eq(0);
111-
cy.log(`Troubleshooting panel pod is now running in namespace: ${MCP.namespace}`);
112-
});
85+
waitForPodsReady('app.kubernetes.io/instance=troubleshooting-panel', MCP.namespace, readyTimeoutMilliseconds);
86+
cy.log(`Troubleshooting panel pod is now running in namespace: ${MCP.namespace}`);
87+
88+
waitForPodsReady('app.kubernetes.io/instance=korrel8r', MCP.namespace, installTimeoutMilliseconds);
89+
cy.log(`Korrel8r pod is now running in namespace: ${MCP.namespace}`);
11390

114-
cy.exec(
115-
`sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/instance=korrel8r -n ${MCP.namespace} --timeout=600s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
116-
{
117-
timeout: installTimeoutMilliseconds,
118-
failOnNonZeroExit: true,
119-
},
120-
).then((result) => {
121-
expect(result.code).to.eq(0);
122-
cy.log(`Korrel8r pod is now running in namespace: ${MCP.namespace}`);
123-
});
124-
125-
cy.log('Reloading the page');
12691
cy.reload(true);
127-
cy.log('Waiting for 10 seconds before clicking the application launcher');
128-
cy.wait(10000);
129-
cy.log('Clicking the application launcher');
130-
cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher).should('be.visible').click();
92+
cy.byLegacyTestID(LegacyTestIDs.ApplicationLauncher, { timeout: 30000 }).should('be.visible').click();
13193
cy.byTestID(DataTestIDs.MastHeadApplicationItem).contains('Signal Correlation').should('be.visible');
13294
},
13395

web/cypress/support/commands/image-patch-commands.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { waitForPodsReady, waitForPodsReadyOrAbsent } from './wait-utils';
2+
13
export { };
24

35
const readyTimeoutMilliseconds = Cypress.config('readyTimeoutMilliseconds') as number;
@@ -22,17 +24,9 @@ export const imagePatchUtils = {
2224
cy.log(`CMO deployment Scaled Down successfully: ${result.stdout}`);
2325
});
2426

25-
cy.exec(
26-
`sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/name=monitoring-plugin -n ${MP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
27-
{
28-
timeout: readyTimeoutMilliseconds,
29-
failOnNonZeroExit: true,
30-
},
31-
).then((result) => {
32-
expect(result.code).to.eq(0);
33-
cy.log(`Monitoring plugin pod is now running in namespace: ${MP.namespace}`);
34-
cy.reload(true);
35-
});
27+
waitForPodsReady('app.kubernetes.io/name=monitoring-plugin', MP.namespace, readyTimeoutMilliseconds);
28+
cy.log(`Monitoring plugin pod is now running in namespace: ${MP.namespace}`);
29+
cy.reload(true);
3630
} else {
3731
cy.log('MP_IMAGE is NOT set. Skipping patching the image in CMO operator CSV.');
3832
}
@@ -106,21 +100,8 @@ export const imagePatchUtils = {
106100
expect(result.code).to.eq(0);
107101
cy.log(`CMO CSV reverted successfully with Monitoring Plugin image: ${result.stdout}`);
108102

109-
cy.exec(
110-
`sleep 15 && oc wait --for=condition=Ready pods --selector=app.kubernetes.io/name=monitoring-plugin -n ${MP.namespace} --timeout=60s --kubeconfig ${Cypress.env('KUBECONFIG_PATH')}`,
111-
{
112-
timeout: readyTimeoutMilliseconds,
113-
failOnNonZeroExit: false,
114-
},
115-
).then((result) => {
116-
if (result.code === 0) {
117-
cy.log(`Monitoring plugin pod is now running in namespace: ${MP.namespace}`);
118-
} else if (result.stderr.includes('no matching resources found')) {
119-
cy.log(`No monitoring-plugin pods found in namespace ${MP.namespace} - this is expected on fresh clusters`);
120-
} else {
121-
throw new Error(`Failed to wait for monitoring-plugin pods: ${result.stderr}`);
122-
}
123-
});
103+
waitForPodsReadyOrAbsent('app.kubernetes.io/name=monitoring-plugin', MP.namespace, readyTimeoutMilliseconds);
104+
cy.log(`Monitoring plugin pods verified in namespace: ${MP.namespace}`);
124105

125106
cy.reload(true);
126107
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'cypress-wait-until';
2+
3+
/**
4+
* Poll until pods matching a label selector reach the Ready condition.
5+
* Replaces the `sleep N && oc wait` anti-pattern — proceeds as soon as
6+
* the pod is ready instead of sleeping a fixed duration first.
7+
*
8+
* Handles the case where pods don't exist yet (oc wait fails immediately
9+
* with "no matching resources") by retrying until the overall timeout.
10+
*/
11+
export function waitForPodsReady(
12+
selector: string,
13+
namespace: string,
14+
timeoutMs?: number,
15+
intervalMs: number = 5000,
16+
): void {
17+
const kubeconfig = Cypress.env('KUBECONFIG_PATH');
18+
const timeout = timeoutMs ?? (Cypress.config('readyTimeoutMilliseconds') as number);
19+
20+
cy.waitUntil(
21+
() =>
22+
cy
23+
.exec(
24+
`oc wait --for=condition=Ready pods --selector=${selector} -n ${namespace} --timeout=10s --kubeconfig ${kubeconfig}`,
25+
{ failOnNonZeroExit: false, timeout: 20000 },
26+
)
27+
.then((result) => result.code === 0),
28+
{
29+
timeout,
30+
interval: intervalMs,
31+
errorMsg: `Pods with selector '${selector}' not ready in '${namespace}' within ${timeout / 1000}s`,
32+
},
33+
);
34+
}
35+
36+
/**
37+
* Like waitForPodsReady but also accepts "no matching resources found"
38+
* as a success condition (useful for optional components on fresh clusters).
39+
*/
40+
export function waitForPodsReadyOrAbsent(
41+
selector: string,
42+
namespace: string,
43+
timeoutMs?: number,
44+
intervalMs: number = 5000,
45+
): void {
46+
const kubeconfig = Cypress.env('KUBECONFIG_PATH');
47+
const timeout = timeoutMs ?? (Cypress.config('readyTimeoutMilliseconds') as number);
48+
49+
cy.waitUntil(
50+
() =>
51+
cy
52+
.exec(
53+
`oc wait --for=condition=Ready pods --selector=${selector} -n ${namespace} --timeout=10s --kubeconfig ${kubeconfig}`,
54+
{ failOnNonZeroExit: false, timeout: 20000 },
55+
)
56+
.then(
57+
(result) =>
58+
result.code === 0 || result.stderr.includes('no matching resources found'),
59+
),
60+
{
61+
timeout,
62+
interval: intervalMs,
63+
errorMsg: `Pods with selector '${selector}' neither ready nor absent in '${namespace}' within ${timeout / 1000}s`,
64+
},
65+
);
66+
}
67+
68+
/**
69+
* Poll until an arbitrary `oc wait` condition is met on a resource.
70+
* Useful for non-pod resources like ServiceMonitors.
71+
*/
72+
export function waitForResourceCondition(
73+
resource: string,
74+
condition: string,
75+
namespace: string,
76+
timeoutMs?: number,
77+
intervalMs: number = 5000,
78+
): void {
79+
const kubeconfig = Cypress.env('KUBECONFIG_PATH');
80+
const timeout = timeoutMs ?? (Cypress.config('readyTimeoutMilliseconds') as number);
81+
82+
cy.waitUntil(
83+
() =>
84+
cy
85+
.exec(
86+
`oc wait --for=${condition} ${resource} -n ${namespace} --timeout=10s --kubeconfig ${kubeconfig}`,
87+
{ failOnNonZeroExit: false, timeout: 20000 },
88+
)
89+
.then((result) => result.code === 0),
90+
{
91+
timeout,
92+
interval: intervalMs,
93+
errorMsg: `Condition '${condition}' not met for '${resource}' in '${namespace}' within ${timeout / 1000}s`,
94+
},
95+
);
96+
}

0 commit comments

Comments
 (0)