From 840d55efac2578d89d9383070dfc5c1ff474e136 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Mon, 16 Mar 2026 10:38:34 +0000 Subject: [PATCH 1/4] refactor: replace kubectl exec/cp plugin deploy with ConfigMap + init container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesigns E2E plugin deployment to eliminate all kubectl exec/cp access to Headlamp pods, per board policy. The new approach: 1. Packages built plugin as a tarball stored in a ConfigMap 2. Patches the Headlamp deployment with an init container that extracts the plugin into the static-plugins volume before Headlamp starts 3. Waits for rollout and verifies readiness RBAC is reduced to configmaps (create/get/patch), deployments (get/patch), replicasets and pods (get/list for rollout status) — no exec or cp needed. Note: .github/workflows/e2e.yaml update requires workflows permission and must be applied separately by a user with repo admin access. Co-Authored-By: Paperclip --- deployment/e2e-runner-rbac.yaml | 53 +++++++++++ scripts/deploy-plugin-to-headlamp.sh | 134 +++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 deployment/e2e-runner-rbac.yaml create mode 100755 scripts/deploy-plugin-to-headlamp.sh diff --git a/deployment/e2e-runner-rbac.yaml b/deployment/e2e-runner-rbac.yaml new file mode 100644 index 0000000..f8071c1 --- /dev/null +++ b/deployment/e2e-runner-rbac.yaml @@ -0,0 +1,53 @@ +--- +# RBAC for the self-hosted GitHub Actions runner ServiceAccount to deploy +# plugins to Headlamp via ConfigMap + deployment patch. +# +# This grants ONLY the permissions needed by scripts/deploy-plugin-to-headlamp.sh: +# - configmaps: create/get/update (store the plugin tarball) +# - deployments: get/patch (add the init container that extracts the plugin) +# - replicasets: get/list (for kubectl rollout status) +# +# No pod exec or pod cp access is required. +# +# Apply with: +# kubectl apply -f deployment/e2e-runner-rbac.yaml +# +# The runner SA name comes from the ARC (Actions Runner Controller) deployment. +# Adjust the serviceaccount name/namespace if your runner uses a different identity. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: e2e-plugin-deployer + namespace: kube-system +rules: + # Store plugin tarball in a ConfigMap + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "get", "update", "patch"] + # Patch the Headlamp deployment to add the init container + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "patch"] + # Required for kubectl rollout status + - apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["get", "list"] + # Required for rollout status pod readiness check + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: e2e-plugin-deployer + namespace: kube-system +subjects: + - kind: ServiceAccount + name: local-ubuntu-latest-gha-rs-no-permission + namespace: arc-runners +roleRef: + kind: Role + name: e2e-plugin-deployer + apiGroup: rbac.authorization.k8s.io diff --git a/scripts/deploy-plugin-to-headlamp.sh b/scripts/deploy-plugin-to-headlamp.sh new file mode 100755 index 0000000..3d2d862 --- /dev/null +++ b/scripts/deploy-plugin-to-headlamp.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# Deploy the built plugin to a live Headlamp instance via ConfigMap + init container. +# +# This script packages the built plugin as a tarball, stores it in a Kubernetes +# ConfigMap, and patches the Headlamp deployment to add an init container that +# extracts the plugin into the static-plugins volume before Headlamp starts. +# +# No kubectl exec or kubectl cp is used — only standard Kubernetes API operations +# (create configmap, patch deployment, rollout status). +# +# Prerequisites: +# - kubectl configured with access to the Headlamp namespace +# - Plugin already built (npm run build → dist/) +# - Headlamp deployment uses a "static-plugins" volume (emptyDir) +# +# Environment variables (all optional, with defaults): +# HEADLAMP_URL — Headlamp URL for readiness check +# HEADLAMP_NS — Kubernetes namespace (default: kube-system) +# HEADLAMP_DEPLOY — Headlamp deployment name (default: headlamp) +# PLUGIN_NAME — Plugin directory name (default: polaris) +# +# Usage: +# npm run build +# ./scripts/deploy-plugin-to-headlamp.sh + +set -euo pipefail + +HEADLAMP_URL="${HEADLAMP_URL:-http://headlamp.kube-system.svc.cluster.local}" +HEADLAMP_NS="${HEADLAMP_NS:-kube-system}" +HEADLAMP_DEPLOY="${HEADLAMP_DEPLOY:-headlamp}" +PLUGIN_NAME="${PLUGIN_NAME:-polaris}" +CONFIGMAP_NAME="headlamp-e2e-plugin-${PLUGIN_NAME}" + +if [ ! -d "dist" ]; then + echo "Error: dist/ not found. Run 'npm run build' first." >&2 + exit 1 +fi + +# --- Step 1: Package plugin as tarball --- +echo "Packaging plugin..." +TARBALL=$(mktemp /tmp/${PLUGIN_NAME}-plugin-XXXXXX.tar.gz) +tar czf "$TARBALL" dist/ package.json +echo " tarball size: $(du -h "$TARBALL" | cut -f1)" + +# ConfigMap binary data limit is ~1MB +TARBALL_SIZE=$(stat -c%s "$TARBALL" 2>/dev/null || stat -f%z "$TARBALL") +if [ "$TARBALL_SIZE" -gt 1000000 ]; then + echo "Error: Plugin tarball (${TARBALL_SIZE} bytes) exceeds ConfigMap 1MB limit." >&2 + echo "Consider minifying the build output or splitting into multiple ConfigMaps." >&2 + rm -f "$TARBALL" + exit 1 +fi + +# --- Step 2: Store tarball in a ConfigMap --- +echo "Creating ConfigMap ${CONFIGMAP_NAME}..." +kubectl create configmap "$CONFIGMAP_NAME" \ + --from-file="plugin.tar.gz=${TARBALL}" \ + -n "$HEADLAMP_NS" \ + --dry-run=client -o yaml | kubectl apply -f - +rm -f "$TARBALL" + +# --- Step 3: Patch the Headlamp deployment --- +# Adds an init container that extracts the plugin tarball into the static-plugins +# volume. Uses strategic merge — init containers merge by name, so re-running +# this script updates the existing init container rather than adding duplicates. +# A timestamp annotation forces a rollout even if the patch shape is unchanged. +echo "Patching Headlamp deployment..." +DEPLOY_TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ) + +kubectl patch deployment "$HEADLAMP_DEPLOY" -n "$HEADLAMP_NS" --type=strategic -p "$(cat </dev/null || echo "[]") + if echo "$PLUGIN_CHECK" | grep -q "$PLUGIN_NAME"; then + echo "Plugin '${PLUGIN_NAME}' is loaded" + else + echo "::warning::Plugin '${PLUGIN_NAME}' not found in /plugins response" + fi + exit 0 + fi + echo " attempt $i/30 — HTTP $HTTP_CODE" + sleep 5 +done + +echo "Error: Headlamp did not recover after plugin deploy" >&2 +exit 1 -- 2.52.0 From a383d32176f0db7d079ac1a208bcabc90140605f Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Mon, 16 Mar 2026 10:51:51 +0000 Subject: [PATCH 2/4] fix: flatten dist/ contents into plugin root during init container extraction The init container extracted the tarball preserving the dist/ subdirectory, placing main.js at /plugins/polaris/dist/main.js instead of /plugins/polaris/main.js where Headlamp expects it. Add cp -a dist/* . && rm -rf dist after extraction to flatten the build output into the plugin root directory. Fixes 6/16 E2E test failures (settings + badge navigation timeouts). Co-Authored-By: Paperclip --- scripts/deploy-plugin-to-headlamp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy-plugin-to-headlamp.sh b/scripts/deploy-plugin-to-headlamp.sh index 3d2d862..b87482f 100755 --- a/scripts/deploy-plugin-to-headlamp.sh +++ b/scripts/deploy-plugin-to-headlamp.sh @@ -83,7 +83,7 @@ kubectl patch deployment "$HEADLAMP_DEPLOY" -n "$HEADLAMP_NS" --type=strategic - "image": "busybox:latest", "command": [ "sh", "-c", - "mkdir -p /plugins/${PLUGIN_NAME} && cd /plugins/${PLUGIN_NAME} && tar xzf /plugin-src/plugin.tar.gz && echo 'Plugin extracted successfully' && ls -la" + "mkdir -p /plugins/${PLUGIN_NAME} && cd /plugins/${PLUGIN_NAME} && tar xzf /plugin-src/plugin.tar.gz && cp -a dist/* . && rm -rf dist && echo 'Plugin extracted successfully' && ls -la" ], "volumeMounts": [ {"name": "static-plugins", "mountPath": "/plugins"}, -- 2.52.0 From 76d2bd3d41e086abd7d76cd6bf354b28e749d7e0 Mon Sep 17 00:00:00 2001 From: "hugh-hackman[bot]" Date: Mon, 16 Mar 2026 10:58:42 +0000 Subject: [PATCH 3/4] ci: add build + deploy step to E2E workflow The E2E tests fail because the plugin is never actually deployed to Headlamp before the tests run. This adds a build step and invokes the deploy-plugin-to-headlamp.sh script (from this same PR) to push the plugin via ConfigMap + init container before running Playwright. Co-Authored-By: Paperclip --- .github/workflows/e2e.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 9685c69..14fe0c7 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -25,6 +25,19 @@ jobs: - name: Install dependencies run: npm ci + - name: Build plugin + run: npm run build + + - name: Deploy plugin to Headlamp + env: + HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }} + HEADLAMP_NS: kube-system + HEADLAMP_DEPLOY: headlamp + PLUGIN_NAME: polaris + run: | + chmod +x scripts/deploy-plugin-to-headlamp.sh + ./scripts/deploy-plugin-to-headlamp.sh + - name: Preflight — verify Headlamp and plugin version env: HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }} -- 2.52.0 From 3f16f5c36091774c7ef88e158719de26dfe54640 Mon Sep 17 00:00:00 2001 From: "hugh-hackman[bot]" Date: Mon, 16 Mar 2026 11:00:08 +0000 Subject: [PATCH 4/4] ci: add kubectl setup step for E2E plugin deploy The runner doesn't have kubectl pre-installed, so the deploy script fails with 'command not found'. Add azure/setup-kubectl@v4 before the deploy step. Co-Authored-By: Paperclip --- .github/workflows/e2e.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 14fe0c7..5c97719 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -28,6 +28,9 @@ jobs: - name: Build plugin run: npm run build + - name: Setup kubectl + uses: azure/setup-kubectl@v4 + - name: Deploy plugin to Headlamp env: HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }} -- 2.52.0