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 <noreply@paperclip.ing>
This commit is contained in:
2026-03-16 11:34:33 +00:00
parent bf3bf63db3
commit 233b93cfdf
5 changed files with 204 additions and 101 deletions
+17 -16
View File
@@ -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
@@ -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
+178
View File
@@ -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"
+1
View File
@@ -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();
+8 -2
View File
@@ -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