From 71649454c9ba0b3d2480a07a4b612894d44e21b1 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Tue, 24 Mar 2026 21:36:04 +0000 Subject: [PATCH 1/8] fix(ci): add missing eslint/prettier devDeps, fix tsconfig types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add eslint@^8.57.0, @headlamp-k8s/eslint-config@^0.6.0, prettier@^2.8.8 as explicit devDependencies — without these the lint and format:check CI steps fail with "eslint: not found" / "prettier: not found" - Remove vite/client and vite-plugin-svgr/client from tsconfig types — these are transitive deps that pnpm does not hoist; polaris plugin omits them too and tsc passes cleanly without them - Update pnpm-lock.yaml to reflect new direct deps --- package.json | 3 +++ pnpm-lock.yaml | 9 +++++++++ tsconfig.json | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 09a61b5..7927a65 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "node-forge": "^1.3.1" }, "devDependencies": { + "@headlamp-k8s/eslint-config": "^0.6.0", "@iconify/react": "^6.0.2", "@kinvolk/headlamp-plugin": "^0.13.0", "@mui/material": "^5.15.14", @@ -64,8 +65,10 @@ "@types/node-forge": "^1.3.11", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "eslint": "^8.57.0", "jsdom": "^24.0.0", "notistack": "^3.0.0", + "prettier": "^2.8.8", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60143e9..30baa0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: specifier: ^1.3.1 version: 1.3.3 devDependencies: + '@headlamp-k8s/eslint-config': + specifier: ^0.6.0 + version: 0.6.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.35.0(eslint@8.57.1))(eslint-plugin-simple-import-sort@12.1.1(eslint@8.57.1))(eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1) '@iconify/react': specifier: ^6.0.2 version: 6.0.2(react@18.3.1) @@ -39,12 +42,18 @@ importers: '@types/react-dom': specifier: ^18.0.0 version: 18.3.7(@types/react@18.3.28) + eslint: + specifier: ^8.57.0 + version: 8.57.1 jsdom: specifier: ^24.0.0 version: 24.1.3 notistack: specifier: ^3.0.0 version: 3.0.2(csstype@3.2.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + prettier: + specifier: ^2.8.8 + version: 2.8.8 react: specifier: ^18.3.1 version: 18.3.1 diff --git a/tsconfig.json b/tsconfig.json index 2eb0176..6601104 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json", "compilerOptions": { - "types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals", "@testing-library/jest-dom"] + "types": ["vitest/globals", "@testing-library/jest-dom"] }, "include": ["src"] } -- 2.52.0 From 5c420e58a4bd1ae15596df598447b8296c7829ff Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Tue, 24 Mar 2026 21:38:23 +0000 Subject: [PATCH 2/8] fix(ci): add typescript as explicit devDependency pnpm strict hoisting means only direct deps are on PATH. The overrides entry pins the version but does not install tsc as a binary. Without an explicit devDependency entry pnpm run tsc fails with "tsc: not found". --- package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index 7927a65..05f1d58 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "react-dom": "^18.3.1", "react-router-dom": "^5.3.0", "typedoc": "^0.28.16", + "typescript": "~5.6.2", "typedoc-plugin-markdown": "^4.10.0", "vitest": "^3.2.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30baa0c..1e5e516 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: typedoc-plugin-markdown: specifier: ^4.10.0 version: 4.11.0(typedoc@0.28.18(typescript@5.6.2)) + typescript: + specifier: ~5.6.2 + version: 5.6.2 vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.13)(@types/node@20.19.37)(jsdom@24.1.3)(msw@2.4.9(typescript@5.6.2))(terser@5.46.1)(yaml@2.8.3) -- 2.52.0 From a7adee4e54bad15f29ab60774c9ac803cc208131 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Tue, 24 Mar 2026 21:41:08 +0000 Subject: [PATCH 3/8] fix(ci): remove typescript from overrides, keep only as devDep npm/pnpm rejects a package.json that specifies the same package in both overrides and devDependencies (EOVERRIDE). Since typescript is now a direct devDependency pinned at ~5.6.2, remove it from overrides. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 05f1d58..a32fa3a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "k8s" ], "overrides": { - "typescript": "5.6.2", "tar": "^7.5.11", "undici": "^7.24.3" }, -- 2.52.0 From 2d7b73466a16c8916f36f16466fcab85b6c62ef5 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Tue, 24 Mar 2026 22:12:39 +0000 Subject: [PATCH 4/8] fix: add packageManager field to package.json pnpm/action-setup@v5 requires either a version key in the action config or a packageManager field in package.json. Add the field to unblock the release workflow. Co-Authored-By: Paperclip --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a32fa3a..f8c49ac 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "homepage": "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin#readme", "author": "privilegedescalation", "license": "Apache-2.0", + "packageManager": "pnpm@10.32.1", "scripts": { "start": "headlamp-plugin start", "build": "headlamp-plugin build", -- 2.52.0 From c223d924bc8588d52354ff6fb101dd9e429fea1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 22:30:53 +0000 Subject: [PATCH 5/8] release: v1.0.0 --- artifacthub-pkg.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml index ac89475..6bc7d1b 100644 --- a/artifacthub-pkg.yml +++ b/artifacthub-pkg.yml @@ -7,7 +7,7 @@ createdAt: "2026-02-12T00:00:00Z" description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption and RBAC-aware UI license: Apache-2.0 homeURL: https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin -appVersion: "0.24.0" +appVersion: "0.36.1" containersImages: - name: sealed-secrets-controller image: docker.io/bitnami/sealed-secrets-controller:v0.24.0 @@ -20,7 +20,7 @@ keywords: - security annotations: headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v1.0.0/sealed-secrets-1.0.0.tar.gz" - headlamp/plugin/archive-checksum: sha256:TBD-set-by-release-workflow + headlamp/plugin/archive-checksum: sha256:d387f156b7bf5628073116ef1e406d8038cf60eabefe46e220a0db2d67f5530a headlamp/plugin/version-compat: ">=0.13.0" headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop" links: -- 2.52.0 From a2ac69c7646a438188a3c023b48cdb5ffbb34fe6 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Tue, 24 Mar 2026 23:19:20 +0000 Subject: [PATCH 6/8] feat: add Playwright E2E smoke tests Follows the pattern established in headlamp-intel-gpu-plugin (PR #25): - e2e/sealed-secrets.spec.ts: 5 smoke tests covering sidebar navigation, list view, sealing keys view, cross-view navigation, and plugin settings - e2e/auth.setup.ts: shared OIDC + token auth setup - playwright.config.ts: fail-fast if HEADLAMP_URL not set (no prod URL fallback) - scripts/deploy-e2e-headlamp.sh: ConfigMap-based plugin injection to privilegedescalation-dev - scripts/teardown-e2e-headlamp.sh: clean teardown of all E2E resources --- .gitignore | 6 + e2e/.auth/.gitkeep | 0 e2e/auth.setup.ts | 81 ++++++++++++ e2e/sealed-secrets.spec.ts | 88 +++++++++++++ package.json | 3 + playwright.config.ts | 27 ++++ scripts/deploy-e2e-headlamp.sh | 204 +++++++++++++++++++++++++++++++ scripts/teardown-e2e-headlamp.sh | 38 ++++++ 8 files changed, 447 insertions(+) create mode 100644 e2e/.auth/.gitkeep create mode 100644 e2e/auth.setup.ts create mode 100644 e2e/sealed-secrets.spec.ts create mode 100644 playwright.config.ts create mode 100755 scripts/deploy-e2e-headlamp.sh create mode 100755 scripts/teardown-e2e-headlamp.sh diff --git a/.gitignore b/.gitignore index 41fd096..023e537 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,9 @@ Thumbs.db npm-debug.log* yarn-debug.log* yarn-error.log* + +# E2E +.env.e2e +e2e/.auth/state.json +playwright-report/ +test-results/ diff --git a/e2e/.auth/.gitkeep b/e2e/.auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts new file mode 100644 index 0000000..1823f81 --- /dev/null +++ b/e2e/auth.setup.ts @@ -0,0 +1,81 @@ +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. + 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/sealed-secrets.spec.ts b/e2e/sealed-secrets.spec.ts new file mode 100644 index 0000000..2af5e91 --- /dev/null +++ b/e2e/sealed-secrets.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Sealed Secrets plugin smoke tests', () => { + test('sidebar contains sealed-secrets 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: /sealed.secrets/i })).toBeVisible(); + }); + + test('sidebar sealed-secrets entry is clickable and navigates to list view', async ({ page }) => { + await page.goto('/'); + const sidebar = page.getByRole('navigation', { name: 'Navigation' }); + await expect(sidebar).toBeVisible({ timeout: 15_000 }); + + const sealedSecretsEntry = sidebar.getByRole('button', { name: /sealed.secrets/i }); + await expect(sealedSecretsEntry).toBeVisible(); + await sealedSecretsEntry.click(); + + await expect(page).toHaveURL(/\/sealedsecrets/); + await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible(); + }); + + test('sealed secrets list page renders table or empty state', async ({ page }) => { + await page.goto('/c/main/sealedsecrets'); + + await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({ + timeout: 15_000, + }); + + // Either a populated table or an empty-state indicator must be visible + const hasTable = await page.locator('table').first().isVisible().catch(() => false); + const hasEmptyState = await page + .locator('text=/no.*sealed|no.*secret|0 item|empty/i') + .first() + .isVisible() + .catch(() => false); + expect(hasTable || hasEmptyState).toBe(true); + }); + + test('sealing keys page renders table or empty state', async ({ page }) => { + await page.goto('/c/main/sealedsecrets/keys'); + + await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible({ + timeout: 15_000, + }); + + const hasTable = await page.locator('table').first().isVisible().catch(() => false); + const hasEmptyState = await page + .locator('text=/no.*key|0 item|empty/i') + .first() + .isVisible() + .catch(() => false); + expect(hasTable || hasEmptyState).toBe(true); + }); + + test('navigation between sealed-secrets views works', async ({ page }) => { + await page.goto('/c/main/sealedsecrets'); + await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({ + timeout: 15_000, + }); + + // Navigate to Sealing Keys via sidebar + const sidebar = page.getByRole('navigation', { name: 'Navigation' }); + const keysLink = sidebar.getByRole('link', { name: /sealing.key/i }); + await expect(keysLink).toBeVisible(); + await keysLink.click(); + + await expect(page).toHaveURL(/\/sealedsecrets\/keys$/); + await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible(); + + // Navigate back to All Sealed Secrets + const allSecretsLink = sidebar.getByRole('link', { name: /all sealed secrets/i }); + await expect(allSecretsLink).toBeVisible(); + await allSecretsLink.click(); + + await expect(page).toHaveURL(/\/sealedsecrets(?!\/keys)/); + await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible(); + }); + + test('plugin settings page shows sealed-secrets plugin entry', async ({ page }) => { + await page.goto('/settings/plugins'); + + // Wait for plugin list to load — plugin scripts load asynchronously + const pluginEntry = page.locator('text=sealed-secrets').first(); + await expect(pluginEntry).toBeVisible({ timeout: 30_000 }); + }); +}); diff --git a/package.json b/package.json index f8c49ac..b0ccb70 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "format:check": "prettier --check src/", "test": "vitest run", "test:watch": "vitest", + "e2e": "playwright test", + "e2e:headed": "playwright test --headed", "storybook": "headlamp-plugin storybook", "storybook-build": "headlamp-plugin storybook-build", "i18n": "headlamp-plugin i18n", @@ -56,6 +58,7 @@ }, "devDependencies": { "@headlamp-k8s/eslint-config": "^0.6.0", + "@playwright/test": "^1.58.2", "@iconify/react": "^6.0.2", "@kinvolk/headlamp-plugin": "^0.13.0", "@mui/material": "^5.15.14", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b916edf --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: 'list', + use: { + baseURL: process.env.HEADLAMP_URL || (() => { throw new Error('HEADLAMP_URL is required — run scripts/deploy-e2e-headlamp.sh first'); })(), + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'setup', testMatch: /auth\.setup\.ts/, timeout: 60_000 }, + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: 'e2e/.auth/state.json', + }, + dependencies: ['setup'], + }, + ], +}); diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh new file mode 100755 index 0000000..8f7a872 --- /dev/null +++ b/scripts/deploy-e2e-headlamp.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# deploy-e2e-headlamp.sh +# +# Deploys a stock Headlamp instance with the sealed-secrets 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 `privilegedescalation-dev` namespace. Nothing +# persists beyond the test run — teardown cleans up all created resources. +# +# Prerequisites: +# - Plugin built (dist/ exists with plugin-main.js + package.json) +# - kubectl configured with cluster access +# - RBAC applied: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml +# +# Environment: +# E2E_NAMESPACE — namespace for E2E Headlamp (default: privilegedescalation-dev) +# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) +# HEADLAMP_VERSION — Headlamp image tag (default: latest) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DIST_DIR="$REPO_ROOT/dist" + +E2E_NAMESPACE="${E2E_NAMESPACE:-privilegedescalation-dev}" +E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" +HEADLAMP_VERSION="${HEADLAMP_VERSION:-latest}" + +if [ ! -d "$DIST_DIR" ]; then + echo "ERROR: dist/ not found. Run 'pnpm build' first." >&2 + exit 1 +fi + +# --- 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-sealed-secrets-plugin \ + -n "$E2E_NAMESPACE" --ignore-not-found + +# Create ConfigMap from dist/ contents and package.json +kubectl create configmap headlamp-sealed-secrets-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 --- +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 new file mode 100755 index 0000000..477cd1a --- /dev/null +++ b/scripts/teardown-e2e-headlamp.sh @@ -0,0 +1,38 @@ +#!/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: privilegedescalation-dev) +# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +E2E_NAMESPACE="${E2E_NAMESPACE:-privilegedescalation-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-sealed-secrets-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 .env.e2e if present +if [ -f "$REPO_ROOT/.env.e2e" ]; then + rm "$REPO_ROOT/.env.e2e" + echo "Removed .env.e2e" +fi + +echo "" +echo "E2E teardown complete." -- 2.52.0 From d20e18f13b6fbb5c0bae5b01e55af52b07e26eca Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Tue, 24 Mar 2026 23:40:35 +0000 Subject: [PATCH 7/8] fix: regenerate pnpm-lock.yaml to include @playwright/test pnpm-lock.yaml was not updated when @playwright/test@^1.58.2 was added to package.json, causing CI to fail with ERR_PNPM_OUTDATED_LOCKFILE. This lockfile-only change resolves that breakage. Closes https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/issues/38 Co-Authored-By: Paperclip --- pnpm-lock.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e5e516..1ccf8d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@mui/material': specifier: ^5.15.14 version: 5.18.0(@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@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(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 @@ -856,6 +859,11 @@ 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==} @@ -2933,6 +2941,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4082,6 +4095,16 @@ 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'} @@ -6332,6 +6355,10 @@ 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)': @@ -8767,6 +8794,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -10164,6 +10194,14 @@ 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): -- 2.52.0 From f832b563d08533ef40a39bbbbcbb2cc36a0faf15 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Wed, 25 Mar 2026 11:29:14 +0000 Subject: [PATCH 8/8] fix: set correct archive checksum for v1.0.0 Co-Authored-By: Paperclip --- artifacthub-pkg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml index ac89475..c234d5d 100644 --- a/artifacthub-pkg.yml +++ b/artifacthub-pkg.yml @@ -20,7 +20,7 @@ keywords: - security annotations: headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v1.0.0/sealed-secrets-1.0.0.tar.gz" - headlamp/plugin/archive-checksum: sha256:TBD-set-by-release-workflow + headlamp/plugin/archive-checksum: sha256:d387f156b7bf5628073116ef1e406d8038cf60eabefe46e220a0db2d67f5530a headlamp/plugin/version-compat: ">=0.13.0" headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop" links: -- 2.52.0