Compare commits

..

1 Commits

6 changed files with 19 additions and 18936 deletions
-22
View File
@@ -1,22 +0,0 @@
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: e2e-${{ github.repository }}
cancel-in-progress: false
jobs:
e2e:
uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@main
with:
node-version: "22"
headlamp-version: v0.40.1
+15 -26
View File
@@ -1,40 +1,34 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
async function waitForSidebar(page: import('@playwright/test').Page) {
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await page.waitForLoadState('networkidle');
return sidebar;
}
test.describe('Sealed Secrets plugin smoke tests', () => { test.describe('Sealed Secrets plugin smoke tests', () => {
test('sidebar contains sealed-secrets entry', async ({ page }) => { test('sidebar contains sealed-secrets entry', async ({ page }) => {
await page.goto('/'); await page.goto('/');
const sidebar = await waitForSidebar(page); const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await expect(sidebar.getByRole('button', { name: /sealed.secrets/i })).toBeVisible(); await expect(sidebar.getByRole('button', { name: /sealed.secrets/i })).toBeVisible();
}); });
test('sidebar sealed-secrets entry is clickable and navigates to list view', async ({ page }) => { test('sidebar sealed-secrets entry is clickable and navigates to list view', async ({ page }) => {
await page.goto('/'); await page.goto('/');
const sidebar = await waitForSidebar(page); const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
const sealedSecretsEntry = sidebar.getByRole('button', { name: /sealed.secrets/i }); const sealedSecretsEntry = sidebar.getByRole('button', { name: /sealed.secrets/i });
await expect(sealedSecretsEntry).toBeVisible(); await expect(sealedSecretsEntry).toBeVisible();
await sealedSecretsEntry.click(); await sealedSecretsEntry.click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/\/sealedsecrets/); await expect(page).toHaveURL(/\/sealedsecrets/);
await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible(); await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible();
}); });
test('sealed secrets list page renders table or empty state', async ({ page }) => { test('sealed secrets list page renders table or empty state', async ({ page }) => {
await page.goto('/c/main/sealedsecrets'); await page.goto('/c/main/sealedsecrets');
await waitForSidebar(page);
await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({ await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({
timeout: 15_000, 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 hasTable = await page.locator('table').first().isVisible().catch(() => false);
const hasEmptyState = await page const hasEmptyState = await page
.locator('text=/no.*sealed|no.*secret|0 item|empty/i') .locator('text=/no.*sealed|no.*secret|0 item|empty/i')
@@ -46,7 +40,6 @@ test.describe('Sealed Secrets plugin smoke tests', () => {
test('sealing keys page renders table or empty state', async ({ page }) => { test('sealing keys page renders table or empty state', async ({ page }) => {
await page.goto('/c/main/sealedsecrets/keys'); await page.goto('/c/main/sealedsecrets/keys');
await waitForSidebar(page);
await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible({ await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible({
timeout: 15_000, timeout: 15_000,
@@ -63,37 +56,33 @@ test.describe('Sealed Secrets plugin smoke tests', () => {
test('navigation between sealed-secrets views works', async ({ page }) => { test('navigation between sealed-secrets views works', async ({ page }) => {
await page.goto('/c/main/sealedsecrets'); await page.goto('/c/main/sealedsecrets');
const sidebar = await waitForSidebar(page); await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({
timeout: 15_000,
const sealedBtn = sidebar.getByRole('button', { name: /sealed.secrets/i }).first(); });
await sealedBtn.click();
await page.waitForLoadState('networkidle');
await expect(page.getByRole('heading', { name: /sealed.secrets/i }).first()).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 }); const keysLink = sidebar.getByRole('link', { name: /sealing.key/i });
await expect(keysLink).toBeVisible(); await expect(keysLink).toBeVisible();
await keysLink.click(); await keysLink.click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/\/sealedsecrets\/keys$/); await expect(page).toHaveURL(/\/sealedsecrets\/keys$/);
await expect(page.getByRole('heading', { name: /sealing.key/i }).first()).toBeVisible(); 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 }); const allSecretsLink = sidebar.getByRole('link', { name: /all sealed secrets/i });
await expect(allSecretsLink).toBeVisible(); await expect(allSecretsLink).toBeVisible();
await allSecretsLink.click(); await allSecretsLink.click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/\/sealedsecrets(?!\/keys)/); await expect(page).toHaveURL(/\/sealedsecrets(?!\/keys)/);
await expect(page.getByRole('heading', { name: /sealed.secrets/i }).first()).toBeVisible(); await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible();
}); });
test('plugin settings page shows sealed-secrets plugin entry', async ({ page }) => { test('plugin settings page shows sealed-secrets plugin entry', async ({ page }) => {
await page.goto('/settings/plugins'); await page.goto('/settings/plugins');
await page.waitForLoadState('networkidle');
await page.waitForSelector('table', { timeout: 10_000 }).catch(() => {});
const pluginEntry = page.locator('text=/sealed.secrets/i').first(); // 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 }); await expect(pluginEntry).toBeVisible({ timeout: 30_000 });
}); });
}); });
-18877
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -53,8 +53,7 @@
"tar": "^7.5.11", "tar": "^7.5.11",
"undici": "^7.24.3", "undici": "^7.24.3",
"vite": ">=6.4.2", "vite": ">=6.4.2",
"lodash": ">=4.18.0", "lodash": ">=4.18.0"
"elliptic": ">=6.6.1"
}, },
"dependencies": { "dependencies": {
"node-forge": "^1.4.0" "node-forge": "^1.4.0"
+3 -6
View File
@@ -11,9 +11,7 @@
# Prerequisites: # Prerequisites:
# - Plugin built (dist/ exists with plugin-main.js + package.json) # - Plugin built (dist/ exists with plugin-main.js + package.json)
# - kubectl configured with cluster access # - kubectl configured with cluster access
# RBAC is managed via Flux from privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml. # - RBAC applied: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml
# The infra repo is the source of truth — do not apply this file directly.
# Apply RBAC first: kubectl apply -f privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml
# #
# Environment: # Environment:
# E2E_NAMESPACE — namespace for E2E Headlamp (default: privilegedescalation-dev) # E2E_NAMESPACE — namespace for E2E Headlamp (default: privilegedescalation-dev)
@@ -37,7 +35,7 @@ fi
echo "Checking RBAC permissions in namespace '${E2E_NAMESPACE}'..." echo "Checking RBAC permissions in namespace '${E2E_NAMESPACE}'..."
if ! kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" --quiet 2>/dev/null; then 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 "ERROR: Missing RBAC — cannot delete configmaps in namespace '${E2E_NAMESPACE}'." >&2
echo " Apply RBAC first: kubectl apply -f privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml" >&2 echo " Apply RBAC first: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml" >&2
exit 1 exit 1
fi fi
@@ -99,7 +97,7 @@ spec:
app.kubernetes.io/instance: ${E2E_RELEASE} app.kubernetes.io/instance: ${E2E_RELEASE}
spec: spec:
serviceAccountName: ${E2E_RELEASE} serviceAccountName: ${E2E_RELEASE}
automountServiceAccountToken: false automountServiceAccountToken: true
securityContext: {} securityContext: {}
containers: containers:
- name: headlamp - name: headlamp
@@ -161,7 +159,6 @@ spec:
EOF EOF
echo "Waiting for rollout..." echo "Waiting for rollout..."
sleep 2
kubectl rollout status "deployment/${E2E_RELEASE}" \ kubectl rollout status "deployment/${E2E_RELEASE}" \
-n "$E2E_NAMESPACE" --timeout=120s -n "$E2E_NAMESPACE" --timeout=120s
-3
View File
@@ -3,9 +3,6 @@
# #
# Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh. # Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh.
# #
# RBAC is managed via Flux from privilegedescalation/infra/base/rbac/e2e-ci-runner-headlamp-rbac.yaml.
# The infra repo is the source of truth — do not apply this file directly.
#
# Environment: # Environment:
# E2E_NAMESPACE — namespace to clean up (default: privilegedescalation-dev) # E2E_NAMESPACE — namespace to clean up (default: privilegedescalation-dev)
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e) # E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)