From 090943f7d9d8e56ca9dd9a1926755ee0358b17f5 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:55:51 +0000 Subject: [PATCH 01/14] Add .github/workflows/e2e.yaml --- .github/workflows/e2e.yaml | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 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..689e4fd --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,95 @@ +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 + +env: + E2E_NAMESPACE: headlamp-dev + E2E_RELEASE: headlamp-e2e-argocd + HEADLAMP_VERSION: v0.40.1 + +jobs: + e2e: + runs-on: runners-privilegedescalation + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + + - name: Setup kubectl + uses: azure/setup-kubectl@v4 + + - name: Install dependencies + run: npm ci + + - name: Build plugin + run: npx @kinvolk/headlamp-plugin build + + - name: Deploy E2E Headlamp instance + run: scripts/deploy-e2e-headlamp.sh + + - name: Load E2E environment + run: | + if [ -f .env.e2e ]; then + cat .env.e2e >> "$GITHUB_ENV" + else + echo "::error::deploy-e2e-headlamp.sh did not produce .env.e2e" + exit 1 + fi + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run E2E tests + run: npm run e2e + env: + HEADLAMP_URL: ${{ env.HEADLAMP_URL }} + HEADLAMP_TOKEN: ${{ env.HEADLAMP_TOKEN }} + + - name: Collect deployment diagnostics on failure + if: failure() + run: | + echo "=== Pod state ===" + kubectl get pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true + echo "=== Pod describe ===" + kubectl describe pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true + echo "=== Recent namespace events ===" + kubectl get events -n "$E2E_NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -20 || true + + - name: Teardown E2E instance + if: always() + run: scripts/teardown-e2e-headlamp.sh + + - name: Upload Playwright report + uses: actions/upload-artifact@v7 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload test results + uses: actions/upload-artifact@v7 + if: failure() + with: + name: test-results + path: test-results/ + retention-days: 7 \ No newline at end of file -- 2.52.0 From eb8b965e711d23e9dc2146fa2cf6363ab0195887 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:55:52 +0000 Subject: [PATCH 02/14] Add scripts/deploy-e2e-headlamp.sh --- scripts/deploy-e2e-headlamp.sh | 173 +++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 scripts/deploy-e2e-headlamp.sh diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh new file mode 100644 index 0000000..6172852 --- /dev/null +++ b/scripts/deploy-e2e-headlamp.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +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-argocd}" +HEADLAMP_VERSION="${HEADLAMP_VERSION:-v0.40.1}" + +if [ ! -d "$DIST_DIR" ]; then + echo "ERROR: dist/ not found. Run 'npm run 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 -- 2.52.0 From dc7a5fd23ca23c696a596959c7729b6c2af401cb Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:55:53 +0000 Subject: [PATCH 03/14] Add scripts/teardown-e2e-headlamp.sh --- scripts/teardown-e2e-headlamp.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 scripts/teardown-e2e-headlamp.sh diff --git a/scripts/teardown-e2e-headlamp.sh b/scripts/teardown-e2e-headlamp.sh new file mode 100644 index 0000000..1e2a71a --- /dev/null +++ b/scripts/teardown-e2e-headlamp.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}" +E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e-argocd}" + +echo "=== E2E Teardown ===" +echo " Namespace: $E2E_NAMESPACE" +echo " Release: $E2E_RELEASE" + +kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found || true +kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found || true +kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found || true +kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found || true +kubectl delete configmap headlamp-argocd-plugin -n "$E2E_NAMESPACE" --ignore-not-found || true + +echo "Teardown complete." \ No newline at end of file -- 2.52.0 From fe8397172a7618db4b4844bd0e9c0fa15d966495 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:55:54 +0000 Subject: [PATCH 04/14] Add e2e/argocd.spec.ts --- e2e/argocd.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 e2e/argocd.spec.ts diff --git a/e2e/argocd.spec.ts b/e2e/argocd.spec.ts new file mode 100644 index 0000000..86d8b9c --- /dev/null +++ b/e2e/argocd.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test.describe('ArgoCD plugin smoke tests', () => { + test('sidebar contains ArgoCD entry', async ({ page }) => { + await page.goto('/'); + const sidebar = page.getByRole('navigation', { name: 'Navigation' }); + await expect(sidebar).toBeVisible({ timeout: 15_000 }); + await expect(sidebar.getByRole('button', { name: 'ArgoCD' })).toBeVisible(); + }); + + test('applications list page loads', async ({ page }) => { + await page.goto('/c/main/argocd'); + + await expect( + page.getByRole('heading', { name: /argo.*cd/i }) + ).toBeVisible({ timeout: 15_000 }); + }); +}); \ No newline at end of file -- 2.52.0 From 5b030e4c2560cb226b2921c1090518014ffa0949 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:55:56 +0000 Subject: [PATCH 05/14] Add e2e/auth.setup.ts --- e2e/auth.setup.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 e2e/auth.setup.ts diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts new file mode 100644 index 0000000..aa56492 --- /dev/null +++ b/e2e/auth.setup.ts @@ -0,0 +1,34 @@ +import { test as setup, expect, Page } from '@playwright/test'; + +const AUTH_STATE_PATH = 'e2e/.auth/state.json'; + +async function authenticateWithToken(page: Page, token: string): Promise { + await page.goto('/'); + await page.waitForURL(/\/(login|token)$/); + + if (page.url().includes('/login')) { + const useTokenBtn = page.getByRole('button', { name: /use a token/i }); + await useTokenBtn.waitFor({ state: 'visible', timeout: 15_000 }); + await useTokenBtn.click(); + await page.waitForURL('**/token'); + } + + await page.getByRole('textbox', { name: /id token/i }).fill(token); + await page.getByRole('button', { name: /authenticate/i }).click(); + + await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({ + timeout: 15_000, + }); +} + +setup('authenticate with Headlamp', async ({ page }) => { + const token = process.env.HEADLAMP_TOKEN; + + if (!token) { + throw new Error('Set HEADLAMP_TOKEN for token auth'); + } + + await authenticateWithToken(page, token); + + await page.context().storageState({ path: AUTH_STATE_PATH }); +}); \ No newline at end of file -- 2.52.0 From 6cac81159db4f89c99b3cf01b6a75f2e3255946c Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:55:57 +0000 Subject: [PATCH 06/14] Add playwright.config.ts --- playwright.config.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 playwright.config.ts diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..1aa86a8 --- /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 || 'http://headlamp-e2e-argocd.headlamp-dev.svc.cluster.local', + 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 1c27b354afb4f0cad33bfd78895b6b4607b2a2b5 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-engineer[bot]" <269729446+privilegedescalation-engineer[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:57:10 +0000 Subject: [PATCH 07/14] Add @playwright/test devDependency --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 753b3f1..9000382 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", @@ -38,6 +40,7 @@ } }, "devDependencies": { + "@playwright/test": "^1.58.2", "@kinvolk/headlamp-plugin": "^0.13.0", "@mui/material": "^5.15.14", "@testing-library/jest-dom": "^6.4.8", -- 2.52.0 From 3e73cc376d31b7944767e1d4603db6bac597d639 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 15:10:59 +0000 Subject: [PATCH 08/14] fix(e2e): use pnpm-capable reusable workflow The argocd plugin uses pnpm (packageManager: pnpm@10.32.1) but the inline workflow was using npm-based commands (npm ci, cache: npm). This caused 'Setup Node.js' to fail because setup-node@v6 with cache: npm has issues when no package-lock.json exists. Switch to the reusable plugin-e2e workflow which properly detects and uses pnpm for projects with pnpm-lock.yaml. --- .github/workflows/e2e.yaml | 82 +++----------------------------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 689e4fd..3f95455 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -14,82 +14,10 @@ concurrency: group: e2e-${{ github.repository }} cancel-in-progress: false -env: - E2E_NAMESPACE: headlamp-dev - E2E_RELEASE: headlamp-e2e-argocd - HEADLAMP_VERSION: v0.40.1 - jobs: e2e: - runs-on: runners-privilegedescalation - timeout-minutes: 15 - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '22' - cache: 'npm' - - - name: Setup kubectl - uses: azure/setup-kubectl@v4 - - - name: Install dependencies - run: npm ci - - - name: Build plugin - run: npx @kinvolk/headlamp-plugin build - - - name: Deploy E2E Headlamp instance - run: scripts/deploy-e2e-headlamp.sh - - - name: Load E2E environment - run: | - if [ -f .env.e2e ]; then - cat .env.e2e >> "$GITHUB_ENV" - else - echo "::error::deploy-e2e-headlamp.sh did not produce .env.e2e" - exit 1 - fi - - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - - name: Run E2E tests - run: npm run e2e - env: - HEADLAMP_URL: ${{ env.HEADLAMP_URL }} - HEADLAMP_TOKEN: ${{ env.HEADLAMP_TOKEN }} - - - name: Collect deployment diagnostics on failure - if: failure() - run: | - echo "=== Pod state ===" - kubectl get pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true - echo "=== Pod describe ===" - kubectl describe pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true - echo "=== Recent namespace events ===" - kubectl get events -n "$E2E_NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -20 || true - - - name: Teardown E2E instance - if: always() - run: scripts/teardown-e2e-headlamp.sh - - - name: Upload Playwright report - uses: actions/upload-artifact@v7 - if: failure() - with: - name: playwright-report - path: playwright-report/ - retention-days: 7 - - - name: Upload test results - uses: actions/upload-artifact@v7 - if: failure() - with: - name: test-results - path: test-results/ - retention-days: 7 \ No newline at end of file + 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 \ No newline at end of file -- 2.52.0 From fe158777ceb9fa0e0d01a66c71c82297b4f555a5 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Tue, 5 May 2026 15:19:19 +0000 Subject: [PATCH 09/14] fix(e2e): use pnpm with proper detection and E2E_RELEASE config The argocd-plugin uses pnpm (packageManager: pnpm@10.32.1) but the original inline workflow used npm commands (npm ci, cache: npm). This caused the workflow to fail. Switch to pnpm detection and commands while preserving the E2E_RELEASE=headlamp-e2e-argocd setting required by the deploy script. --- .github/workflows/e2e.yaml | 137 +++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 3f95455..460442c 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -14,10 +14,137 @@ concurrency: group: e2e-${{ github.repository }} cancel-in-progress: false +env: + E2E_NAMESPACE: headlamp-dev + E2E_RELEASE: headlamp-e2e-argocd + HEADLAMP_VERSION: v0.40.1 + 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 \ No newline at end of file + runs-on: runners-privilegedescalation + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Detect package manager + id: pkg-manager + run: | + if [ -f "pnpm-lock.yaml" ]; then + echo "manager=pnpm" >> $GITHUB_OUTPUT + PM=$(python3 -c "import json,sys; d=json.load(open('package.json')); print('true' if d.get('packageManager','').startswith('pnpm@') else 'false')" 2>/dev/null || echo "false") + echo "has_package_manager=$PM" >> $GITHUB_OUTPUT + else + echo "manager=npm" >> $GITHUB_OUTPUT + echo "has_package_manager=false" >> $GITHUB_OUTPUT + fi + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }} + + - name: Setup pnpm (Corepack, respects packageManager field) + if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'true' + run: | + npm install -g corepack + corepack enable pnpm + corepack prepare $(node -p "require('./package.json').packageManager") --activate + + - name: Setup pnpm (version latest, no packageManager field) + if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'false' + uses: pnpm/action-setup@v5 + with: + run_install: false + version: latest + + - name: Get pnpm store directory + id: pnpm-store + if: steps.pkg-manager.outputs.manager == 'pnpm' + run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + if: steps.pkg-manager.outputs.manager == 'pnpm' + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-store.outputs.dir }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Setup kubectl + uses: azure/setup-kubectl@v4 + + - name: Install dependencies + run: | + if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then + pnpm install --frozen-lockfile + else + npm ci + fi + + - name: Build plugin + run: npx @kinvolk/headlamp-plugin build + + - name: Deploy E2E Headlamp instance + run: scripts/deploy-e2e-headlamp.sh + + - name: Load E2E environment + run: | + if [ -f .env.e2e ]; then + cat .env.e2e >> "$GITHUB_ENV" + else + echo "::error::deploy-e2e-headlamp.sh did not produce .env.e2e" + exit 1 + fi + + - name: Install Playwright browsers + run: | + if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then + pnpm exec playwright install --with-deps chromium + else + npx playwright install --with-deps chromium + fi + + - name: Run E2E tests + run: | + if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then + pnpm run e2e + else + npm run e2e + fi + env: + HEADLAMP_URL: ${{ env.HEADLAMP_URL }} + HEADLAMP_TOKEN: ${{ env.HEADLAMP_TOKEN }} + + - name: Collect deployment diagnostics on failure + if: failure() + run: | + echo "=== Pod state ===" + kubectl get pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true + echo "=== Pod describe ===" + kubectl describe pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true + echo "=== Recent namespace events ===" + kubectl get events -n "$E2E_NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -20 || true + + - name: Teardown E2E instance + if: always() + run: scripts/teardown-e2e-headlamp.sh + + - name: Upload Playwright report + uses: actions/upload-artifact@v7 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload test results + uses: actions/upload-artifact@v7 + if: failure() + with: + name: test-results + path: test-results/ + retention-days: 7 \ No newline at end of file -- 2.52.0 From 5931220ee3983c14d59179d373d768ebcd23fe56 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 6 May 2026 13:30:07 +0000 Subject: [PATCH 10/14] fix(e2e): set executable bit on deploy/teardown scripts Scripts were committed as 100644 which causes exit code 126 (Permission denied) when the CI runner tries to execute them. Co-Authored-By: Paperclip --- scripts/deploy-e2e-headlamp.sh | 0 scripts/teardown-e2e-headlamp.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/deploy-e2e-headlamp.sh mode change 100644 => 100755 scripts/teardown-e2e-headlamp.sh diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh old mode 100644 new mode 100755 diff --git a/scripts/teardown-e2e-headlamp.sh b/scripts/teardown-e2e-headlamp.sh old mode 100644 new mode 100755 -- 2.52.0 From 9143847019c350751dda746b2d2d0c42483915a1 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 6 May 2026 13:45:45 +0000 Subject: [PATCH 11/14] fix(e2e): add kubeconfig setup, RBAC, kubectl logs diagnostics - Add Get kubeconfig step (matches polaris pattern for ARC runners) - Add Apply RBAC for E2E pipeline step + deployment/e2e-ci-runner-rbac.yaml - Pin kubectl to latest (addresses azure/setup-kubectl@v4 Node.js 20 warning) - Add kubectl logs (current + previous) to failure diagnostics so pod crash root cause is visible in CI output Co-Authored-By: Paperclip --- .github/workflows/e2e.yaml | 70 +++++++++++++++++++++++++++++- deployment/e2e-ci-runner-rbac.yaml | 36 +++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 deployment/e2e-ci-runner-rbac.yaml diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 460442c..4a83de2 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -10,6 +10,9 @@ on: permissions: contents: read +# Only one E2E run at a time — the shared E2E_RELEASE in headlamp-dev cannot +# be shared across concurrent runs. cancel-in-progress: false queues rather +# than cancels to avoid skipping the teardown step. concurrency: group: e2e-${{ github.repository }} cancel-in-progress: false @@ -76,6 +79,65 @@ jobs: - name: Setup kubectl uses: azure/setup-kubectl@v4 + with: + version: 'latest' + + - name: Get kubeconfig + run: | + set -euo pipefail + echo "=== Runner kubeconfig diagnostic ===" + echo "KUBECONFIG=${KUBECONFIG:-}" + for path in /runner/config /home/runner/.kube/config "${HOME:-}/.kube/config"; do + if [ -f "$path" ]; then + echo "FOUND kubeconfig at: $path" + fi + done + echo "" + echo "=== In-cluster service account check ===" + in_cluster=false + if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then + echo "Service account token present — in-cluster mode available" + in_cluster=true + fi + if [ -f /runner/config ]; then + echo "KUBECONFIG=/runner/config" >> "$GITHUB_ENV" + elif [ -f /home/runner/.kube/config ]; then + echo "KUBECONFIG=/home/runner/.kube/config" >> "$GITHUB_ENV" + elif [ -f "${HOME:-}/.kube/config" ]; then + echo "KUBECONFIG=${HOME:-}/.kube/config" >> "$GITHUB_ENV" + elif [ "$in_cluster" = true ]; then + echo "No static kubeconfig found — generating in-cluster kubeconfig" + KUBECFG_DIR="${HOME:-}/.kube" + mkdir -p "$KUBECFG_DIR" + kubectl config set-cluster in-cluster \ + --server="https://${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc}:${KUBERNETES_SERVICE_PORT:-443}" \ + --certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + --embed-certs=true \ + --kubeconfig="$KUBECFG_DIR/config" + kubectl config set-credentials in-cluster \ + --token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ + --kubeconfig="$KUBECFG_DIR/config" + kubectl config set-context in-cluster \ + --cluster=in-cluster \ + --user=in-cluster \ + --kubeconfig="$KUBECFG_DIR/config" + kubectl config use-context in-cluster \ + --kubeconfig="$KUBECFG_DIR/config" + echo "KUBECONFIG=$KUBECFG_DIR/config" >> "$GITHUB_ENV" + else + echo "::error::No kubeconfig found" + exit 1 + fi + + - name: Apply RBAC for E2E pipeline + run: | + set -x + kubectl apply -f deployment/e2e-ci-runner-rbac.yaml + echo "Waiting for RBAC propagation..." + sleep 5 + kubectl get role e2e-ci-runner -n headlamp-dev + kubectl get rolebinding e2e-ci-runner-binding -n headlamp-dev 2>&1 | tail -3 || true + set +x - name: Install dependencies run: | @@ -126,6 +188,12 @@ jobs: kubectl get pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true echo "=== Pod describe ===" kubectl describe pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true + echo "=== Container logs (current) ===" + kubectl logs -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" \ + --tail=100 2>&1 || true + echo "=== Container logs (previous, if crashed) ===" + kubectl logs -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" \ + --previous --tail=100 2>&1 || true echo "=== Recent namespace events ===" kubectl get events -n "$E2E_NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -20 || true @@ -147,4 +215,4 @@ jobs: with: name: test-results path: test-results/ - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/deployment/e2e-ci-runner-rbac.yaml b/deployment/e2e-ci-runner-rbac.yaml new file mode 100644 index 0000000..67bea4c --- /dev/null +++ b/deployment/e2e-ci-runner-rbac.yaml @@ -0,0 +1,36 @@ +--- +# RBAC for the GitHub Actions CI runner to manage the E2E Headlamp instance. +# CI-only test fixture — NOT for production use. +# +# Grants the ARC runner service account permissions in the headlamp-dev +# namespace to deploy and tear down a dedicated Headlamp instance. +# E2E resources run in `headlamp-dev` — nothing persists beyond a test run. +# +# Plugin is loaded via ConfigMap volume mount — no custom Docker images. +# +# Note: This RBAC is mirrored in privilegedescalation/infra (base/rbac/) +# and managed by Flux GitOps. The infra repo is the source of truth. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: e2e-ci-runner + namespace: headlamp-dev +rules: + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "create", "update", "patch", "delete", "watch"] + - apiGroups: [""] + resources: ["services", "serviceaccounts", "configmaps", "secrets", "events"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] + - apiGroups: [""] + resources: ["serviceaccounts/token"] + verbs: ["create"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings"] + verbs: ["get", "list", "create", "update", "patch", "delete"] -- 2.52.0 From 15069c368a7a0181502bc2498c161c3b0afc7d49 Mon Sep 17 00:00:00 2001 From: "privilegedescalation-ceo[bot]" <269721483+privilegedescalation-ceo[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 13:46:56 +0000 Subject: [PATCH 12/14] ci(e2e): trigger PR workflow check via API commit -- 2.52.0 From 420504a43d674460595e69d88171c3a4f30d6747 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 6 May 2026 16:15:20 +0000 Subject: [PATCH 13/14] chore: trigger E2E workflow -- 2.52.0 From 5997fb5e7e5341cc6698807ee8cf8214db022098 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 6 May 2026 16:17:45 +0000 Subject: [PATCH 14/14] fix(e2e): remove pods/log from RBAC to fix escalation error The CI runner SA cannot grant permissions it doesn't hold. Adding pods/log caused: "attempting to grant RBAC permissions not currently held". Remove it to match the infra-managed Role. Co-Authored-By: Paperclip --- deployment/e2e-ci-runner-rbac.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/deployment/e2e-ci-runner-rbac.yaml b/deployment/e2e-ci-runner-rbac.yaml index 67bea4c..b5a0853 100644 --- a/deployment/e2e-ci-runner-rbac.yaml +++ b/deployment/e2e-ci-runner-rbac.yaml @@ -25,9 +25,6 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["pods/log"] - verbs: ["get"] - apiGroups: [""] resources: ["serviceaccounts/token"] verbs: ["create"] -- 2.52.0