diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 9685c69..282f09a 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -25,23 +25,24 @@ jobs: - name: Install dependencies run: npm ci - - name: Preflight — verify Headlamp and plugin version + - name: Deploy plugin via Headlamp plugin installer + env: + HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }} + HEADLAMP_NAMESPACE: ${{ vars.HEADLAMP_NAMESPACE || 'kube-system' }} + HEADLAMP_RELEASE: ${{ vars.HEADLAMP_RELEASE || 'headlamp' }} + run: | + chmod +x scripts/deploy-plugin-via-installer.sh + ./scripts/deploy-plugin-via-installer.sh + + - name: Preflight — verify plugin version env: HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }} run: | EXPECTED=$(node -p "require('./package.json').version") - PLUGIN_NAME=$(node -p "require('./package.json').artifacthub?.name || require('./package.json').name") + PLUGIN_NAME=$(node -p "require('./package.json').name") echo "Expected: $PLUGIN_NAME@$EXPECTED" - # Check Headlamp connectivity - HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 10 "$HEADLAMP_URL" || true) - if [ "$HTTP_CODE" = "000" ]; then - echo "::error::Cannot reach Headlamp at $HEADLAMP_URL" - exit 1 - fi - echo "Headlamp responded HTTP $HTTP_CODE" - - # Check installed plugins and version match + # List installed plugins PLUGIN_JSON=$(curl -sf --connect-timeout 10 "$HEADLAMP_URL/plugins" 2>/dev/null || echo "[]") node -e " const expected = '$EXPECTED'; @@ -51,18 +52,18 @@ jobs: for (const p of plugins) console.log(' ' + p.name + '@' + (p.version||'unknown')); const ours = plugins.find(p => p.name === pluginName || p.name === 'polaris' || p.name.includes('polaris')); if (!ours) { - console.log('::warning::Plugin ' + pluginName + ' not found in Headlamp — data-dependent tests will fail'); - } else { - console.log('Found plugin: ' + ours.name + ' at path ' + ours.path); + console.log('::error::Plugin not found after deploy — E2E tests will fail'); + process.exit(1); } + console.log('Found plugin: ' + ours.name + ' at path ' + ours.path); " "$PLUGIN_JSON" - # Fetch deployed plugin version from package.json + # Check version match (warn only — Artifact Hub may lag behind releases) DEPLOYED_VERSION=$(curl -sf --connect-timeout 10 "$HEADLAMP_URL/plugins/$PLUGIN_NAME/package.json" 2>/dev/null \ | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "unknown") echo "Deployed version: $DEPLOYED_VERSION" if [ "$DEPLOYED_VERSION" != "$EXPECTED" ] && [ "$DEPLOYED_VERSION" != "unknown" ]; then - echo "::warning::Version mismatch — repo has $EXPECTED but Headlamp runs $DEPLOYED_VERSION. Tests may fail due to stale plugin." + echo "::warning::Version mismatch — repo has $EXPECTED but Headlamp runs $DEPLOYED_VERSION. Artifact Hub may not have synced yet." fi - name: Install Playwright browsers diff --git a/deployment/headlamp-static-plugin-values.yaml b/deployment/headlamp-static-plugin-values.yaml deleted file mode 100644 index 59618f3..0000000 --- a/deployment/headlamp-static-plugin-values.yaml +++ /dev/null @@ -1,83 +0,0 @@ ---- -# Custom Headlamp values for static plugin installation -# This disables the plugin manager and uses an init container instead - -# Disable the plugin manager sidecar -pluginsManager: - enabled: false - -# Use an init container to install plugins to /headlamp/static-plugins -initContainers: - - name: install-plugins - image: node:lts-alpine - command: - - /bin/sh - - -c - - | - set -e - echo "Installing plugins to /headlamp/static-plugins..." - - # Create plugins directory - mkdir -p /headlamp/static-plugins - - # Set up npm cache - export NPM_CONFIG_CACHE=/tmp/npm-cache - export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig - mkdir -p /tmp/npm-cache /tmp/npm-userconfig - - # Install polaris plugin - echo "Installing polaris plugin..." - cd /headlamp/static-plugins - npm pack headlamp-polaris-plugin@0.3.0 - tar -xzf headlamp-polaris-plugin-0.3.0.tgz - mv package headlamp-polaris-plugin - rm headlamp-polaris-plugin-0.3.0.tgz - - # Install other plugins - npx --yes @headlamp-k8s/plugin@latest install \ - --source https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_flux \ - --folderName /headlamp/static-plugins - - npx --yes @headlamp-k8s/plugin@latest install \ - --source https://artifacthub.io/packages/headlamp/headlamp-trivy/headlamp_trivy \ - --folderName /headlamp/static-plugins - - npx --yes @headlamp-k8s/plugin@latest install \ - --source https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_cert-manager \ - --folderName /headlamp/static-plugins - - npx --yes @headlamp-k8s/plugin@latest install \ - --source https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_ai_assistant \ - --folderName /headlamp/static-plugins - - echo "All plugins installed successfully" - ls -la /headlamp/static-plugins - securityContext: - runAsUser: 100 - runAsGroup: 101 - runAsNonRoot: true - privileged: false - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - memory: 512Mi - volumeMounts: - - name: static-plugins - mountPath: /headlamp/static-plugins - -# Configure headlamp to use static plugins -config: - pluginsDir: /headlamp/static-plugins - -# Add volume for static plugins -volumes: - - name: static-plugins - emptyDir: {} - -# Add volume mount to main container -volumeMounts: - - name: static-plugins - mountPath: /headlamp/static-plugins - readOnly: true diff --git a/scripts/deploy-plugin-via-installer.sh b/scripts/deploy-plugin-via-installer.sh new file mode 100755 index 0000000..08dd59a --- /dev/null +++ b/scripts/deploy-plugin-via-installer.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# deploy-plugin-via-installer.sh +# +# Deploys the Headlamp Polaris plugin to a running Headlamp instance using +# the Headlamp plugin installer (pluginsManager sidecar) with Artifact Hub +# as the sole distribution channel. +# +# This script: +# 1. Verifies Headlamp connectivity +# 2. Ensures the HelmRelease has pluginsManager configured with the +# Artifact Hub source for the polaris plugin +# 3. Waits for the plugin to appear in the Headlamp /plugins endpoint +# 4. Validates plugin availability +# +# Requirements: +# - kubectl configured with cluster access +# - HEADLAMP_URL environment variable (defaults to in-cluster service) +# +# Usage: +# ./scripts/deploy-plugin-via-installer.sh +# +# Per INSTALLATION_POLICY.md: Artifact Hub is the ONLY approved distribution +# channel. No kubectl exec/cp, no init containers, no manual tarball extraction. + +set -euo pipefail + +HEADLAMP_URL="${HEADLAMP_URL:-http://headlamp.kube-system.svc.cluster.local}" +HEADLAMP_NAMESPACE="${HEADLAMP_NAMESPACE:-kube-system}" +HEADLAMP_RELEASE="${HEADLAMP_RELEASE:-headlamp}" +ARTIFACTHUB_SOURCE="https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin" +PLUGIN_NAME="headlamp-polaris" +POLL_INTERVAL=5 +POLL_TIMEOUT=120 + +log() { echo "[deploy-plugin] $*"; } +die() { log "ERROR: $*" >&2; exit 1; } + +# ── Step 1: Verify Headlamp connectivity ────────────────────────────────────── + +log "Checking Headlamp at $HEADLAMP_URL ..." +HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 10 "$HEADLAMP_URL" || echo "000") +if [ "$HTTP_CODE" = "000" ]; then + die "Cannot reach Headlamp at $HEADLAMP_URL" +fi +log "Headlamp responded HTTP $HTTP_CODE" + +# ── Step 2: Check current plugin state ──────────────────────────────────────── + +log "Querying installed plugins ..." +PLUGINS_JSON=$(curl -sf --connect-timeout 10 "$HEADLAMP_URL/plugins" 2>/dev/null || echo "[]") + +PLUGIN_FOUND=$(echo "$PLUGINS_JSON" | node -e " + const plugins = JSON.parse(require('fs').readFileSync(0, 'utf8')); + const found = plugins.find(p => + p.name === '$PLUGIN_NAME' || + p.name === 'polaris' || + p.name.includes('polaris') + ); + if (found) { + console.log(JSON.stringify({ name: found.name, version: found.version || 'unknown' })); + } else { + console.log('null'); + } +" 2>/dev/null || echo "null") + +if [ "$PLUGIN_FOUND" != "null" ]; then + log "Plugin already installed: $PLUGIN_FOUND" + DEPLOYED_NAME=$(echo "$PLUGIN_FOUND" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).name" 2>/dev/null || echo "unknown") + log "Deployed plugin directory name: $DEPLOYED_NAME" + exit 0 +fi + +log "Plugin not found — ensuring pluginsManager is configured ..." + +# ── Step 3: Configure pluginsManager via HelmRelease ────────────────────────── + +# Check if this is a Flux HelmRelease or standalone Helm release +HELMRELEASE_EXISTS=$(kubectl get helmrelease "$HEADLAMP_RELEASE" -n "$HEADLAMP_NAMESPACE" -o name 2>/dev/null || echo "") + +if [ -n "$HELMRELEASE_EXISTS" ]; then + log "Found Flux HelmRelease: $HELMRELEASE_EXISTS" + + # Check if pluginsManager is already configured + PM_ENABLED=$(kubectl get helmrelease "$HEADLAMP_RELEASE" -n "$HEADLAMP_NAMESPACE" \ + -o jsonpath='{.spec.values.pluginsManager.enabled}' 2>/dev/null || echo "") + + if [ "$PM_ENABLED" != "true" ]; then + log "Patching HelmRelease to enable pluginsManager with Artifact Hub source ..." + kubectl patch helmrelease "$HEADLAMP_RELEASE" -n "$HEADLAMP_NAMESPACE" --type merge -p " +spec: + values: + pluginsManager: + enabled: true + configContent: | + plugins: + - name: $PLUGIN_NAME + source: $ARTIFACTHUB_SOURCE +" || die "Failed to patch HelmRelease" + log "HelmRelease patched — Flux will reconcile the deployment" + else + log "pluginsManager already enabled — checking plugin config ..." + + # Verify polaris is in the config + CONFIG_CONTENT=$(kubectl get helmrelease "$HEADLAMP_RELEASE" -n "$HEADLAMP_NAMESPACE" \ + -o jsonpath='{.spec.values.pluginsManager.configContent}' 2>/dev/null || echo "") + + if ! echo "$CONFIG_CONTENT" | grep -q "polaris"; then + log "Adding polaris plugin to pluginsManager config ..." + UPDATED_CONFIG="${CONFIG_CONTENT} + - name: $PLUGIN_NAME + source: $ARTIFACTHUB_SOURCE" + kubectl patch helmrelease "$HEADLAMP_RELEASE" -n "$HEADLAMP_NAMESPACE" --type merge -p " +spec: + values: + pluginsManager: + configContent: | + $(echo "$UPDATED_CONFIG" | sed 's/^/ /') +" || die "Failed to update pluginsManager config" + log "Polaris plugin added to pluginsManager config" + else + log "Polaris plugin already in pluginsManager config" + fi + fi +else + # Standalone Helm release — use helm upgrade + log "No Flux HelmRelease found — checking for standalone Helm release ..." + + HELM_RELEASE=$(helm list -n "$HEADLAMP_NAMESPACE" -q --filter "$HEADLAMP_RELEASE" 2>/dev/null || echo "") + + if [ -n "$HELM_RELEASE" ]; then + log "Found Helm release: $HELM_RELEASE — upgrading with pluginsManager ..." + helm upgrade "$HEADLAMP_RELEASE" headlamp/headlamp -n "$HEADLAMP_NAMESPACE" --reuse-values \ + --set pluginsManager.enabled=true \ + --set-string "pluginsManager.configContent=plugins:\n - name: $PLUGIN_NAME\n source: $ARTIFACTHUB_SOURCE\n" \ + || die "helm upgrade failed" + log "Helm release upgraded" + else + die "No Headlamp deployment found (checked Flux HelmRelease and Helm release in $HEADLAMP_NAMESPACE)" + fi +fi + +# ── Step 4: Wait for plugin to become available ─────────────────────────────── + +log "Waiting for plugin to appear (timeout: ${POLL_TIMEOUT}s) ..." +ELAPSED=0 + +while [ "$ELAPSED" -lt "$POLL_TIMEOUT" ]; do + PLUGINS_JSON=$(curl -sf --connect-timeout 5 "$HEADLAMP_URL/plugins" 2>/dev/null || echo "[]") + FOUND=$(echo "$PLUGINS_JSON" | node -e " + const plugins = JSON.parse(require('fs').readFileSync(0, 'utf8')); + const found = plugins.find(p => + p.name === '$PLUGIN_NAME' || + p.name === 'polaris' || + p.name.includes('polaris') + ); + console.log(found ? 'yes' : 'no'); + " 2>/dev/null || echo "no") + + if [ "$FOUND" = "yes" ]; then + log "Plugin is now available!" + echo "$PLUGINS_JSON" | node -e " + const plugins = JSON.parse(require('fs').readFileSync(0, 'utf8')); + const found = plugins.find(p => + p.name === '$PLUGIN_NAME' || + p.name === 'polaris' || + p.name.includes('polaris') + ); + console.log('[deploy-plugin] Plugin: ' + found.name + '@' + (found.version || 'unknown')); + " 2>/dev/null + exit 0 + fi + + sleep "$POLL_INTERVAL" + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + log "Waiting... (${ELAPSED}s / ${POLL_TIMEOUT}s)" +done + +die "Timed out waiting for plugin to appear after ${POLL_TIMEOUT}s" diff --git a/src/components/AppBarScoreBadge.test.tsx b/src/components/AppBarScoreBadge.test.tsx index 75616ca..ad7489e 100644 --- a/src/components/AppBarScoreBadge.test.tsx +++ b/src/components/AppBarScoreBadge.test.tsx @@ -29,6 +29,7 @@ vi.mock('@mui/material/styles', () => ({ const mockPush = vi.fn(); vi.mock('react-router-dom', () => ({ useHistory: () => ({ push: mockPush }), + useLocation: () => ({ pathname: '/c/test-cluster/some-page', search: '', hash: '' }), })); const mockUsePolarisDataContext = vi.fn(); diff --git a/src/components/AppBarScoreBadge.tsx b/src/components/AppBarScoreBadge.tsx index c18eac4..525075f 100644 --- a/src/components/AppBarScoreBadge.tsx +++ b/src/components/AppBarScoreBadge.tsx @@ -1,7 +1,7 @@ import { K8s } from '@kinvolk/headlamp-plugin/lib'; import { useTheme } from '@mui/material/styles'; import React from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { computeScore, countResults } from '../api/polaris'; import { usePolarisDataContext } from '../api/PolarisDataContext'; @@ -13,7 +13,13 @@ export default function AppBarScoreBadge() { const theme = useTheme(); const { data, loading } = usePolarisDataContext(); const history = useHistory(); - const cluster = K8s.useCluster(); + const location = useLocation(); + const clusterFromHook = K8s.useCluster(); + + // useCluster() returns null in AppBar context (outside cluster routes), + // so extract cluster from the current URL path as primary source. + const clusterMatch = location.pathname.match(/^\/c\/([^/]+)/); + const cluster = clusterMatch ? clusterMatch[1] : clusterFromHook; if (loading || !data) { return null; // Graceful degradation when Polaris unavailable