Files
headlamp-polaris-plugin/e2e
gandalf-the-greybeard[bot] 2a60029104 e2e: shared volume plugin deployment for CI tests (#59)
* e2e: shared volume plugin deployment replacing init container approach

Replace the init container plugin installation with a shared PVC volume
between the CI runner and Headlamp pod. The runner builds the plugin and
copies it to the shared mount; Headlamp reads from the same volume.

- Add deployment/headlamp-e2e-values.yaml (PVC-backed shared volume)
- Add deployment/headlamp-plugins-pvc.yaml (PVC manifest)
- Add scripts/deploy-plugin-via-volume.sh (build + copy + restart)
- Remove deployment/headlamp-static-plugin-values.yaml (init container)

This is CI-only test infrastructure — ArtifactHub remains the sole
user-facing distribution channel.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: update e2e workflow for shared volume plugin deployment

Replace the old preflight-only approach with a build-and-deploy flow
that uses a shared volume (hostPath) between the CI runner and the
Headlamp pod. The workflow now builds the plugin from source, copies
the artifact to a shared volume path, and optionally calls Gandalf's
deploy script for Headlamp rollout coordination.

Removes kubectl exec/cp references and version-match preflight in
favor of deploying the PR's actual build artifact.

Refs: PRI-216, PRI-195

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: align e2e workflow with Gandalf's deploy script interface

Simplify deploy step to call scripts/deploy-plugin-via-volume.sh
directly instead of duplicating copy logic. Align env var names
(PLUGIN_VOLUME_PATH, HEADLAMP_DEPLOY) with the deploy script's
expected interface from PR #59.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: deploy plugin via temporary pod instead of assuming local PVC mount

The deploy script assumed the PVC was mounted on the CI runner at
/mnt/headlamp-plugins, but the runner pod doesn't have that mount.
Fix by using a temporary pod (kubectl run) that mounts the PVC,
receives the plugin tarball via stdin, and extracts it.

Also adds missing workflow steps to create the PVC and upgrade
Headlamp with the shared volume helm values before deploying.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: add kubectl, helm, and helm repo setup steps to e2e workflow

The self-hosted runner doesn't have kubectl or helm pre-installed.
Add setup steps using azure/setup-kubectl and azure/setup-helm
actions, and add the Headlamp helm repo before the upgrade step.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: update Headlamp Helm repo URL from headlamp-k8s to kubernetes-sigs

The Headlamp project moved to the kubernetes-sigs org. The old Helm chart
repository URL (headlamp-k8s.github.io) returns 404, causing E2E workflow
failure at the `helm repo add` step.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* chore: add RBAC manifest for E2E CI runner

Documents the Role and RoleBinding applied to the cluster for the ARC
runner service account. Grants permissions in kube-system needed for
shared volume plugin deployment (PVCs, pods, Helm resources).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: remove .github/workflows/e2e.yaml changes from PR

The workflow changes should be handled separately by Hugh Hackman
per PRI-215. This PR should only contain deployment manifests and
scripts, not CI workflow modifications.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: add shared volume plugin deployment to E2E workflow

Adds the build, Helm, PVC, and plugin deploy steps needed for the
shared volume E2E approach. Uses the correct kubernetes-sigs Helm repo
URL and overrides config.sessionTTL=0 to avoid schema validation error.

This is the workflow counterpart to the deployment manifests and scripts
already in this PR (PVC, values overlay, deploy script).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): set sessionTTL=1 to satisfy Helm schema minimum

The Headlamp Helm chart schema enforces a minimum of 1 for
config.sessionTTL. Setting it to 0 caused helm upgrade to fail
with a schema validation error.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): add cluster-scoped RBAC for CI runner

The Headlamp Helm chart manages ClusterRole and ClusterRoleBinding
resources. The CI runner SA needs cluster-level permissions to
get/update these during helm upgrade. Added ClusterRole and
ClusterRoleBinding alongside the existing namespace-scoped Role.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): replace helm upgrade with kubectl patch to avoid cluster RBAC

The CI runner SA cannot access cluster-scoped resources (ClusterRole,
ClusterRoleBinding) needed by helm upgrade's 3-way merge. Replace the
helm upgrade step with kubectl patch commands that add the shared volume
mount directly to the Headlamp deployment.

This eliminates the need for cluster-admin intervention:
- kubectl patch adds PVC volume + volumeMount to the deployment
- kubectl set env configures the plugins directory
- kubectl rollout status waits for the update

Also removes the now-unnecessary ClusterRole/ClusterRoleBinding from the
RBAC manifest — only namespace-scoped Role/RoleBinding is needed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): improve volume mount idempotency check

Check for existing volume mount by mountPath and PVC claimName, not
just by volume name. A prior helm upgrade may have created mounts
with different names but the same path, causing kubectl patch to fail
with "mountPath must be unique".

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): schedule deploy pod on same node as Headlamp

The headlamp-plugins PVC is ReadWriteOnce, so the temporary deploy
pod must run on the same node as the Headlamp pod to mount it.
Look up the Headlamp pod's node and set nodeName in the pod spec.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use Job with base64 tarball instead of kubectl run stdin

The kubectl run --rm -i stdin pipe times out in the ARC runner
environment. Replace with a Kubernetes Job that receives the plugin
tarball as base64-encoded data in the container command. This avoids
the unreliable attach/stdin mechanism entirely.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use ConfigMap for tarball instead of inline base64

Embedding base64 data in the YAML spec broke parsing. Store the plugin
tarball in a ConfigMap via --from-file and mount it in the deploy Job.
This avoids both the stdin pipe issue and the YAML escaping issue.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use temp file for Job YAML to avoid heredoc escaping

Variable expansion inside heredocs breaks YAML parsing when values
contain colons and quotes (like nodeName). Write the Job manifest to
a temp file with literal YAML, then sed-substitute the dynamic values.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use Pod instead of Job for plugin deploy

The CI runner SA has permission to create Pods but not Jobs in
kube-system. Switch from a Job to a plain Pod with restartPolicy:Never.
Use ConfigMap mount for tarball data (no stdin piping needed).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align registerPluginSettings name with deployed plugin directory

The plugin is deployed to the 'polaris' directory but was registered with
'headlamp-polaris', causing Headlamp to not match the settings component
with the loaded plugin. This fixes all 5 failing E2E settings tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: use package name for registerPluginSettings, not directory name

Headlamp identifies plugins by their package.json name (headlamp-polaris),
not the deploy directory name (polaris). The previous commit incorrectly
changed this to 'polaris', causing the settings component to never render
in the plugin settings page — breaking all 5 E2E settings tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align registerPluginSettings name with deploy directory 'polaris'

The shared volume deploy script places the plugin at /headlamp/plugins/polaris/,
so Headlamp matches settings by directory name 'polaris', not the package.json
name 'headlamp-polaris'. This reverts commit b9d718b which incorrectly changed
the registration name back to 'headlamp-polaris'.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align plugin deploy dir with package.json name, clean stale dirs

The PVC had a stale headlamp-polaris directory from a previous install.
Headlamp loads plugins by scanning the plugins dir and reading package.json
from each subdirectory — it was loading the old build from headlamp-polaris/
while the deploy script was writing to polaris/. The settings registration
name needs to match the plugin name Headlamp identifies.

Changes:
- Deploy script now uses headlamp-polaris as the directory name (matching
  package.json name field)
- Deploy pod cleans up both polaris/ and headlamp-polaris/ before deploying
  to ensure no stale copies remain
- registerPluginSettings uses headlamp-polaris to match Headlamp's plugin
  identifier

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align registerPluginSettings and E2E test with package.json name

Headlamp identifies plugins by reading package.json from the plugin
directory. Since package.json name is 'headlamp-polaris', both the
registerPluginSettings call and the E2E settings test must use
'headlamp-polaris', not 'polaris'.

- registerPluginSettings('polaris') → registerPluginSettings('headlamp-polaris')
- E2E test locator: text=polaris → text=headlamp-polaris

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): load main page before settings to ensure plugin list is populated

Headlamp's PluginSettings component initializes its state from
localStorage on mount and never syncs when props.plugins updates later.
If the settings page loads before fetchAndExecutePlugins completes,
the plugin list stays empty and the test can't find "headlamp-polaris".

Fix: navigate to the main page first, wait for the Polaris sidebar
entry to confirm the plugin is loaded (which populates localStorage),
then navigate to the settings page.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use client-side routing for settings navigation

The PluginSettings component reads the plugin registry once on mount
and never re-renders when new plugins register. Using page.goto() for
the settings URL re-initializes the SPA, causing PluginSettings to
mount before async plugin scripts finish calling registerPluginSettings().

Replace page.goto() with pushState + popstate to do client-side routing.
This preserves the already-loaded plugin registrations from the main
page, so PluginSettings sees the plugin immediately on mount.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use correct HOME-context URL for plugin settings page

The settings page is at /settings/plugins (HOME sidebar context), not
/c/main/settings/plugins (in-cluster context). The in-cluster URL
doesn't match any route, so PluginSettings never mounted and the
plugin entry was never visible.

With the correct URL, no preloading or client-side routing hacks are
needed — PluginSettings uses useTypedSelector on the Redux plugin store,
so it re-renders automatically when registerPluginSettings() fires.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Hugh Hackman <hugh@privilegedescalation.com>
Co-authored-by: Hugh Hackman <hugh-hackman[bot]@users.noreply.github.com>
2026-03-18 02:42:42 +00:00
..

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) uses either Authentik OIDC or token-based authentication via repository secrets.

Required GitHub Secrets

Configure these in GitHub repository settings (Settings → Secrets and variables → Actions):

Secret Required Description
HEADLAMP_URL Optional Headlamp instance URL (defaults to https://headlamp.animaniacs.farh.net)
AUTHENTIK_USERNAME OIDC Authentik email or username for a CI user with Headlamp access
AUTHENTIK_PASSWORD OIDC Password for that user
HEADLAMP_TOKEN Token Kubernetes service account token (alternative to OIDC)

Set either AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD or HEADLAMP_TOKEN. OIDC takes priority if both are set.

Running Locally

Option 1: OIDC via Authentik (same as CI)

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)

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):

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 (fallback auth)

Set either AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD or HEADLAMP_TOKEN. OIDC takes priority if both are set.

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#<namespace>
    • Verifies drawer automatically opens
    • Checks namespace details are displayed

Prerequisites

Cluster Requirements

  1. Polaris Deployment

    # Verify Polaris is running
    kubectl -n polaris get pods
    kubectl -n polaris get svc polaris-dashboard
    
  2. Polaris Audit Data

    # 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

# 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:

npm run e2e:headed

Enable Debug Mode

Step through tests with Playwright Inspector:

npx playwright test --debug

Generate Trace

Record full trace for failed tests:

npx playwright test --trace on
npx playwright show-trace test-results/<test-name>/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

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

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

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.

Required Secrets

Configure these in GitHub repository settings (Settings → Secrets and variables → Actions):

  • HEADLAMP_URL (optional): Headlamp instance URL
  • AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD (for OIDC auth)
  • OR HEADLAMP_TOKEN (for token-based auth)

Workflow Overview

  1. Checkout code
  2. Setup Node.js 20 with npm cache
  3. Install dependencies (npm ci)
  4. Install Playwright browsers (chromium only)
  5. Run auth setup (creates session in e2e/.auth/state.json)
  6. Run all E2E tests
  7. Upload artifacts on failure:
    • playwright-report/ - HTML test report
    • test-results/ - Screenshots, traces, videos

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