diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml deleted file mode 100644 index 688cae3..0000000 --- a/.github/workflows/e2e.yaml +++ /dev/null @@ -1,201 +0,0 @@ -name: E2E Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -# Only one E2E run at a time: the shared E2E_RELEASE (headlamp-e2e) in -# headlamp-dev cannot be shared across concurrent runs. -# cancel-in-progress: false (queue, don't cancel) — cancelling in-flight -# runs may skip the if:always() teardown, leaving dangling cluster resources. -concurrency: - group: e2e-${{ github.repository }} - cancel-in-progress: false - -env: - E2E_NAMESPACE: headlamp-dev - E2E_RELEASE: headlamp-e2e - # Pin to a known-good Headlamp version. Using :latest is risky because - # the tag can change between CI runs, causing flaky failures when a newer - # image is pulled on some nodes but not others (IfNotPresent pull policy). - # Update this when Headlamp is upgraded in production (kube-system). - 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: Get kubeconfig - run: | - set -euo pipefail - echo "=== Runner environment diagnostic ===" - echo "HOME=${HOME:-}" - echo "KUBECONFIG=${KUBECONFIG:-}" - echo "ACTIONS_KUBECONFIG=${ACTIONS_KUBECONFIG:-}" - echo "RUNNER_CONFIG=${RUNNER_CONFIG:-}" - echo "RUNNER_CONFIG_DIR=${RUNNER_CONFIG_DIR:-}" - echo "" - echo "=== Checking known kubeconfig locations ===" - for path in /runner/config /home/runner/.kube/config "${HOME:-}/.kube/config" "${HOME:-}/.kube"; do - if [ -f "$path" ]; then - echo "FOUND kubeconfig at: $path" - elif [ -d "$path" ]; then - echo "DIR exists at: $path, contents:" - ls -la "$path" 2>&1 || echo " (cannot list)" - else - echo "NOT FOUND: $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" - echo "KUBERNETES_SERVICE_HOST=${KUBERNETES_SERVICE_HOST:-}" - echo "KUBERNETES_SERVICE_PORT=${KUBERNETES_SERVICE_PORT:-}" - in_cluster=true - else - echo "No service account token at /var/run/secrets/kubernetes.io/serviceaccount/" - fi - echo "" - if [ -f /runner/config ]; then - echo "KUBECONFIG=/runner/config" >> "$GITHUB_ENV" - echo "Using kubeconfig from /runner/config" - elif [ -f /home/runner/.kube/config ]; then - echo "KUBECONFIG=/home/runner/.kube/config" >> "$GITHUB_ENV" - echo "Using kubeconfig from /home/runner/.kube/config" - elif [ -f "${HOME:-}/.kube/config" ]; then - echo "KUBECONFIG=${HOME:-}/.kube/config" >> "$GITHUB_ENV" - echo "Using kubeconfig from HOME" - 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" 2>&1 - kubectl config set-credentials in-cluster \ - --token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ - --kubeconfig="$KUBECFG_DIR/config" 2>&1 - kubectl config set-context in-cluster \ - --cluster=in-cluster \ - --user=in-cluster \ - --kubeconfig="$KUBECFG_DIR/config" 2>&1 - kubectl config use-context in-cluster \ - --kubeconfig="$KUBECFG_DIR/config" 2>&1 - echo "KUBECONFIG=$KUBECFG_DIR/config" >> "$GITHUB_ENV" - echo "Generated in-cluster kubeconfig at $KUBECFG_DIR/config" - else - echo "::error::No kubeconfig found in /runner/config, /home/runner/.kube/config, HOME, or in-cluster service account" - exit 1 - fi - - - name: Apply RBAC for E2E pipeline - run: | - set -x - kubectl apply -f deployment/e2e-ci-runner-rbac.yaml --dry-run=server 2>&1 || true - kubectl apply -f deployment/e2e-ci-runner-rbac.yaml 2>&1 - echo "exit code: $?" - echo "Waiting for RBAC propagation..." - sleep 5 - echo "Verifying RBAC resources were created..." - kubectl get role e2e-ci-runner -n headlamp-dev 2>&1 | tail -3 - kubectl get role e2e-ci-runner-polaris -n headlamp-dev 2>&1 | tail -3 - kubectl get rolebinding e2e-ci-runner-binding -n headlamp-dev 2>&1 | tail -3 - set +x - - - name: Apply Polaris dashboard RBAC - run: kubectl apply -f deployment/polaris-rbac.yaml - - - name: RBAC pre-flight check - run: | - echo "Checking RBAC resources..." - MISSING=0 - kubectl get role polaris-dashboard-proxy-reader -n polaris -o name >/dev/null 2>&1 || MISSING=1 - kubectl get rolebinding polaris-dashboard-proxy-reader -n polaris -o name >/dev/null 2>&1 || MISSING=1 - kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" 2>/dev/null || MISSING=1 - if [ "$MISSING" -eq 0 ]; then - echo "RBAC pre-flight check passed." - else - echo "::error::RBAC pre-flight check failed. Missing required permissions." - exit 1 - fi - - - 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 diff --git a/.gitignore b/.gitignore index a022014..4b055d8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,6 @@ node_modules/ dist/ .headlamp-plugin/ *.tar.gz -e2e/.auth/ -test-results/ -.playwright-mcp/ .env -.env.e2e .env.local .eslintcache diff --git a/deployment/PLUGIN_LOADING_FIX.md b/deployment/PLUGIN_LOADING_FIX.md deleted file mode 100644 index e176a2b..0000000 --- a/deployment/PLUGIN_LOADING_FIX.md +++ /dev/null @@ -1,58 +0,0 @@ -# Headlamp Plugin Loading Issue - Root Cause and Fix - -## Problem -Headlamp v0.39.0 was not loading plugins installed via the plugin manager. Plugins appeared in Settings → Plugins but: -- No sidebar entries appeared -- No plugin settings were available -- Plugin JavaScript was not being executed in the browser - -## Root Cause -When `config.watchPlugins: true` (the default), Headlamp treats catalog-managed plugins in `/headlamp/plugins/` as "development directory" plugins. This causes: -- Backend serves plugin metadata correctly -- Backend logs show "Treating catalog-installed plugin in development directory as user plugin" -- **Frontend does NOT execute the plugin JavaScript** -- Plugin registrations (`registerSidebarEntry`, `registerRoute`, etc.) never happen - -## Solution -Set `config.watchPlugins: false` in the Headlamp HelmRelease values: - -```yaml -spec: - values: - config: - watchPlugins: false - pluginsManager: - enabled: true - configContent: | - plugins: - - name: polaris - source: https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin - # ... other plugins -``` - -## Why This Works -With `watchPlugins: false`: -- Headlamp no longer treats catalog-managed plugins as "development" plugins -- Frontend properly loads and executes plugin JavaScript on startup -- Plugin registrations happen correctly -- All plugin features (sidebar, routes, settings, etc.) work as expected - -## Testing -After applying this fix: -1. Verify plugins are installed: `kubectl logs -n kube-system -c headlamp-plugin` -2. Verify watchPlugins is false: `kubectl logs -n kube-system -c headlamp | grep "Watch Plugins"` -3. Hard refresh browser (Cmd+Shift+R / Ctrl+Shift+F5) to clear cached JavaScript -4. Verify plugin sidebar entries appear -5. Verify plugin functionality works - -## Additional Notes -- This appears to be a bug/limitation in Headlamp v0.39.0 -- The `watchPlugins` feature is intended for development scenarios where plugins are being actively modified -- For production deployments with catalog-managed plugins, `watchPlugins: false` is the correct configuration -- Once plugins are loaded, subsequent restarts or updates work correctly as long as `watchPlugins` remains false - -## References -- Headlamp Helm Chart: https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp -- Plugin Manager: https://github.com/headlamp-k8s/headlamp/tree/main/plugins/headlamp-plugin -- Issue discovered: 2026-02-11 -- Fix applied: 2026-02-12 diff --git a/deployment/e2e-ci-runner-rbac.yaml b/deployment/e2e-ci-runner-rbac.yaml deleted file mode 100644 index 069c5ee..0000000 --- a/deployment/e2e-ci-runner-rbac.yaml +++ /dev/null @@ -1,74 +0,0 @@ ---- -# 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 via Helm. -# 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: - # Helm needs to manage these resources for the Headlamp chart - - 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"] - # Token creation for E2E test auth - - apiGroups: [""] - resources: ["serviceaccounts/token"] - verbs: ["create"] - # Apply Polaris dashboard RBAC in the polaris namespace - - apiGroups: ["rbac.authorization.k8s.io"] - resources: ["roles", "rolebindings"] - verbs: ["get", "list", "create", "update", "patch", "delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: e2e-ci-runner-polaris - namespace: polaris -rules: - - apiGroups: ["rbac.authorization.k8s.io"] - resources: ["roles", "rolebindings"] - verbs: ["get", "list", "create", "update", "patch", "delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: e2e-ci-runner-polaris - namespace: polaris -subjects: - - kind: ServiceAccount - name: runners-privilegedescalation-gha-rs-no-permission - namespace: arc-runners -roleRef: - kind: Role - name: e2e-ci-runner-polaris - apiGroup: rbac.authorization.k8s.io ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: e2e-ci-runner-binding - namespace: headlamp-dev -subjects: - - kind: ServiceAccount - name: runners-privilegedescalation-gha-rs-no-permission - namespace: arc-runners -roleRef: - kind: Role - name: e2e-ci-runner - apiGroup: rbac.authorization.k8s.io diff --git a/deployment/polaris-rbac.yaml b/deployment/polaris-rbac.yaml deleted file mode 100644 index a3b3629..0000000 --- a/deployment/polaris-rbac.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# RBAC to allow authenticated users to proxy to the Polaris dashboard service. -# The polaris plugin reads audit data via the Kubernetes service proxy: -# /api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/results.json -# Without this Role + RoleBinding, users get a 403 when Headlamp proxies the request. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: polaris-dashboard-proxy-reader - namespace: polaris -rules: - - apiGroups: [""] - resources: ["services/proxy"] - resourceNames: ["polaris-dashboard", "http:polaris-dashboard:80"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: polaris-dashboard-proxy-reader - namespace: polaris -subjects: - - kind: Group - name: system:authenticated - apiGroup: rbac.authorization.k8s.io -roleRef: - kind: Role - name: polaris-dashboard-proxy-reader - apiGroup: rbac.authorization.k8s.io diff --git a/e2e/README.md b/e2e/README.md deleted file mode 100644 index b93d390..0000000 --- a/e2e/README.md +++ /dev/null @@ -1,303 +0,0 @@ -# E2E Smoke Tests - -Playwright-based smoke tests that validate the Polaris plugin against a live Headlamp deployment. - -## CI - -E2E tests run automatically in GitHub Actions on pushes to `main` and pull requests. The workflow (`.github/workflows/e2e.yaml`): - -1. Builds the plugin (`npm run build`) -2. Creates a ConfigMap from the built `dist/` output -3. Deploys a stock Headlamp instance via Helm with the plugin mounted as a ConfigMap volume -4. Generates a ServiceAccount token for test auth -5. Runs Playwright tests against the E2E instance -6. Tears down the E2E instance - -This approach uses the stock `ghcr.io/headlamp-k8s/headlamp` image with no custom Docker builds. The plugin is loaded via `HEADLAMP_PLUGINS_DIR` volume mount. - -### Required GitHub Secrets - -Configure these in GitHub repository settings (Settings → Secrets and variables → Actions): - -| Secret | Required | Description | -| -------------------- | -------- | -------------------------------------------------------------- | -| `AUTHENTIK_USERNAME` | OIDC | Authentik email or username for a CI user with Headlamp access | -| `AUTHENTIK_PASSWORD` | OIDC | Password for that user | - -Token-based auth is auto-generated by the deploy script. OIDC secrets are only needed if testing against the shared Headlamp instance. - -No `GHCR_TOKEN` or Docker registry secrets are needed — the stock Headlamp image is public. - -## Running Locally - -### Option 1: OIDC via Authentik (same as CI) - -```bash -AUTHENTIK_USERNAME=you@example.com AUTHENTIK_PASSWORD=... npm run e2e -``` - -The default base URL is `https://headlamp.animaniacs.farh.net`. Override with `HEADLAMP_URL` if needed. - -### Option 2: K8s bearer token (port-forward) - -```bash -kubectl port-forward -n kube-system svc/headlamp 4466:80 -export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system) -HEADLAMP_URL=http://localhost:4466 npm run e2e -``` - -Or in headed mode (opens a browser window): - -```bash -HEADLAMP_URL=http://localhost:4466 npm run e2e:headed -``` - -## Environment Variables - -| Variable | Required | Default | Description | -| -------------------- | -------- | -------------------------------------- | --------------------------------------- | -| `HEADLAMP_URL` | No | `https://headlamp.animaniacs.farh.net` | Base URL of the Headlamp instance | -| `AUTHENTIK_USERNAME` | OIDC | — | Authentik email/username | -| `AUTHENTIK_PASSWORD` | OIDC | — | Authentik password | -| `HEADLAMP_TOKEN` | Token | — | Kubernetes bearer token (auto-generated in CI) | - -In CI, `HEADLAMP_URL` and `HEADLAMP_TOKEN` are set automatically by the deploy script. For local runs, set either OIDC credentials or a token manually. - -## What the Tests Validate - -- **Sidebar entry** — The Polaris sidebar item appears after login -- **Overview page** — Cluster score and check distribution render correctly -- **Namespaces page** — Table of namespaces loads with clickable links -- **Namespace detail** — Clicking a namespace shows its score and resource table - -These are smoke tests against real cluster data. They verify the plugin loads and renders without errors, not specific data values. - -## Test Coverage - -### Current Tests (`polaris.spec.ts`) - -1. **`sidebar contains Polaris entry`** - - Verifies Polaris appears in the navigation sidebar - - Ensures plugin successfully registered sidebar entry - -2. **`overview page renders cluster score`** - - Navigates to `/c/main/polaris` - - Checks for "Polaris — Overview" heading - - Verifies cluster score percentage is displayed - - Validates data fetching and rendering - -3. **`namespaces page renders table with namespace buttons`** - - Navigates to `/c/main/polaris/namespaces` - - Checks for "Polaris — Namespaces" heading - - Verifies table is visible with at least one row - - Ensures namespace buttons are clickable - -4. **`namespace detail drawer opens from table button`** - - Clicks first namespace button in table - - Verifies drawer opens with namespace name in heading - - Checks "Namespace Score" section is visible - - Confirms "Resources" table is displayed - - Validates URL hash is updated with namespace name - -5. **`namespace detail drawer closes with Escape key`** - - Opens namespace drawer - - Presses Escape key - - Verifies drawer closes - - Checks URL hash is cleared - -6. **`namespace detail drawer opens from URL hash`** - - Navigates directly to `/c/main/polaris/namespaces#` - - Verifies drawer automatically opens - - Checks namespace details are displayed - -## Prerequisites - -### Cluster Requirements - -1. **Polaris Deployment** - ```bash - # Verify Polaris is running - kubectl -n polaris get pods - kubectl -n polaris get svc polaris-dashboard - ``` - -2. **Polaris Audit Data** - ```bash - # Check if Polaris has generated audit results - kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq '.AuditTime' - ``` - -3. **RBAC Permissions** - - Headlamp service account (or test user) needs `get` on `services/proxy` for `polaris-dashboard` - - See main README for RBAC setup - -### Local Setup - -```bash -# 1. Install dependencies -npm install -npx playwright install chromium - -# 2. Create .env file (optional, for persistent config) -cp .env.example .env - -# 3. Set environment variables -export HEADLAMP_URL=https://your-headlamp-instance.com -export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system) - -# 4. Run tests -npm run e2e -``` - -## Debugging - -### Run in Headed Mode - -See the browser UI while tests run: - -```bash -npm run e2e:headed -``` - -### Enable Debug Mode - -Step through tests with Playwright Inspector: - -```bash -npx playwright test --debug -``` - -### Generate Trace - -Record full trace for failed tests: - -```bash -npx playwright test --trace on -npx playwright show-trace test-results//trace.zip -``` - -### Screenshot on Failure - -Tests automatically capture screenshots on failure in `test-results/` - -### Common Issues - -**Auth fails with "Sign In button not found":** -- Check HEADLAMP_URL is correct -- Verify Headlamp is accessible -- Ensure OIDC is configured if using Authentik - -**Polaris sidebar entry not found:** -- Plugin may not be installed: Check Settings → Plugins in Headlamp -- Plugin may have failed to load: Check browser console -- Clear browser cache and hard refresh - -**Cluster score not displayed:** -- Polaris may not have audit data yet -- Check Polaris is running: `kubectl -n polaris get pods` -- Verify service proxy: `kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json` - -**Namespace table empty:** -- Polaris hasn't run audit yet (wait a few minutes) -- Check Polaris logs: `kubectl -n polaris logs -l app.kubernetes.io/name=polaris` - -## Writing New Tests - -### Example: Testing Plugin Settings - -```typescript -test('plugin settings page shows Polaris configuration', async ({ page }) => { - await page.goto('/c/main/settings/plugins'); - - // Find and click Polaris plugin - await page.getByText('headlamp-polaris-plugin').click(); - - // Check settings are visible - await expect(page.getByText('Polaris Settings')).toBeVisible(); - await expect(page.getByText('Refresh Interval')).toBeVisible(); - await expect(page.getByText('Dashboard URL')).toBeVisible(); -}); -``` - -### Example: Testing App Bar Badge - -```typescript -test('app bar displays Polaris score badge', async ({ page }) => { - await page.goto('/c/main'); - - // Badge should be visible in app bar - const badge = page.getByRole('button', { name: /Polaris: \d+%/ }); - await expect(badge).toBeVisible(); - - // Clicking should navigate to overview - await badge.click(); - await expect(page).toHaveURL(/\/c\/main\/polaris$/); -}); -``` - -### Example: Testing Dark Mode - -```typescript -test('plugin UI adapts to dark mode', async ({ page }) => { - await page.goto('/c/main/polaris'); - - // Toggle dark mode - await page.getByRole('button', { name: /theme/i }).click(); - - // Check background color changes - const body = page.locator('body'); - await expect(body).toHaveCSS('background-color', 'rgb(18, 18, 18)'); - - // Plugin components should adapt - const sectionBox = page.locator('[class*="MuiPaper"]').first(); - await expect(sectionBox).not.toHaveCSS('background-color', 'rgb(255, 255, 255)'); -}); -``` - -## CI/CD Integration - -Tests run automatically in GitHub Actions on pushes to `main` and pull requests. See `.github/workflows/e2e.yaml` for workflow configuration. - -### Architecture - -The E2E workflow deploys a **dedicated Headlamp instance** for each test run: - -1. Build plugin (`npm run build`) -2. Create ConfigMap from `dist/` output (`scripts/deploy-e2e-headlamp.sh`) -3. Deploy stock Headlamp via Helm with ConfigMap volume mount -4. Run Playwright tests against the E2E instance -5. Tear down (`scripts/teardown-e2e-headlamp.sh`) - -No custom Docker images, no PVCs, no kubectl exec/cp, no patching of existing deployments. The plugin is mounted from a ConfigMap into the stock Headlamp image. - -### Cluster Prerequisites - -One-time setup by a cluster admin: - -```bash -kubectl apply -f deployment/e2e-ci-runner-rbac.yaml -``` - -### Manual Trigger - -You can manually trigger E2E tests from GitHub Actions: -1. Go to Actions → E2E Tests -2. Click "Run workflow" -3. Select branch and run - -## Best Practices - -1. **Use semantic selectors**: `getByRole`, `getByText` over CSS selectors -2. **Wait for visibility**: Use `await expect(...).toBeVisible()` instead of `waitForTimeout` -3. **Keep tests independent**: Each test should work in isolation -4. **Test user flows**: Complete journeys, not just page loads -5. **Clean up state**: Close drawers/modals after tests -6. **Use storage state**: Reuse auth across tests (already configured) -7. **Parallelize carefully**: Currently disabled due to shared state - -## Resources - -- [Playwright Documentation](https://playwright.dev/) -- [Playwright Best Practices](https://playwright.dev/docs/best-practices) -- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/) -- [Project Main README](../README.md) diff --git a/e2e/appbar.spec.ts b/e2e/appbar.spec.ts deleted file mode 100644 index ccb4788..0000000 --- a/e2e/appbar.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Polaris app bar badge', () => { - test('badge displays cluster score in app bar', async ({ page }) => { - await page.goto('/c/main'); - - // Wait for page to load - await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible(); - - // Badge should be visible in app bar with score percentage - const badge = page.getByRole('button', { name: /Polaris: \d+%/ }); - await expect(badge).toBeVisible({ timeout: 15_000 }); - - // Badge should show shield emoji - await expect(badge).toContainText('🛡️'); - }); - - test('clicking badge navigates to overview page', async ({ page }) => { - await page.goto('/c/main'); - - // Find and click the badge - const badge = page.getByRole('button', { name: /Polaris: \d+%/ }); - await expect(badge).toBeVisible({ timeout: 15_000 }); - await badge.click(); - - // Should navigate to Polaris overview - await expect(page).toHaveURL(/\/c\/main\/polaris$/); - await expect(page.getByRole('heading', { name: 'Polaris — Overview' })).toBeVisible(); - }); - - test('badge color reflects score level', async ({ page }) => { - await page.goto('/c/main'); - - // Get the badge - const badge = page.getByRole('button', { name: /Polaris: \d+%/ }); - await expect(badge).toBeVisible({ timeout: 15_000 }); - - // Extract score from button text - const badgeText = await badge.textContent(); - const scoreMatch = badgeText?.match(/(\d+)%/); - expect(scoreMatch).toBeTruthy(); - - const score = parseInt(scoreMatch![1]); - - // Check background color matches score level - const bgColor = await badge.evaluate(el => - window.getComputedStyle(el).backgroundColor - ); - - // Verify that the badge has a non-default background color applied - // (theme-dependent RGB values vary across Headlamp versions, so we - // only assert that a real color is set rather than transparent/default) - expect(bgColor).not.toBe('rgba(0, 0, 0, 0)'); - expect(bgColor).not.toBe('transparent'); - expect(bgColor).toMatch(/^rgb/); - }); - - test('badge updates when navigating between clusters', async ({ page }) => { - // This test assumes multi-cluster setup; skip if only one cluster - await page.goto('/c/main'); - - // Get initial badge score - const badge = page.getByRole('button', { name: /Polaris: \d+%/ }); - await expect(badge).toBeVisible({ timeout: 15_000 }); - const initialScore = await badge.textContent(); - - // Try to switch clusters (if available) - const clusterSelector = page.getByRole('button', { name: /cluster/i }); - if (await clusterSelector.isVisible()) { - // Note: This part will only work in multi-cluster setups - // For single-cluster, this test will just verify badge persists - await clusterSelector.click(); - - // Select different cluster if available - const clusterOptions = page.getByRole('menuitem'); - const count = await clusterOptions.count(); - - if (count > 1) { - await clusterOptions.nth(1).click(); - - // Badge should update or disappear (if new cluster doesn't have Polaris) - // This is just verifying no crash occurs - await page.waitForTimeout(2000); - } - } - - // Badge should still be functional - await expect(badge).toBeEnabled(); - }); -}); diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts deleted file mode 100644 index 2b4ecb9..0000000 --- a/e2e/auth.setup.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { test as setup, expect, Page } from '@playwright/test'; - -const AUTH_STATE_PATH = 'e2e/.auth/state.json'; - -async function authenticateWithOIDC(page: Page, username: string, password: string): Promise { - // Navigate to login — Headlamp redirects / to /c/main/login - await page.goto('/'); - await page.waitForURL('**/login'); - - // Click "Sign In" and capture the Authentik popup - const popupPromise = page.waitForEvent('popup'); - await page.getByRole('button', { name: /sign in/i }).click(); - const popup = await popupPromise; - - // Wait for the Authentik popup to fully load before interacting - await popup.waitForLoadState('domcontentloaded'); - await popup.waitForLoadState('networkidle'); - - // Authentik step 1: fill username — wait for the form to render - const usernameField = popup.getByRole('textbox', { name: /email or username/i }); - await usernameField.waitFor({ state: 'visible', timeout: 15_000 }); - await usernameField.fill(username); - await popup.getByRole('button', { name: /log in/i }).click(); - - // Authentik step 2: fill password — wait for the next step to load - await popup.waitForLoadState('networkidle'); - const passwordField = popup.getByRole('textbox', { name: /password/i }); - await passwordField.waitFor({ state: 'visible', timeout: 15_000 }); - await passwordField.fill(password); - await popup.getByRole('button', { name: /continue|log in/i }).click(); - - // Wait for the popup to close (Authentik redirects back, Headlamp processes callback) - await popup.waitForEvent('close', { timeout: 15_000 }); - - // Original page should now be authenticated — wait for sidebar - await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({ - timeout: 15_000, - }); -} - -async function authenticateWithToken(page: Page, token: string): Promise { - await page.goto('/'); - // Headlamp goes to /token directly when no OIDC is configured, - // or through /login when OIDC is configured - await page.waitForURL(/\/(login|token)$/); - - if (page.url().includes('/login')) { - // OIDC login page — click "use a token" to reach token auth. - // Wait explicitly before clicking so failures surface at 15 s - // with a clear message rather than silently timing out at 60 s. - 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'); - } - - // Fill the "ID token" field and submit - await page.getByRole('textbox', { name: /id token/i }).fill(token); - await page.getByRole('button', { name: /authenticate/i }).click(); - - // Wait for the main UI to load - await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({ - timeout: 15_000, - }); -} - -setup('authenticate with Headlamp', async ({ page }) => { - const username = process.env.AUTHENTIK_USERNAME; - const password = process.env.AUTHENTIK_PASSWORD; - const token = process.env.HEADLAMP_TOKEN; - - if (username && password) { - await authenticateWithOIDC(page, username, password); - } else if (token) { - await authenticateWithToken(page, token); - } else { - throw new Error( - 'Set AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD for OIDC auth, or HEADLAMP_TOKEN for token auth' - ); - } - - await page.context().storageState({ path: AUTH_STATE_PATH }); -}); diff --git a/e2e/polaris.spec.ts b/e2e/polaris.spec.ts deleted file mode 100644 index 1d6df99..0000000 --- a/e2e/polaris.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Polaris plugin smoke tests', () => { - test('sidebar contains Polaris entry', async ({ page }) => { - await page.goto('/'); - // The sidebar is the "Navigation" nav element (not "Appbar Tools") - const sidebar = page.getByRole('navigation', { name: 'Navigation' }); - await expect(sidebar).toBeVisible({ timeout: 15_000 }); - await expect(sidebar.getByRole('button', { name: 'Polaris' })).toBeVisible(); - }); - - test('overview page renders cluster score', async ({ page }) => { - await page.goto('/c/main/polaris'); - - // SectionHeader renders a heading - await expect(page.getByRole('heading', { name: 'Polaris \u2014 Overview' })).toBeVisible(); - - // "Cluster Score" section exists with a percentage - await expect(page.getByText('Cluster Score')).toBeVisible(); - await expect(page.locator('main').getByText(/%/).first()).toBeVisible(); - }); - - test('namespaces page renders table with namespace buttons', async ({ page }) => { - await page.goto('/c/main/polaris/namespaces'); - - await expect(page.getByRole('heading', { name: 'Polaris \u2014 Namespaces' })).toBeVisible(); - - // Table should have at least one row with a namespace button - const table = page.locator('table'); - await expect(table).toBeVisible(); - const rows = table.locator('tbody tr'); - await expect(rows.first()).toBeVisible(); - - // Each namespace row should contain a button (now buttons instead of links for drawer) - const firstButton = rows.first().locator('button'); - await expect(firstButton).toBeVisible(); - }); - - test('namespace detail drawer opens from table button', async ({ page }) => { - await page.goto('/c/main/polaris/namespaces'); - - // Click the first namespace button in the table - const table = page.locator('table'); - await expect(table).toBeVisible(); - const firstButton = table.locator('tbody tr').first().locator('button'); - const namespaceName = await firstButton.textContent(); - await firstButton.click(); - - // Drawer should open and show the namespace name in the heading - await expect( - page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` }) - ).toBeVisible(); - - // "Namespace Score" section should be present in drawer - await expect(page.getByText('Namespace Score')).toBeVisible(); - - // Resources table should exist in drawer - await expect(page.getByRole('heading', { name: 'Resources' })).toBeVisible(); - - // URL hash should be updated with namespace name - await expect(page).toHaveURL(/\/polaris\/namespaces#/); - }); - - test('namespace detail drawer closes with Escape key', async ({ page }) => { - await page.goto('/c/main/polaris/namespaces'); - - // Open the drawer by clicking a namespace button - const table = page.locator('table'); - await expect(table).toBeVisible(); - const firstButton = table.locator('tbody tr').first().locator('button'); - const namespaceName = await firstButton.textContent(); - await firstButton.click(); - - // Verify drawer is open - await expect( - page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` }) - ).toBeVisible(); - - // Press Escape key - await page.keyboard.press('Escape'); - - // Drawer should close (heading should not be visible anymore) - await expect( - page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` }) - ).not.toBeVisible(); - - // URL hash should be cleared - await expect(page).toHaveURL(/\/polaris\/namespaces$/); - }); - - test('namespace detail drawer opens from URL hash', async ({ page }) => { - // Get a namespace name first - await page.goto('/c/main/polaris/namespaces'); - const table = page.locator('table'); - await expect(table).toBeVisible(); - const firstButton = table.locator('tbody tr').first().locator('button'); - const namespaceName = await firstButton.textContent(); - - // Navigate directly to URL with hash - await page.goto(`/c/main/polaris/namespaces#${namespaceName}`); - - // Drawer should automatically open with the namespace details - await expect( - page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` }) - ).toBeVisible(); - - // "Namespace Score" section should be present - await expect(page.getByText('Namespace Score')).toBeVisible(); - }); -}); diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts deleted file mode 100644 index 333a0b4..0000000 --- a/e2e/settings.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { test, expect, Page } from '@playwright/test'; - -/** Navigate to the Polaris plugin settings page and wait for settings to render. */ -async function goToPolarisSettings(page: Page) { - // Headlamp's plugin settings page is a HOME-context route at /settings/plugins, - // not an in-cluster route (/c/main/settings/plugins would 404). Headlamp loads - // plugin scripts asynchronously on SPA init. When registerPluginSettings() fires, - // it dispatches a Redux action — PluginSettings uses useTypedSelector so it - // re-renders automatically once the plugin registers. No preloading needed. - await page.goto('/settings/plugins'); - - // Wait for the plugin to appear in the settings list. The timeout covers - // async plugin script loading + registration. - const pluginEntry = page.locator('text=headlamp-polaris').first(); - await expect(pluginEntry).toBeVisible({ timeout: 30_000 }); - await pluginEntry.click(); - - // Wait for the PolarisSettings component to render - await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 }); -} - -test.describe('Polaris plugin settings', () => { - test('settings page shows configuration options', async ({ page }) => { - await goToPolarisSettings(page); - - // SectionBox title should be visible - await expect(page.getByText('Polaris Settings')).toBeVisible(); - }); - - test('refresh interval setting is configurable', async ({ page }) => { - await goToPolarisSettings(page); - - // Find the refresh interval dropdown - const intervalSelect = page.locator('select').filter({ hasText: /minute|second/ }); - await expect(intervalSelect).toBeVisible(); - - // Get current value - const currentValue = await intervalSelect.inputValue(); - - // Change to a different value - const newValue = currentValue === '300' ? '600' : '300'; - await intervalSelect.selectOption(newValue); - - // Value should be updated - await expect(intervalSelect).toHaveValue(newValue); - }); - - test('dashboard URL setting is configurable', async ({ page }) => { - await goToPolarisSettings(page); - - // Find the dashboard URL input - const urlInput = page.getByPlaceholder(/polaris-dashboard/); - await expect(urlInput).toBeVisible(); - - // Input should have the default proxy URL or custom URL - const currentUrl = await urlInput.inputValue(); - expect(currentUrl).toBeTruthy(); - - // Examples text should be visible - await expect(page.getByText('Examples:')).toBeVisible(); - await expect(page.getByText(/K8s proxy:/)).toBeVisible(); - }); - - test('connection test button is available', async ({ page }) => { - await goToPolarisSettings(page); - - // Find and verify test connection button - const testButton = page.getByRole('button', { name: /test connection/i }); - await expect(testButton).toBeVisible(); - await expect(testButton).toBeEnabled(); - }); - - test('connection test works with valid URL', async ({ page }) => { - await goToPolarisSettings(page); - - // Click test connection - const testButton = page.getByRole('button', { name: /test connection/i }); - await testButton.click(); - - // Wait for either success or error message - // Note: This will succeed if Polaris is accessible, fail otherwise - await page.waitForSelector('text=/Connected successfully|Connection failed/', { - timeout: 15_000, - }); - - // Either success or failure is acceptable (depends on environment) - const result = await page.textContent('body'); - expect(result).toMatch(/(Connected successfully|Connection failed)/); - }); -}); diff --git a/package.json b/package.json index 059fa7d..9ed9baa 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "format": "prettier --write src/", "format:check": "prettier --check src/", "test": "vitest run", - "test:watch": "vitest", - "e2e": "playwright test", - "e2e:headed": "playwright test --headed" + "test:watch": "vitest" }, "peerDependencies": { "react": "^18.0.0", @@ -45,7 +43,6 @@ "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", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 16808ba..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 || 'https://headlamp.animaniacs.farh.net', - 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'], - }, - ], -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aff7413..92e2f19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ 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.58.2 '@testing-library/jest-dom': specifier: ^6.4.8 version: 6.9.1 @@ -876,11 +873,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} - engines: {node: '>=18'} - hasBin: true - '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -3048,11 +3040,6 @@ 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} @@ -4258,16 +4245,6 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} - engines: {node: '>=18'} - hasBin: true - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -6185,7 +6162,7 @@ snapshots: jsdom: 24.1.3 jsonpath-plus: 10.4.0 lodash: 4.18.1 - material-react-table: 2.13.3(330725fe5432f245d076f0c0dda1a7a7) + material-react-table: 2.13.3(0078ddeddc9e779fa84c03996c1db10e) 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.2)) @@ -6592,10 +6569,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.58.2': - dependencies: - playwright: 1.58.2 - '@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)': @@ -9099,9 +9072,6 @@ snapshots: fs.realpath@1.0.0: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -9897,7 +9867,7 @@ snapshots: '@types/minimatch': 3.0.5 minimatch: 3.1.5 - material-react-table@2.13.3(330725fe5432f245d076f0c0dda1a7a7): + material-react-table@2.13.3(0078ddeddc9e779fa84c03996c1db10e): 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) @@ -10529,14 +10499,6 @@ snapshots: dependencies: find-up: 5.0.0 - playwright-core@1.58.2: {} - - playwright@1.58.2: - dependencies: - playwright-core: 1.58.2 - optionalDependencies: - fsevents: 2.3.2 - possible-typed-array-names@1.1.0: {} postcss-modules-extract-imports@3.1.0(postcss@8.5.8): diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh deleted file mode 100755 index 8314b7d..0000000 --- a/scripts/deploy-e2e-headlamp.sh +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env bash -# deploy-e2e-headlamp.sh -# -# Deploys a stock Headlamp instance with the polaris plugin loaded via -# a ConfigMap volume mount. No custom Docker images — the plugin is built -# in CI and injected as a ConfigMap. -# -# E2E resources are deployed to the `headlamp-dev` namespace. Nothing -# persists beyond a test run — teardown cleans up all created resources. -# -# Prerequisites: -# - Plugin built (dist/ exists with plugin-main.js + package.json) -# - kubectl configured with cluster access -# - RBAC applied (managed by Flux GitOps in privilegedescalation/infra) -# -# 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: v0.40.1, pinned to match production) -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:-v0.40.1}" - -if [ ! -d "$DIST_DIR" ]; then - echo "ERROR: dist/ not found. Run 'npm run build' first." >&2 - exit 1 -fi - -# --- Preflight: verify RBAC before touching the cluster --- -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 - echo " Apply RBAC first: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml" >&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" - -# --- Create ConfigMap from built plugin --- -echo "" -echo "Creating ConfigMap with plugin files..." - -# Delete existing ConfigMap if present (idempotent redeploy) -kubectl delete configmap headlamp-polaris-plugin \ - -n "$E2E_NAMESPACE" --ignore-not-found - -# Create ConfigMap from dist/ contents and package.json -kubectl create configmap headlamp-polaris-plugin \ - -n "$E2E_NAMESPACE" \ - --from-file="$DIST_DIR" \ - --from-file=package.json="$REPO_ROOT/package.json" - -# --- Tear down any existing E2E deployment for a clean start --- -# kubectl apply without prior deletion only patches in-place: if the pod spec is -# unchanged between runs, no new rollout is triggered and a degraded pod keeps -# serving. Delete first to guarantee a fresh pod regardless of prior state. -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 - -# --- Deploy Headlamp via kubectl apply --- -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 " export HEADLAMP_URL=${SVC_URL}" - -# --- Generate a token for test auth --- -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 " export HEADLAMP_TOKEN=" - echo "" - 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. Set HEADLAMP_TOKEN manually or use OIDC." -fi - -echo "" -echo "E2E deployment complete." diff --git a/scripts/teardown-e2e-headlamp.sh b/scripts/teardown-e2e-headlamp.sh deleted file mode 100755 index 00d4f5a..0000000 --- a/scripts/teardown-e2e-headlamp.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/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-polaris-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 - -# Clean up local env file -rm -f "$REPO_ROOT/.env.e2e" - -echo "Teardown complete."