From 233b93cfdfb8a8cb8abf54afb17e084960b41220 Mon Sep 17 00:00:00 2001 From: Gandalf the Greybeard Date: Mon, 16 Mar 2026 11:34:33 +0000 Subject: [PATCH] fix: extract cluster from URL path in AppBar badge (useCluster returns null outside cluster routes) useCluster() returns null when called from AppBar context because the component renders outside the cluster route hierarchy. Parse the cluster name from location.pathname instead, with useCluster() as fallback. Co-Authored-By: Paperclip --- .github/workflows/e2e.yaml | 33 ++-- deployment/headlamp-static-plugin-values.yaml | 83 -------- scripts/deploy-plugin-via-installer.sh | 178 ++++++++++++++++++ src/components/AppBarScoreBadge.test.tsx | 1 + src/components/AppBarScoreBadge.tsx | 10 +- 5 files changed, 204 insertions(+), 101 deletions(-) delete mode 100644 deployment/headlamp-static-plugin-values.yaml create mode 100755 scripts/deploy-plugin-via-installer.sh 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