From 7997eb29fa5d1e3e9539736923941cdf9e53fab1 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 6 Feb 2026 20:50:07 -0500 Subject: [PATCH 1/2] feat: query Polaris dashboard API instead of ConfigMap The plugin now fetches audit data from the Polaris dashboard service via the Kubernetes service proxy instead of reading from a ConfigMap. This works with the standard Polaris dashboard deployment without requiring additional configuration. - Replace ConfigMap.useGet with ApiProxy.request to /results.json - Compute score from result counts (pass/total) since the API response doesn't include a pre-computed score - Update error messages for service proxy context - Update CLAUDE.md to reflect new data source Co-Authored-By: Claude Opus 4.6 --- src/api/polaris.ts | 126 +++++++++++++-------------------- src/components/PolarisView.tsx | 4 +- 2 files changed, 54 insertions(+), 76 deletions(-) diff --git a/src/api/polaris.ts b/src/api/polaris.ts index 87b0f75..808b3d8 100644 --- a/src/api/polaris.ts +++ b/src/api/polaris.ts @@ -1,4 +1,4 @@ -import { K8s } from '@kinvolk/headlamp-plugin/lib'; +import { ApiProxy } from '@kinvolk/headlamp-plugin/lib'; import React from 'react'; // --- Polaris AuditData schema (matches pkg/validator/output.go) --- @@ -52,7 +52,6 @@ export interface AuditData { DisplayName: string; ClusterInfo: ClusterInfo; Results: Result[]; - Score: number; } // --- Result counting --- @@ -112,8 +111,18 @@ export function setRefreshInterval(seconds: number): void { localStorage.setItem(STORAGE_KEY, String(seconds)); } +// --- Score computation --- + +export function computeScore(counts: ResultCounts): number { + if (counts.total === 0) return 0; + return Math.round((counts.pass / counts.total) * 100); +} + // --- Data fetching hook --- +const POLARIS_API_PATH = + '/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json'; + interface PolarisDataState { data: AuditData | null; loading: boolean; @@ -121,87 +130,54 @@ interface PolarisDataState { } export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState { - const [configMap, fetchError] = K8s.ResourceClasses.ConfigMap.useGet( - 'polaris-dashboard', - 'polaris' - ); - const [cachedData, setCachedData] = React.useState(null); - const [parseError, setParseError] = React.useState(null); - const [lastFetchTime, setLastFetchTime] = React.useState(0); - const [, setTick] = React.useState(0); + const [data, setData] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const [tick, setTick] = React.useState(0); - // Parse ConfigMap data when it arrives React.useEffect(() => { - if (!configMap) { - return; - } - const dataMap = configMap.data as Record | undefined; - const raw = dataMap?.['dashboard.json']; - if (!raw) { - setParseError('ConfigMap exists but dashboard.json key is missing.'); - return; - } - try { - const parsed: AuditData = JSON.parse(raw); - setCachedData(parsed); - setParseError(null); - setLastFetchTime(Date.now()); - } catch { - setParseError('Failed to parse dashboard.json: malformed JSON.'); - } - }, [configMap]); + let cancelled = false; - // Periodic refresh via re-render trigger - React.useEffect(() => { - if (refreshIntervalSeconds <= 0) { - return; + async function fetchData() { + try { + const result: AuditData = await ApiProxy.request(POLARIS_API_PATH); + if (!cancelled) { + setData(result); + setError(null); + setLoading(false); + } + } catch (err: unknown) { + if (cancelled) return; + const status = (err as { status?: number }).status; + if (status === 403) { + setError( + 'Access denied (403). Check that your RBAC permissions allow proxying to the Polaris service.' + ); + } else if (status === 404 || status === 503) { + setError( + 'Polaris dashboard not reachable. Ensure Polaris is installed in the polaris namespace.' + ); + } else { + setError(`Failed to fetch Polaris data: ${String(err)}`); + } + setLoading(false); + } } + + fetchData(); + return () => { + cancelled = true; + }; + }, [tick]); + + // Periodic refresh + React.useEffect(() => { + if (refreshIntervalSeconds <= 0) return; const intervalId = window.setInterval(() => { setTick(t => t + 1); }, refreshIntervalSeconds * 1000); return () => window.clearInterval(intervalId); }, [refreshIntervalSeconds]); - // Determine error state - if (fetchError) { - const status = (fetchError as { status?: number }).status; - if (status === 403) { - return { - data: cachedData, - loading: false, - error: - 'Access denied (403). Check that your RBAC permissions allow reading ConfigMaps in the polaris namespace.', - }; - } - if (status === 404) { - return { - data: cachedData, - loading: false, - error: - 'Polaris dashboard ConfigMap not found (404). Ensure Polaris is installed in the polaris namespace.', - }; - } - return { - data: cachedData, - loading: false, - error: `Failed to fetch Polaris data: ${String(fetchError)}`, - }; - } - - if (parseError) { - return { data: cachedData, loading: false, error: parseError }; - } - - const isLoading = !configMap && !fetchError; - - // Return cached data while loading if we have it - if (isLoading && cachedData && lastFetchTime > 0) { - return { data: cachedData, loading: false, error: null }; - } - - return { - data: cachedData, - loading: isLoading, - error: null, - }; + return { data, loading, error }; } diff --git a/src/components/PolarisView.tsx b/src/components/PolarisView.tsx index 762f896..7a7313f 100644 --- a/src/components/PolarisView.tsx +++ b/src/components/PolarisView.tsx @@ -2,6 +2,7 @@ import { Loader, SectionBox, SectionHeader } from '@kinvolk/headlamp-plugin/lib/ import React from 'react'; import { AuditData, + computeScore, countResults, getRefreshInterval, ResultCounts, @@ -69,10 +70,11 @@ function ScoreBadge(props: { score: number }) { } function OverviewSection(props: { data: AuditData; counts: ResultCounts }) { + const score = computeScore(props.counts); return ( <> - +
Date: Fri, 6 Feb 2026 21:10:00 -0500 Subject: [PATCH 2/2] docs: update CLAUDE.md to reflect API proxy data source Co-Authored-By: Claude Opus 4.6 --- claude.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/claude.md b/claude.md index 970c438..ef33762 100644 --- a/claude.md +++ b/claude.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Headlamp plugin that surfaces Fairwinds Polaris audit results inside the Headlamp UI. Reads from `ConfigMap/polaris-dashboard` in the `polaris` namespace (key: `dashboard.json`). Target Headlamp ≥ v0.26. +Headlamp plugin that surfaces Fairwinds Polaris audit results inside the Headlamp UI. Queries the Polaris dashboard API via the Kubernetes service proxy (`/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`). Target Headlamp ≥ v0.26. ## Build & Development Commands @@ -36,11 +36,11 @@ src/ └── PolarisView.tsx # Main page: score badge, check summary, cluster info, error states, refresh interval selector ``` -Single sidebar page at `/polaris`. Data is cached in React state and refreshed on a user-configurable interval (stored in localStorage under `polaris-plugin-refresh-interval`, default 5 minutes). The `usePolarisData` hook wraps `ConfigMap.useGet` with caching so stale data is shown while refreshing. +Single sidebar page at `/polaris`. Data is fetched via `ApiProxy.request` to the Polaris dashboard service proxy and refreshed on a user-configurable interval (stored in localStorage under `polaris-plugin-refresh-interval`, default 5 minutes). Score is computed from result counts (pass/total). ## Key Constraints -- **Data source**: `ConfigMap/polaris-dashboard` in `polaris` namespace, key `dashboard.json`. No CRDs, no external API calls, no cluster write operations. +- **Data source**: Polaris dashboard API via K8s service proxy. Requires Polaris deployed in the `polaris` namespace with a `polaris-dashboard` service. No CRDs, no cluster write operations. - **UI components**: Use only Headlamp-provided components (`@kinvolk/headlamp-plugin/lib/CommonComponents`). Do not import raw MUI packages. No custom theming. - **Error handling**: Must handle 403 (RBAC denied), 404 (Polaris not installed), malformed JSON, and loading states with distinct visual states. - **TypeScript strictness**: No `any`, no implicit `unknown` casting, no dead code, no unused imports.