diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 9685c69..5c97719 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -25,6 +25,22 @@ jobs: - name: Install dependencies run: npm ci + - 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' }} + 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' }} 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..b87482f --- /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