Compare commits

..

18 Commits

Author SHA1 Message Date
Chris Farhood dc1f354449 fix(e2e): remove 'local' keyword outside function context
The 'local' bash keyword can only be used inside a function. Using it
at top-level of a run: block causes 'local: can only be used in a
function' error and exits the script with code 1.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:42:21 +00:00
Chris Farhood b371b626ee fix(e2e): generate in-cluster kubeconfig when no static kubeconfig is found
The ARC runner has no static kubeconfig at any of the expected paths
(/runner/config, ~/.kube/config). It DOES have a service account token
(/var/run/secrets/kubernetes.io/serviceaccount/token) and
KUBERNETES_SERVICE_HOST=10.43.0.1, confirming in-cluster access.

This commit adds a third fallback tier: when no static kubeconfig is
found AND the runner is in-cluster (service account token present),
generate a kubeconfig from the in-cluster service account credentials.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:39:46 +00:00
Chris Farhood 30f8c92a09 fix(e2e): use ${VAR:-} syntax to avoid unbound variable errors
The previous diagnostic step used $KUBECONFIG and $HOME directly,
which causes 'unbound variable' exit when run with set -euo pipefail
and KUBECONFIG is unset. Use ${VAR:-} defaults throughout.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:36:15 +00:00
Chris Farhood 48947ce2c6 debug(e2e): add diagnostic step to discover kubeconfig location on ARC runner
Adds a comprehensive diagnostic block that prints env vars, lists all
known kubeconfig paths, checks in-cluster service account, and attempts
kubectl config view. This will reveal the actual path on the runner.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:33:11 +00:00
Chris Farhood 20453c7223 fix(e2e): explicit kubeconfig path with fail-fast instead of silent fallback
The previous loop silently skipped if no kubeconfig was found, causing
kubectl commands to fall back to localhost:8080. Use explicit paths
in priority order with a hard error if none exist.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:27:07 +00:00
Chris Farhood 7c55bfac01 fix(e2e): remove impersonation check, verify RBAC resources directly
Replace the impersonation check with direct verification of RBAC
resources. The kubectl auth can-i --as check fails with
localhost:8080 because kubectl cannot find kubeconfig. Instead,
directly verify that the Role and RoleBinding were created
by kubectl apply.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:16:45 +00:00
Chris Farhood 74f8264630 fix(e2e): clean kubeconfig discovery without diagnostic overhead
Simplified kubeconfig discovery. Search standard paths and exit 0
immediately upon finding one.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:14:24 +00:00
Chris Farhood a10c5628e1 debug(e2e): test kubectl apply and can-i with and without kubeconfig
Test if kubectl apply dry-run works without KUBECONFIG (the original
behavior that succeeded). Also test kubectl auth can-i without KUBECONFIG
(to confirm the failure mode). Compare with KUBECONFIG set to service account.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:10:47 +00:00
Chris Farhood dfee2f4b87 fix(e2e): use in-cluster service account token for kubeconfig
ARC runner has no kubeconfig file. Use the service account
token at /var/run/secrets/kubernetes.io/serviceaccount/ to build
a kubeconfig that connects to the Kubernetes API server from
within the pod. This is the standard in-cluster access pattern.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:05:19 +00:00
Chris Farhood 3f61e49092 debug(e2e): test kubectl with no KUBECONFIG set
Test if kubectl can find kubeconfig without explicit KUBECONFIG
on the ARC runner. kubectl config view --raw shows the config
content if it exists, kubectl cluster-info tests connectivity.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 20:01:03 +00:00
Chris Farhood ea7f36e48e fix(e2e): remove errant /github listing that causes exit 2
ls -la /github/ exits with code 2 when /github/ doesn't exist,
causing set -e to fail the step. Remove that listing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 19:58:34 +00:00
Chris Farhood 21abbc8cee debug(e2e): search expanded kubeconfig paths including GITHUB_WORKSPACE
Also add GITHUB_WORKSPACE/.kube to search and print ls of key dirs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 19:56:40 +00:00
Chris Farhood 40626839e4 fix(e2e): search all standard kubeconfig paths
Check /paperclip/.kube, /paperclip/.kube/config, /home/runner/.kube,
/home/runner/.kube/config, /runner, and /runner/config. Export
KUBECONFIG so kubectl uses the real cluster.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 19:54:33 +00:00
Chris Farhood 1fc5b45aa8 fix(e2e): search k8s and k8s-novolume for kubeconfig
ARC runner stores kubeconfig in /home/runner/k8s/config (mounted
by Actions Runtime). Add both k8s and k8s-novolume to the search
paths and remove non-existent paths from diagnostics.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 19:51:29 +00:00
Chris Farhood 31036d49e7 debug(e2e): add diagnostic step to locate kubeconfig
Add ls and echo diagnostics to understand where ARC runners store
kubeconfig. Include ACTIONS_KUBECONFIG and HOME env vars.
Also add $HOME/.kube to the search paths.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 19:49:23 +00:00
Chris Farhood fcb0018216 Fix E2E kubeconfig: locate kubeconfig before RBAC step
The 'kubectl auth can-i --as' impersonation check was falling back to
localhost:8080 because KUBECONFIG was not set and the ARC runner's
kubeconfig was not in the default location. azure/setup-kubectl@v4
does not set KUBECONFIG — it installs kubectl and relies on the runner's
existing kubeconfig in /runner/.kube/config (ARC runner home).

Add a 'Locate kubeconfig for ARC runner' step that searches the known
runner kubeconfig paths before the RBAC step runs, exports KUBECONFIG
to GITHUB_ENV, and verifies cluster connectivity before proceeding.

Fixes: PRI-785
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 19:47:08 +00:00
Chris Farhood c79a4bdfa9 ci: re-trigger E2E to confirm stable (PRI-324) 2026-05-05 19:35:28 +00:00
Chris Farhood d126010eaf fix(e2e): make workflow self-sufficient with RBAC apply steps (PRI-324)
- Apply e2e-ci-runner RBAC + polaris RBAC in workflow before pre-flight check
- Add e2e-ci-runner-polaris Role+RoleBinding so CI runner can manage polaris namespace RBAC
- Add roles/rolebindings CRUD to e2e-ci-runner Role (headlamp-dev namespace)
- Collapsed MISSING_ROLE/MISSING_ROLEBINDING into single MISSING flag (QA nit)
- Drop non-standard --quiet flag on kubectl auth can-i (QA nit)

Address PRI-324 QA feedback: workflow now applies its own RBAC so the pre-flight
check is meaningful and the green path is achievable.
2026-05-05 19:29:47 +00:00
44 changed files with 21212 additions and 674 deletions
+4 -202
View File
@@ -2,210 +2,12 @@ name: CI
on:
push:
branches: ['**']
branches: [main]
pull_request:
branches: [main, dev, uat]
branches: [main]
workflow_dispatch:
permissions:
contents: read
workflow_call:
jobs:
ci:
runs-on: ubuntu-latest
timeout-minutes: 10
container: node:22-slim
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Python
run: apt-get update && apt-get install -y --no-install-recommends python3 python3-yaml
- name: Validate artifacthub-pkg.yml
run: |
python3 - <<'EOF'
import sys, re
try:
import yaml
except ImportError:
print("::warning::PyYAML not available, skipping artifacthub-pkg.yml validation")
sys.exit(0)
try:
with open("artifacthub-pkg.yml") as f:
pkg = yaml.safe_load(f)
except FileNotFoundError:
print("::error::artifacthub-pkg.yml not found")
sys.exit(1)
except yaml.YAMLError as e:
print(f"::error::artifacthub-pkg.yml is invalid YAML: {e}")
sys.exit(1)
errors = []
for field in ["version", "name", "description", "homeURL"]:
if not pkg.get(field):
errors.append(f"Missing required field: {field}")
version = pkg.get("version", "")
if version and not re.match(r'^\d+\.\d+\.\d+$', str(version)):
errors.append(f"version '{version}' is not SemVer (expected X.Y.Z)")
annotations = pkg.get("annotations", {}) or {}
archive_url = annotations.get("headlamp/plugin/archive-url", "")
archive_checksum = annotations.get("headlamp/plugin/archive-checksum", "")
if not archive_url:
errors.append("Missing annotation: headlamp/plugin/archive-url")
if not archive_checksum:
errors.append("Missing annotation: headlamp/plugin/archive-checksum")
elif not re.match(r'^sha256:[0-9a-f]{64}$', str(archive_checksum)):
errors.append(f"archive-checksum has unexpected format: '{archive_checksum}' (expected sha256:<64 hex chars>)")
if errors:
for e in errors:
print(f"::error::{e}")
sys.exit(1)
print(f"artifacthub-pkg.yml valid: name={pkg['name']} version={pkg['version']}")
EOF
- name: Detect package manager
id: pkg-manager
run: |
if [ -f "pnpm-lock.yaml" ]; then
echo "manager=pnpm" >> $GITHUB_OUTPUT
PM=$(python3 -c "import json,sys; d=json.load(open('package.json')); print('true' if d.get('packageManager','').startswith('pnpm@') else 'false')" 2>/dev/null || echo "false")
echo "has_package_manager=$PM" >> $GITHUB_OUTPUT
else
echo "manager=npm" >> $GITHUB_OUTPUT
echo "has_package_manager=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'
cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }}
- name: Setup pnpm (via Corepack, reads version from packageManager field)
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'true'
run: |
npm install -g corepack
corepack enable pnpm
corepack install
- name: Setup pnpm (version latest)
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'false'
uses: pnpm/action-setup@v5
with:
run_install: false
version: latest
- name: Get pnpm store directory
id: pnpm-store
if: steps.pkg-manager.outputs.manager == 'pnpm'
run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
if: steps.pkg-manager.outputs.manager == 'pnpm'
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Validate pnpm lockfile freshness
if: steps.pkg-manager.outputs.manager == 'pnpm'
run: |
if [ ! -f "pnpm-lock.yaml" ]; then
echo "No pnpm-lock.yaml found, skipping lockfile freshness check"
exit 0
fi
if ! grep -q 'overrides:' pnpm-lock.yaml 2>/dev/null; then
echo "No overrides section in pnpm-lock.yaml, skipping lockfile freshness check"
exit 0
fi
echo "Detected pnpm-lock.yaml with overrides section. Checking lockfile freshness..."
ERR_FILE=$(mktemp)
if pnpm install --frozen-lockfile 2>&1 | tee "$ERR_FILE"; then
echo "Lockfile is fresh."
else
if grep -q "CONFIG_MISMATCH\|EBADLOCKFILE\|ERR_PNPM_LOCKFILE" "$ERR_FILE"; then
echo ""
echo "::error::pnpm-lock.yaml is out of sync with package.json overrides."
echo "::error::Run 'pnpm install' to regenerate the lockfile and commit the updated pnpm-lock.yaml."
rm -f "$ERR_FILE"
exit 1
fi
rm -f "$ERR_FILE"
echo "::warning::Install failed with a different error. Will retry in the Install dependencies step."
fi
- name: Install dependencies
run: |
max_attempts=3
attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts"
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile && break
else
npm ci && break
fi
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Install step failed on attempt $attempt. Retrying in 5 seconds..."
sleep 5
fi
attempt=$((attempt + 1))
done
if [ $attempt -gt $max_attempts ]; then
echo "::error::Install step failed after $max_attempts attempts."
exit 1
fi
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Lint
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run lint
else
npm run lint
fi
- name: Type-check
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run tsc
else
npm run tsc
fi
- name: Format check
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run format:check
else
npm run format:check
fi
- name: Run tests
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm test
else
npm test
fi
- name: Security audit
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
npx audit-ci --pnpm --audit-level=high --config ./audit-ci.jsonc
else
npx audit-ci --npm --audit-level=high --config ./audit-ci.jsonc
fi
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
+11 -103
View File
@@ -1,112 +1,20 @@
name: Promotion Gate
name: Dual Approval (CTO + QA)
# dev PRs: no gate (engineer self-merges).
# uat PRs: QA approval required.
# main PRs: UAT approval required (uat→main promotions).
# Calls the shared dual-approval-check workflow.
# Passes when both privilegedescalation-cto and privilegedescalation-qa
# have approved the PR. Add "Dual Approval (CTO + QA)" to required_status_checks
# in branch protection to enforce this gate.
on:
pull_request_review:
types: [submitted, dismissed]
pull_request:
branches: [uat, main]
branches: [main]
types: [opened, reopened, synchronize]
jobs:
promotion-gate:
name: Promotion Gate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check promotion approval
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.base_ref }}
run: |
if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ]; then
echo "::notice::No PR number in context. Skipping promotion gate."
exit 0
fi
echo "Checking promotion gate for PR #${PR_NUMBER} targeting ${BASE_REF} in ${REPO}"
if [ -z "${BASE_REF}" ] && [ -n "${PR_NUMBER}" ] && [ "${PR_NUMBER}" != "null" ]; then
BASE_REF=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Accept: application/json" \
"https://git.farh.net/api/v1/repos/${REPO}/pulls/${PR_NUMBER}" | jq -r '.base.ref')
echo "BASE_REF was empty; resolved from PR #${PR_NUMBER} API: ${BASE_REF}"
fi
# Determine required reviewer based on target branch
case "${BASE_REF}" in
dev)
echo "Target is dev — no review required. Engineers self-merge."
exit 0
;;
uat)
REQUIRED_REVIEWER="pe_regina"
GATE_NAME="QA"
;;
main)
REQUIRED_REVIEWER="pe_regina"
GATE_NAME="QA"
# For plugin repos (Pipeline A), UAT approval is needed for uat→main
# Check if the source branch is uat
SOURCE_REF=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Accept: application/json" \
"https://git.farh.net/api/v1/repos/${REPO}/pulls/${PR_NUMBER}" | jq -r '.head.ref')
if [ "${SOURCE_REF}" = "uat" ]; then
REQUIRED_REVIEWER="pe_patty"
GATE_NAME="UAT"
fi
;;
*)
echo "::notice::Target branch '${BASE_REF}' has no promotion gate configured."
exit 0
;;
esac
echo "Required reviewer: ${REQUIRED_REVIEWER} (${GATE_NAME})"
# For uat→main promotions, pe_patty may not be able to review (bot account).
# Accept pe_nancy (CTO) as a valid alternative reviewer.
ALT_REVIEWER=""
if [ "${REQUIRED_REVIEWER}" = "pe_patty" ]; then
ALT_REVIEWER="pe_nancy"
fi
REVIEWS=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Accept: application/json" \
"https://git.farh.net/api/v1/repos/${REPO}/pulls/${PR_NUMBER}/reviews")
if [ -z "${REVIEWS}" ] || [ "${REVIEWS}" = "null" ]; then
echo "::warning::Could not fetch reviews for PR #${PR_NUMBER}."
exit 1
fi
REVIEWER_APPROVED=$(echo "${REVIEWS}" | jq -r --arg user "${REQUIRED_REVIEWER}" \
'[.[] | select(.user.login == $user)] | last | if .state then .state == "APPROVED" else false end')
echo "${GATE_NAME} (${REQUIRED_REVIEWER}) approved: ${REVIEWER_APPROVED}"
# Fallback: check if CTO approved as alternative for uat→main
if [ "${REVIEWER_APPROVED}" != "true" ] && [ -n "${ALT_REVIEWER}" ]; then
REVIEWER_APPROVED=$(echo "${REVIEWS}" | jq -r --arg user "${ALT_REVIEWER}" \
'[.[] | select(.user.login == $user)] | last | if .state then .state == "APPROVED" else false end')
if [ "${REVIEWER_APPROVED}" = "true" ]; then
echo "CTO (${ALT_REVIEWER}) approved as fallback for UAT gate."
fi
fi
if [ "${REVIEWER_APPROVED}" = "true" ]; then
echo "Promotion gate passed: ${GATE_NAME} has approved."
else
echo "Promotion gate failed: waiting for ${GATE_NAME} approval from ${REQUIRED_REVIEWER}."
exit 1
fi
dual-approval:
uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main
secrets: inherit
with:
pr_number: ${{ github.event.pull_request.number }}
+201
View File
@@ -0,0 +1,201 @@
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
+9 -69
View File
@@ -4,80 +4,20 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g. 1.0.1)'
description: 'Release version (e.g. 1.0.0)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Get tarball path
id: tarball
run: |
# headlamp-plugin package outputs the tarball path, e.g.:
# "Packaged: /path/to/headlamp-polaris-1.0.0.tar.gz"
output=$(pnpm run package 2>&1)
echo "output=$output"
# Extract tarball name, e.g. headlamp-polaris-1.0.0.tar.gz
tarball_name=$(echo "$output" | grep -oP 'headlamp-polaris-\d+\.\d+\.\d+\.tar\.gz' | tail -1)
echo "tarball_name=$tarball_name" >> $GITHUB_OUTPUT
- name: Create Gitea Release
env:
GITEA_URL: https://git.farh.net
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
REPO: privilegedescalation/headlamp-polaris-plugin
run: |
VERSION="${{ inputs.version }}"
ASSET_NAME="headlamp-polaris-${VERSION}.tar.gz"
# Create the release via Gitea API
RELEASE_RESPONSE=$(
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases" \
-d "{
\"tag_name\": \"v${VERSION}\",
\"name\": \"v${VERSION}\",
\"draft\": false,
\"prerelease\": false
}"
)
echo "Release response: ${RELEASE_RESPONSE}"
RELEASE_ID=$(echo "${RELEASE_RESPONSE}" | python3 -c "import sys, json; print(json.load(sys.stdin).get('id', ''))")
if [ -z "$RELEASE_ID" ]; then
echo "Failed to create release"
exit 1
fi
# Upload the tarball asset
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
-T "${{ steps.tarball.outputs.tarball_name }}" \
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${ASSET_NAME}"
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
with:
version: ${{ inputs.version }}
upstream-repo: 'FairwindsOps/polaris'
+4 -1
View File
@@ -2,7 +2,10 @@ node_modules/
dist/
.headlamp-plugin/
*.tar.gz
e2e/.auth/
test-results/
.playwright-mcp/
.env
.env.e2e
.env.local
.eslintcache
package-lock.json
-53
View File
@@ -1,53 +0,0 @@
{
"config": {
// Line length — not enforced for docs with code examples
"MD013": false,
// First line heading — files use YAML frontmatter, not headings
"MD041": false,
// Emphasis as heading — common pattern for Option 1/2/3 sections
"MD036": false,
// No duplicate heading — changelog files repeat section names intentionally
"MD024": false,
// Fenced code language — not always applicable for diagram blocks
"MD040": false,
// Table column style — table alignment is visual, not semantic
"MD060": false,
// Ordered list item prefix — number resets are intentional in documents
"MD029": false,
// No inline HTML — each elements are valid in valid Markdown
"MD033": false,
// List marker space — spacing after list markers varies by editor
"MD030": false,
// Blanks around headings — not always needed in compact docs
"MD022": false,
// Blanks around lists — not always needed in compact docs
"MD032": false,
// Blanks around fences — not always needed between adjacent blocks
"MD031": false,
// Multiple blanks — editor artifacts, not semantic
"MD012": false,
// Single title — files may have multiple H1 sections
"MD025": false,
// Trailing spaces — editor artifacts
"MD009": false,
// Bare URLs — URL shortening not always needed
"MD034": false,
// Single trailing newline — editor artifacts
"MD047": false,
// Trailing punctuation — heading punctuation is intentional
"MD026": false,
// Space in emphasis — double-asterisk bold spacing varies by renderer
"MD037": false,
// No hard tabs — some generated docs use tabs for indentation
"MD010": false,
// Code block style — generated docs may use inconsistent styles
"MD046": false,
// Comment style — generated docs have no comments
"MD048": false,
// Commands show output — shell examples intentionally show only commands
"MD014": false
},
"ignores": [
"docs/api-reference/generated/**"
]
}
-1
View File
@@ -1 +0,0 @@
docs/api-reference/generated/**
+2
View File
@@ -25,6 +25,8 @@ npm run format:check # Prettier check
npm test # vitest run
npm run test:watch # vitest watch mode
npx vitest run src/api/polaris.test.ts # run a single test file
npm run e2e # Playwright E2E tests
npm run e2e:headed # Playwright headed mode
```
All tests and `tsc` must pass before committing.
+575
View File
@@ -0,0 +1,575 @@
# CONTEXT.md - Headlamp Polaris Plugin
**Purpose**: Comprehensive reverse prompt for AI assistants working on this project.
---
## Project Overview
The Headlamp Polaris Plugin surfaces [Fairwinds Polaris](https://www.fairwinds.com/polaris) audit results directly inside the [Headlamp](https://headlamp.dev) Kubernetes UI. It provides a read-only dashboard showing cluster-wide security, reliability, and efficiency scores derived from Polaris policy checks.
- **Stack**: React + TypeScript plugin for Headlamp (v0.26+)
- **Data Source**: Polaris dashboard API via Kubernetes service proxy (read-only)
- **Current Version**: v0.4.1
- **Key Constraint**: No direct Kubernetes resource access - all data fetched through service proxy
## Architecture & Data Flow
### Component Hierarchy
```
src/index.tsx # Entry point: registers routes, sidebar, settings
├── PolarisDataContext.tsx # Shared data fetch with auto-refresh
├── components/
│ ├── DashboardView.tsx # Overview (score, checks, top issues)
│ ├── NamespacesListView.tsx # Namespace list with scores
│ ├── NamespaceDetailView.tsx # Per-namespace drill-down (drawer)
│ ├── PolarisSettings.tsx # Settings (refresh interval, URL, test)
│ ├── AppBarScoreBadge.tsx # Cluster score badge in top nav
│ └── InlineAuditSection.tsx # Injected into workload detail views
└── api/
└── polaris.ts # Types, hooks, utilities
```
### Data Source
- **Service Proxy Path**: `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
- **Schema**: `AuditData` with `ClusterInfo`, `Results[]` containing nested `PodResult` and `ContainerResults`
- **Method**: `ApiProxy.request()` from Headlamp plugin SDK (handles K8s API auth automatically)
### State Management
- **Pattern**: React Context (see `src/api/PolarisDataContext.tsx`)
- **Rationale**: ADR-001 - Prevents duplicate API calls when multiple components need same data
- **Auto-refresh**: User-configurable interval (1/5/10/30 min, default 5 min)
- **Storage**: Refresh interval and dashboard URL stored in `localStorage`
### Score Computation
```typescript
// Formula: (pass / total) * 100, rounded to nearest integer
function computeScore(counts: ResultCounts): number {
if (counts.total === 0) return 0;
return Math.round((counts.pass / counts.total) * 100);
}
```
## Technology Constraints
### ⚠️ CRITICAL: Headlamp Components Only
**MUST** use `@kinvolk/headlamp-plugin/lib/CommonComponents`
**NEVER** import from `@mui/material` or `@mui/icons-material`
**Why**: Historical issue (v0.3.2) - MUI imports caused plugin load failures. Headlamp provides all needed components as re-exports.
```typescript
// ✅ Correct
import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
// ❌ Wrong - will break plugin
import { Box, Chip } from '@mui/material';
```
### Other Constraints
- **TypeScript Strictness**: No `any`, explicit types, strict mode enabled
- **Packaging**: `@kinvolk/headlamp-plugin` is peer dependency - don't bundle React/MUI
- **Theme Handling**: Use CSS variables (`--mui-palette-*`), not theme imports
- **Sidebar Limitation**: Headlamp only supports 2-level nesting (parent → children)
## Component Patterns & Gotchas
### Headlamp Component Issues
1. **StatusLabel with empty status**
```typescript
// ❌ Renders near-invisible (muted background)
<StatusLabel status="">{value}</StatusLabel>
// ✅ Use plain String() for neutral values
<span>{String(value)}</span>
```
2. **Link component crashes on plugin routes**
```typescript
// ❌ Headlamp Link crashes on plugin-registered routes
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
// ✅ Use react-router-dom Link with Router.createRouteURL
import { Link } from 'react-router-dom';
import { Router } from '@kinvolk/headlamp-plugin/lib';
<Link to={Router.createRouteURL('/polaris/namespaces')}>View</Link>
```
3. **Visual components that work well**
- `PercentageCircle` - Great for score display
- `PercentageBar` - Great for check distribution
- `SimpleTable` - Fast, clean tables
- `NameValueTable` - Key-value pairs
- `SectionBox` - Card containers with titles
### Code Conventions
- **Functional Components**: Always use function components with hooks
- **Named Exports**: Prefer named exports over default exports
- **Props Interfaces**: Define as TypeScript interfaces, not inline types
- **Import Order**: React → third-party → Headlamp → local (auto-sorted by eslint)
## RBAC & Security
### Minimal Permission Required
The plugin requires **only** this RBAC permission:
| Verb | API Group | Resource | Resource Name | Namespace |
|------|-----------|----------|---------------|-----------|
| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` |
### Example Role
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: polaris-proxy-reader
namespace: polaris
rules:
- apiGroups: [""]
resources: ["services/proxy"]
resourceNames: ["polaris-dashboard"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: headlamp-polaris-proxy
namespace: polaris
subjects:
- kind: ServiceAccount
name: headlamp
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
apiGroup: rbac.authorization.k8s.io
```
### Security Notes
- **Namespaced Role**: MUST be namespaced Role, NOT ClusterRole
- **ResourceNames Required**: Always specify `resourceNames: ["polaris-dashboard"]`
- **No Write Operations**: Plugin only performs GET, never create/update/delete
- **Token-Auth Mode**: When Headlamp uses user tokens, each user needs the RoleBinding
- **Network Policy**: If enforced, allow API server → `polaris-dashboard:80` ingress
- **Audit Logging**: Every proxy request logged as K8s API audit event
## Development Workflow
### Commands
```bash
# Install dependencies
npm install
# Start development mode (hot reload at localhost:4466)
npm start
# Build plugin
npm run build
# Create tarball for distribution
npm run package
# Type-check without emitting
npm run tsc
# Lint
npm run lint
# Run unit tests
npm test
# Run E2E tests (requires cluster access)
npm run e2e
# Format code
npm run format
# Check formatting (CI)
npm run format:check
```
### Branching Strategy
- ✅ **ALWAYS use feature branches** for code changes (`feat/*`, `fix/*`, `docs/*`)
- ✅ **MAY push directly to main** for: documentation-only changes, version bump commits
- ❌ **NEVER push code changes directly to main**
### Commit Convention
Use Conventional Commits:
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation only
- `chore:` - Maintenance (deps, config)
- `test:` - Test changes
- `ci:` - CI/CD changes
### PR Process
All PRs must pass:
1. Build (`npm run build`)
2. Lint (`npm run lint`)
3. Type-check (`npm run tsc`)
4. Unit tests (`npm test`)
5. Format check (`npm run format:check`)
**Before committing**: Always run `npx prettier --write src/`
## Testing Strategy
### Unit Tests (Vitest)
```bash
npm test # Run once
npm run test:watch # Watch mode
```
- **Framework**: Vitest with jsdom environment
- **Test files**: `*.test.ts`, `*.test.tsx` in `src/`
- **Setup**: `vitest.setup.ts` with `@testing-library/jest-dom`
- **Coverage**: Focus on meaningful tests, not just numbers
- **Test utilities**: `src/test-utils.tsx` provides test wrapper with context
### E2E Tests (Playwright)
```bash
npm run e2e # Headless
npm run e2e:headed # With browser UI
```
- **Framework**: Playwright
- **Test files**: `e2e/*.spec.ts`
- `polaris.spec.ts` - Sidebar, overview, namespaces, detail drawer
- `settings.spec.ts` - Plugin settings page
- `appbar.spec.ts` - App bar score badge
- **Auth**: Supports both OIDC (Authentik) and token-based auth (see `e2e/auth.setup.ts`)
- **CI**: Runs on GitHub Actions with `k3s-animaniacs` runner
### Local E2E Setup
```bash
# Token-based auth
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
npm run e2e
# OIDC auth (Authentik)
export AUTHENTIK_USERNAME=your-username
export AUTHENTIK_PASSWORD=your-password
npm run e2e
```
## CI/CD & Release
### CI Workflow (`.github/workflows/ci.yaml`)
Runs on push to main and all PRs:
1. Checkout
2. `npm ci`
3. `npm run build`
4. `npm run lint`
5. `npm run tsc`
6. `npm run format:check`
7. `npm test`
Runner: `local-ubuntu-latest`
### E2E Workflow (`.github/workflows/e2e.yaml`)
Runs on push, PR, and manual trigger:
1. Checkout
2. `npm ci`
3. `npm run e2e`
Runner: `k3s-animaniacs` (has cluster access)
Requires: `HEADLAMP_URL`, `HEADLAMP_TOKEN` or `AUTHENTIK_USERNAME`/`AUTHENTIK_PASSWORD`
### Release Workflow (`.github/workflows/release.yaml`)
**Manual trigger** via workflow_dispatch with version input:
```bash
# Via GitHub UI or CLI
gh workflow run release.yaml -f version=0.4.2
```
Steps:
1. Validate version format (semver)
2. Bump `package.json` + `artifacthub-pkg.yml`
3. Build plugin
4. Package tarball
5. Compute SHA256 checksum
6. Commit version bump
7. Create git tag
8. Create GitHub release
9. Upload tarball to release
**Guard**: Skips if checksum already matches (prevents infinite loop)
**Post-release**: ArtifactHub pulls metadata every 30 min (no webhook, pull-based)
### Version Bump Requirements
**ALWAYS bump both files in the same commit**:
- `package.json` - `version` field
- `artifacthub-pkg.yml` - `version` field + `digest` (checksum) + `archive.url`
## Known Issues & Workarounds
### ⚠️ Headlamp v0.39.0 Known Issues
**AutoSizer JavaScript Error**
- **Symptom**: Console shows `TypeError: undefined is not an object (evaluating 'io.AutoSizer')`
- **Impact**: Cosmetic error in Settings page, doesn't break functionality
- **Root Cause**: Headlamp core bug, not plugin-related
- **Workaround**: None needed, can be ignored
**Plugin Loading (RESOLVED)**
- **Old Issue**: Previously thought `config.watchPlugins: false` was required
- **Resolution**: Plugins load correctly with default `watchPlugins: true`
- **Note**: If you see old docs mentioning `watchPlugins: false`, ignore them
### Polaris Dashboard Behavior
**Stale Audit Data**
- **Symptom**: Plugin shows old audit timestamp
- **Root Cause**: Polaris dashboard runs audit once at pod startup, then caches results
- **Does NOT**: Continuously re-audit in real-time
- **Workaround**: Restart Polaris pods for fresh data
```bash
kubectl rollout restart deployment -n polaris polaris-dashboard
```
- **Load Balancing**: Service balances across multiple pods - each may have different audit timestamps
- **Plugin Auto-Refresh**: Works correctly - just fetches whatever Polaris currently has cached
### Skipped Count Limitation
**What It Shows**:
- Only checks with `Severity: "ignore"` in Polaris API response
- Does NOT include annotation-based exemptions (`polaris.fairwinds.com/*-exempt`)
**Why**:
- Polaris omits exempted checks from `results.json`
- Plugin has no access to raw K8s resources to compute exemptions
- By design: service proxy limitation
**Workaround**:
- Link to native Polaris dashboard for full exemption count
- UI tooltip explains this limitation
## Deployment Patterns
### Plugin Manager (Recommended)
Install via Headlamp UI (Settings → Plugins → Catalog) or Helm values:
```yaml
pluginsManager:
enabled: true
configContent: |
plugins:
- name: polaris
source: https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin
```
### Sidecar Container (Alternative)
```yaml
spec:
containers:
- name: headlamp
# ... main container
- name: headlamp-plugin
image: node:lts-alpine
command:
- /bin/sh
- -c
- |
npx @headlamp-k8s/pluginctl@latest install \
--config /config/plugin.yml \
--folderName /headlamp/plugins \
--watch
volumeMounts:
- name: plugins-dir
mountPath: /headlamp/plugins
- name: plugin-config
mountPath: /config
volumes:
- name: plugins-dir
emptyDir: {}
- name: plugin-config
configMap:
name: headlamp-plugin-config
```
### Manual Tarball
```bash
# Download release
wget https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.4.1/headlamp-polaris-plugin-0.4.1.tgz
# Extract to plugin directory
tar -xzf headlamp-polaris-plugin-0.4.1.tgz -C /headlamp/plugins/
# Restart Headlamp
kubectl rollout restart deployment headlamp -n kube-system
```
## Project Files Reference
```
src/
index.tsx # Entry point: registers sidebar, routes, settings, etc.
api/
polaris.ts # Core types, usePolarisData hook, utilities
PolarisDataContext.tsx # React Context provider for shared data
components/
DashboardView.tsx # Overview page (score, checks, top issues)
NamespacesListView.tsx # Namespace table with scores
NamespaceDetailView.tsx # Drawer panel with per-namespace drill-down
PolarisSettings.tsx # Settings page (refresh, URL, test)
AppBarScoreBadge.tsx # Cluster score chip in top nav bar
InlineAuditSection.tsx # Injected into resource detail views
test-utils.tsx # Test helpers (wrapper with context)
.github/workflows/
ci.yaml # Lint, type-check, build, test
e2e.yaml # Playwright E2E tests
release.yaml # Automated releases
e2e/ # Playwright tests
polaris.spec.ts # Main plugin functionality
settings.spec.ts # Settings page
appbar.spec.ts # App bar badge
auth.setup.ts # OIDC/token auth setup
docs/ # Comprehensive documentation
architecture/ # Overview, design decisions, ADRs
deployment/ # Helm, Kubernetes, production guides
troubleshooting/ # Common issues, RBAC, network problems
getting-started/ # Quick start, prerequisites, installation
package.json # Version, scripts, dependencies
artifacthub-pkg.yml # ArtifactHub metadata (version, checksum)
tsconfig.json # Extends @kinvolk/headlamp-plugin config
vitest.config.mts # Vitest config (jsdom, excludes e2e/)
.eslintrc.js # Extends @headlamp-k8s/eslint-config
.prettierrc.js # Uses @headlamp-k8s prettier config
```
## MCP Servers (Claude Code)
- **GitHub**: Source control (`github-mcp-server`), repo at `cpfarhood/headlamp-polaris-plugin`
- **Kubernetes (local)**: Cluster access via `kubernetes-mcp-server`
- **Flux (local)**: Flux Operator access via `flux-operator-mcp`
- **Playwright**: Browser automation via `@playwright/mcp`
## Common Tasks Quick Reference
```bash
# Start development
npm install && npm start
# Run all checks before PR
npm run build && npm run lint && npm run tsc && npm test && npm run format
# Create release (maintainers only)
# 1. Edit CHANGELOG.md
# 2. Trigger release workflow:
gh workflow run release.yaml -f version=0.4.2
# Run E2E tests locally
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
npm run e2e
# Fix formatting issues
npx prettier --write src/
# Check Polaris audit freshness
kubectl get --raw "/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json" | jq -r '.AuditTime'
# Restart Polaris for fresh audit
kubectl rollout restart deployment -n polaris polaris-dashboard
```
## Anti-Patterns (What NOT to Do)
- ❌ Import from `@mui/material` or `@mui/icons-material` → breaks plugin
- ❌ Use `any` type → strict TypeScript required
- ❌ Push code changes directly to main → always use feature branches
- ❌ Grant broader RBAC than `get services/proxy` → security risk
- ❌ Use ClusterRole instead of namespaced Role → violates least privilege
- ❌ Forget to run `npx prettier --write src/` → CI will fail
- ❌ Use inline styles without CSS variables → breaks dark mode
- ❌ Try to query K8s resources directly → plugin only has service proxy access
- ❌ Import Headlamp `Link` for plugin routes → use react-router-dom `Link` + `Router.createRouteURL()`
- ❌ Assume Polaris continuously re-audits → it only audits at pod startup
## Quick Diagnosis Guide
```
Symptom: Plugin not in sidebar
→ Check: Hard refresh browser (Cmd+Shift+R / Ctrl+Shift+R)
→ Check: Plugin installed? kubectl get configmap headlamp-plugin-config -n kube-system
Symptom: 403 Access Denied
→ Check: RBAC binding exists? kubectl get role,rolebinding -n polaris
→ Fix: Apply RBAC example from docs/deployment/rbac.md
Symptom: 404 or 503
→ Check: Polaris installed? kubectl get pods -n polaris
→ Check: Service exists? kubectl get svc polaris-dashboard -n polaris
Symptom: Stale audit data
→ Fix: kubectl rollout restart deployment -n polaris polaris-dashboard
→ Verify: Check AuditTime in UI matches current date
Symptom: Settings page empty or broken
→ Check: Plugin version ≥ v0.3.3?
→ Fix: Upgrade plugin and hard refresh browser
Symptom: CI prettier check fails
→ Fix: npx prettier --write src/
→ Commit: Include formatting fixes in your PR
Symptom: Dark mode white backgrounds
→ Check: Plugin version ≥ v0.3.5?
→ Fix: Upgrade and hard refresh browser
```
## Historical Context
### Why Service Proxy Instead of ConfigMaps?
Early versions (< v0.0.10) incorrectly documented ConfigMap RBAC. The plugin **never** accessed ConfigMaps - it always used the service proxy. This was clarified in v0.0.10.
### Why No MUI Imports?
v0.3.2 removed direct MUI imports because they caused plugin load failures. Headlamp provides all needed MUI components as re-exports through `CommonComponents`.
### Why React Context?
ADR-001 documents the switch to React Context. Before v0.3.0, each component called `usePolarisData()` independently, causing duplicate API requests. Context ensures a single shared fetch.
### Why No Continuous Polaris Audits?
Polaris dashboard mode runs a one-time audit at pod startup and caches results. This is by design in Polaris itself. For continuous auditing, Polaris would need to be configured in webhook mode (admission controller), which is a different deployment pattern.
---
**Last Updated**: 2026-02-12
**Version**: v0.4.1
**Target Headlamp**: v0.26+
**Target Polaris**: v9.x
+290
View File
@@ -0,0 +1,290 @@
# Headlamp Polaris Plugin - Project Assessment
**Date:** 2026-02-11
**Version:** v0.3.0
**Status:** Active Development
## Executive Summary
This assessment identifies critical issues and improvement opportunities for the headlamp-polaris-plugin project. The plugin is currently non-functional in production due to Headlamp v0.39.0 compatibility issues, and has several TypeScript compilation errors that need immediate attention.
---
## 🔴 Critical Issues (Must Fix Immediately)
### 1. TypeScript Compilation Errors
**Severity:** CRITICAL
**Impact:** Build failures, type safety compromised
**Issues:**
- `src/index.tsx:72` - `registerDetailsViewSection` expects 1 argument, got 2
- `src/index.tsx:87` - `registerAppBarAction` expects 1 argument, got 2
**Recommendation:**
Update Headlamp plugin API calls to match the current version. Check @kinvolk/headlamp-plugin version compatibility.
**Action Items:**
- [ ] Review Headlamp plugin API documentation
- [ ] Update `registerDetailsViewSection` and `registerAppBarAction` calls
- [ ] Run `npm run tsc` to verify fixes
- [ ] Update CI to fail on TypeScript errors
---
### 2. Production Plugin Loading Failure
**Severity:** CRITICAL
**Impact:** Plugin is completely non-functional in production
**Root Cause:**
Headlamp v0.39.0 with default `watchPlugins: true` treats catalog-managed plugins as "development directory" plugins, preventing frontend JavaScript execution.
**Current Status:**
- Deployment patched to install plugins to `/headlamp/static-plugins`
- `watchPlugins: false` configured
- Waiting for user to test if plugins now load
**Action Items:**
- [ ] Confirm plugins load after recent deployment changes
- [ ] Document the fix in deployment guide
- [ ] Update MEMORY.md with final resolution
- [ ] Consider downgrading Headlamp if issue persists
---
### 3. Test Failures
**Severity:** HIGH
**Impact:** CI failures, reduced confidence in changes
**Current Status:**
- 1 test file failing (DashboardView)
- 49 tests passing
- Error related to `SimpleTable` component mock
**Action Items:**
- [ ] Fix DashboardView test mocking
- [ ] Ensure all tests pass before merging PRs
- [ ] Add test for top issues feature
- [ ] Increase test coverage to >80%
---
## 🟡 High Priority Improvements
### 4. Type Safety Enhancements
**Severity:** HIGH
**Impact:** Better developer experience, catch errors earlier
**Recommendations:**
- Enable stricter TypeScript checks in `tsconfig.json`
- Add type definitions for all Headlamp plugin APIs
- Ensure no `any` types in production code
- Add JSDoc comments for complex types
**Action Items:**
- [ ] Audit codebase for `any` types
- [ ] Enable `noImplicitAny` and `strictNullChecks`
- [ ] Add type guards for API responses
- [ ] Document complex type structures
---
### 5. Security Hardening
**Severity:** HIGH
**Impact:** Prevent vulnerabilities, protect user data
**Current Risks:**
- Direct Kubernetes API access via service proxy
- User input in exemption annotations (potential injection)
- External URL configuration for Polaris dashboard
**Recommendations:**
- Validate and sanitize all user inputs
- Implement input validation for dashboard URL
- Add CSRF protection for exemption management
- Audit dependencies for known vulnerabilities
**Action Items:**
- [ ] Add input validation utilities
- [ ] Sanitize exemption annotation values
- [ ] Validate URL format for dashboard configuration
- [ ] Run `npm audit` and fix vulnerabilities
- [ ] Add security testing to CI/CD
---
### 6. Error Handling & User Experience
**Severity:** MEDIUM
**Impact:** Better error messages, improved debugging
**Current Gaps:**
- Generic error messages don't help users troubleshoot
- No retry logic for transient API failures
- Missing loading states in some components
**Recommendations:**
- Provide specific, actionable error messages
- Implement retry logic with exponential backoff
- Add loading skeletons for all async operations
- Show connection test results with specific failure reasons
**Action Items:**
- [ ] Create error message constants with solutions
- [ ] Add retry logic to API calls
- [ ] Implement loading skeletons
- [ ] Improve connection test error messages
---
## 🟢 Medium Priority Enhancements
### 7. Testing Coverage
**Severity:** MEDIUM
**Impact:** Confidence in changes, regression prevention
**Current Coverage:**
- Unit tests: Good coverage for API utilities
- Component tests: Some coverage, gaps exist
- E2E tests: Minimal (Playwright configured but underutilized)
**Recommendations:**
- Add E2E tests for critical user flows
- Test error scenarios and edge cases
- Add visual regression tests
- Test RBAC permission denied scenarios
**Action Items:**
- [ ] Write E2E test for complete audit workflow
- [ ] Add tests for error states
- [ ] Test exemption management flow
- [ ] Add Playwright tests to CI
---
### 8. Performance Optimization
**Severity:** MEDIUM
**Impact:** Faster load times, better UX
**Opportunities:**
- Memoize expensive calculations (score computation)
- Lazy load namespace detail views
- Debounce search/filter operations
- Cache Polaris data with stale-while-revalidate
**Action Items:**
- [ ] Add React.memo to pure components
- [ ] Memoize score calculations
- [ ] Implement data caching strategy
- [ ] Profile component render times
---
### 9. Code Quality & Maintainability
**Severity:** MEDIUM
**Impact:** Easier maintenance, onboarding
**Recommendations:**
- Extract magic strings to constants
- Reduce component complexity
- Add JSDoc comments for public APIs
- Improve code organization
**Action Items:**
- [ ] Create constants file for check IDs
- [ ] Split large components (DashboardView, NamespaceDetailView)
- [ ] Add comments for complex logic
- [ ] Establish code review checklist
---
## 🔵 Low Priority / Future Enhancements
### 10. Documentation
**Severity:** LOW
**Impact:** Better onboarding, user adoption
**Gaps:**
- No architecture documentation
- Limited inline code comments
- Missing troubleshooting guide
- No contributor guidelines
**Action Items:**
- [ ] Create architecture diagram
- [ ] Document component hierarchy
- [ ] Add troubleshooting section to README
- [ ] Create CONTRIBUTING.md
---
### 11. CI/CD Pipeline Optimization
**Severity:** LOW
**Impact:** Faster feedback, automated releases
**Opportunities:**
- Run tests in parallel
- Cache npm dependencies
- Add automated security scanning
- Implement semantic versioning
**Action Items:**
- [ ] Parallelize test execution
- [ ] Add npm cache to GitHub Actions
- [x] Renovate is configured org-wide via `github>privilegedescalation/.github:renovate-config`
- [ ] Add semantic-release
---
## Summary & Prioritization
### Week 1 (Immediate)
1. ✅ Fix TypeScript compilation errors
2. ✅ Resolve production plugin loading issue
3. ✅ Fix failing DashboardView test
### Week 2 (High Priority)
4. Enhance type safety (strict mode)
5. Implement security hardening
6. Improve error handling and UX
### Week 3-4 (Medium Priority)
7. Increase test coverage to >80%
8. Optimize performance (memoization, caching)
9. Refactor for maintainability
### Ongoing (Low Priority)
10. Documentation improvements
11. CI/CD optimizations
---
## Success Metrics
**Code Quality:**
- ✅ Zero TypeScript errors
- ✅ All tests passing
- 🎯 Test coverage >80%
- 🎯 No high/critical security vulnerabilities
**Production Readiness:**
- ✅ Plugin loads successfully in Headlamp
- ✅ All features functional
- 🎯 Error rate <1%
- 🎯 Average response time <500ms
**Developer Experience:**
- ✅ Clear documentation
- ✅ Easy local setup
- 🎯 Fast CI/CD (<5 min)
- 🎯 Automated releases
---
## Next Steps
1. **Immediate:** Fix TypeScript errors and verify plugin loads
2. **Short-term:** Complete Week 1-2 priorities
3. **Long-term:** Address medium and low priority items
4. **Continuous:** Monitor metrics and iterate
**Recommended First Action:**
Fix the TypeScript compilation errors in `src/index.tsx` by updating the Headlamp plugin API calls.
+2 -2
View File
@@ -97,7 +97,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp # adjust to match your Headlamp service account
namespace: <your-namespace>
namespace: kube-system # adjust to match the namespace Headlamp runs in
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -197,7 +197,7 @@ npm test
npm run test:watch
# E2E tests (Playwright)
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n <your-namespace> --duration=24h)
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
npm run e2e
npm run e2e:headed # see browser
```
+3 -3
View File
@@ -71,7 +71,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -149,7 +149,7 @@ spec:
### Service Account (Default)
Headlamp runs with a dedicated service account (`headlamp` in the namespace where Headlamp is installed). All users share the same permissions defined by this service account's RBAC bindings.
Headlamp runs with a dedicated service account (`headlamp` in `kube-system`). All users share the same permissions defined by this service account's RBAC bindings.
**Security Considerations:**
- All users have identical access to the plugin
@@ -317,7 +317,7 @@ All service proxy requests are logged in Kubernetes API audit logs (if enabled):
"verb": "get",
"requestURI": "/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json",
"user": {
"username": "system:serviceaccount:<your-namespace>:headlamp",
"username": "system:serviceaccount:kube-system:headlamp",
"groups": ["system:serviceaccounts", "system:authenticated"]
}
}
+98
View File
@@ -0,0 +1,98 @@
# PRI-324 Spec: Make E2E Workflow Self-Sufficient with RBAC
## Context
PR #123 introduced an RBAC pre-flight check to the E2E workflow. QA (Nancy, acting as QA) verified the "fails fast without RBAC" path works, but found that the "with RBAC passes" path had no green CI evidence — the workflow did not apply RBAC before the pre-flight check.
PR #131 attempted to fix this by adding `kubectl apply` steps and extending the CI runner RBAC, but its merge commit (739db6fe) was reverted by the next commit on main (aa1db921) due to a vulnerability fix PR (#128).
The current E2E workflow on `main` lacks the RBAC apply steps and CI runner permissions needed to make the pre-flight check meaningful.
## Required Changes
### 1. `.github/workflows/e2e.yaml`
Add between the "Setup kubectl" and "Install dependencies" steps:
```yaml
- 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 CI runner permissions..."
kubectl auth can-i create roles -n headlamp-dev --as="system:serviceaccount:arc-runners:runners-privilegedescalation-gha-rs-no-permission" 2>&1 || { echo "::error::CI runner still lacks roles permission after propagation wait"; exit 1; }
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" --quiet 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
```
### 2. `deployment/e2e-ci-runner-rbac.yaml`
Add a new Role + RoleBinding for the `polaris` namespace (from PR #131):
```yaml
---
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
```
And add to the existing `e2e-ci-runner` Role in the `headlamp-dev` namespace:
```yaml
# Apply Polaris dashboard RBAC in the polaris namespace
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["roles", "rolebindings"]
verbs: ["get", "list", "create", "update", "patch", "delete"]
```
## Acceptance Criteria
- [ ] Workflow applies `deployment/e2e-ci-runner-rbac.yaml` before the pre-flight check
- [ ] Workflow applies `deployment/polaris-rbac.yaml` before the pre-flight check
- [ ] CI runner has RBAC to apply the manifests (added via new Role+RoleBinding in polaris namespace)
- [ ] E2E pipeline passes on the PR branch (proof of green path)
- [ ] `kubectl get … --quiet` flag removed (QA nit)
- [ ] `MISSING_ROLE`/`MISSING_ROLEBINDING` collapsed to single `MISSING` flag (QA nit)
## Definition of Done
PR #123 QA changes-requested are addressed: the workflow is self-sufficient (applies its own RBAC), the green path is demonstrated, and QA review is re-requested.
+43 -48
View File
@@ -1,28 +1,30 @@
version: 1.0.1
version: "1.0.0"
name: headlamp-polaris
displayName: Polaris
createdAt: '2026-05-20T00:00:00Z'
description: Surfaces Fairwinds Polaris audit results inside the Headlamp UI. Shows
cluster score, check summary, and per-namespace drill-downs with per-resource pass/warning/danger
breakdowns. Data is fetched read-only via the Kubernetes service proxy to the Polaris
dashboard. Requires a Role granting `get` on `services/proxy` for the `polaris-dashboard`
service in the `polaris` namespace.
createdAt: "2026-02-05T19:00:00Z"
description: >-
Surfaces Fairwinds Polaris audit results inside the Headlamp UI.
Shows cluster score, check summary, and per-namespace drill-downs
with per-resource pass/warning/danger breakdowns. Data is fetched
read-only via the Kubernetes service proxy to the Polaris dashboard.
Requires a Role granting `get` on `services/proxy` for the
`polaris-dashboard` service in the `polaris` namespace.
license: Apache-2.0
homeURL: https://github.com/privilegedescalation/headlamp-polaris-plugin
appVersion: 10.1.6
homeURL: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
appVersion: "10.1.6"
category: security
keywords:
- polaris
- fairwinds
- security
- audit
- headlamp
- kubernetes
- polaris
- fairwinds
- security
- audit
- headlamp
- kubernetes
links:
- name: Source
url: https://github.com/privilegedescalation/headlamp-polaris-plugin
- name: Polaris
url: https://polaris.docs.fairwinds.com/
- name: Source
url: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
- name: Polaris
url: "https://polaris.docs.fairwinds.com/"
install: |
## Installation
@@ -48,34 +50,27 @@ install: |
For more information, see the [README](https://github.com/privilegedescalation/headlamp-polaris-plugin/blob/main/README.md).
changes:
- kind: security
description: Patched 8 npm audit vulnerabilities via pnpm.overrides
- kind: added
description: Dual-approval required CI check — PRs must be approved by both CTO
and QA before merge
- kind: added
description: ExemptionManager test suite — full coverage of annotation-based exemption
flows
- kind: fixed
description: E2E infrastructure overhauled — ConfigMap volume mount replaces Dockerfile-based
approach, tests run in privilegedescalation-dev namespace
- kind: fixed
description: E2E workflow uses token auth and waits for HTTP reachability before
running tests
- kind: fixed
description: Added explicit direct devDependencies (typescript, eslint, prettier,
@headlamp-k8s/eslint-config) to prevent phantom dep failures
- kind: changed
description: pnpm version pinned via packageManager field; GitHub Actions SHA-pinned
via Renovate pinDigests
- kind: changed
description: v1.0.0 stable release — plugin API (routes, sidebar, settings schema,
app bar action) is stable and will not change without a major version bump
- kind: security
description: Patched 8 npm audit vulnerabilities via pnpm.overrides
- kind: added
description: Dual-approval required CI check — PRs must be approved by both CTO and QA before merge
- kind: added
description: ExemptionManager test suite — full coverage of annotation-based exemption flows
- kind: fixed
description: E2E infrastructure overhauled — ConfigMap volume mount replaces Dockerfile-based approach, tests run in privilegedescalation-dev namespace
- kind: fixed
description: E2E workflow uses token auth and waits for HTTP reachability before running tests
- kind: fixed
description: Added explicit direct devDependencies (typescript, eslint, prettier, @headlamp-k8s/eslint-config) to prevent phantom dep failures
- kind: changed
description: pnpm version pinned via packageManager field; GitHub Actions SHA-pinned via Renovate pinDigests
- kind: changed
description: v1.0.0 stable release — plugin API (routes, sidebar, settings schema, app bar action) is stable and will not change without a major version bump
maintainers:
- name: privilegedescalation
email: chris@farhood.org
- name: privilegedescalation
email: "chris@farhood.org"
annotations:
headlamp/plugin/archive-url: https://git.farh.net/privilegedescalation/headlamp-polaris-plugin/releases/download/v1.0.1/headlamp-polaris-1.0.1.tar.gz
headlamp/plugin/version-compat: '>=0.26'
headlamp/plugin/archive-checksum: sha256:1e05d079c7032cf55ebde85e116cb65b686d207f4b6a3b0f716f0af93f933e7e
headlamp/plugin/distro-compat: in-cluster,web,desktop
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v1.0.0/headlamp-polaris-1.0.0.tar.gz"
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/archive-checksum: sha256:a165e871b40f11a44950aa9f10eb7f7883276f749026ae7a4f886278ecd9bd7d
headlamp/plugin/distro-compat: "in-cluster,web,desktop"
-20
View File
@@ -1,20 +0,0 @@
{
// Allowlist for inherited dev-dependency CVEs from @kinvolk/headlamp-plugin
// CTO decision (PRI-854): these high-severity vulns are dev/build-time only,
// trace to @kinvolk/headlamp-plugin transitive deps (Picomatch, Vite, lodash),
// and do NOT ship in production plugin artifacts.
"allowlist": [
{
"id": "GHSA-hhpm-516h-p3p6",
"reason": "Picomatch ReDoS: devDependency only, does not ship in production plugin bundle"
},
{
"id": "GHSA-36xf-7xpp-53w5",
"reason": "Vite arbitrary file read: devDependency only, does not ship in production plugin bundle"
},
{
"id": "GHSA-jf8v-p3pp-93qh",
"reason": "lodash code injection via _.template: devDependency only, does not ship in production plugin bundle"
}
]
}
+58
View File
@@ -0,0 +1,58 @@
# 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 <headlamp-pod> -c headlamp-plugin`
2. Verify watchPlugins is false: `kubectl logs -n kube-system <headlamp-pod> -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
+74
View File
@@ -0,0 +1,74 @@
---
# 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
+28
View File
@@ -0,0 +1,28 @@
# 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
+2 -2
View File
@@ -33,7 +33,7 @@ kubectl -n polaris get svc polaris-dashboard
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
# Verify Headlamp is deployed
kubectl -n <your-namespace> get pods -l app.kubernetes.io/name=headlamp
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
```
## Installation Methods
@@ -59,7 +59,7 @@ kubectl -n <your-namespace> get pods -l app.kubernetes.io/name=headlamp
```bash
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml
```
+3 -2
View File
@@ -268,9 +268,10 @@ npm run e2e
```bash
# Create token
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n <your-namespace> --duration=24h)
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
kubectl port-forward -n <your-namespace> svc/headlamp 4466:80
# Port-forward for local testing
kubectl port-forward -n kube-system svc/headlamp 4466:80
# Run tests
HEADLAMP_URL=http://localhost:4466 npm run e2e
+16 -16
View File
@@ -33,7 +33,7 @@ This guide covers common issues encountered when using the Headlamp Polaris Plug
```bash
# View Headlamp pod logs (plugin sidecar)
kubectl logs -n <your-namespace> deployment/headlamp -c headlamp-plugin
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
# Expected output:
# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
@@ -43,7 +43,7 @@ kubectl logs -n <your-namespace> deployment/headlamp -c headlamp-plugin
**Verify plugin files exist**:
```bash
kubectl exec -n <your-namespace> deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
# Should show: headlamp-polaris-plugin/
```
@@ -118,7 +118,7 @@ Expected subjects:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
```
For OIDC mode:
@@ -154,7 +154,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -169,7 +169,7 @@ Service account mode:
```bash
# Impersonate Headlamp service account
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
--resource-name=polaris-dashboard \
-n polaris
# Expected: yes
@@ -189,7 +189,7 @@ kubectl auth can-i get services/proxy \
After applying RBAC changes:
```bash
kubectl rollout restart deployment headlamp -n <your-namespace>
kubectl rollout restart deployment headlamp -n kube-system
```
---
@@ -490,7 +490,7 @@ Run this script to test all RBAC components:
#!/bin/bash
NS="polaris"
SA="headlamp"
SA_NS="<your-namespace>"
SA_NS="kube-system"
echo "=== Testing RBAC for Polaris Plugin ==="
@@ -529,8 +529,8 @@ echo "=== Test complete ==="
Test connectivity from Headlamp to Polaris:
```bash
# Create debug pod in headlamp namespace
kubectl run netdebug -n <your-namespace> --rm -it --image=nicolaka/netshoot -- bash
# Create debug pod in kube-system namespace
kubectl run netdebug -n kube-system --rm -it --image=nicolaka/netshoot -- bash
# Inside pod, test DNS and HTTP
nslookup polaris-dashboard.polaris.svc.cluster.local
@@ -545,11 +545,11 @@ If you have audit logging enabled, check for denied requests:
```bash
# View recent audit logs (location varies by cluster)
kubectl logs -n <your-namespace> kube-apiserver-* | grep polaris-dashboard
kubectl logs -n kube-system kube-apiserver-* | grep polaris-dashboard
# Look for lines with:
# "reason": "Forbidden"
# "user": "system:serviceaccount:<your-namespace>:headlamp"
# "user": "system:serviceaccount:kube-system:headlamp"
```
---
@@ -567,7 +567,7 @@ kubectl logs -n <your-namespace> kube-apiserver-* | grep polaris-dashboard
**Check sidecar logs**:
```bash
kubectl logs -n <your-namespace> deployment/headlamp -c headlamp-plugin
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
```
**Common errors**:
@@ -591,7 +591,7 @@ Error: 404 Not Found
**Solution**: Verify `archive-url` in plugin config matches GitHub release:
```bash
kubectl get configmap headlamp-plugin-config -n <your-namespace> -o yaml
kubectl get configmap headlamp-plugin-config -n kube-system -o yaml
```
Expected format:
@@ -677,13 +677,13 @@ If none of these solutions work, gather debugging information and open an issue:
1. **Version Information**:
```bash
kubectl get pods -n <your-namespace> -l app.kubernetes.io/name=headlamp -o yaml | grep image:
kubectl get pods -n kube-system -l app.kubernetes.io/name=headlamp -o yaml | grep image:
```
2. **Plugin Version**:
- Check Settings → Plugins in Headlamp UI
- Or: `kubectl exec -n <your-namespace> deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
- Or: `kubectl exec -n kube-system deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
3. **Browser Console Output**:
@@ -698,7 +698,7 @@ If none of these solutions work, gather debugging information and open an issue:
5. **Pod Logs**:
```bash
kubectl logs -n <your-namespace> deployment/headlamp -c headlamp --tail=100
kubectl logs -n kube-system deployment/headlamp -c headlamp --tail=100
kubectl logs -n polaris deployment/polaris-dashboard --tail=100
```
+20 -20
View File
@@ -41,11 +41,11 @@ pluginsManager:
```bash
# Install Headlamp
helm install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml
# Wait for deployment
kubectl -n <your-namespace> wait --for=condition=available deployment/headlamp --timeout=300s
kubectl -n kube-system wait --for=condition=available deployment/headlamp --timeout=300s
```
After installation, install the plugin via Headlamp UI (**Settings → Plugins → Catalog**).
@@ -131,7 +131,7 @@ Deploy:
```bash
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml \
--wait \
--timeout 5m
@@ -177,7 +177,7 @@ apiVersion: v1
kind: ConfigMap
metadata:
name: headlamp-plugin-config
namespace: <your-namespace>
namespace: kube-system
data:
plugin.yml: |
- name: headlamp-polaris-plugin
@@ -191,7 +191,7 @@ Apply ConfigMap then deploy Headlamp:
kubectl apply -f headlamp-plugin-config.yaml
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml
```
@@ -221,7 +221,7 @@ apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: headlamp
namespace: <your-namespace>
namespace: kube-system
spec:
interval: 30m
chart:
@@ -300,7 +300,7 @@ kubectl apply -f helmrepository.yaml
kubectl apply -f helmrelease.yaml
# Watch deployment
flux get helmreleases -n <your-namespace> --watch
flux get helmreleases -n kube-system --watch
```
## RBAC Configuration
@@ -329,7 +329,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -349,7 +349,7 @@ helm repo update
# Upgrade Headlamp (preserves plugin configuration)
helm upgrade headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml \
--wait
```
@@ -365,15 +365,15 @@ helm upgrade headlamp headlamp/headlamp \
```bash
# Update ConfigMap with new version
kubectl -n <your-namespace> edit configmap headlamp-plugin-config
kubectl -n kube-system edit configmap headlamp-plugin-config
# Update version and URL:
# version: 0.3.6
# url: https://github.com/.../v0.3.6/polaris-0.3.10.tar.gz
# Restart deployment to trigger init container
kubectl -n <your-namespace> rollout restart deployment/headlamp
kubectl -n <your-namespace> rollout status deployment/headlamp
kubectl -n kube-system rollout restart deployment/headlamp
kubectl -n kube-system rollout status deployment/headlamp
```
## Troubleshooting
@@ -382,25 +382,25 @@ kubectl -n <your-namespace> rollout status deployment/headlamp
```bash
# Check Headlamp values
helm get values headlamp -n <your-namespace>
helm get values headlamp -n kube-system
# Verify plugin files exist
kubectl -n <your-namespace> exec deployment/headlamp -c headlamp -- \
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
ls -la /headlamp/plugins/headlamp-polaris-plugin/
# If missing, reinstall plugin via UI or check init container logs
kubectl -n <your-namespace> logs deployment/headlamp -c install-polaris-plugin
kubectl -n kube-system logs deployment/headlamp -c install-polaris-plugin
```
### Helm Release Stuck
```bash
# Check Helm release status
helm list -n <your-namespace>
helm list -n kube-system
# If stuck, force upgrade
helm upgrade headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml \
--force \
--wait
@@ -410,13 +410,13 @@ helm upgrade headlamp headlamp/headlamp \
```bash
# Check HelmRelease status
flux get helmreleases -n <your-namespace>
flux get helmreleases -n kube-system
# Check events
kubectl -n <your-namespace> describe helmrelease headlamp
kubectl -n kube-system describe helmrelease headlamp
# Force reconciliation
flux reconcile helmrelease headlamp -n <your-namespace>
flux reconcile helmrelease headlamp -n kube-system
```
## Next Steps
+21 -21
View File
@@ -47,7 +47,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -71,7 +71,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
# Test permission
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
@@ -90,7 +90,7 @@ apiVersion: v1
kind: ConfigMap
metadata:
name: headlamp-plugin-config
namespace: <your-namespace>
namespace: kube-system
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/component: plugin-config
@@ -109,7 +109,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: headlamp
namespace: <your-namespace>
namespace: kube-system
labels:
app.kubernetes.io/name: headlamp
spec:
@@ -194,7 +194,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: headlamp
namespace: <your-namespace>
namespace: kube-system
labels:
app.kubernetes.io/name: headlamp
@@ -204,7 +204,7 @@ apiVersion: v1
kind: Service
metadata:
name: headlamp
namespace: <your-namespace>
namespace: kube-system
labels:
app.kubernetes.io/name: headlamp
spec:
@@ -235,27 +235,27 @@ kubectl apply -f headlamp-service.yaml
kubectl apply -f headlamp-serviceaccount.yaml
# Wait for deployment to be ready
kubectl -n <your-namespace> wait --for=condition=available deployment/headlamp --timeout=300s
kubectl -n kube-system wait --for=condition=available deployment/headlamp --timeout=300s
```
### 2. Verify Deployment
```bash
# Check pods are running
kubectl -n <your-namespace> get pods -l app.kubernetes.io/name=headlamp
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
# Expected output:
# NAME READY STATUS RESTARTS AGE
# headlamp-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
# Check init container logs
kubectl -n <your-namespace> logs deployment/headlamp -c install-plugins
kubectl -n kube-system logs deployment/headlamp -c install-plugins
# Expected output:
# Plugin installation complete
# Verify plugin files exist
kubectl -n <your-namespace> exec deployment/headlamp -c headlamp -- \
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
ls -la /headlamp/plugins/headlamp-polaris-plugin/
# Expected output:
@@ -273,7 +273,7 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
```bash
# Port-forward to access locally
kubectl -n <your-namespace> port-forward service/headlamp 8080:80
kubectl -n kube-system port-forward service/headlamp 8080:80
# Open browser to http://localhost:8080
```
@@ -309,7 +309,7 @@ k8s/
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: <your-namespace>
namespace: kube-system
commonLabels:
app.kubernetes.io/name: headlamp
@@ -401,7 +401,7 @@ spec:
- apiVersion: apps/v1
kind: Deployment
name: headlamp
namespace: <your-namespace>
namespace: kube-system
```
## Upgrading the Plugin
@@ -410,24 +410,24 @@ spec:
```bash
# Edit ConfigMap with new version
kubectl -n <your-namespace> edit configmap headlamp-plugin-config
kubectl -n kube-system edit configmap headlamp-plugin-config
# Update version and URL:
# version: 0.3.6
# url: https://github.com/.../v0.3.6/polaris-0.3.10.tar.gz
# Restart deployment to trigger init container
kubectl -n <your-namespace> rollout restart deployment/headlamp
kubectl -n kube-system rollout restart deployment/headlamp
# Wait for rollout to complete
kubectl -n <your-namespace> rollout status deployment/headlamp
kubectl -n kube-system rollout status deployment/headlamp
```
### Verify Upgrade
```bash
# Check init container logs
kubectl -n <your-namespace> logs deployment/headlamp -c install-plugins
kubectl -n kube-system logs deployment/headlamp -c install-plugins
# Verify new version in UI
# Navigate to Settings → Plugins in Headlamp
@@ -439,7 +439,7 @@ kubectl -n <your-namespace> logs deployment/headlamp -c install-plugins
```bash
# Check init container logs
kubectl -n <your-namespace> logs deployment/headlamp -c install-plugins
kubectl -n kube-system logs deployment/headlamp -c install-plugins
# Common issues:
# 1. Network connectivity to GitHub
@@ -451,14 +451,14 @@ kubectl -n <your-namespace> logs deployment/headlamp -c install-plugins
```bash
# Verify HEADLAMP_CONFIG_WATCH_PLUGINS is false
kubectl -n <your-namespace> get deployment headlamp -o yaml | grep WATCH_PLUGINS
kubectl -n kube-system get deployment headlamp -o yaml | grep WATCH_PLUGINS
# Expected output:
# - name: HEADLAMP_CONFIG_WATCH_PLUGINS
# value: "false"
# If not set or "true", update deployment
kubectl -n <your-namespace> edit deployment headlamp
kubectl -n kube-system edit deployment headlamp
```
### RBAC Permissions Denied
@@ -466,7 +466,7 @@ kubectl -n <your-namespace> edit deployment headlamp
```bash
# Test RBAC
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
+15 -15
View File
@@ -37,8 +37,8 @@ kubectl -n polaris get svc polaris-dashboard
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
# Verify Headlamp
kubectl -n <your-namespace> get deployment headlamp
kubectl -n <your-namespace> get svc headlamp
kubectl -n kube-system get deployment headlamp
kubectl -n kube-system get svc headlamp
```
## Production Checklist
@@ -60,17 +60,17 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
# 2. Verify RBAC permissions
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
# Expected: yes
# 3. Check Headlamp logs for plugin loading
kubectl -n <your-namespace> logs deployment/headlamp | grep -i polaris
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
# Expected: No errors related to plugin loading
# 4. Verify plugin files exist
kubectl -n <your-namespace> exec deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
kubectl -n kube-system exec deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
# Expected: dist/, package.json present
```
@@ -241,7 +241,7 @@ apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: headlamp-pdb
namespace: <your-namespace>
namespace: kube-system
spec:
minAvailable: 1
selector:
@@ -295,7 +295,7 @@ apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: headlamp
namespace: <your-namespace>
namespace: kube-system
spec:
selector:
matchLabels:
@@ -312,10 +312,10 @@ spec:
```bash
# View logs
kubectl -n <your-namespace> logs deployment/headlamp -f
kubectl -n kube-system logs deployment/headlamp -f
# Filter for plugin-related logs
kubectl -n <your-namespace> logs deployment/headlamp | grep -i polaris
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
```
**Polaris Dashboard Logs:**
@@ -341,14 +341,14 @@ apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: headlamp-alerts
namespace: <your-namespace>
namespace: kube-system
spec:
groups:
- name: headlamp
interval: 30s
rules:
- alert: HeadlampPodNotReady
expr: kube_pod_status_ready{namespace="<your-namespace>", pod=~"headlamp-.*"} == 0
expr: kube_pod_status_ready{namespace="kube-system", pod=~"headlamp-.*"} == 0
for: 5m
labels:
severity: warning
@@ -422,9 +422,9 @@ If Headlamp or plugin becomes unavailable:
2. **Redeploy Headlamp:**
```bash
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--values headlamp-values.yaml
helm upgrade --install headlamp headlamp/headlamp \
--namespace kube-system \
--values headlamp-values.yaml
```
3. **Reapply RBAC:**
@@ -436,7 +436,7 @@ helm upgrade --install headlamp headlamp/headlamp \
4. **Verify plugin files:**
```bash
kubectl -n <your-namespace> exec deployment/headlamp -- \
kubectl -n kube-system exec deployment/headlamp -- \
ls /headlamp/plugins/headlamp-polaris-plugin/
```
+3 -2
View File
@@ -268,9 +268,10 @@ npm run e2e
```bash
# Create token
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n <your-namespace> --duration=24h)
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
kubectl port-forward -n <your-namespace> svc/headlamp 4466:80
# Port-forward for local testing
kubectl port-forward -n kube-system svc/headlamp 4466:80
# Run tests
HEADLAMP_URL=http://localhost:4466 npm run e2e
+13 -13
View File
@@ -72,7 +72,7 @@ Deploy or update Headlamp:
```bash
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml
```
@@ -122,7 +122,7 @@ apiVersion: v1
kind: ConfigMap
metadata:
name: headlamp-plugin-config
namespace: <your-namespace>
namespace: kube-system
data:
plugin.yml: |
- name: headlamp-polaris-plugin
@@ -138,14 +138,14 @@ kubectl apply -f headlamp-plugin-config.yaml
# Deploy/update Headlamp with sidecar
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml
# Wait for pod to be ready
kubectl -n <your-namespace> wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
# Verify plugin files
kubectl -n <your-namespace> exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
# Expected output:
# drwxr-xr-x dist/
@@ -270,7 +270,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -284,10 +284,10 @@ See [RBAC Permissions](../user-guide/rbac-permissions.md) for detailed RBAC conf
```bash
# If you updated Helm values or ConfigMaps
kubectl -n <your-namespace> rollout restart deployment/headlamp
kubectl -n kube-system rollout restart deployment/headlamp
# Wait for pod to be ready
kubectl -n <your-namespace> wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
```
### 3. Clear Browser Cache
@@ -312,14 +312,14 @@ kubectl -n <your-namespace> wait --for=condition=ready pod -l app.kubernetes.io/
```bash
# Verify plugin files exist
kubectl -n <your-namespace> exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
# Expected output:
# drwxr-xr-x dist/
# -rw-r--r-- package.json
# Check Headlamp logs for errors
kubectl -n <your-namespace> logs deployment/headlamp | grep -i polaris
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
# Expected: No errors related to plugin loading
@@ -345,13 +345,13 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
```bash
# 1. Verify plugin files exist
kubectl -n <your-namespace> exec deployment/headlamp -c headlamp -- \
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
ls -la /headlamp/plugins/headlamp-polaris-plugin/
# Expected: dist/, package.json present
# 2. Check Headlamp logs for plugin errors
kubectl -n <your-namespace> logs deployment/headlamp | grep -i polaris
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
# 3. Hard refresh browser (Cmd+Shift+R or Ctrl+Shift+R)
@@ -404,7 +404,7 @@ helm install polaris fairwinds-stable/polaris \
```bash
# Wait 30 minutes for ArtifactHub sync
# Or manually force Headlamp restart:
kubectl -n <your-namespace> rollout restart deployment/headlamp
kubectl -n kube-system rollout restart deployment/headlamp
```
## Next Steps
+5 -5
View File
@@ -67,14 +67,14 @@ kubectl -n polaris wait --for=condition=ready pod -l app.kubernetes.io/name=pola
```bash
# Check Headlamp is deployed
kubectl -n <your-namespace> get pods -l app.kubernetes.io/name=headlamp
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
# Expected output:
# NAME READY STATUS RESTARTS AGE
# headlamp-xxxxxxxxxx-xxxxx 1/1 Running 0 1h
# Check Headlamp version (must be v0.26+)
kubectl -n <your-namespace> get deployment headlamp -o jsonpath='{.spec.template.spec.containers[0].image}'
kubectl -n kube-system get deployment headlamp -o jsonpath='{.spec.template.spec.containers[0].image}'
# Expected output:
# ghcr.io/headlamp-k8s/headlamp:v0.39.0 (or similar)
@@ -89,12 +89,12 @@ helm repo update
# Install Headlamp
helm install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--set config.pluginsDir="/headlamp/plugins" \
--set pluginsManager.enabled=true
# Wait for pod to be ready
kubectl -n <your-namespace> wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
```
## RBAC Requirements
@@ -112,7 +112,7 @@ The plugin requires permissions to access the Polaris dashboard via Kubernetes s
```bash
# Test if Headlamp service account has permission
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
+5 -5
View File
@@ -38,7 +38,7 @@ EOF
# Update Headlamp
helm upgrade --install headlamp headlamp/headlamp \
--namespace <your-namespace> \
--namespace kube-system \
--values headlamp-values.yaml
```
@@ -70,7 +70,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -111,7 +111,7 @@ EOF
```bash
# Verify plugin files exist
kubectl -n <your-namespace> exec -it deployment/headlamp -c headlamp -- \
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
ls /headlamp/plugins/headlamp-polaris-plugin/dist/
# Expected output:
@@ -119,7 +119,7 @@ kubectl -n <your-namespace> exec -it deployment/headlamp -c headlamp -- \
# Verify RBAC is correct
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
@@ -185,7 +185,7 @@ Cluster score badge in top navigation:
```bash
# Verify plugin files exist
kubectl -n <your-namespace> exec -it deployment/headlamp -c headlamp -- \
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
ls /headlamp/plugins/headlamp-polaris-plugin/
# If missing, reinstall via Headlamp UI or sidecar method
+5 -5
View File
@@ -38,17 +38,17 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
# 3. Verify RBAC permissions
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
# Expected output: yes
# 4. Check Headlamp pod is running
kubectl -n <your-namespace> get pods -l app.kubernetes.io/name=headlamp
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
# 5. Check Headlamp logs for plugin errors
kubectl -n <your-namespace> logs deployment/headlamp | grep -i polaris
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
# Expected: No errors
```
@@ -57,7 +57,7 @@ kubectl -n <your-namespace> logs deployment/headlamp | grep -i polaris
```bash
# Verify plugin files exist
kubectl -n <your-namespace> exec deployment/headlamp -c headlamp -- \
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
ls -la /headlamp/plugins/headlamp-polaris-plugin/
# Expected output:
@@ -76,7 +76,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
# Test permission (service account mode)
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
+16 -16
View File
@@ -33,7 +33,7 @@ This guide covers common issues encountered when using the Headlamp Polaris Plug
```bash
# View Headlamp pod logs (plugin sidecar)
kubectl logs -n <your-namespace> deployment/headlamp -c headlamp-plugin
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
# Expected output:
# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
@@ -43,7 +43,7 @@ kubectl logs -n <your-namespace> deployment/headlamp -c headlamp-plugin
**Verify plugin files exist**:
```bash
kubectl exec -n <your-namespace> deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
# Should show: headlamp-polaris-plugin/
```
@@ -118,7 +118,7 @@ Expected subjects:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
```
For OIDC mode:
@@ -154,7 +154,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -169,7 +169,7 @@ Service account mode:
```bash
# Impersonate Headlamp service account
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
--resource-name=polaris-dashboard \
-n polaris
# Expected: yes
@@ -189,7 +189,7 @@ kubectl auth can-i get services/proxy \
After applying RBAC changes:
```bash
kubectl rollout restart deployment headlamp -n <your-namespace>
kubectl rollout restart deployment headlamp -n kube-system
```
---
@@ -490,7 +490,7 @@ Run this script to test all RBAC components:
#!/bin/bash
NS="polaris"
SA="headlamp"
SA_NS="<your-namespace>"
SA_NS="kube-system"
echo "=== Testing RBAC for Polaris Plugin ==="
@@ -529,8 +529,8 @@ echo "=== Test complete ==="
Test connectivity from Headlamp to Polaris:
```bash
# Create debug pod in the namespace where Headlamp is installed
kubectl run netdebug -n <your-namespace> --rm -it --image=nicolaka/netshoot -- bash
# Create debug pod in kube-system namespace
kubectl run netdebug -n kube-system --rm -it --image=nicolaka/netshoot -- bash
# Inside pod, test DNS and HTTP
nslookup polaris-dashboard.polaris.svc.cluster.local
@@ -545,11 +545,11 @@ If you have audit logging enabled, check for denied requests:
```bash
# View recent audit logs (location varies by cluster)
kubectl logs -n <your-namespace> kube-apiserver-* | grep polaris-dashboard
kubectl logs -n kube-system kube-apiserver-* | grep polaris-dashboard
# Look for lines with:
# "reason": "Forbidden"
# "user": "system:serviceaccount:<your-namespace>:headlamp"
# "user": "system:serviceaccount:kube-system:headlamp"
```
---
@@ -567,7 +567,7 @@ kubectl logs -n <your-namespace> kube-apiserver-* | grep polaris-dashboard
**Check sidecar logs**:
```bash
kubectl logs -n <your-namespace> deployment/headlamp -c headlamp-plugin
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
```
**Common errors**:
@@ -591,7 +591,7 @@ Error: 404 Not Found
**Solution**: Verify `archive-url` in plugin config matches GitHub release:
```bash
kubectl get configmap headlamp-plugin-config -n <your-namespace> -o yaml
kubectl get configmap headlamp-plugin-config -n kube-system -o yaml
```
Expected format:
@@ -677,13 +677,13 @@ If none of these solutions work, gather debugging information and open an issue:
1. **Version Information**:
```bash
kubectl get pods -n <your-namespace> -l app.kubernetes.io/name=headlamp -o yaml | grep image:
kubectl get pods -n kube-system -l app.kubernetes.io/name=headlamp -o yaml | grep image:
```
2. **Plugin Version**:
- Check Settings → Plugins in Headlamp UI
- Or: `kubectl exec -n <your-namespace> deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
- Or: `kubectl exec -n kube-system deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
3. **Browser Console Output**:
@@ -698,7 +698,7 @@ If none of these solutions work, gather debugging information and open an issue:
5. **Pod Logs**:
```bash
kubectl logs -n <your-namespace> deployment/headlamp -c headlamp --tail=100
kubectl logs -n kube-system deployment/headlamp -c headlamp --tail=100
kubectl logs -n polaris deployment/polaris-dashboard --tail=100
```
+2 -2
View File
@@ -43,7 +43,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -83,7 +83,7 @@ roleRef:
```bash
# Test service account (in-cluster mode)
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
+1 -1
View File
@@ -317,7 +317,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
# Test permission
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
```
+8 -8
View File
@@ -65,7 +65,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp # Adjust to your Headlamp SA name
namespace: <your-namespace>
namespace: kube-system # Adjust to Headlamp's namespace
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -75,7 +75,7 @@ roleRef:
**Adjust for your environment:**
- `subjects[0].name` - Your Headlamp service account name (often `headlamp`)
- `subjects[0].namespace` - Namespace where Headlamp is installed
- `subjects[0].namespace` - Namespace where Headlamp runs (often `kube-system`)
### Step 3: Apply and Verify
@@ -91,7 +91,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
# Test permission
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
@@ -109,7 +109,7 @@ In token-auth mode, **each user's own identity** is used for Kubernetes API requ
With service account mode:
- Single RoleBinding grants access to all Headlamp users
- Kubernetes sees all requests as `system:serviceaccount:<your-namespace>:headlamp`
- Kubernetes sees all requests as `system:serviceaccount:kube-system:headlamp`
With token-auth mode:
@@ -267,7 +267,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -281,7 +281,7 @@ metadata:
subjects:
- kind: ServiceAccount
name: headlamp
namespace: <your-namespace>
namespace: kube-system
roleRef:
kind: Role
name: polaris-proxy-reader
@@ -411,7 +411,7 @@ Every plugin data fetch creates a Kubernetes API audit log entry.
"level": "Metadata",
"verb": "get",
"user": {
"username": "system:serviceaccount:<your-namespace>:headlamp"
"username": "system:serviceaccount:kube-system:headlamp"
},
"sourceIPs": ["10.96.0.1"],
"objectRef": {
@@ -494,7 +494,7 @@ If using a log aggregator (e.g., Elasticsearch), create filters to exclude or do
```bash
# Service account mode
kubectl auth can-i get services/proxy \
--as=system:serviceaccount:<your-namespace>:headlamp \
--as=system:serviceaccount:kube-system:headlamp \
-n polaris \
--resource-name=polaris-dashboard
+303
View File
@@ -0,0 +1,303 @@
# 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#<namespace>`
- 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/<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
```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)
+90
View File
@@ -0,0 +1,90 @@
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();
});
});
+83
View File
@@ -0,0 +1,83 @@
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<void> {
// 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<void> {
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 });
});
+110
View File
@@ -0,0 +1,110 @@
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();
});
});
+90
View File
@@ -0,0 +1,90 @@
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)/);
});
});
+18642
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -23,7 +23,9 @@
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"e2e": "playwright test",
"e2e:headed": "playwright test --headed"
},
"peerDependencies": {
"react": "^18.0.0",
@@ -36,14 +38,13 @@
"flatted": "^3.4.2",
"lodash": ">=4.18.0",
"picomatch": ">=4.0.4",
"vite": ">=6.4.2",
"elliptic": ">=6.6.1",
"fast-uri": ">=3.1.2"
"vite": ">=6.4.2"
}
},
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.14.0",
"@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",
+27
View File
@@ -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 || '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'],
},
],
});
+80 -34
View File
@@ -11,8 +11,6 @@ overrides:
lodash: '>=4.18.0'
picomatch: '>=4.0.4'
vite: '>=6.4.2'
elliptic: '>=6.6.1'
fast-uri: '>=3.1.2'
importers:
@@ -22,11 +20,14 @@ importers:
specifier: ^0.6.0
version: 0.6.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(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.56.1(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.56.1(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1)
'@kinvolk/headlamp-plugin':
specifier: ^0.14.0
version: 0.14.0(@swc/core@1.15.18)(@types/debug@4.1.12)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.6.2))(csstype@3.2.3)(esbuild@0.25.12)(immer@11.1.4)(openapi-types@12.1.3)(redux@5.0.1)(rollup@4.59.0)(terser@5.46.0)(webpack@5.105.4(@swc/core@1.15.18)(esbuild@0.25.12))
specifier: ^0.13.0
version: 0.13.1(@swc/core@1.15.18)(@types/debug@4.1.12)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.6.2))(csstype@3.2.3)(esbuild@0.25.12)(immer@11.1.4)(openapi-types@12.1.3)(redux@5.0.1)(rollup@4.59.0)(terser@5.46.0)(webpack@5.105.4(@swc/core@1.15.18)(esbuild@0.25.12))
'@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
@@ -604,8 +605,8 @@ packages:
peerDependencies:
jsep: ^0.4.0||^1.0.0
'@kinvolk/headlamp-plugin@0.14.0':
resolution: {integrity: sha512-oVIqpSzf2zZfZG44gwrGI8xTLImCIKupUJ26k7ZhVrFSUBY9Ga+R66tfCdN4Q/ShYha/8J+qlpy5ac9PjRq2KA==}
'@kinvolk/headlamp-plugin@0.13.1':
resolution: {integrity: sha512-aoAGs5w8HIS43p3YBcjzkIWZZlh18b/e02d+r/rr6+99vc48vOd9tKAIBZMVg4j+cVzbPtL1+t1tDE/UdeHcWQ==}
hasBin: true
'@mdx-js/react@3.1.1':
@@ -874,6 +875,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==}
@@ -2940,8 +2946,8 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -3041,6 +3047,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}
@@ -4246,6 +4257,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'}
@@ -4285,6 +4306,10 @@ packages:
resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -6090,7 +6115,7 @@ snapshots:
dependencies:
jsep: 1.4.0
'@kinvolk/headlamp-plugin@0.14.0(@swc/core@1.15.18)(@types/debug@4.1.12)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.6.2))(csstype@3.2.3)(esbuild@0.25.12)(immer@11.1.4)(openapi-types@12.1.3)(redux@5.0.1)(rollup@4.59.0)(terser@5.46.0)(webpack@5.105.4(@swc/core@1.15.18)(esbuild@0.25.12))':
'@kinvolk/headlamp-plugin@0.13.1(@swc/core@1.15.18)(@types/debug@4.1.12)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.6.2))(csstype@3.2.3)(esbuild@0.25.12)(immer@11.1.4)(openapi-types@12.1.3)(redux@5.0.1)(rollup@4.59.0)(terser@5.46.0)(webpack@5.105.4(@swc/core@1.15.18)(esbuild@0.25.12))':
dependencies:
'@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3)
'@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1)
@@ -6159,7 +6184,7 @@ snapshots:
jsdom: 24.1.3
jsonpath-plus: 10.4.0
lodash: 4.18.1
material-react-table: 2.13.3(0078ddeddc9e779fa84c03996c1db10e)
material-react-table: 2.13.3(330725fe5432f245d076f0c0dda1a7a7)
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))
@@ -6566,6 +6591,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)':
@@ -7683,7 +7712,7 @@ snapshots:
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.2
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -8222,12 +8251,12 @@ snapshots:
css-loader@6.11.0(webpack@5.105.4(@swc/core@1.15.18)(esbuild@0.25.12)):
dependencies:
icss-utils: 5.1.0(postcss@8.5.13)
postcss: 8.5.13
postcss-modules-extract-imports: 3.1.0(postcss@8.5.13)
postcss-modules-local-by-default: 4.2.0(postcss@8.5.13)
postcss-modules-scope: 3.2.1(postcss@8.5.13)
postcss-modules-values: 4.0.0(postcss@8.5.13)
icss-utils: 5.1.0(postcss@8.5.8)
postcss: 8.5.8
postcss-modules-extract-imports: 3.1.0(postcss@8.5.8)
postcss-modules-local-by-default: 4.2.0(postcss@8.5.8)
postcss-modules-scope: 3.2.1(postcss@8.5.8)
postcss-modules-values: 4.0.0(postcss@8.5.8)
postcss-value-parser: 4.2.0
semver: 7.7.4
optionalDependencies:
@@ -8931,7 +8960,7 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-uri@3.1.2: {}
fast-uri@3.1.0: {}
fastq@1.20.1:
dependencies:
@@ -9069,6 +9098,9 @@ snapshots:
fs.realpath@1.0.0: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -9389,9 +9421,9 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
icss-utils@5.1.0(postcss@8.5.13):
icss-utils@5.1.0(postcss@8.5.8):
dependencies:
postcss: 8.5.13
postcss: 8.5.8
ieee754@1.2.1: {}
@@ -9644,7 +9676,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 22.19.15
'@types/node': 20.19.37
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -9864,7 +9896,7 @@ snapshots:
'@types/minimatch': 3.0.5
minimatch: 3.1.5
material-react-table@2.13.3(0078ddeddc9e779fa84c03996c1db10e):
material-react-table@2.13.3(330725fe5432f245d076f0c0dda1a7a7):
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)
@@ -9882,7 +9914,7 @@ snapshots:
md5.js@1.3.5:
dependencies:
hash-base: 3.1.2
hash-base: 3.0.5
inherits: 2.0.4
safe-buffer: 5.2.1
@@ -10496,28 +10528,36 @@ 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.13):
postcss-modules-extract-imports@3.1.0(postcss@8.5.8):
dependencies:
postcss: 8.5.13
postcss: 8.5.8
postcss-modules-local-by-default@4.2.0(postcss@8.5.13):
postcss-modules-local-by-default@4.2.0(postcss@8.5.8):
dependencies:
icss-utils: 5.1.0(postcss@8.5.13)
postcss: 8.5.13
icss-utils: 5.1.0(postcss@8.5.8)
postcss: 8.5.8
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
postcss-modules-scope@3.2.1(postcss@8.5.13):
postcss-modules-scope@3.2.1(postcss@8.5.8):
dependencies:
postcss: 8.5.13
postcss: 8.5.8
postcss-selector-parser: 7.1.1
postcss-modules-values@4.0.0(postcss@8.5.13):
postcss-modules-values@4.0.0(postcss@8.5.8):
dependencies:
icss-utils: 5.1.0(postcss@8.5.13)
postcss: 8.5.13
icss-utils: 5.1.0(postcss@8.5.8)
postcss: 8.5.8
postcss-selector-parser@7.1.1:
dependencies:
@@ -10532,6 +10572,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.8:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
prelude-ls@1.2.1: {}
prettier@2.8.8: {}
@@ -11802,7 +11848,7 @@ snapshots:
chokidar: 3.6.0
p-map: 7.0.4
picocolors: 1.1.1
tinyglobby: 0.2.16
tinyglobby: 0.2.15
vite: 8.0.10(@types/node@20.19.37)(esbuild@0.25.12)(terser@5.46.0)(yaml@2.8.2)
vite-plugin-svgr@4.5.0(rollup@4.59.0)(typescript@5.6.2)(vite@8.0.10(@types/node@20.19.37)(esbuild@0.25.12)(terser@5.46.0)(yaml@2.8.2)):
+210
View File
@@ -0,0 +1,210 @@
#!/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 - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
template:
metadata:
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
serviceAccountName: ${E2E_RELEASE}
automountServiceAccountToken: true
securityContext: {}
containers:
- name: headlamp
image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}
imagePullPolicy: IfNotPresent
securityContext:
runAsNonRoot: true
privileged: false
runAsUser: 100
runAsGroup: 101
args:
- "-in-cluster"
- "-in-cluster-context-name=main"
- "-plugins-dir=/headlamp/plugins"
ports:
- name: http
containerPort: 4466
protocol: TCP
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: polaris-plugin
mountPath: /headlamp/plugins/headlamp-polaris
readOnly: true
volumes:
- name: polaris-plugin
configMap:
name: headlamp-polaris-plugin
---
apiVersion: v1
kind: Service
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
EOF
echo "Waiting for rollout..."
kubectl rollout status "deployment/${E2E_RELEASE}" \
-n "$E2E_NAMESPACE" --timeout=120s
# --- Generate a service URL for tests ---
SVC_URL="http://${E2E_RELEASE}.${E2E_NAMESPACE}.svc.cluster.local"
# --- Wait for DNS and HTTP reachability ---
# rollout status only confirms the pod is ready per readinessProbe.
# Kubernetes Service DNS may still be propagating to the runner pod.
# Poll until the service is reachable over HTTP before handing off.
echo ""
echo "Waiting for ${SVC_URL} to be reachable..."
ATTEMPTS=0
MAX_ATTEMPTS=24 # 24 × 5s = 120s max
until curl -sf --max-time 5 "${SVC_URL}" -o /dev/null 2>/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=<generated>"
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."
+34
View File
@@ -0,0 +1,34 @@
#!/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."