From 818f4bc9cb42fc37b6bfd73a9a16668d4f25181b Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 6 Feb 2026 22:21:05 -0500 Subject: [PATCH 1/4] chore: update repo URLs after rename to headlamp-polaris-plugin Co-Authored-By: Claude Opus 4.6 --- .gitea/workflows/release.yaml | 6 +++--- README.md | 14 +++++++------- artifacthub-pkg.yml | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index d5e0451..a096c94 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: - name: Check if release is already finalized run: | VERSION=${GITHUB_REF_NAME#v} - TARBALL_URL="https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/${GITHUB_REF_NAME}/polaris-headlamp-plugin-${VERSION}.tar.gz" + TARBALL_URL="https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/${GITHUB_REF_NAME}/polaris-headlamp-plugin-${VERSION}.tar.gz" HTTP_CODE=$(curl -sL -o /tmp/release.tar.gz -w "%{http_code}" "$TARBALL_URL" 2>/dev/null) if [ "$HTTP_CODE" = "200" ]; then ACTUAL="sha256:$(sha256sum /tmp/release.tar.gz | awk '{print $1}')" @@ -163,7 +163,7 @@ jobs: VERSION=${GITHUB_REF_NAME#v} git checkout main sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml - sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/${GITHUB_REF_NAME}/polaris-headlamp-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml + sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/${GITHUB_REF_NAME}/polaris-headlamp-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml git config user.name "gitea-actions[bot]" git config user.email "gitea-actions[bot]@git.farh.net" @@ -178,7 +178,7 @@ jobs: git tag -f ${GITHUB_REF_NAME} git push -f origin ${GITHUB_REF_NAME} # Also push to GitHub directly to avoid waiting for mirror sync - git remote add github https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/polaris-headlamp-plugin.git 2>/dev/null || true + git remote add github https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true git push github main 2>/dev/null || true git push -f github ${GITHUB_REF_NAME} 2>/dev/null || true echo "Tag ${GITHUB_REF_NAME} aligned with updated metadata" diff --git a/README.md b/README.md index 3a2fd61..11a0716 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,14 @@ Headlamp will fetch and install the plugin on startup. ### Option 2: Docker init container -The plugin ships as a container image at `git.farh.net/farhoodliquor/polaris-headlamp-plugin`. +The plugin ships as a container image at `git.farh.net/farhoodliquor/headlamp-polaris-plugin`. Add it as an init container in your Headlamp Helm values: ```yaml initContainers: - name: polaris-plugin - image: git.farh.net/farhoodliquor/polaris-headlamp-plugin:v0.0.1 + image: git.farh.net/farhoodliquor/headlamp-polaris-plugin:v0.0.1 command: ["sh", "-c", "cp -r /plugins/* /headlamp/plugins/"] volumeMounts: - name: plugins @@ -64,7 +64,7 @@ volumeMounts: ### Option 3: Manual tarball install -Download the `.tar.gz` from the [GitHub releases page](https://github.com/cpfarhood/polaris-headlamp-plugin/releases) or the [Gitea releases page](https://git.farh.net/farhoodliquor/polaris-headlamp-plugin/releases), then extract into Headlamp's plugin directory: +Download the `.tar.gz` from the [GitHub releases page](https://github.com/cpfarhood/headlamp-polaris-plugin/releases) or the [Gitea releases page](https://git.farh.net/farhoodliquor/headlamp-polaris-plugin/releases), then extract into Headlamp's plugin directory: ```bash tar xzf polaris-headlamp-plugin-0.0.1.tar.gz -C /headlamp/plugins/ @@ -112,7 +112,7 @@ subjects: ### Setup ```bash -git clone https://github.com/cpfarhood/polaris-headlamp-plugin.git +git clone https://github.com/cpfarhood/headlamp-polaris-plugin.git cd polaris-headlamp-plugin npm install ``` @@ -193,7 +193,7 @@ This triggers two CI pipelines: **Gitea Actions** (`.gitea/workflows/release.yaml`): 1. Build the plugin in a `node:20` container 2. Package a `.tar.gz` tarball -3. Build and push a Docker image to `git.farh.net/farhoodliquor/polaris-headlamp-plugin:{tag}` and `:latest` +3. Build and push a Docker image to `git.farh.net/farhoodliquor/headlamp-polaris-plugin:{tag}` and `:latest` 4. Create a Gitea release with the tarball attached **GitHub Actions** (`.github/workflows/release.yml`): @@ -220,8 +220,8 @@ When releasing a new version, update `artifacthub-pkg.yml`: ## Links - [Artifact Hub](https://artifacthub.io/packages/headlamp/polaris-headlamp-plugin/polaris-headlamp-plugin) -- [GitHub (mirror)](https://github.com/cpfarhood/polaris-headlamp-plugin) -- [Gitea (source of truth)](https://git.farh.net/farhoodliquor/polaris-headlamp-plugin) +- [GitHub (mirror)](https://github.com/cpfarhood/headlamp-polaris-plugin) +- [Gitea (source of truth)](https://git.farh.net/farhoodliquor/headlamp-polaris-plugin) - [Headlamp](https://headlamp.dev/) - [Fairwinds Polaris](https://polaris.docs.fairwinds.com/) diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml index 8442452..fb9131e 100644 --- a/artifacthub-pkg.yml +++ b/artifacthub-pkg.yml @@ -4,7 +4,7 @@ displayName: Polaris createdAt: "2026-02-05T19:00:00Z" description: Surfaces Fairwinds Polaris audit results inside the Headlamp UI. license: MIT -homeURL: "https://github.com/cpfarhood/polaris-headlamp-plugin" +homeURL: "https://github.com/cpfarhood/headlamp-polaris-plugin" category: security keywords: - polaris @@ -15,14 +15,14 @@ keywords: - kubernetes links: - name: Source - url: "https://github.com/cpfarhood/polaris-headlamp-plugin" + url: "https://github.com/cpfarhood/headlamp-polaris-plugin" - name: Polaris url: "https://polaris.docs.fairwinds.com/" maintainers: - name: cpfarhood email: "chris@farhood.org" annotations: - headlamp/plugin/archive-url: "https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/v0.0.6/polaris-headlamp-plugin-0.0.6.tar.gz" + headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.0.6/polaris-headlamp-plugin-0.0.6.tar.gz" headlamp/plugin/version-compat: ">=0.26" headlamp/plugin/archive-checksum: sha256:afc57a1e869898b0197364e568205426f32572b703c638246463bb5c7898f4d2 headlamp/plugin/distro-compat: in-cluster From b217a8119e6415c2630e2cc9bd33848ca61ed2d3 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 6 Feb 2026 23:09:40 -0500 Subject: [PATCH 2/4] feat: add per-namespace detail pages with dynamic sidebar sub-items Add drill-down namespace views under the Polaris sidebar entry. Each namespace gets a sidebar sub-item registered dynamically from audit data, linking to /polaris/:namespace with a score summary and per-resource table. Introduces a shared PolarisDataContext so the sidebar registrar and view components share a single data fetch. Also updates the Artifact Hub repository ID. Co-Authored-By: Claude Opus 4.6 --- artifacthub-repo.yml | 2 +- src/api/PolarisDataContext.tsx | 27 +++++ src/api/polaris.ts | 24 +++- src/components/DynamicSidebarRegistrar.tsx | 29 +++++ src/components/NamespaceDetailView.tsx | 135 +++++++++++++++++++++ src/components/PolarisView.tsx | 13 +- src/index.tsx | 23 +++- 7 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 src/api/PolarisDataContext.tsx create mode 100644 src/components/DynamicSidebarRegistrar.tsx create mode 100644 src/components/NamespaceDetailView.tsx diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml index 9b2bde3..4006fd6 100644 --- a/artifacthub-repo.yml +++ b/artifacthub-repo.yml @@ -1,4 +1,4 @@ -repositoryID: fb4c3789-de2b-4667-8fff-34f22e5648da +repositoryID: fc3397f6-a75a-4950-ab50-da75c08a8089 owners: - name: cpfarhood email: "chris@farhood.org" diff --git a/src/api/PolarisDataContext.tsx b/src/api/PolarisDataContext.tsx new file mode 100644 index 0000000..9161874 --- /dev/null +++ b/src/api/PolarisDataContext.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { AuditData, getRefreshInterval, usePolarisData } from './polaris'; + +interface PolarisDataContextValue { + data: AuditData | null; + loading: boolean; + error: string | null; +} + +const PolarisDataContext = React.createContext(null); + +export function PolarisDataProvider(props: { children: React.ReactNode }) { + const interval = getRefreshInterval(); + const state = usePolarisData(interval); + + return ( + {props.children} + ); +} + +export function usePolarisDataContext(): PolarisDataContextValue { + const ctx = React.useContext(PolarisDataContext); + if (ctx === null) { + throw new Error('usePolarisDataContext must be used within a PolarisDataProvider'); + } + return ctx; +} diff --git a/src/api/polaris.ts b/src/api/polaris.ts index f40d5d5..c27e2c6 100644 --- a/src/api/polaris.ts +++ b/src/api/polaris.ts @@ -77,9 +77,9 @@ function countResultSet(rs: ResultSet, counts: ResultCounts): void { } } -export function countResults(data: AuditData): ResultCounts { +function countResultItems(results: Result[]): ResultCounts { const counts: ResultCounts = { total: 0, pass: 0, warning: 0, danger: 0 }; - for (const result of data.Results) { + for (const result of results) { countResultSet(result.Results, counts); if (result.PodResult) { countResultSet(result.PodResult.Results, counts); @@ -91,6 +91,26 @@ export function countResults(data: AuditData): ResultCounts { return counts; } +export function countResults(data: AuditData): ResultCounts { + return countResultItems(data.Results); +} + +export function countResultsForItems(results: Result[]): ResultCounts { + return countResultItems(results); +} + +export function getNamespaces(data: AuditData): string[] { + const namespaces = new Set(); + for (const result of data.Results) { + namespaces.add(result.Namespace); + } + return Array.from(namespaces).sort(); +} + +export function filterResultsByNamespace(data: AuditData, namespace: string): Result[] { + return data.Results.filter(r => r.Namespace === namespace); +} + // --- Settings --- export const INTERVAL_OPTIONS = [ diff --git a/src/components/DynamicSidebarRegistrar.tsx b/src/components/DynamicSidebarRegistrar.tsx new file mode 100644 index 0000000..6f4b00f --- /dev/null +++ b/src/components/DynamicSidebarRegistrar.tsx @@ -0,0 +1,29 @@ +import { registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; +import React from 'react'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; +import { getNamespaces } from '../api/polaris'; + +const registeredNamespaces = new Set(); + +export default function DynamicSidebarRegistrar() { + const { data } = usePolarisDataContext(); + + React.useEffect(() => { + if (!data) return; + + const namespaces = getNamespaces(data); + for (const ns of namespaces) { + if (registeredNamespaces.has(ns)) continue; + registeredNamespaces.add(ns); + registerSidebarEntry({ + parent: 'polaris', + name: `polaris-ns-${ns}`, + label: ns, + url: `/polaris/${ns}`, + icon: 'mdi:folder-outline', + }); + } + }, [data]); + + return null; +} diff --git a/src/components/NamespaceDetailView.tsx b/src/components/NamespaceDetailView.tsx new file mode 100644 index 0000000..ce4587b --- /dev/null +++ b/src/components/NamespaceDetailView.tsx @@ -0,0 +1,135 @@ +import { + Loader, + NameValueTable, + SectionBox, + SectionHeader, + SimpleTable, + StatusLabel, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; +import { + computeScore, + countResultsForItems, + filterResultsByNamespace, + Result, +} from '../api/polaris'; + +function scoreStatus(score: number): 'success' | 'warning' | 'error' { + if (score >= 80) return 'success'; + if (score >= 50) return 'warning'; + return 'error'; +} + +function resourceCounts(result: Result) { + const counts = countResultsForItems([result]); + return counts; +} + +export default function NamespaceDetailView() { + const { namespace } = useParams<{ namespace: string }>(); + const { data, loading, error } = usePolarisDataContext(); + + if (loading) { + return ; + } + + if (error) { + return ( + <> + + + {error}, + }, + ]} + /> + + + ); + } + + if (!data) { + return ( + <> + + + + + + ); + } + + const results = filterResultsByNamespace(data, namespace); + const counts = countResultsForItems(results); + const score = computeScore(counts); + const status = scoreStatus(score); + + return ( + <> + + + + {score}%, + }, + { name: 'Total Checks', value: String(counts.total) }, + { + name: 'Pass', + value: {counts.pass}, + }, + { + name: 'Warning', + value: {counts.warning}, + }, + { + name: 'Danger', + value: {counts.danger}, + }, + ]} + /> + + + + row.Name }, + { label: 'Kind', getter: (row: Result) => row.Kind }, + { + label: 'Pass', + getter: (row: Result) => { + const c = resourceCounts(row); + return {c.pass}; + }, + }, + { + label: 'Warning', + getter: (row: Result) => { + const c = resourceCounts(row); + return {c.warning}; + }, + }, + { + label: 'Danger', + getter: (row: Result) => { + const c = resourceCounts(row); + return {c.danger}; + }, + }, + ]} + data={results} + emptyMessage={`No resources found in namespace "${namespace}".`} + /> + + + ); +} diff --git a/src/components/PolarisView.tsx b/src/components/PolarisView.tsx index c7b79ff..51a2828 100644 --- a/src/components/PolarisView.tsx +++ b/src/components/PolarisView.tsx @@ -6,14 +6,8 @@ import { StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React from 'react'; -import { - AuditData, - computeScore, - countResults, - getRefreshInterval, - ResultCounts, - usePolarisData, -} from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; +import { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris'; function scoreStatus(score: number): 'success' | 'warning' | 'error' { if (score >= 80) return 'success'; @@ -71,8 +65,7 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) { } export default function PolarisView() { - const interval = getRefreshInterval(); - const { data, loading, error } = usePolarisData(interval); + const { data, loading, error } = usePolarisDataContext(); if (loading) { return ; diff --git a/src/index.tsx b/src/index.tsx index fdce70e..f4f569f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,9 @@ import { registerSidebarEntry, } from '@kinvolk/headlamp-plugin/lib'; import React from 'react'; +import { PolarisDataProvider } from './api/PolarisDataContext'; +import DynamicSidebarRegistrar from './components/DynamicSidebarRegistrar'; +import NamespaceDetailView from './components/NamespaceDetailView'; import PolarisSettings from './components/PolarisSettings'; import PolarisView from './components/PolarisView'; @@ -20,7 +23,25 @@ registerRoute({ sidebar: 'polaris', name: 'polaris', exact: true, - component: () => , + component: () => ( + + + + + ), +}); + +registerRoute({ + path: '/polaris/:namespace', + sidebar: 'polaris', + name: 'polaris-namespace', + exact: true, + component: () => ( + + + + + ), }); registerPluginSettings('polaris-headlamp-plugin', PolarisSettings, true); From 40df014b6b0a2244c6d1eb582d816401064a3f0d Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 6 Feb 2026 23:30:22 -0500 Subject: [PATCH 3/4] style: fix import sorting and prettier formatting Co-Authored-By: Claude Opus 4.6 --- src/api/PolarisDataContext.tsx | 4 +--- src/components/DynamicSidebarRegistrar.tsx | 2 +- src/components/NamespaceDetailView.tsx | 6 ++---- src/components/PolarisView.tsx | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/api/PolarisDataContext.tsx b/src/api/PolarisDataContext.tsx index 9161874..e7c8a9f 100644 --- a/src/api/PolarisDataContext.tsx +++ b/src/api/PolarisDataContext.tsx @@ -13,9 +13,7 @@ export function PolarisDataProvider(props: { children: React.ReactNode }) { const interval = getRefreshInterval(); const state = usePolarisData(interval); - return ( - {props.children} - ); + return {props.children}; } export function usePolarisDataContext(): PolarisDataContextValue { diff --git a/src/components/DynamicSidebarRegistrar.tsx b/src/components/DynamicSidebarRegistrar.tsx index 6f4b00f..6f198b0 100644 --- a/src/components/DynamicSidebarRegistrar.tsx +++ b/src/components/DynamicSidebarRegistrar.tsx @@ -1,7 +1,7 @@ import { registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import React from 'react'; -import { usePolarisDataContext } from '../api/PolarisDataContext'; import { getNamespaces } from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; const registeredNamespaces = new Set(); diff --git a/src/components/NamespaceDetailView.tsx b/src/components/NamespaceDetailView.tsx index ce4587b..9eedfeb 100644 --- a/src/components/NamespaceDetailView.tsx +++ b/src/components/NamespaceDetailView.tsx @@ -8,13 +8,13 @@ import { } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React from 'react'; import { useParams } from 'react-router-dom'; -import { usePolarisDataContext } from '../api/PolarisDataContext'; import { computeScore, countResultsForItems, filterResultsByNamespace, Result, } from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; function scoreStatus(score: number): 'success' | 'warning' | 'error' { if (score >= 80) return 'success'; @@ -58,9 +58,7 @@ export default function NamespaceDetailView() { <> - + ); diff --git a/src/components/PolarisView.tsx b/src/components/PolarisView.tsx index 51a2828..fdabce9 100644 --- a/src/components/PolarisView.tsx +++ b/src/components/PolarisView.tsx @@ -6,8 +6,8 @@ import { StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React from 'react'; -import { usePolarisDataContext } from '../api/PolarisDataContext'; import { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; function scoreStatus(score: number): 'success' | 'warning' | 'error' { if (score >= 80) return 'success'; From 1b86407d8b965300e30e3a26f8cbe72771aae914 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 6 Feb 2026 23:38:10 -0500 Subject: [PATCH 4/4] refactor: precompute resource counts and add return type annotation Avoid recalculating per-resource counts 3x per table row by precomputing them into a Map. Add explicit ResultCounts return type to resourceCounts. Co-Authored-By: Claude Opus 4.6 --- src/components/NamespaceDetailView.tsx | 36 +++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/NamespaceDetailView.tsx b/src/components/NamespaceDetailView.tsx index 9eedfeb..12799e6 100644 --- a/src/components/NamespaceDetailView.tsx +++ b/src/components/NamespaceDetailView.tsx @@ -13,6 +13,7 @@ import { countResultsForItems, filterResultsByNamespace, Result, + ResultCounts, } from '../api/polaris'; import { usePolarisDataContext } from '../api/PolarisDataContext'; @@ -22,9 +23,8 @@ function scoreStatus(score: number): 'success' | 'warning' | 'error' { return 'error'; } -function resourceCounts(result: Result) { - const counts = countResultsForItems([result]); - return counts; +function resourceCounts(result: Result): ResultCounts { + return countResultsForItems([result]); } export default function NamespaceDetailView() { @@ -69,6 +69,15 @@ export default function NamespaceDetailView() { const score = computeScore(counts); const status = scoreStatus(score); + const countsPerResource = new Map(); + for (const r of results) { + countsPerResource.set(`${r.Namespace}/${r.Kind}/${r.Name}`, resourceCounts(r)); + } + + function getResourceCounts(row: Result): ResultCounts { + return countsPerResource.get(`${row.Namespace}/${row.Kind}/${row.Name}`) ?? resourceCounts(row); + } + return ( <> @@ -104,24 +113,21 @@ export default function NamespaceDetailView() { { label: 'Kind', getter: (row: Result) => row.Kind }, { label: 'Pass', - getter: (row: Result) => { - const c = resourceCounts(row); - return {c.pass}; - }, + getter: (row: Result) => ( + {getResourceCounts(row).pass} + ), }, { label: 'Warning', - getter: (row: Result) => { - const c = resourceCounts(row); - return {c.warning}; - }, + getter: (row: Result) => ( + {getResourceCounts(row).warning} + ), }, { label: 'Danger', - getter: (row: Result) => { - const c = resourceCounts(row); - return {c.danger}; - }, + getter: (row: Result) => ( + {getResourceCounts(row).danger} + ), }, ]} data={results}