Compare commits

..

31 Commits

Author SHA1 Message Date
Chris Farhood 57250a995d ci: update artifact hub checksum for v0.0.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:22:42 -05:00
Chris Farhood 702be12fc8 chore: bump version to 0.0.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:14:00 -05:00
Chris Farhood 95aaaa96bd Merge pull request 'feat: query Polaris dashboard API instead of ConfigMap' (#10) from feat/polaris-api-datasource into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#10
2026-02-07 02:11:22 +00:00
Chris Farhood b891b3a624 docs: update CLAUDE.md to reflect API proxy data source
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:10:00 -05:00
Chris Farhood 7997eb29fa 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 <noreply@anthropic.com>
2026-02-06 20:50:07 -05:00
Chris Farhood 9885dc44c0 Merge pull request 'chore: add AI code review workflow for PRs' (#9) from chore/add-ai-review into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#9
2026-02-06 22:21:35 +00:00
Chris Farhood 72998cfbca fix: add container image for ai-review workflow
The default gitea/act_runner image has no Node.js, which actions/checkout@v4
requires. Use catthehacker/ubuntu:act-latest like the kubernetes repo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 17:18:07 -05:00
Chris Farhood 6f7217f400 chore: add AI code review workflow for PRs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 17:15:50 -05:00
gitea-actions[bot] 8b8c447983 ci: update artifact hub metadata for v0.0.4 2026-02-06 21:57:57 +00:00
Chris Farhood 7b794f540f Merge pull request 'chore: bump version to 0.0.4' (#8) from release/v0.0.4 into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#8
2026-02-06 21:55:13 +00:00
Chris Farhood 0f00fd2f29 chore: bump version to 0.0.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:54:24 -05:00
Chris Farhood f95a74c6ae Merge pull request 'fix: include package.json in Docker plugin directory' (#7) from fix/dockerfile-package-json into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#7
2026-02-06 21:53:57 +00:00
Chris Farhood 60fc377442 fix: include package.json in Docker plugin directory
Headlamp's plugin discovery requires both main.js and package.json in
the plugin directory. The Dockerfile only copied dist/ (main.js),
causing the plugin to not be discovered at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:43:56 -05:00
Chris Farhood dd3e877580 Merge pull request 'chore: add linting, formatting, and type-checking' (#6) from chore/add-linting-formatting into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#6
2026-02-06 21:39:37 +00:00
Chris Farhood da1ef7e0c3 chore: add linting, formatting, and type-checking
Add ESLint, Prettier, and TypeScript config files extending the shared
Headlamp plugin configs. Add npm scripts for lint/format. Auto-fix
existing source files. Add CI workflow for PRs and main pushes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 16:35:42 -05:00
gitea-actions[bot] 39878f63cc ci: update artifact hub metadata for v0.0.3 2026-02-06 21:01:11 +00:00
Chris Farhood 374e2f5b57 Merge pull request 'chore: bump version to 0.0.3' (#5) from release/v0.0.3 into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#5
2026-02-06 20:58:43 +00:00
Chris Farhood 581219ceae chore: bump version to 0.0.3
AH doesn't re-process existing versions when a tag is force-moved,
so v0.0.2 is permanently stuck with a stale checksum. Releasing v0.0.3
so AH indexes it as a new version with the correct checksum from the
aligned tag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:57:41 -05:00
gitea-actions[bot] 1b905d2bc6 ci: update artifact hub metadata for v0.0.2 2026-02-06 19:33:13 +00:00
Chris Farhood 43b284a0f4 Merge pull request 'fix: align tag with metadata after release' (#4) from fix/release-tag-alignment into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#4
2026-02-06 19:29:58 +00:00
Chris Farhood f54795f34f fix: align tag with metadata after release to solve AH checksum mismatch
The CI builds a non-reproducible tarball after the tag is created, then
updates artifacthub-pkg.yml on main with the correct checksum. But
Artifact Hub reads from the tag ref, not main, so it sees the stale
checksum and Headlamp rejects the plugin with "Checksum mismatch".

Changes:
- Add guard step: if the GitHub release tarball checksum already matches
  the metadata in the current commit, skip the entire build (prevents
  infinite retrigger loop)
- After updating metadata on main, force-move the tag to that commit
  so AH reads the correct checksum
- Push main + tag directly to GitHub to avoid mirror sync delay
- Replace akkuman/gitea-release-action with curl-based approach so all
  steps use the same shell guard pattern

Release flow: tag push -> build -> publish releases -> update metadata
on main -> force-move tag -> (retriggered run hits guard -> exits)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 14:20:03 -05:00
gitea-actions[bot] be75ff55d4 ci: update artifact hub metadata for v0.0.2 2026-02-06 19:09:59 +00:00
gitea-actions[bot] 25a093c131 ci: update artifact hub metadata for v0.0.2 2026-02-06 15:26:58 +00:00
Chris Farhood ed9afd02d6 Merge pull request 'fix: remove GitHub Actions workflow to eliminate release race' (#3) from fix/remove-github-ci-race into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#3
2026-02-06 14:46:27 +00:00
Chris Farhood 2dabb1c731 fix: remove GitHub Actions workflow and handle existing release assets
The GitHub Actions fallback workflow raced with the Gitea CI — it ran
first and created the GitHub release with its own tarball (different
checksum), causing the Gitea CI's upload to fail and leaving a
checksum mismatch on Artifact Hub.

- Remove .github/workflows/release.yml entirely (Gitea CI handles both
  Gitea and GitHub releases)
- Fix the Gitea CI's GitHub release step to delete existing assets
  before uploading, so re-runs and race conditions are handled gracefully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 09:02:19 -05:00
gitea-actions[bot] 8941f9ac16 ci: update artifact hub metadata for v0.0.2 2026-02-06 13:19:36 +00:00
Chris Farhood 4810893440 Merge pull request 'fix: use git push instead of Gitea API for checksum update' (#2) from fix/ci-use-git-push into main
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#2
2026-02-06 13:18:31 +00:00
Chris Farhood e37904a377 fix: use git push instead of Gitea API for checksum update
The Gitea Contents API returned HTTP error (curl exit 22) when the CI
tried to update artifacthub-pkg.yml. Switch to using git checkout/commit/push
which reuses the auth already configured by actions/checkout. Also added
fetch-depth: 0 so the main branch is available for checkout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 08:16:51 -05:00
claude e16776d5f1 fix: correct archive checksum and automate CI updates (#1)
## Summary
- Fix the v0.0.1 archive checksum in `artifacthub-pkg.yml` to match the actual GitHub release tarball (was causing "Checksum mismatch" on Headlamp plugin install)
- Gitea CI now computes the checksum after packaging and updates `artifacthub-pkg.yml` on `main` via the Gitea API, then uploads the **same tarball** to GitHub releases (requires `GH_PAT` secret) so both releases serve identical artifacts
- GitHub CI becomes a fallback — skips entirely if the Gitea CI already created the release, preventing a second build from producing a mismatched tarball

## Setup required
Add a `GH_PAT` secret to the Gitea repo containing a GitHub personal access token with `repo` scope. Without it, the GitHub release step gracefully skips and the GitHub Actions fallback handles it.

## Test plan
- [ ] Verify `GH_PAT` secret is set in Gitea repo settings
- [ ] Tag and push a new release (`v0.0.2`)
- [ ] Confirm Gitea CI updates `artifacthub-pkg.yml` checksum on `main`
- [ ] Confirm GitHub release is created by Gitea CI with matching tarball
- [ ] Confirm GitHub Actions fallback skips (release already exists)
- [ ] Verify Headlamp plugin installs without checksum mismatch

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Farhood <chris@farhood.org>
Reviewed-on: farhoodliquor/polaris-headlamp-plugin#1
Co-authored-by: claude <no-reply.claude@farh.net>
Co-committed-by: claude <no-reply.claude@farh.net>
2026-02-06 13:13:44 +00:00
Chris Farhood 2ad61e90cc feat: add Artifact Hub metadata and GitHub Actions release workflow
Artifact Hub requires a GitHub-hosted repo for Headlamp plugins.
Since Gitea push-mirrors git objects but not releases, a GitHub
Actions workflow builds and publishes GitHub Releases with the
tarball that Artifact Hub needs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 17:58:00 -05:00
Chris Farhood dd330f1c14 docs: add comprehensive README with setup, deploy, and release instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 16:45:33 -05:00
17 changed files with 449 additions and 269 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: ['@headlamp-k8s/eslint-config'],
};
+36
View File
@@ -0,0 +1,36 @@
name: AI Code Review
on:
pull_request:
branches:
- main
jobs:
ai-review:
name: AI Code Review
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: AI Review
uses: Nikita-Filonov/ai-review@v0.56.0
with:
review-command: run
env:
LLM__PROVIDER: "OPENAI"
LLM__META__MODEL: ${{ vars.AI_REVIEW_MODEL }}
LLM__META__MAX_TOKENS: "15000"
LLM__META__TEMPERATURE: "0.3"
LLM__HTTP_CLIENT__API_URL: "https://api.openai.com/v1"
LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.OPENAI_API_KEY }}
VCS__PROVIDER: "GITEA"
VCS__PIPELINE__OWNER: ${{ github.repository_owner }}
VCS__PIPELINE__REPO: ${{ github.event.repository.name }}
VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number }}
VCS__HTTP_CLIENT__API_URL: ${{ github.server_url }}/api/v1
VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.AI_REVIEW_GITEA_TOKEN }}
+27
View File
@@ -0,0 +1,27 @@
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
container: node:20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Lint
run: npx eslint --ext .ts,.tsx src/
- name: Type-check
run: npx tsc --noEmit
- name: Format check
run: npx prettier --check src/
+157 -15
View File
@@ -12,31 +12,173 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- 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"
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}')"
EXPECTED=$(grep 'archive-checksum' artifacthub-pkg.yml | awk '{print $2}')
echo "Release tarball checksum: $ACTUAL"
echo "Metadata checksum: $EXPECTED"
if [ "$ACTUAL" = "$EXPECTED" ]; then
echo "SKIP_BUILD=true" >> $GITHUB_ENV
echo "Checksums match - release is finalized, nothing to do"
fi
else
echo "No existing release (HTTP $HTTP_CODE) - will build"
fi
rm -f /tmp/release.tar.gz
- name: Install dependencies
run: npm ci
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
npm ci
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
npx @kinvolk/headlamp-plugin build
- name: Package tarball
run: npx @kinvolk/headlamp-plugin package
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
npx @kinvolk/headlamp-plugin package
- name: Compute tarball checksum
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
TARBALL=$(ls *.tar.gz)
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
echo "Tarball: $TARBALL"
echo "Checksum: sha256:$CHECKSUM"
- name: Install Docker CLI
run: apt-get update && apt-get install -y docker.io
- name: Build Docker image
run: docker build -t git.farh.net/${{ github.repository }}:${{ github.ref_name }} -t git.farh.net/${{ github.repository }}:latest .
- name: Push Docker image
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
apt-get update && apt-get install -y docker.io
- name: Build and push Docker image
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
docker build -t git.farh.net/${{ github.repository }}:${{ github.ref_name }} -t git.farh.net/${{ github.repository }}:latest .
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.farh.net -u ${{ github.actor }} --password-stdin
docker push git.farh.net/${{ github.repository }}:${{ github.ref_name }}
docker push git.farh.net/${{ github.repository }}:latest
- name: Create release
uses: akkuman/gitea-release-action@v1
with:
files: |
*.tar.gz
token: ${{ github.token }}
- name: Create Gitea release
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
# Create release (or get existing)
RELEASE=$(curl -s -X POST \
-H "Authorization: token ${{ github.token }}" \
-H "Content-Type: application/json" \
"${API_URL}/releases" \
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\"}")
RELEASE_ID=$(echo "$RELEASE" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
if [ "$RELEASE_ID" = "undefined" ]; then
RELEASE=$(curl -sf \
-H "Authorization: token ${{ github.token }}" \
"${API_URL}/releases/tags/${GITHUB_REF_NAME}")
RELEASE_ID=$(echo "$RELEASE" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
fi
echo "Gitea Release ID: $RELEASE_ID"
# Delete existing assets
ASSETS=$(curl -sf \
-H "Authorization: token ${{ github.token }}" \
"${API_URL}/releases/${RELEASE_ID}/assets")
echo "$ASSETS" | node -e "
process.stdin.resume();let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
JSON.parse(d).forEach(a=>console.log(a.id));
})" | while read -r ASSET_ID; do
curl -sf -X DELETE \
-H "Authorization: token ${{ github.token }}" \
"${API_URL}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
done
# Upload tarball
curl -sf -X POST \
-H "Authorization: token ${{ github.token }}" \
-F "attachment=@${TARBALL}" \
"${API_URL}/releases/${RELEASE_ID}/assets?name=${TARBALL}"
echo "Gitea release updated"
- name: Create GitHub release
continue-on-error: true
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
GH_API="https://api.github.com/repos/cpfarhood/polaris-headlamp-plugin"
# Create release or fetch existing one
BODY=$(curl -s -X POST \
-H "Authorization: token ${{ secrets.GH_PAT }}" \
-H "Accept: application/vnd.github+json" \
"${GH_API}/releases" \
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"generate_release_notes\":true}")
RELEASE_ID=$(echo "$BODY" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
if [ "$RELEASE_ID" = "undefined" ]; then
echo "Release already exists, fetching it..."
BODY=$(curl -sf \
-H "Authorization: token ${{ secrets.GH_PAT }}" \
-H "Accept: application/vnd.github+json" \
"${GH_API}/releases/tags/${GITHUB_REF_NAME}")
RELEASE_ID=$(echo "$BODY" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
fi
echo "GitHub Release ID: $RELEASE_ID"
# Delete existing assets with the same name
ASSETS=$(curl -sf \
-H "Authorization: token ${{ secrets.GH_PAT }}" \
-H "Accept: application/vnd.github+json" \
"${GH_API}/releases/${RELEASE_ID}/assets")
echo "$ASSETS" | node -e "
process.stdin.resume();let d='';
process.stdin.on('data',c=>d+=c);
process.stdin.on('end',()=>{
const assets=JSON.parse(d);
assets.filter(a=>a.name==='${TARBALL}').forEach(a=>console.log(a.id));
})" | while read -r ASSET_ID; do
echo "Deleting existing asset $ASSET_ID..."
curl -sf -X DELETE \
-H "Authorization: token ${{ secrets.GH_PAT }}" \
"${GH_API}/releases/assets/${ASSET_ID}"
done
# Upload tarball
curl -sf -X POST \
-H "Authorization: token ${{ secrets.GH_PAT }}" \
-H "Content-Type: application/gzip" \
"https://uploads.github.com/repos/cpfarhood/polaris-headlamp-plugin/releases/${RELEASE_ID}/assets?name=${TARBALL}" \
--data-binary "@${TARBALL}"
echo "GitHub release updated with same tarball"
- name: Update metadata and align tag
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
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|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@git.farh.net"
git add artifacthub-pkg.yml
git diff --cached --quiet || {
git commit -m "ci: update artifact hub metadata for ${GITHUB_REF_NAME}"
git push origin main
}
# Force-move tag to the commit with correct checksum.
# This triggers a new CI run, but the guard step will detect
# that the release checksum already matches and skip the build.
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 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"
-37
View File
@@ -1,37 +0,0 @@
name: GitHub Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Package tarball
run: npx @kinvolk/headlamp-plugin package
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: "*.tar.gz"
generate_release_notes: true
+1
View File
@@ -0,0 +1 @@
module.exports = require('@headlamp-k8s/eslint-config/prettier-config');
+1
View File
@@ -7,3 +7,4 @@ RUN npx @kinvolk/headlamp-plugin build
FROM alpine:3.20
COPY --from=build /app/dist/ /plugins/polaris-headlamp-plugin/
COPY --from=build /app/package.json /plugins/polaris-headlamp-plugin/
+135 -99
View File
@@ -1,5 +1,7 @@
# polaris-headlamp-plugin
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/polaris-headlamp-plugin)](https://artifacthub.io/packages/headlamp/polaris-headlamp-plugin/polaris-headlamp-plugin)
A [Headlamp](https://headlamp.dev/) plugin that surfaces [Fairwinds Polaris](https://polaris.docs.fairwinds.com/) audit results directly in the Headlamp UI.
## What It Does
@@ -22,12 +24,95 @@ Error states are handled explicitly: RBAC denied (403), Polaris not installed (4
- **Polaris** installed via the [official Helm chart](https://github.com/FairwindsOps/polaris) with the dashboard component enabled
- The Headlamp service account must have RBAC permission to `get` ConfigMaps in the `polaris` namespace
## Installing
### Option 1: Artifact Hub + Headlamp plugin manager (recommended)
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/polaris-headlamp-plugin/polaris-headlamp-plugin). Configure Headlamp's `pluginsManager` in your Helm values to install it automatically:
```yaml
pluginsManager:
sources:
- url: https://artifacthub.io/packages/headlamp/polaris-headlamp-plugin/polaris-headlamp-plugin
```
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`.
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
command: ["sh", "-c", "cp -r /plugins/* /headlamp/plugins/"]
volumeMounts:
- name: plugins
mountPath: /headlamp/plugins
volumes:
- name: plugins
emptyDir: {}
volumeMounts:
- name: plugins
mountPath: /headlamp/plugins
```
### 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:
```bash
tar xzf polaris-headlamp-plugin-0.0.1.tar.gz -C /headlamp/plugins/
```
### Option 4: Build from source
```bash
npm install
npm run build
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
```
## RBAC
The plugin reads a single ConfigMap. Minimum RBAC required for the Headlamp service account:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: headlamp-polaris-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["polaris-dashboard"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: headlamp-polaris-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: headlamp-polaris-reader
subjects:
- kind: ServiceAccount
name: headlamp
namespace: headlamp
```
## Development
### Setup
```bash
git clone https://git.farh.net/farhoodliquor/polaris-headlamp-plugin.git
git clone https://github.com/cpfarhood/polaris-headlamp-plugin.git
cd polaris-headlamp-plugin
npm install
```
@@ -67,104 +152,6 @@ src/
cluster info, error states, refresh interval selector.
```
## Deploying to Headlamp
### Option 1: Docker init container (recommended for Kubernetes)
The plugin ships as a container image at `git.farh.net/farhoodliquor/polaris-headlamp-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
command: ["sh", "-c", "cp -r /plugins/* /headlamp/plugins/"]
volumeMounts:
- name: plugins
mountPath: /headlamp/plugins
volumes:
- name: plugins
emptyDir: {}
volumeMounts:
- name: plugins
mountPath: /headlamp/plugins
```
### Option 2: Manual tarball install
Download the `.tar.gz` from the [releases page](https://git.farh.net/farhoodliquor/polaris-headlamp-plugin/releases), then extract into Headlamp's plugin directory:
```bash
tar xzf polaris-headlamp-plugin-0.0.1.tar.gz -C /headlamp/plugins/
```
### Option 3: Build from source
```bash
npm install
npm run build
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
```
## Releasing
Releases are automated via Gitea Actions. To cut a release:
```bash
# Bump version in package.json, then:
git add package.json package-lock.json
git commit -m "chore: bump version to 0.0.2"
git tag v0.0.2
git push origin main v0.0.2
```
The CI pipeline (`.gitea/workflows/release.yaml`) will:
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`
4. Create a Gitea release with the tarball attached
### CI secrets
| Secret | Purpose |
|---|---|
| `REGISTRY_TOKEN` | Gitea personal access token with `package:write` scope, used to push Docker images to the container registry |
The release creation itself uses the built-in `github.token` -- no extra secret needed for that.
## RBAC
The plugin reads a single ConfigMap. Minimum RBAC required for the Headlamp service account:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: headlamp-polaris-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["polaris-dashboard"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: headlamp-polaris-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: headlamp-polaris-reader
subjects:
- kind: ServiceAccount
name: headlamp
namespace: headlamp
```
## Data Source
The plugin reads from:
@@ -189,6 +176,55 @@ AuditData
Each check in a `ResultSet` has `Success` (bool) and `Severity` (`"warning"` or `"danger"`).
## Releasing
Releases are automated via CI. To cut a release:
```bash
# Bump version in package.json and artifacthub-pkg.yml, then:
git add package.json package-lock.json artifacthub-pkg.yml
git commit -m "chore: bump version to 0.0.2"
git tag v0.0.2
git push origin main v0.0.2
```
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`
4. Create a Gitea release with the tarball attached
**GitHub Actions** (`.github/workflows/release.yml`):
1. Build and package the plugin
2. Create a GitHub release with the tarball attached (required for Artifact Hub)
The Gitea repo push-mirrors to GitHub automatically, so both pipelines trigger from a single `git push`.
### CI secrets
| Secret | Where | Purpose |
|---|---|---|
| `REGISTRY_TOKEN` | Gitea | Personal access token with `package:write` scope for Docker image push |
The Gitea release uses the built-in `github.token`. The GitHub release uses the default `GITHUB_TOKEN` with `contents: write` permission.
### Updating Artifact Hub
When releasing a new version, update `artifacthub-pkg.yml`:
- `version` field
- `headlamp/plugin/archive-url` annotation (update the version in the download URL)
- `headlamp/plugin/archive-checksum` annotation (SHA256 of the new tarball, printed by the CI build)
## 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)
- [Headlamp](https://headlamp.dev/)
- [Fairwinds Polaris](https://polaris.docs.fairwinds.com/)
## License
MIT
+4 -4
View File
@@ -1,4 +1,4 @@
version: 0.0.1
version: 0.0.5
name: polaris-headlamp-plugin
displayName: Polaris
createdAt: "2026-02-05T19:00:00Z"
@@ -20,9 +20,9 @@ links:
url: "https://polaris.docs.fairwinds.com/"
maintainers:
- name: cpfarhood
email: ""
email: "chris@farhood.org"
annotations:
headlamp/plugin/archive-url: "https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/v0.0.1/polaris-headlamp-plugin-0.0.1.tar.gz"
headlamp/plugin/archive-url: "https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/v0.0.5/polaris-headlamp-plugin-0.0.5.tar.gz"
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/archive-checksum: sha256:456f09cf8b126816b80c723b6c6f300b2af0c2e1288ee67da13f435b0e35c04d
headlamp/plugin/archive-checksum: sha256:afc57a1e869898b0197364e568205426f32572b703c638246463bb5c7898f4d2
headlamp/plugin/distro-compat: in-cluster
+2 -2
View File
@@ -1,4 +1,4 @@
repositoryID: polaris-headlamp-plugin
repositoryID: fb4c3789-de2b-4667-8fff-34f22e5648da
owners:
- name: cpfarhood
email: ""
email: "chris@farhood.org"
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "polaris-headlamp-plugin",
"version": "0.0.1",
"version": "0.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "polaris-headlamp-plugin",
"version": "0.0.1",
"version": "0.0.3",
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0"
}
+6 -2
View File
@@ -1,12 +1,16 @@
{
"name": "polaris-headlamp-plugin",
"version": "0.0.1",
"version": "0.0.5",
"description": "Headlamp plugin for Fairwinds Polaris audit results",
"scripts": {
"start": "headlamp-plugin start",
"build": "headlamp-plugin build",
"package": "headlamp-plugin package",
"tsc": "tsc --noEmit"
"tsc": "tsc --noEmit",
"lint": "eslint --ext .ts,.tsx src/",
"lint:fix": "eslint --ext .ts,.tsx --fix src/",
"format": "prettier --write src/",
"format:check": "prettier --check src/"
},
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0"
+52 -76
View File
@@ -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<AuditData | null>(null);
const [parseError, setParseError] = React.useState<string | null>(null);
const [lastFetchTime, setLastFetchTime] = React.useState<number>(0);
const [, setTick] = React.useState(0);
const [data, setData] = React.useState<AuditData | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [tick, setTick] = React.useState(0);
// Parse ConfigMap data when it arrives
React.useEffect(() => {
if (!configMap) {
return;
}
const dataMap = configMap.data as Record<string, string> | 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);
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 };
}
+15 -25
View File
@@ -1,11 +1,8 @@
import {
Loader,
SectionBox,
SectionHeader,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Loader, SectionBox, SectionHeader } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import {
AuditData,
computeScore,
countResults,
getRefreshInterval,
ResultCounts,
@@ -20,19 +17,16 @@ const INTERVAL_OPTIONS = [
{ label: '30 minutes', value: 1800 },
];
function RefreshSettings(props: {
interval: number;
onChange: (seconds: number) => void;
}) {
function RefreshSettings(props: { interval: number; onChange: (seconds: number) => void }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<label htmlFor="polaris-refresh-interval">Refresh interval:</label>
<select
id="polaris-refresh-interval"
value={props.interval}
onChange={(e) => props.onChange(Number(e.target.value))}
onChange={e => props.onChange(Number(e.target.value))}
>
{INTERVAL_OPTIONS.map((opt) => (
{INTERVAL_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
@@ -69,19 +63,18 @@ function ScoreBadge(props: { score: number }) {
const color = props.score >= 80 ? '#4caf50' : props.score >= 50 ? '#ff9800' : '#f44336';
return (
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<div style={{ fontSize: '3rem', fontWeight: 'bold', color }}>
{props.score}%
</div>
<div style={{ fontSize: '3rem', fontWeight: 'bold', color }}>{props.score}%</div>
<div style={{ fontSize: '0.875rem', opacity: 0.8 }}>Cluster Score</div>
</div>
);
}
function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
const score = computeScore(props.counts);
return (
<>
<SectionBox title="Score">
<ScoreBadge score={props.data.Score} />
<ScoreBadge score={score} />
</SectionBox>
<SectionBox title="Check Summary">
<div
@@ -135,13 +128,12 @@ export default function PolarisView() {
return (
<>
<SectionHeader title="Polaris" actions={[
<RefreshSettings
key="refresh"
interval={interval}
onChange={handleIntervalChange}
/>,
]} />
<SectionHeader
title="Polaris"
actions={[
<RefreshSettings key="refresh" interval={interval} onChange={handleIntervalChange} />,
]}
/>
{error && (
<SectionBox title="Error">
@@ -153,9 +145,7 @@ export default function PolarisView() {
{!data && !error && (
<SectionBox title="No Data">
<div style={{ padding: '16px' }}>
No Polaris audit results found.
</div>
<div style={{ padding: '16px' }}>No Polaris audit results found.</div>
</SectionBox>
)}
</>
+1 -4
View File
@@ -1,7 +1,4 @@
import {
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib';
import React from 'react';
import PolarisView from './components/PolarisView';
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json",
"include": ["src"]
}