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 e2de88d..935fd23 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:12e72b6a64e3f1c73f542b6328d56391c2cc2906a9a9d7eff58fbf27f14a8680 headlamp/plugin/distro-compat: in-cluster 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..e7c8a9f --- /dev/null +++ b/src/api/PolarisDataContext.tsx @@ -0,0 +1,25 @@ +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..6f198b0 --- /dev/null +++ b/src/components/DynamicSidebarRegistrar.tsx @@ -0,0 +1,29 @@ +import { registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; +import React from 'react'; +import { getNamespaces } from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; + +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..12799e6 --- /dev/null +++ b/src/components/NamespaceDetailView.tsx @@ -0,0 +1,139 @@ +import { + Loader, + NameValueTable, + SectionBox, + SectionHeader, + SimpleTable, + StatusLabel, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { + computeScore, + countResultsForItems, + filterResultsByNamespace, + Result, + ResultCounts, +} from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; + +function scoreStatus(score: number): 'success' | 'warning' | 'error' { + if (score >= 80) return 'success'; + if (score >= 50) return 'warning'; + return 'error'; +} + +function resourceCounts(result: Result): ResultCounts { + return countResultsForItems([result]); +} + +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); + + 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 ( + <> + + + + {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) => ( + {getResourceCounts(row).pass} + ), + }, + { + label: 'Warning', + getter: (row: Result) => ( + {getResourceCounts(row).warning} + ), + }, + { + label: 'Danger', + getter: (row: Result) => ( + {getResourceCounts(row).danger} + ), + }, + ]} + data={results} + emptyMessage={`No resources found in namespace "${namespace}".`} + /> + + + ); +} diff --git a/src/components/PolarisView.tsx b/src/components/PolarisView.tsx index c7b79ff..fdabce9 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 { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; 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);