Skip to content

Commit a113e4e

Browse files
authored
Non-Admin Support - Webpack & Github Actions (opendatahub-io#6254)
* Fix non-admin test execution with oc user switching and optimized token refresh - Add oc user switching in cy.visitWithLogin for localhost tests - Implement lazy token refresh in webpack (5s cache) to avoid blocking event loop - Add cleanup step for old test artifacts (>2 days) - Ensure ODH_DASHBOARD_HOST is always passed to webpack - Add IS_NON_ADMIN_RUN flag to skip admin setup hooks - Add @ci-dashboard-set-2 tag to non-admin cluster settings test - Pass OC_SERVER from workflow to Cypress for dynamic user switching Fixes: Project creation and cluster storage tests that were failing due to blocking getCurrentToken() calls breaking WebSocket connections. * Address code review feedback: Make oc login errors fatal and fix cleanup - Make missing OC_SERVER fatal: Throw error instead of logging warning - Make oc login failure fatal: Throw error with exit code and output - Remove Cypress cache cleanup: Avoid deleting binary on shared runners These changes ensure test failures are explicit when user switching cannot occur, rather than silently continuing with wrong permissions. * Mask dashboard URLs and fix hostname extraction - Add ::add-mask:: for DASHBOARD_URL to prevent exposure in logs - Add ::add-mask:: for DASHBOARD_HOST to prevent exposure in logs - Fix sed regex: Use -E flag for proper extended regex (https? instead of https\?) The previous sed command was broken and extracted 'https:' instead of the actual hostname. The -E flag enables extended regex where ? means 'optional' rather than a literal character. Critical: This prevents sensitive cluster URLs from appearing in CI logs.
1 parent 09c12cc commit a113e4e

File tree

5 files changed

+179
-8
lines changed

5 files changed

+179
-8
lines changed

.github/workflows/cypress-e2e-test.yml

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,32 @@ jobs:
341341
echo "PORT_INFO_FILE=$PORT_INFO_DIR/port-${WEBPACK_PORT}.run_id" >> $GITHUB_ENV
342342
echo "📍 Using port ${WEBPACK_PORT} for ${{ matrix.tag }} (run_id: ${{ github.run_id }})"
343343
344+
- name: Cleanup old test artifacts
345+
continue-on-error: true
346+
run: |
347+
echo "🧹 Cleaning up old test artifacts (>2 days)..."
348+
349+
# Clean old Cypress results/screenshots/videos from workspace (>2 days old)
350+
find ${{ github.workspace }}/packages/cypress/results -type f -mtime +2 -delete 2>/dev/null || true
351+
find ${{ github.workspace }}/packages/cypress/screenshots -type f -mtime +2 -delete 2>/dev/null || true
352+
find ${{ github.workspace }}/packages/cypress/videos -type f -mtime +2 -delete 2>/dev/null || true
353+
354+
# Clean old webpack logs (>2 days old)
355+
find /tmp -name "webpack_*.log" -type f -mtime +2 -delete 2>/dev/null || true
356+
357+
# Note: ~/.cache/Cypress is managed by actions/cache and should not be cleaned here
358+
# to avoid removing the Cypress binary on shared self-hosted runners
359+
360+
# Clean old temporary yaml files (>2 days old)
361+
find /tmp -name "cypress-yaml-*.yaml" -type f -mtime +2 -delete 2>/dev/null || true
362+
363+
# Clean empty directories
364+
find ${{ github.workspace }}/packages/cypress/results -type d -empty -delete 2>/dev/null || true
365+
find ${{ github.workspace }}/packages/cypress/screenshots -type d -empty -delete 2>/dev/null || true
366+
find ${{ github.workspace }}/packages/cypress/videos -type d -empty -delete 2>/dev/null || true
367+
368+
echo "✅ Cleanup complete (non-critical, continued on any errors)"
369+
344370
- name: Checkout code
345371
uses: actions/checkout@v4
346372
with:
@@ -397,9 +423,15 @@ jobs:
397423
run: |
398424
TEST_VARS_FILE="${{ github.workspace }}/packages/cypress/test-variables.yml"
399425
400-
# Extract credentials
401-
OC_USERNAME=$(grep -A 10 "^OCP_ADMIN_USER:" "$TEST_VARS_FILE" | grep "USERNAME:" | head -1 | sed 's/.*USERNAME: //' | tr -d ' ')
402-
OC_PASSWORD=$(grep -A 10 "^OCP_ADMIN_USER:" "$TEST_VARS_FILE" | grep "PASSWORD:" | head -1 | sed 's/.*PASSWORD: //' | tr -d ' ')
426+
# Extract credentials based on test type
427+
if [[ "${{ matrix.tag }}" == "@NonAdmin" ]]; then
428+
echo "🔑 Using non-admin credentials (TEST_USER_3) for @NonAdmin tests"
429+
OC_USERNAME=$(grep -A 10 "^TEST_USER_3:" "$TEST_VARS_FILE" | grep "USERNAME:" | head -1 | sed 's/.*USERNAME: //' | tr -d ' ')
430+
OC_PASSWORD=$(grep -A 10 "^TEST_USER_3:" "$TEST_VARS_FILE" | grep "PASSWORD:" | head -1 | sed 's/.*PASSWORD: //' | tr -d ' ')
431+
else
432+
OC_USERNAME=$(grep -A 10 "^OCP_ADMIN_USER:" "$TEST_VARS_FILE" | grep "USERNAME:" | head -1 | sed 's/.*USERNAME: //' | tr -d ' ')
433+
OC_PASSWORD=$(grep -A 10 "^OCP_ADMIN_USER:" "$TEST_VARS_FILE" | grep "PASSWORD:" | head -1 | sed 's/.*PASSWORD: //' | tr -d ' ')
434+
fi
403435
echo "::add-mask::$OC_PASSWORD"
404436
echo "::add-mask::$OC_USERNAME"
405437
@@ -457,9 +489,17 @@ jobs:
457489
exit 1
458490
fi
459491
492+
# Mask dashboard URL to prevent exposure in logs
493+
echo "::add-mask::$DASHBOARD_URL"
494+
460495
# Set dashboard URL for selected cluster
461496
sed -i "s|^ODH_DASHBOARD_URL:.*|ODH_DASHBOARD_URL: $DASHBOARD_URL|" "$TEST_VARS_FILE"
462497
498+
# Export dashboard host (without protocol) for webpack ODH_DASHBOARD_HOST
499+
DASHBOARD_HOST=$(echo "$DASHBOARD_URL" | sed -E 's|https?://||' | sed 's|/.*||')
500+
echo "::add-mask::$DASHBOARD_HOST"
501+
echo "DASHBOARD_HOST=$DASHBOARD_HOST" >> $GITHUB_ENV
502+
463503
if [ -z "$ODH_NAMESPACES" ]; then
464504
echo "⚠️ ODH_NAMESPACES secret not set, skipping namespace override"
465505
exit 0
@@ -559,8 +599,8 @@ jobs:
559599
echo "✅ Port ${WEBPACK_PORT} is free and claimed by run_id: $CURRENT_RUN_ID"
560600
echo "🚀 Starting webpack dev server on port ${WEBPACK_PORT} ($CLUSTER_NAME)..."
561601
562-
# Start webpack and filter sensitive output
563-
cd frontend && ODH_PORT=${WEBPACK_PORT} npm run start:dev:ext > /tmp/webpack_${WEBPACK_PORT}.log 2>&1 &
602+
# Start webpack with explicit dashboard host (ensures correct proxy target for all tests)
603+
cd frontend && env ODH_DASHBOARD_HOST=${DASHBOARD_HOST} ODH_PORT=${WEBPACK_PORT} npm run start:dev:ext > /tmp/webpack_${WEBPACK_PORT}.log 2>&1 &
564604
SERVER_PID=$!
565605
echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV
566606
echo "$SERVER_PID" > "$PORT_INFO_DIR/port-${WEBPACK_PORT}.pid"
@@ -599,6 +639,9 @@ jobs:
599639
done
600640
601641
- name: Run E2E Tests
642+
env:
643+
OC_SERVER_PRIMARY: ${{ secrets.OC_SERVER_PRIMARY }}
644+
OC_SERVER_SECONDARY: ${{ secrets.OC_SERVER }}
602645
run: |
603646
cd frontend
604647
@@ -609,8 +652,25 @@ jobs:
609652
export CY_RESULTS_DIR="${{ github.workspace }}/packages/cypress/results/${{ matrix.tag }}"
610653
mkdir -p "$CY_RESULTS_DIR"
611654
655+
# Determine OC_SERVER based on cluster (for oc user switching in tests)
656+
if [ "$CLUSTER_NAME" = "dash-e2e-int" ]; then
657+
OC_SERVER="$OC_SERVER_PRIMARY"
658+
elif [ "$CLUSTER_NAME" = "dash-e2e" ]; then
659+
OC_SERVER="$OC_SERVER_SECONDARY"
660+
else
661+
echo "⚠️ Unknown cluster: $CLUSTER_NAME, defaulting to OC_SERVER_PRIMARY"
662+
OC_SERVER="$OC_SERVER_PRIMARY"
663+
fi
664+
665+
# Set IS_NON_ADMIN_RUN flag for non-admin tests to skip admin-only setup hooks
666+
EXTRA_CYPRESS_ENV="OC_SERVER=${OC_SERVER},"
667+
if [[ "${{ matrix.tag }}" == "@NonAdmin" ]]; then
668+
EXTRA_CYPRESS_ENV="${EXTRA_CYPRESS_ENV}IS_NON_ADMIN_RUN=true,"
669+
echo "🔐 Running in non-admin mode - admin setup hooks will be skipped"
670+
fi
671+
612672
BASE_URL=http://localhost:${WEBPACK_PORT} npm run cypress:run:chrome -- \
613-
--env skipTags="@Bug @Maintain @NonConcurrent",grepTags="${{ matrix.tag }}",grepFilterSpecs=true \
673+
--env ${EXTRA_CYPRESS_ENV}skipTags="@Bug @Maintain @NonConcurrent",grepTags="${{ matrix.tag }}",grepFilterSpecs=true \
614674
--config video=true,screenshotsFolder="$CY_RESULTS_DIR/screenshots",videosFolder="$CY_RESULTS_DIR/videos"
615675
616676
- name: Upload test results

frontend/config/webpack.dev.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ module.exports = smp.wrap(
6969
proxy: (() => {
7070
if (process.env.EXT_CLUSTER) {
7171
// Environment variables:
72+
// - ODH_DASHBOARD_HOST: Explicit dashboard hostname (bypasses oc get routes)
7273
// - DEV_LEGACY=true: Forces legacy behavior for oauth-proxy clusters
7374
// (uses old subdomain format and sends x-forwarded-access-token header)
7475
const devLegacy = process.env.DEV_LEGACY === 'true';
75-
let dashboardHost;
76+
let dashboardHost = process.env.ODH_DASHBOARD_HOST;
7677
let token;
7778

7879
try {
@@ -87,10 +88,44 @@ module.exports = smp.wrap(
8788
throw new Error('Login with `oc login` prior to starting dev server.');
8889
}
8990

91+
// Token refresh mechanism (for when oc user is switched during tests)
92+
// Only refreshes when token is actually used, with 5s minimum interval to avoid blocking
93+
let cachedToken = token;
94+
let lastTokenFetch = Date.now();
95+
const TOKEN_REFRESH_MIN_INTERVAL = 5000; // Don't refresh more than once per 5 seconds
96+
97+
const getCurrentToken = () => {
98+
const now = Date.now();
99+
// Only refresh if enough time has passed since last fetch (prevents excessive blocking)
100+
if (now - lastTokenFetch > TOKEN_REFRESH_MIN_INTERVAL) {
101+
try {
102+
const newToken = execSync('oc whoami --show-token', {
103+
stdio: ['pipe', 'pipe', 'ignore'],
104+
})
105+
.toString()
106+
.trim();
107+
// Only update if token actually changed
108+
if (newToken !== cachedToken) {
109+
console.info('Token refreshed (oc user may have switched)');
110+
cachedToken = newToken;
111+
}
112+
lastTokenFetch = now;
113+
} catch (e) {
114+
// If refresh fails, keep using cached token
115+
console.warn('Failed to refresh oc token, using cached token');
116+
}
117+
}
118+
return cachedToken;
119+
};
120+
90121
const odhProject = process.env.OC_PROJECT || 'opendatahub';
91122
const app = process.env.ODH_APP || 'odh-dashboard';
92123
console.info('Using project:', odhProject);
93124

125+
if (dashboardHost) {
126+
console.info('Using explicit ODH_DASHBOARD_HOST:', dashboardHost);
127+
}
128+
94129
// try to get dashboard host from HttpRoute and Gateway
95130
try {
96131
// Get the HttpRoute resource as JSON
@@ -189,6 +224,13 @@ module.exports = smp.wrap(
189224
secure: false,
190225
changeOrigin: true,
191226
headers,
227+
onProxyReq: (proxyReq) => {
228+
const currentToken = getCurrentToken();
229+
proxyReq.setHeader('Authorization', `Bearer ${currentToken}`);
230+
if (shouldFwdAccessToken) {
231+
proxyReq.setHeader('x-forwarded-access-token', currentToken);
232+
}
233+
},
192234
},
193235
{
194236
context: ['/wss/k8s'],
@@ -197,6 +239,13 @@ module.exports = smp.wrap(
197239
ws: true,
198240
changeOrigin: true,
199241
headers,
242+
onProxyReq: (proxyReq) => {
243+
const currentToken = getCurrentToken();
244+
proxyReq.setHeader('Authorization', `Bearer ${currentToken}`);
245+
if (shouldFwdAccessToken) {
246+
proxyReq.setHeader('x-forwarded-access-token', currentToken);
247+
}
248+
},
200249
},
201250
];
202251
}

packages/cypress/cypress/support/commands/application.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,60 @@ Cypress.Commands.add('visitWithLogin', (relativeUrl, credentials = HTPASSWD_CLUS
313313
}
314314
cy.step(`Navigate to: ${fullUrl}`);
315315

316+
// When running against localhost (webpack dev server), check if we need to switch oc user
317+
const baseUrl = Cypress.config('baseUrl') || '';
318+
if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) {
319+
cy.log('🔍 Localhost detected - checking if oc user switch is needed');
320+
cy.exec('oc whoami', { failOnNonZeroExit: false, log: false }).then((result) => {
321+
const currentOcUser = result.stdout.trim();
322+
const requestedUser = credentials.USERNAME;
323+
324+
if (result.code !== 0) {
325+
cy.log(
326+
`⚠️ oc whoami failed (exit code: ${result.code}) - may not be logged into cluster`,
327+
);
328+
return cy.wrap(null);
329+
}
330+
331+
if (currentOcUser !== requestedUser) {
332+
cy.log(
333+
`🔄 Switching oc user from ${currentOcUser ? '***' : 'none'} to ${
334+
requestedUser ? '***' : 'none'
335+
}`,
336+
);
337+
338+
const ocServer = Cypress.env('OC_SERVER');
339+
if (!ocServer) {
340+
const errorMsg =
341+
'OC_SERVER is required to switch oc user but was not set in Cypress env. ' +
342+
'Set CYPRESS_OC_SERVER environment variable or pass via --env OC_SERVER=...';
343+
cy.log(`❌ ${errorMsg}`);
344+
throw new Error(errorMsg);
345+
}
346+
347+
return cy
348+
.exec(
349+
`oc login -u "${requestedUser}" -p "${credentials.PASSWORD}" --server="${ocServer}" --insecure-skip-tls-verify`,
350+
{ failOnNonZeroExit: false, log: false },
351+
)
352+
.then((loginResult) => {
353+
if (loginResult.code === 0) {
354+
cy.log(`✅ oc user switched successfully`);
355+
} else {
356+
const errorMsg =
357+
`oc login failed (exit code: ${loginResult.code}). ` +
358+
`Output: ${loginResult.stdout || loginResult.stderr || 'No output'}`;
359+
cy.log(`❌ ${errorMsg}`);
360+
throw new Error(errorMsg);
361+
}
362+
});
363+
}
364+
365+
cy.log(`✅ Already logged in as correct oc user`);
366+
return cy.wrap(null);
367+
});
368+
}
369+
316370
if (isBYOIDCCluster) {
317371
cy.log('BYOIDC cluster detected - using Keycloak authentication');
318372
}

packages/cypress/cypress/tests/e2e/settings/clusterSettings/testAdminClusterSettings.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ describe('Verify that only the Cluster Admin can access Cluster Settings', () =>
187187

188188
it(
189189
'Test User - should not have access rights to view the Cluster Settings tab',
190-
{ tags: ['@Smoke', '@SmokeSet2', '@ODS-1216', '@Dashboard'] },
190+
{ tags: ['@Smoke', '@SmokeSet2', '@ODS-1216', '@Dashboard', '@ci-dashboard-set-2'] },
191191
() => {
192192
cy.step('Log into the application');
193193
cy.visitWithLogin('/', LDAP_CONTRIBUTOR_USER);

packages/cypress/cypress/utils/retryableHooks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export const retryableBefore = <T>(fn: () => void | Promise<void> | Cypress.Chai
1414
if (this.currentTest?.isPending() || !shouldRun) {
1515
return;
1616
}
17+
18+
// Skip admin-only setup in non-admin mode (controlled by workflow)
19+
if (Cypress.env('IS_NON_ADMIN_RUN')) {
20+
cy.log('⏭️ Skipping admin setup (running in non-admin mode)');
21+
shouldRun = false;
22+
return;
23+
}
24+
1725
shouldRun = false;
1826
setupPerformed = true;
1927
cy.wrap(null).then(fn);

0 commit comments

Comments
 (0)