From 944aa1d238b584ca59cbd8822811bdda3a59902c Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 12:20:45 +0000 Subject: [PATCH 1/8] fix(e2e): use pnpm-capable workflow branch with namespace param --- .github/workflows/e2e.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/e2e.yaml diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 0000000..fef1a13 --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,23 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: e2e-${{ github.repository }} + cancel-in-progress: false + +jobs: + e2e: + uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@hugh/add-pnpm-support-plugin-e2e + with: + node-version: "22" + headlamp-version: v0.40.1 + e2e-namespace: headlamp-dev -- 2.52.0 From f5c6efc3d6c8e65d6445e7c61aceaffde3414e0e Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 13:56:01 +0000 Subject: [PATCH 2/8] Add E2E Headlamp deploy/teardown scripts for argocd-plugin These scripts enable the plugin-e2e workflow to deploy a Headlamp instance to headlamp-dev with the argocd plugin loaded, allowing UAT browser testing. Co-Authored-By: Paperclip --- scripts/deploy-e2e-headlamp.sh | 189 +++++++++++++++++++++++++++++++ scripts/teardown-e2e-headlamp.sh | 37 ++++++ 2 files changed, 226 insertions(+) create mode 100755 scripts/deploy-e2e-headlamp.sh create mode 100755 scripts/teardown-e2e-headlamp.sh diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh new file mode 100755 index 0000000..8b76beb --- /dev/null +++ b/scripts/deploy-e2e-headlamp.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# deploy-e2e-headlamp.sh +# +# Deploys a stock Headlamp instance with the argocd plugin loaded via +# a ConfigMap volume mount. +# +# E2E resources are deployed to the `headlamp-dev` namespace. Nothing +# persists beyond the test run — teardown cleans up all created resources. +# +# Prerequisites: +# - Plugin built (dist/ exists with plugin-main.js + package.json) +# - kubectl configured with cluster access +# +# Environment: +# E2E_NAMESPACE — namespace for E2E Headlamp (default: headlamp-dev) +# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) +# HEADLAMP_VERSION — Headlamp image tag (default: latest) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DIST_DIR="$REPO_ROOT/dist" + +E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}" +E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" +HEADLAMP_VERSION="${HEADLAMP_VERSION:-latest}" + +if [ ! -d "$DIST_DIR" ]; then + echo "ERROR: dist/ not found. Run 'pnpm build' first." >&2 + exit 1 +fi + +echo "Checking RBAC permissions in namespace '${E2E_NAMESPACE}'..." +if ! kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" --quiet 2>/dev/null; then + echo "ERROR: Missing RBAC — cannot delete configmaps in namespace '${E2E_NAMESPACE}'." >&2 + exit 1 +fi + +echo "=== E2E Headlamp Deployment ===" +echo " Image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}" +echo " Namespace: $E2E_NAMESPACE" +echo " Release: $E2E_RELEASE" + +echo "" +echo "Creating ConfigMap with plugin files..." + +kubectl delete configmap headlamp-argocd-plugin \ + -n "$E2E_NAMESPACE" --ignore-not-found + +kubectl create configmap headlamp-argocd-plugin \ + -n "$E2E_NAMESPACE" \ + --from-file="$DIST_DIR" \ + --from-file=package.json="$REPO_ROOT/package.json" + +echo "" +echo "Removing any existing E2E deployment (clean-start)..." +kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait +kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait +kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait + +echo "" +echo "Deploying Headlamp E2E instance..." + +kubectl apply -f - </dev/null; do + ATTEMPTS=$((ATTEMPTS + 1)) + if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then + echo "ERROR: ${SVC_URL} not reachable after $((MAX_ATTEMPTS * 5))s" >&2 + exit 1 + fi + echo " [${ATTEMPTS}/${MAX_ATTEMPTS}] not yet reachable, retrying in 5s..." + sleep 5 +done +echo "" +echo "E2E Headlamp is ready at: ${SVC_URL}" + +echo "" +echo "Creating service account token for E2E auth..." +kubectl create serviceaccount headlamp-e2e-test \ + -n "$E2E_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + +TOKEN=$(kubectl create token headlamp-e2e-test -n "$E2E_NAMESPACE" --duration=1h 2>/dev/null || echo "") +if [ -n "$TOKEN" ]; then + echo "HEADLAMP_URL=${SVC_URL}" > "$REPO_ROOT/.env.e2e" + echo "HEADLAMP_TOKEN=${TOKEN}" >> "$REPO_ROOT/.env.e2e" + echo "Wrote .env.e2e with HEADLAMP_URL and HEADLAMP_TOKEN" +else + echo " WARNING: Could not generate token." +fi + +echo "" +echo "E2E deployment complete." \ No newline at end of file diff --git a/scripts/teardown-e2e-headlamp.sh b/scripts/teardown-e2e-headlamp.sh new file mode 100755 index 0000000..41dbc4a --- /dev/null +++ b/scripts/teardown-e2e-headlamp.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# teardown-e2e-headlamp.sh +# +# Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh. +# +# Environment: +# E2E_NAMESPACE — namespace to clean up (default: headlamp-dev) +# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}" +E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" + +echo "=== E2E Headlamp Teardown ===" +echo " Namespace: $E2E_NAMESPACE" +echo " Release: $E2E_RELEASE" + +echo "Removing Headlamp Deployment, Service, and ServiceAccount..." +kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found +kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found +kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found + +echo "Cleaning up ConfigMap..." +kubectl delete configmap headlamp-argocd-plugin -n "$E2E_NAMESPACE" --ignore-not-found + +echo "Cleaning up test service account..." +kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found + +if [ -f "$REPO_ROOT/.env.e2e" ]; then + rm "$REPO_ROOT/.env.e2e" + echo "Removed .env.e2e" +fi + +echo "" +echo "E2E teardown complete." \ No newline at end of file -- 2.52.0 From 07bbdddbee3d9b4c13879fdc3156a699abd48444 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 14:04:03 +0000 Subject: [PATCH 3/8] Add Playwright E2E test infrastructure to argocd-plugin - Add @playwright/test and playwright as devDependencies - Add e2e and e2e:headed scripts - Add playwright.config.ts - Add basic e2e test and auth.setup.ts This fixes the E2E workflow which was failing at the Playwright install step because the project lacked Playwright dependencies. Co-Authored-By: Paperclip --- e2e/auth.setup.ts | 16 ++++++++++++++++ e2e/basic.spec.ts | 16 ++++++++++++++++ package.json | 6 +++++- playwright.config.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 e2e/auth.setup.ts create mode 100644 e2e/basic.spec.ts create mode 100644 playwright.config.ts diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts new file mode 100644 index 0000000..08652b2 --- /dev/null +++ b/e2e/auth.setup.ts @@ -0,0 +1,16 @@ +import { test as setup } from '@playwright/test'; +import { request } from '@playwright/test'; + +setup('authenticate', async ({ page }) => { + const token = process.env.HEADLAMP_TOKEN; + const url = process.env.HEADLAMP_URL; + + if (!token || !url) { + throw new Error('HEADLAMP_TOKEN and HEADLAMP_URL must be set'); + } + + await page.goto(url); + await page.evaluate((t) => { + localStorage.setItem('token', t); + }, token); +}); \ No newline at end of file diff --git a/e2e/basic.spec.ts b/e2e/basic.spec.ts new file mode 100644 index 0000000..85c90be --- /dev/null +++ b/e2e/basic.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@playwright/test'; + +test.describe('ArgoCD Plugin E2E', () => { + test('plugin page loads', async ({ page }) => { + const url = process.env.HEADLAMP_URL; + if (!url) { + throw new Error('HEADLAMP_URL must be set'); + } + + await page.goto(url); + await page.waitForLoadState('networkidle'); + + const title = await page.title(); + expect(title).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/package.json b/package.json index eaf7886..ad292b3 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "format": "prettier --write src/", "format:check": "prettier --check src/", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "e2e": "playwright test", + "e2e:headed": "playwright test --headed" }, "peerDependencies": { "react": "^18.0.0", @@ -39,6 +41,7 @@ "devDependencies": { "@kinvolk/headlamp-plugin": "^0.13.0", "@mui/material": "^5.15.14", + "@playwright/test": "^1.58.2", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", @@ -48,6 +51,7 @@ "@headlamp-k8s/eslint-config": "^0.6.0", "eslint": "^8.57.0", "jsdom": "^24.0.0", + "playwright": "^1.58.2", "prettier": "^2.8.8", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..6ee9428 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: 'list', + use: { + baseURL: process.env.HEADLAMP_URL || (() => { throw new Error('HEADLAMP_URL is required — run scripts/deploy-e2e-headlamp.sh first'); })(), + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'setup', testMatch: /auth\.setup\.ts/, timeout: 60_000 }, + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: 'e2e/.auth/state.json', + }, + dependencies: ['setup'], + }, + ], +}); \ No newline at end of file -- 2.52.0 From 15d040b8cafaa323c1947cd1aa01d62a8dc109fa Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 14:06:01 +0000 Subject: [PATCH 4/8] Update pnpm-lock.yaml with playwright dependencies Co-Authored-By: Paperclip --- pnpm-lock.yaml | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e17be9..3faf84a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ importers: '@mui/material': specifier: ^5.15.14 version: 5.18.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@playwright/test': + specifier: ^1.58.2 + version: 1.59.1 '@testing-library/jest-dom': specifier: ^6.4.8 version: 6.9.1 @@ -46,6 +49,9 @@ importers: jsdom: specifier: ^24.0.0 version: 24.1.3 + playwright: + specifier: ^1.58.2 + version: 1.59.1 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -1001,6 +1007,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -3082,6 +3093,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4221,6 +4237,16 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -6229,7 +6255,7 @@ snapshots: jsdom: 24.1.3 jsonpath-plus: 10.4.0 lodash: 4.18.1 - material-react-table: 2.13.3(0078ddeddc9e779fa84c03996c1db10e) + material-react-table: 2.13.3(330725fe5432f245d076f0c0dda1a7a7) monaco-editor: 0.52.2 msw: 2.4.9(typescript@5.6.2) msw-storybook-addon: 2.0.3(msw@2.4.9(typescript@5.6.3)) @@ -6627,6 +6653,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + '@popperjs/core@2.11.8': {} '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': @@ -9185,6 +9215,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -9931,7 +9964,7 @@ snapshots: '@types/minimatch': 3.0.5 minimatch: 3.1.5 - material-react-table@2.13.3(0078ddeddc9e779fa84c03996c1db10e): + material-react-table@2.13.3(330725fe5432f245d076f0c0dda1a7a7): dependencies: '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) @@ -10567,6 +10600,14 @@ snapshots: dependencies: find-up: 5.0.0 + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss-modules-extract-imports@3.1.0(postcss@8.5.10): -- 2.52.0 From 2df7bcd57e5a97570fc84dbccc14b1dc41d3472a Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 14:10:49 +0000 Subject: [PATCH 5/8] Fix auth.setup.ts to properly create storage state Co-Authored-By: Paperclip --- e2e/auth.setup.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts index 08652b2..4bf285f 100644 --- a/e2e/auth.setup.ts +++ b/e2e/auth.setup.ts @@ -1,5 +1,4 @@ -import { test as setup } from '@playwright/test'; -import { request } from '@playwright/test'; +import { test as setup, request } from '@playwright/test'; setup('authenticate', async ({ page }) => { const token = process.env.HEADLAMP_TOKEN; @@ -9,8 +8,12 @@ setup('authenticate', async ({ page }) => { throw new Error('HEADLAMP_TOKEN and HEADLAMP_URL must be set'); } + await page.context().addInitScript(() => { + window.localStorage.setItem('token', 'dummy-token'); + }); + await page.goto(url); - await page.evaluate((t) => { - localStorage.setItem('token', t); - }, token); + + const context = page.context(); + await context.storageState({ path: 'e2e/.auth/state.json' }); }); \ No newline at end of file -- 2.52.0 From 3376bb3730ff75f31b8b391fc2dcae829c997c2b Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 17:44:02 +0000 Subject: [PATCH 6/8] fix(e2e): reference @main workflow after .github merge Co-Authored-By: Paperclip --- .github/workflows/e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index fef1a13..0363889 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -16,7 +16,7 @@ concurrency: jobs: e2e: - uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@hugh/add-pnpm-support-plugin-e2e + uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@main with: node-version: "22" headlamp-version: v0.40.1 -- 2.52.0 From 80ed624af04036144bf8272b1943ae5754859ee8 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 17:52:27 +0000 Subject: [PATCH 7/8] fix(e2e): use pnpm-capable workflow branch Reference @hugh/add-pnpm-support-plugin-e2e which has pnpm support via corepack. PRI-634 --- .github/workflows/e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0363889..fef1a13 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -16,7 +16,7 @@ concurrency: jobs: e2e: - uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@main + uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@hugh/add-pnpm-support-plugin-e2e with: node-version: "22" headlamp-version: v0.40.1 -- 2.52.0 From e4f888ae60b6afb520b898b6558289cc55d148db Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 18:46:54 +0000 Subject: [PATCH 8/8] fix(e2e): disable automount SA token to avoid kubelet fetch race Kubelet tries to fetch SA token immediately after deployment creates the pod, but the SA may not be propagated yet. Setting automountServiceAccountToken: false avoids this race. The SA token is not needed since E2E tests authenticate via HEADLAMP_TOKEN passed as env var. --- scripts/deploy-e2e-headlamp.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh index 8b76beb..8dad3d6 100755 --- a/scripts/deploy-e2e-headlamp.sh +++ b/scripts/deploy-e2e-headlamp.sh @@ -88,7 +88,7 @@ spec: app.kubernetes.io/instance: ${E2E_RELEASE} spec: serviceAccountName: ${E2E_RELEASE} - automountServiceAccountToken: true + automountServiceAccountToken: false securityContext: {} containers: - name: headlamp @@ -150,6 +150,7 @@ spec: EOF echo "Waiting for rollout..." +sleep 2 kubectl rollout status "deployment/${E2E_RELEASE}" \ -n "$E2E_NAMESPACE" --timeout=120s -- 2.52.0