Compare commits

..

15 Commits

Author SHA1 Message Date
hugh-hackman[bot] 7413f699de release: bump version to v0.7.0 (#30)
Updates package.json and artifacthub-pkg.yml for the v0.7.0 release.
Includes all changes since v0.6.0:
- RBAC fix for Polaris dashboard proxy access (PR #22)
- Settings test selector fix (PR #22)
- Package name correction from solaris to polaris (PR #26)
- E2E preflight check (PR #24)
- Missing test dependencies (PR #28)

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 06:34:14 -04:00
gandalf-the-greybeard[bot] 497c040dbe fix: add missing test dependencies to devDependencies (#28)
vitest, @testing-library/react, @testing-library/user-event,
@testing-library/jest-dom, jsdom, react, react-dom, @mui/material,
and react-router-dom were all used directly but only available as
transitive dependencies through @kinvolk/headlamp-plugin. pnpm's
strict module resolution prevented them from being resolved.

Also adds process.env.NODE_ENV="test" to vitest config so React
loads its development build (required for act() support in tests).

Fixes #27

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:08:43 -04:00
hugh-hackman[bot] 29bc953522 ci: add E2E preflight check for Headlamp connectivity and plugin version (#24)
Adds a diagnostic step before E2E tests that:
- Logs the expected plugin version from package.json
- Verifies Headlamp is reachable (fails fast if not)
- Attempts to list installed plugins for debugging

This surfaces version mismatches and connectivity issues immediately
instead of requiring analysis of cryptic test timeout failures.

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:07:46 -04:00
gandalf-the-greybeard[bot] 0bd5223587 fix: correct package name from solaris to polaris (#26)
* ci: add E2E preflight check for Headlamp connectivity and plugin version

Adds a diagnostic step before E2E tests that:
- Logs the expected plugin version from package.json
- Verifies Headlamp is reachable (fails fast if not)
- Attempts to list installed plugins for debugging

This surfaces version mismatches and connectivity issues immediately
instead of requiring analysis of cryptic test timeout failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct package name from headlamp-solaris to headlamp-polaris

The package name was misspelled as "solaris" instead of "polaris" in
artifacthub-pkg.yml, package.json, and package-lock.json.

Fixes PRI-49

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct package name from headlamp-solaris to headlamp-polaris

Fixes the ArtifactHub package name typo introduced in 0.6.0.
Only changes the name field in artifacthub-pkg.yml, package.json,
and package-lock.json. No dependency or workflow changes.

Fixes #25

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:07:19 -04:00
hugh-hackman[bot] 2c441bf867 chore: rename Artifact Hub package to headlamp-solaris (#23)
Update package name and Artifact Hub repository ID to reflect the
rename from polaris to headlamp-solaris (new ID: 0243bdaf-c926-44dc-b411-a7c291bf1fcd).

Files updated:
- package.json: name polaris -> headlamp-solaris
- package-lock.json: name polaris -> headlamp-solaris
- artifacthub-pkg.yml: name headlamp-polaris-plugin -> headlamp-solaris
- artifacthub-repo.yml: repositoryID updated to new ID

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
2026-03-08 22:08:53 +00:00
gandalf-the-greybeard[bot] 222346759e fix: E2E tests — RBAC for Polaris service proxy + settings selector (#22)
* fix: correct settings test selector to match plugin name

The settings E2E test looked for 'headlamp-polaris-plugin' but the
plugin is registered as 'polaris' (package.json name and
registerPluginSettings call). Fix the selector to match.

Refs: PRI-28

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add RBAC manifest for Polaris dashboard service proxy access

E2E tests fail with 403 because users lack RBAC to proxy to the Polaris
dashboard service. The plugin reads audit data via the K8s service proxy
at /api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/.

Add deployment/polaris-rbac.yaml with:
- Role granting `get` on `services/proxy` for polaris-dashboard
- RoleBinding granting this to all authenticated users (read-only)

The E2E workflow also needs a `kubectl apply -f deployment/polaris-rbac.yaml`
step added before running tests. This requires the `workflows` permission
on the GitHub App, which is tracked separately.

Refs: PRI-28

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add Polaris RBAC apply and readiness check to E2E workflow

The E2E tests fail because the CI runner lacks RBAC permissions to
proxy to the Polaris dashboard service. Apply the RBAC manifest
(added in this PR) and verify Polaris is reachable before running tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: remove kubectl steps from E2E workflow

The CI runner (local-ubuntu-latest) has no kubectl or cluster access.
E2E tests are browser-only via Playwright against a remote Headlamp URL.
The Polaris RBAC fix (deployment/polaris-rbac.yaml) must be applied
directly to the cluster by an operator with kubectl access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:08:51 +00:00
hugh-hackman[bot] d543e3bf9d feat: add upstream appVersion tracking to release workflow (#21)
Configures the reusable release workflow to fetch the latest release
tag from FairwindsOps/polaris and set appVersion in artifacthub-pkg.yml.
This keeps our Artifact Hub listing in sync with the upstream project.

Co-authored-by: Hugh Hackman <hugh@privilegedescalation.dev>
2026-03-08 13:10:00 -04:00
hugh-hackman[bot] 4e66a4b7cc Merge PR #20
Enable manual triggering of the CI workflow via GitHub Actions UI.
The release workflow already supports workflow_dispatch.

Co-authored-by: hugh-hackman[bot] <hugh-hackman[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:16:32 +00:00
gandalf-the-greybeard[bot] e800adfc19 fix: restore badge emoji, fix aria-label, and correct service proxy URL (#19)
* fix: restore badge emoji, fix aria-label, and correct service proxy URL

Three root causes for E2E test failures since March 4:

1. Service proxy URL missing http: protocol prefix — Kubernetes requires
   the format http:service-name:port, not service-name:port. This caused
   all data fetches to fail, making data-dependent components render
   empty states instead of expected content.

2. AppBarScoreBadge aria-label "Polaris cluster score: X%" doesn't match
   the E2E test regex /Polaris: \d+%/. Simplified to "Polaris: X%".

3. Shield emoji was removed from badge in commit 514de78 but E2E tests
   still assert its presence.

Fixes PRI-20

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: format polaris.ts to pass prettier check

The service proxy URL fix in 61bf1fe exceeded the line length limit.
Run prettier to split the long line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:13:28 -05:00
hugh-hackman[bot] b3349b71d5 ci: switch to org-level reusable workflows (#18)
Co-authored-by: hugh-hackman[bot] <hugh-hackman[bot]@users.noreply.github.com>
2026-03-07 22:12:47 -05:00
hugh-hackman[bot] ceb7f31257 ci: align E2E workflow Node version to 22 (#17)
The CI and release workflows use Node 22, but E2E was still on Node 20.
This aligns all workflows to the same Node version for consistency.

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:12:30 -05:00
gandalf-the-greybeard[bot] 8f69329764 Enhance Renovate configuration (#16)
- Target main branch explicitly
- Set weekly schedule (weekends)
- Limit concurrent PRs to 10
- Group minor/patch updates for npm and github-actions to reduce PR noise

Ref: PRI-16

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:01:20 +00:00
Chris Farhood 0882d663fd chore: add LICENSE and FUNDING.yml (#14)
* chore: add Apache-2.0 LICENSE file

* chore: add FUNDING.yml
2026-03-07 10:37:37 -05:00
DevContainer User 6c7064faf0 docs: add architecture decision records for service proxy, error boundary, settings, and exemptions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:49:56 +00:00
DevContainer User fb445954e0 Add artifacthub-headlamp agent skill
Adds Claude Code agent skill for ArtifactHub metadata and publishing,
sourced from headlamp-agent-skills repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:36:42 +00:00
25 changed files with 12521 additions and 143 deletions
+241
View File
@@ -0,0 +1,241 @@
---
name: artifacthub-headlamp
description: Use when working with ArtifactHub metadata, releases, or publishing for Headlamp plugins. Covers artifacthub-repo.yml, artifacthub-pkg.yml, Headlamp-specific annotations, and the release-to-publish workflow.
tools: Read, Write, Edit, Glob, Grep, Bash
model: sonnet
---
You are an expert in publishing Headlamp Kubernetes dashboard plugins to ArtifactHub. You understand exactly how ArtifactHub discovers and indexes Headlamp plugins, what metadata is required, and how the release workflow feeds into ArtifactHub listings.
Before editing any metadata files, read the existing `artifacthub-repo.yml`, `artifacthub-pkg.yml`, and `package.json` to understand the current state.
---
## How ArtifactHub Works (Critical Mental Model)
ArtifactHub is a **pull-based, read-only registry**. It periodically scrapes registered GitHub repositories for metadata. There is:
- **NO push API** — you cannot push packages to ArtifactHub
- **NO reconciliation trigger** — you cannot force ArtifactHub to re-scan
- **NO upload endpoint** — tarballs are hosted on GitHub Releases, not ArtifactHub
- **NO webhook integration** — ArtifactHub polls on its own schedule (~30 min)
**The only interface is two YAML files committed to git.** ArtifactHub reads them, and that's it.
---
## Repository Registration
### artifacthub-repo.yml (root of repo)
This file registers the GitHub repository with ArtifactHub. Created once, rarely changed.
```yaml
# Artifact Hub repository metadata file
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml
repositoryID: <uuid> # Assigned by ArtifactHub when you add the repo via the web UI
owners:
- name: <github-username-or-org>
email: <email>
```
**How to get the repositoryID:**
1. Log into artifacthub.io
2. Go to Control Panel → Repositories → Add
3. Select repository kind: "Headlamp plugins"
4. Provide the GitHub repo URL
5. ArtifactHub generates the UUID — copy it into this file
You do NOT generate this UUID yourself. It comes from ArtifactHub's web UI.
---
## Package Metadata
### artifacthub-pkg.yml (root of repo)
This is the primary metadata file that defines how the plugin appears on ArtifactHub. Updated with each release.
```yaml
version: "X.Y.Z" # MUST match package.json version
name: <package-name> # npm package name from package.json
displayName: <Human Readable Name> # Shown on ArtifactHub listing
createdAt: "YYYY-MM-DDTHH:MM:SSZ" # ISO 8601 — update each release
description: >-
Multi-line description of what the plugin does.
Be specific about features and requirements.
license: Apache-2.0
homeURL: https://github.com/<owner>/<repo>
appVersion: "X.Y.Z" # Version of upstream project (optional)
category: <category> # See categories below
keywords:
- headlamp
- kubernetes
- <plugin-specific>
maintainers:
- name: <name>
email: <email>
provider:
name: <name>
links:
- name: GitHub
url: https://github.com/<owner>/<repo>
- name: Issues
url: https://github.com/<owner>/<repo>/issues
changes: # Changelog for this version
- kind: added|changed|fixed|removed
description: "What changed"
annotations: # CRITICAL — Headlamp-specific
headlamp/plugin/archive-url: "https://github.com/<owner>/<repo>/releases/download/v<VERSION>/<pkgname>-<VERSION>.tar.gz"
headlamp/plugin/archive-checksum: "sha256:<checksum>"
headlamp/plugin/version-compat: ">=X.Y.Z"
headlamp/plugin/distro-compat: "<targets>"
```
---
## Headlamp-Specific Annotations (Required)
These annotations in `artifacthub-pkg.yml` are what make ArtifactHub treat the package as a Headlamp plugin:
### headlamp/plugin/archive-url
**Required.** Direct download URL to the plugin tarball on GitHub Releases.
Format: `https://github.com/<owner>/<repo>/releases/download/v<VERSION>/<pkgname>-<VERSION>.tar.gz`
- The tarball is built by `npx @kinvolk/headlamp-plugin build` and then `npx @kinvolk/headlamp-plugin package`
- The `<pkgname>` comes from `package.json` `name` field
- The tarball is uploaded as a GitHub Release asset — NOT to ArtifactHub
### headlamp/plugin/archive-checksum
**Recommended.** SHA256 checksum of the tarball.
Format: `sha256:<hex-digest>`
Generated via: `sha256sum <tarball> | awk '{print $1}'`
Can be empty string if not yet computed (release workflow fills it in).
### headlamp/plugin/version-compat
**Required.** Minimum Headlamp version the plugin works with.
Format: `>=X.Y.Z` (e.g., `>=0.20.0`, `>=0.26`)
### headlamp/plugin/distro-compat
**Required.** Comma-separated list of supported Headlamp deployment targets.
Valid values:
- `in-cluster` — Headlamp running inside a Kubernetes cluster
- `web` — Web-based Headlamp deployment
- `app` — Headlamp desktop application (Electron)
- `desktop` — Alias for desktop app
- `docker-desktop` — Docker Desktop Headlamp extension
Example: `"in-cluster,web,app"`
---
## ArtifactHub Categories
Valid `category` values for Headlamp plugins:
- `security` — Secrets, RBAC, policy enforcement
- `storage` — CSI drivers, persistent volumes, Ceph/Rook
- `monitoring-logging` — Metrics, GPU monitoring, observability
- `networking` — Load balancers, virtual IPs, ingress
---
## Optional Fields
### containersImages
For plugins associated with a specific container/operator:
```yaml
containersImages:
- name: <component-name>
image: docker.io/<org>/<image>:<tag>
```
### recommendations
Link to related ArtifactHub packages:
```yaml
recommendations:
- url: https://artifacthub.io/packages/helm/<repo>/<chart>
```
### install
Custom installation instructions (markdown):
```yaml
install: |
## Install via Headlamp Plugin Manager
...
```
### logoPath
Path to a logo image file in the repo (relative to root).
---
## The Release → ArtifactHub Pipeline
This is the actual flow. There is NO other way to publish:
```
1. Developer triggers release workflow (workflow_dispatch with version)
2. CI runs tests
3. Workflow updates:
- package.json (npm version)
- artifacthub-pkg.yml (version, archive-url, checksum, createdAt, changes)
4. Plugin is built: npx @kinvolk/headlamp-plugin build
5. Plugin is packaged: creates <pkgname>-<version>.tar.gz
6. SHA256 checksum is computed and written to artifacthub-pkg.yml
7. Changes committed to main
8. Git tag created: v<version>
9. GitHub Release created with tarball attached
10. ArtifactHub polls the repo (~30 min) and picks up the new metadata
11. Plugin appears/updates on artifacthub.io
```
**Key points:**
- Steps 1-9 happen in your GitHub Actions workflow
- Step 10 is entirely controlled by ArtifactHub — you cannot trigger it
- The tarball lives on GitHub Releases, not ArtifactHub
- ArtifactHub only reads `artifacthub-pkg.yml` to discover the download URL
---
## Common Mistakes to Avoid
1. **Trying to push/trigger ArtifactHub** — There is no API for this. Just commit metadata and wait.
2. **Version mismatch**`version` in `artifacthub-pkg.yml` MUST match `package.json`. The release workflow should update both.
3. **Wrong archive-url** — Must point to the actual GitHub Release asset URL. Verify the tarball filename matches what the build produces.
4. **Missing checksum** — While optional, missing checksums may cause warnings. The release workflow should compute and write it.
5. **Forgetting createdAt** — Must be updated each release. ArtifactHub uses this for sorting.
6. **Stale changes section** — The `changes` list should reflect the current version's changelog only, not cumulative history.
7. **Assuming ArtifactHub hosts anything** — It's an index/catalog. All artifacts are hosted elsewhere (GitHub Releases).
8. **Trying to generate repositoryID** — This UUID comes from ArtifactHub's web UI when you register the repo. Don't make one up.
---
## Tarball Structure
The plugin tarball built by `@kinvolk/headlamp-plugin` contains:
```
<pkgname>/
main.js # Bundled plugin code
package.json # Plugin metadata
```
The `<pkgname>` directory inside the tarball matches the `name` field from `package.json`.
---
## Validating Metadata
Before committing, check:
1. `version` matches across `package.json` and `artifacthub-pkg.yml`
2. `archive-url` version tag matches the `version` field
3. `name` in `artifacthub-pkg.yml` matches `package.json` `name`
4. `createdAt` is a valid ISO 8601 timestamp
5. All required annotations are present
6. `changes` entries use valid `kind` values: `added`, `changed`, `fixed`, `removed`
+1
View File
@@ -0,0 +1 @@
github: [privilegedescalation]
+2 -30
View File
@@ -5,37 +5,9 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
workflow_call:
jobs:
ci:
runs-on: local-ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Lint
run: npm run lint
- name: Type-check
run: npm run tsc
- name: Format check
run: npm run format:check
- name: Run tests
run: npm test
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
+31 -1
View File
@@ -19,12 +19,42 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Preflight — verify Headlamp connectivity
env:
HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }}
run: |
echo "::group::Expected plugin version"
EXPECTED=$(node -p "require('./package.json').version")
echo "Plugin version in repo: $EXPECTED"
echo "::endgroup::"
echo "::group::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 — E2E tests will fail"
exit 1
fi
echo "Headlamp responded with HTTP $HTTP_CODE at $HEADLAMP_URL"
echo "::endgroup::"
echo "::group::Installed plugins"
# Headlamp serves plugin metadata at /plugins — no auth required
curl -sf --connect-timeout 10 "$HEADLAMP_URL/plugins" 2>/dev/null \
| node -e "
const d = require('fs').readFileSync(0,'utf8');
try {
const plugins = JSON.parse(d);
for (const p of plugins) console.log(' ' + p.name + '@' + (p.version||'unknown'));
} catch { console.log(' (could not parse plugin list)'); }
" || echo " (plugin list endpoint not available — tests will validate at runtime)"
echo "::endgroup::"
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
+4 -91
View File
@@ -11,96 +11,9 @@ on:
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
jobs:
ci:
uses: ./.github/workflows/ci.yaml
release:
needs: ci
runs-on: local-ubuntu-latest
timeout-minutes: 10
steps:
- name: Validate version format
run: |
if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in X.Y.Z format"
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Update version in package.json
run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
- name: Update artifacthub-pkg.yml
run: |
VERSION="${{ inputs.version }}"
PKG_NAME=$(jq -r .name package.json)
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${PKG_NAME}-${VERSION}.tar.gz"
sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Package plugin
run: npx @kinvolk/headlamp-plugin package
- name: Prepare release tarball
run: |
VERSION="${{ inputs.version }}"
PKG_NAME=$(jq -r .name package.json)
TARBALL="${PKG_NAME}-${VERSION}.tar.gz"
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
- name: Validate tarball
run: |
echo "Tarball: ${{ env.TARBALL }}"
ls -lh "${{ env.TARBALL }}"
tar -tzf "${{ env.TARBALL }}" | head -20
tar -tzf "${{ env.TARBALL }}" | grep -q "main.js" || { echo "Error: main.js not found in tarball"; exit 1; }
- name: Compute checksum
run: |
CHECKSUM=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}')
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
- name: Commit and tag
run: |
VERSION="${{ inputs.version }}"
git add package.json package-lock.json artifacthub-pkg.yml
git commit -m "release: v${VERSION}"
git tag "v${VERSION}"
git push origin main --tags
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ inputs.version }}"
files: ${{ env.TARBALL }}
fail_on_unmatched_files: true
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
with:
version: ${{ inputs.version }}
upstream-repo: 'FairwindsOps/polaris'
+1 -1
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project
Headlamp plugin surfacing Fairwinds Polaris audit results. Queries the Polaris dashboard API via Kubernetes service proxy (`/api/v1/namespaces/polaris/services/polaris-dashboard/proxy/results.json`). Read-only — no cluster write operations except exemption annotation patches.
Headlamp plugin surfacing Fairwinds Polaris audit results. Queries the Polaris dashboard API via Kubernetes service proxy (`/api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/results.json`). Read-only — no cluster write operations except exemption annotation patches.
- **Plugin name**: `polaris`
- **Target**: Headlamp >= v0.26
+73
View File
@@ -0,0 +1,73 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+3 -3
View File
@@ -1,5 +1,5 @@
version: "0.6.0"
name: headlamp-polaris-plugin
version: "0.7.0"
name: headlamp-polaris
displayName: Polaris
createdAt: "2026-02-05T19:00:00Z"
description: >-
@@ -28,7 +28,7 @@ maintainers:
- name: privilegedescalation
email: "chris@farhood.org"
annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.6.0/polaris-0.6.0.tar.gz"
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.7.0/headlamp-polaris-0.7.0.tar.gz"
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/archive-checksum: sha256:c271590b71424b7f3e70e51309074f64531bb55063fcd9b8c18663579916cb97
headlamp/plugin/distro-compat: in-cluster
+1 -1
View File
@@ -1,4 +1,4 @@
repositoryID: fc3397f6-a75a-4950-ab50-da75c08a8089
repositoryID: 0243bdaf-c926-44dc-b411-a7c291bf1fcd
owners:
- name: privilegedescalation
email: "chris@farhood.org"
+28
View File
@@ -0,0 +1,28 @@
# RBAC to allow authenticated users to proxy to the Polaris dashboard service.
# The polaris plugin reads audit data via the Kubernetes service proxy:
# /api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/results.json
# Without this Role + RoleBinding, users get a 403 when Headlamp proxies the request.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: polaris-dashboard-proxy-reader
namespace: polaris
rules:
- apiGroups: [""]
resources: ["services/proxy"]
resourceNames: ["polaris-dashboard", "http:polaris-dashboard:80"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: polaris-dashboard-proxy-reader
namespace: polaris
subjects:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: polaris-dashboard-proxy-reader
apiGroup: rbac.authorization.k8s.io
@@ -0,0 +1,128 @@
# ADR-002: Service Proxy as Single Data Source
**Status:** Accepted
**Date:** 2026-03-05
**Deciders:** Plugin maintainers
## Context
The Polaris plugin needs audit data from the Polaris dashboard. Polaris dashboard exposes a `/results.json` endpoint containing pre-computed audit results for all workloads in the cluster.
Several approaches were considered for obtaining this data:
1. Query Kubernetes resources directly and re-implement Polaris audit logic
2. Use the Polaris CLI as a sidecar container
3. Use the Polaris dashboard's REST API via Kubernetes service proxy
4. Embed Polaris as a Go/JS library
The service proxy approach uses the Kubernetes API server's built-in service proxy capability to reach the Polaris dashboard at `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`. This means the plugin receives pre-computed audit results without needing to understand Polaris internals.
**Constraints:**
- Headlamp plugins can make API calls via `ApiProxy.request()` (proxied through the Headlamp backend) or direct `fetch()` for external URLs
- The Polaris dashboard service name, namespace, and port may vary across cluster setups
- Some users may run Polaris externally (not in-cluster)
**Requirements:**
- Retrieve all audit data in a single API call
- Support configurable endpoint URL for different cluster configurations
- Support external Polaris instances via full HTTP/HTTPS URLs
- Work through existing Kubernetes RBAC without additional configuration
## Decision
Use **`ApiProxy.request()`** to fetch from the Polaris dashboard service proxy as the single data source for all audit data.
**Implementation:**
- Default endpoint: `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
- URL is configurable via plugin settings stored in `localStorage` (key: `polaris-plugin-dashboard-url`)
- For full URLs starting with `http://` or `https://`, use browser `fetch()` directly to support external Polaris instances
- `getPolarisApiPath()` in `polaris.ts` resolves the configured URL, with `isFullUrl()` determining the fetch strategy
- Single fetch shared across all views via `PolarisDataProvider` (see ADR-001)
## Consequences
### Positive
-**Single API call gets all audit data** - One request to `/results.json` returns scores for every workload
-**No need to understand Polaris internals** - Plugin receives pre-computed results, no audit logic duplication
-**Works through existing K8s RBAC** - Service proxy uses standard Kubernetes RBAC (`get` on `services/proxy`)
-**Configurable endpoint** - Users can customize namespace, service name, or point to an external instance
-**Minimal plugin complexity** - No CRD watches, no custom controllers, no library dependencies
### Negative
-**Requires Polaris dashboard to be deployed and accessible** - Plugin has no data without the dashboard
- **Mitigated by:** Clear error messages guiding users to install Polaris (404/503 → install guidance)
-**Single point of failure** - If the dashboard service is down, the plugin shows no data
- **Mitigated by:** Status-code-specific error messages (403 → RBAC guidance, 404/503 → deployment guidance)
-**Dashboard must be running continuously** - Unlike CRD-based approaches where data persists
- **Mitigated by:** Polaris dashboard is typically deployed as a long-running service
### Neutral
- The Polaris dashboard is a lightweight Go service with minimal resource requirements
- Service proxy is a standard Kubernetes pattern used by many tools (kubectl port-forward, dashboard proxying)
- The configurable URL approach supports both in-cluster and external Polaris deployments
## Alternatives Considered
### Option 1: Query Polaris CRDs Directly
**Pros:**
- No dependency on Polaris dashboard being running
- Data persists in CRDs even if dashboard restarts
**Cons:**
- Polaris audit logic is complex and would need to be duplicated in the plugin
- Would require watching multiple CRD types
- Plugin would need to be updated whenever Polaris changes its audit rules
**Decision:** Rejected (would duplicate Polaris internals, maintenance burden)
### Option 2: Use Polaris CLI as a Sidecar
**Pros:**
- CLI has full audit capability
- Could run audits on-demand
**Cons:**
- Adds operational complexity (sidecar container management)
- Not suitable for a browser-based plugin (CLI runs server-side)
- Would require a separate backend service to bridge CLI output to the plugin
**Decision:** Rejected (operational complexity, not suitable for plugin architecture)
### Option 3: Embed Polaris as a Library
**Pros:**
- Full control over audit execution
- No external service dependency
**Cons:**
- Polaris is a Go library, not available in JavaScript/TypeScript plugin runtime
- Would massively increase bundle size
- Would duplicate the entire Polaris engine
**Decision:** Rejected (not available in plugin runtime, massive dependency)
## References
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster-services/)
- [Polaris Dashboard](https://polaris.docs.fairwinds.com/dashboard/)
- [Plugin Implementation](../../api/polaris.ts)
- [Data Context](../../api/PolarisDataContext.tsx)
## Revision History
| Date | Author | Change |
| ---------- | ----------- | ---------------- |
| 2026-03-05 | Plugin Team | Initial decision |
@@ -0,0 +1,124 @@
# ADR-003: Error Boundary as Class Component Exception
**Status:** Accepted
**Date:** 2026-03-05
**Deciders:** Plugin maintainers
## Context
The plugin follows a strict "functional components only" convention (see CLAUDE.md). However, React error boundaries require the `getDerivedStateFromError` and `componentDidCatch` lifecycle methods, which are only available on class components. As of React 18, there is no hooks-based error boundary API, and the React team has not announced a timeline for one.
The plugin registers components at multiple Headlamp integration points:
- Routes (dashboard, namespaces list)
- Detail view sections (Deployment, StatefulSet, DaemonSet, Job, CronJob)
- App bar action (score badge)
- Plugin settings page
An unhandled error in any one of these registered components would crash the entire Headlamp UI, not just the plugin. This is because Headlamp renders plugin components inline within its own React tree.
**Constraints:**
- React does not support error boundaries via hooks or functional components
- The `react-error-boundary` library is not available as a peer dependency in Headlamp plugins
- Plugin errors must not crash the host Headlamp application
**Requirements:**
- Catch and contain errors in all plugin-registered components
- Provide user-friendly error display with recovery option
- Isolate failures per registration point (an error in the app bar badge should not affect the dashboard view)
## Decision
Define **`PolarisErrorBoundary`** as a class component directly in `index.tsx`. This is the sole exception to the functional-component-only convention.
**Implementation:**
- `PolarisErrorBoundary` is a React class component with `getDerivedStateFromError` and `componentDidCatch`
- Every registered component (routes, detail sections, app bar action) is wrapped in this boundary
- On error, displays a user-friendly fallback with an option to retry
- Error details are logged to the console for debugging
- The boundary is minimal (~30 lines) and co-located in `index.tsx` to minimize the convention violation
## Consequences
### Positive
-**Prevents plugin errors from crashing Headlamp** - Errors are caught and contained within the boundary
-**User-friendly error display** - Shows a clear message with recovery option instead of a blank screen
-**Isolated per registration point** - Each registered component has its own boundary instance
-**No external dependencies** - Uses built-in React class component API
-**Minimal implementation** - Small class component, easy to understand and maintain
### Negative
-**Breaks functional-only convention** - One class component in an otherwise functional codebase
- **Mitigated by:** Kept minimal and co-located in `index.tsx` with clear documentation of why
-**Class component syntax less familiar to contributors** - Modern React developers may not be fluent in class components
- **Mitigated by:** The boundary is simple (no complex state, no lifecycle methods beyond error handling)
### Neutral
- This is a well-known React limitation acknowledged by the React team
- Many React projects that otherwise use functional components make this same exception for error boundaries
- The pattern is explicitly documented in the React documentation
## Alternatives Considered
### Option 1: No Error Boundary
**Pros:**
- No class component needed
- Simpler code
**Cons:**
- Plugin errors would crash the entire Headlamp UI
- Users would see a blank screen with no recovery option
- Poor user experience and potential data loss in other Headlamp features
**Decision:** Rejected (unacceptable risk of crashing host application)
### Option 2: react-error-boundary Library
**Pros:**
- Provides a functional component API for error boundaries
- Well-maintained, widely used library
- Supports error recovery and reset
**Cons:**
- External dependency not available in Headlamp plugin runtime
- Cannot add peer dependencies that Headlamp does not provide
**Decision:** Rejected (dependency not available in plugin environment)
### Option 3: Wait for React Hooks-Based Error Boundary API
**Pros:**
- Would maintain functional-only convention
- Official React solution
**Cons:**
- No timeline from the React team for this feature
- Plugin needs error boundaries now, not at some future date
- May never be implemented (React team has not committed to this)
**Decision:** Rejected (no timeline, cannot ship without error boundaries)
## References
- [React Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
- [React getDerivedStateFromError](https://react.dev/reference/react/Component#static-getderivedstatefromerror)
- [Plugin Implementation](../../../src/index.tsx)
## Revision History
| Date | Author | Change |
| ---------- | ----------- | ---------------- |
| 2026-03-05 | Plugin Team | Initial decision |
@@ -0,0 +1,132 @@
# ADR-004: Browser localStorage for User Settings
**Status:** Accepted
**Date:** 2026-03-05
**Deciders:** Plugin maintainers
## Context
The plugin has two user-configurable settings:
1. **Auto-refresh interval** (1-30 minutes, default 5 minutes) - how often to re-fetch Polaris audit data
2. **Polaris dashboard URL** - endpoint for the Polaris dashboard service (supports custom namespaces/service names and external instances)
These are per-user preferences, not cluster configuration. They should persist across browser sessions and page reloads.
Several storage mechanisms are available:
- Browser `localStorage` - simple key-value store, persistent, synchronous API
- Headlamp `ConfigStore` API - backed by Redux, reactive, integrated with Headlamp's state management
- React state only - in-memory, lost on page reload
- URL query parameters - visible in URL, lost on navigation
**Constraints:**
- Settings need to be reactive: the `PolarisDataProvider` must detect changes made on the settings page
- Headlamp provides `registerPluginSettings` which renders a settings component - the settings page and the data provider are separate component trees
- Only two scalar values need to be stored
**Requirements:**
- Persist settings across browser sessions and page reloads
- React to setting changes without requiring a full page reload
- Simple implementation for two scalar values
- Work with Headlamp's `registerPluginSettings` API
## Decision
Use **browser `localStorage`** directly for persisting plugin settings.
**Implementation:**
- Refresh interval stored at key `polaris-plugin-refresh-interval` (value in minutes as string)
- Dashboard URL stored at key `polaris-plugin-dashboard-url` (URL string or empty for default)
- `PolarisSettings` component (registered via `registerPluginSettings`) reads/writes these keys
- `PolarisDataProvider` polls `localStorage` via `setInterval` every 1 second to detect setting changes
- Helper functions in `polaris.ts` (`getRefreshInterval()`, `getPolarisApiPath()`) encapsulate localStorage access
## Consequences
### Positive
-**Simple and well-understood API** - `localStorage.getItem`/`setItem` is straightforward
-**Persists across browser sessions** - Data survives page reloads, tab closes, browser restarts
-**No dependency on Headlamp store internals** - Decoupled from Headlamp's Redux implementation
-**Works with `registerPluginSettings`** - Settings page and data provider communicate via shared localStorage keys
-**Minimal code** - No state management boilerplate for two simple values
### Negative
-**Not reactive by default** - localStorage has no built-in change notification within the same tab
- **Mitigated by:** 1-second polling interval in `PolarisDataProvider` to detect changes
-**Settings are browser-local** - Not synced across devices or browsers
- **Mitigated by:** These are user preferences, browser-local storage is appropriate
-**No type safety on stored values** - All values stored as strings
- **Mitigated by:** Helper functions with `parseInt` and default values handle type conversion
### Neutral
- localStorage has a 5-10 MB limit per origin, more than sufficient for two string values
- The 1-second polling interval has negligible performance impact (reading two string keys)
- The `storage` event could detect cross-tab changes but does not fire for same-tab writes
## Alternatives Considered
### Option 1: Headlamp ConfigStore API
**Pros:**
- Integrated with Headlamp's Redux store
- Reactive (Redux state changes trigger re-renders)
- Type-safe with TypeScript
**Cons:**
- Couples plugin to Headlamp's internal Redux store implementation
- More complex API for two scalar values
- ConfigStore API may change across Headlamp versions
**Decision:** Not chosen (localStorage is simpler for two scalar values, avoids coupling to Headlamp's Redux internals)
### Option 2: React State Only (No Persistence)
**Pros:**
- Simplest implementation
- Fully reactive
- No side effects
**Cons:**
- Settings lost on page reload - users must reconfigure every session
- Poor user experience for frequently changed settings
**Decision:** Rejected (settings must persist across page reloads)
### Option 3: URL Query Parameters
**Pros:**
- Shareable via URL
- No storage API needed
**Cons:**
- Lost on navigation to different routes
- Clutters the URL
- Not suitable for persistent settings
**Decision:** Rejected (does not persist across navigation)
## References
- [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)
- [Headlamp Plugin Settings](https://headlamp.dev/docs/latest/development/plugins/)
- [Settings Component](../../../src/components/PolarisSettings.tsx)
- [Data Context](../../../src/api/PolarisDataContext.tsx)
## Revision History
| Date | Author | Change |
| ---------- | ----------- | ---------------- |
| 2026-03-05 | Plugin Team | Initial decision |
@@ -0,0 +1,131 @@
# ADR-005: Annotation-Based Exemption Management
**Status:** Accepted
**Date:** 2026-03-05
**Deciders:** Plugin maintainers
## Context
Polaris allows exempting specific workloads from audit checks. When a workload is exempt, Polaris skips all audit checks for that resource. The exemption mechanism uses the annotation `polaris.fairwinds.com/exempt=true` on the workload resource.
The plugin needs to let users manage these exemptions directly from the Headlamp UI. Several approaches were considered:
1. Use Polaris's native annotation-based exemption mechanism
2. Create a separate exemption ConfigMap
3. Define a custom ExemptionPolicy CRD
4. Read-only display with kubectl instructions
**Constraints:**
- Polaris only recognizes `polaris.fairwinds.com/exempt` annotations on workload resources
- The plugin is otherwise read-only (this would be the only write operation)
- Users need appropriate RBAC permissions to patch workload resources
- Supported workload types: Deployments, StatefulSets, DaemonSets, Jobs, CronJobs
**Requirements:**
- Allow users to toggle exemptions for workloads from the Headlamp UI
- Use a mechanism that Polaris actually respects (exemptions must take effect on next scan)
- Support all workload types that Polaris audits
- Respect Kubernetes RBAC (only authorized users can manage exemptions)
## Decision
Use **Polaris's native annotation-based exemption mechanism**. The `ExemptionManager` component patches `polaris.fairwinds.com/exempt` annotations onto workload resources via `ApiProxy.request`.
**Implementation:**
- `ExemptionManager` component in `ExemptionManager.tsx` provides a toggle UI for each workload
- Exemptions are applied via `ApiProxy.request` with `method: 'PATCH'` and `Content-Type: application/strategic-merge-patch+json`
- Patch payload sets `metadata.annotations["polaris.fairwinds.com/exempt"]` to `"true"` or removes the annotation
- This is the only write operation in the entire plugin
- RBAC is enforced by Kubernetes - users without `patch` permission on the workload resource will receive a 403 error
## Consequences
### Positive
-**Uses Polaris's own exemption mechanism** - No custom storage or translation layer needed
-**Exemptions visible in standard kubectl output** - `kubectl get deployment -o yaml` shows the annotation
-**No additional CRDs or ConfigMaps** - No custom resources to manage or clean up
-**Polaris automatically respects annotations** - Exemptions take effect on the next audit scan
-**Standard Kubernetes pattern** - Annotations are the idiomatic way to attach metadata to resources
### Negative
-**Requires write RBAC on workload resources** - Users need `patch` permission on deployments, statefulsets, etc.
- **Mitigated by:** RBAC scoping - only users with patch permission can manage exemptions; UI shows clear error for 403
-**Annotation changes not versioned or auditable** - Beyond standard Kubernetes resource history
- **Mitigated by:** Kubernetes audit logging captures annotation patches; resource `metadata.managedFields` tracks changes
-**Only supports full-resource exemption** - Cannot exempt individual checks (Polaris limitation)
- **Mitigated by:** This matches Polaris's own annotation-level granularity
### Neutral
- Strategic merge patch is the standard Kubernetes patching strategy for adding/removing annotations
- The annotation key (`polaris.fairwinds.com/exempt`) is defined by Polaris and unlikely to change
- Exemption state is stored on the workload resource itself, so it moves with the resource if migrated
## Alternatives Considered
### Option 1: Separate Exemption ConfigMap
**Pros:**
- Centralizes all exemptions in one place
- Does not require write access to workload resources
- Easy to audit all exemptions at once
**Cons:**
- Polaris does not read exemptions from ConfigMaps - it only checks annotations
- Would require a custom reconciliation controller to sync ConfigMap entries to annotations
- Adds operational complexity
**Decision:** Rejected (Polaris does not support ConfigMap-based exemptions)
### Option 2: Custom ExemptionPolicy CRD
**Pros:**
- Dedicated resource type for exemption management
- Could support per-check exemptions, time-based exemptions, etc.
- Clean separation of concerns
**Cons:**
- Over-engineering for what is essentially an annotation toggle
- Would require a custom controller to reconcile CRDs to annotations
- Adds CRD installation as a prerequisite
- Polaris still needs the annotation, so the CRD would be an indirection layer
**Decision:** Rejected (over-engineering for annotation toggle, would require a controller)
### Option 3: Read-Only Display with kubectl Instructions
**Pros:**
- No write operations in the plugin
- No RBAC requirements beyond read access
- Simpler implementation
**Cons:**
- Poor user experience - users must switch to terminal to manage exemptions
- Defeats the purpose of a UI plugin
- Error-prone (users may mistype annotation keys)
**Decision:** Rejected (poor UX compared to in-UI toggle)
## References
- [Polaris Exemptions Documentation](https://polaris.docs.fairwinds.com/customization/exemptions/)
- [Kubernetes Annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)
- [Strategic Merge Patch](https://kubernetes.io/docs/tasks/manage-kubernetes-resources/update-api-object-kubectl-patch/#use-a-strategic-merge-patch-to-update-a-deployment)
- [Plugin Implementation](../../../src/components/ExemptionManager.tsx)
## Revision History
| Date | Author | Change |
| ---------- | ----------- | ---------------- |
| 2026-03-05 | Plugin Team | Initial decision |
+4 -2
View File
@@ -70,8 +70,10 @@ What becomes easier or more difficult to do because of this change?
| ADR | Title | Status | Date |
| ------------------------------------- | -------------------------------------- | -------- | ---------- |
| [001](001-react-context-for-state.md) | Use React Context for State Management | Accepted | 2026-02-12 |
**Note:** Additional ADRs documenting other significant decisions (service proxy approach, drawer navigation, MUI import restrictions) can be created following the template above.
| [002](002-service-proxy-data-source.md) | Service Proxy as Single Data Source | Accepted | 2026-03-05 |
| [003](003-error-boundary-class-component.md) | Error Boundary as Class Component Exception | Accepted | 2026-03-05 |
| [004](004-localstorage-settings.md) | Browser localStorage for User Settings | Accepted | 2026-03-05 |
| [005](005-annotation-exemption-management.md) | Annotation-Based Exemption Management | Accepted | 2026-03-05 |
## Creating a New ADR
+1 -1
View File
@@ -5,7 +5,7 @@ test.describe('Polaris plugin settings', () => {
await page.goto('/c/main/settings/plugins');
// Find Polaris plugin in the list
const pluginCard = page.locator('text=headlamp-polaris-plugin').first();
const pluginCard = page.locator('text=polaris').first();
await expect(pluginCard).toBeVisible();
// Click to view settings (if settings are displayed inline, they should already be visible)
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "polaris",
"version": "0.6.0",
"name": "headlamp-polaris",
"version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "polaris",
"version": "0.6.0",
"name": "headlamp-polaris",
"version": "0.7.0",
"license": "Apache-2.0",
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0",
+16 -3
View File
@@ -1,6 +1,6 @@
{
"name": "polaris",
"version": "0.6.0",
"name": "headlamp-polaris",
"version": "0.7.0",
"description": "Headlamp plugin for Fairwinds Polaris audit results",
"repository": {
"type": "git",
@@ -26,8 +26,21 @@
"e2e": "playwright test",
"e2e:headed": "playwright test --headed"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0",
"@playwright/test": "^1.58.2"
"@mui/material": "^5.15.14",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"jsdom": "^24.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^5.3.0",
"vitest": "^3.0.5"
}
}
+11570
View File
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -1,4 +1,19 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
"extends": ["config:recommended"],
"baseBranches": ["main"],
"schedule": ["every weekend"],
"prConcurrentLimit": 10,
"packageRules": [
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "npm minor and patch"
},
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "github-actions minor and patch"
}
]
}
+2 -1
View File
@@ -218,7 +218,8 @@ const REFRESH_STORAGE_KEY = 'polaris-plugin-refresh-interval';
const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
const URL_STORAGE_KEY = 'polaris-plugin-dashboard-url';
const DEFAULT_DASHBOARD_URL = '/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
const DEFAULT_DASHBOARD_URL =
'/api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/';
/**
* Retrieves the configured refresh interval from localStorage.
+1 -1
View File
@@ -131,6 +131,6 @@ describe('AppBarScoreBadge', () => {
mockUsePolarisDataContext.mockReturnValue({ data, loading: false });
render(<AppBarScoreBadge />);
expect(screen.getByLabelText('Polaris cluster score: 100%')).toBeInTheDocument();
expect(screen.getByLabelText('Polaris: 100%')).toBeInTheDocument();
});
});
+2 -1
View File
@@ -54,8 +54,9 @@ export default function AppBarScoreBadge() {
alignItems: 'center',
gap: '4px',
}}
aria-label={`Polaris cluster score: ${score}%`}
aria-label={`Polaris: ${score}%`}
>
<span>{'\u{1F6E1}\uFE0F'}</span>
<span>Polaris: {score}%</span>
</button>
);
+2 -2
View File
@@ -105,7 +105,7 @@ export default function PolarisSettings(props: PluginSettingsProps) {
type="text"
value={currentUrl}
onChange={handleUrlChange}
placeholder="/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/"
placeholder="/api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/"
style={{
width: '100%',
padding: '4px 8px',
@@ -125,7 +125,7 @@ export default function PolarisSettings(props: PluginSettingsProps) {
>
Examples:
<br /> K8s proxy:{' '}
<code>/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/</code>
<code>/api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/</code>
<br /> Full URL: <code>https://my-polaris.example.com</code>
</div>
</div>
+3
View File
@@ -1,6 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
define: {
'process.env.NODE_ENV': '"test"',
},
test: {
globals: true,
environment: 'jsdom',