gandalf-the-greybeard[bot] 2a60029104 e2e: shared volume plugin deployment for CI tests (#59)
* e2e: shared volume plugin deployment replacing init container approach

Replace the init container plugin installation with a shared PVC volume
between the CI runner and Headlamp pod. The runner builds the plugin and
copies it to the shared mount; Headlamp reads from the same volume.

- Add deployment/headlamp-e2e-values.yaml (PVC-backed shared volume)
- Add deployment/headlamp-plugins-pvc.yaml (PVC manifest)
- Add scripts/deploy-plugin-via-volume.sh (build + copy + restart)
- Remove deployment/headlamp-static-plugin-values.yaml (init container)

This is CI-only test infrastructure — ArtifactHub remains the sole
user-facing distribution channel.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: update e2e workflow for shared volume plugin deployment

Replace the old preflight-only approach with a build-and-deploy flow
that uses a shared volume (hostPath) between the CI runner and the
Headlamp pod. The workflow now builds the plugin from source, copies
the artifact to a shared volume path, and optionally calls Gandalf's
deploy script for Headlamp rollout coordination.

Removes kubectl exec/cp references and version-match preflight in
favor of deploying the PR's actual build artifact.

Refs: PRI-216, PRI-195

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: align e2e workflow with Gandalf's deploy script interface

Simplify deploy step to call scripts/deploy-plugin-via-volume.sh
directly instead of duplicating copy logic. Align env var names
(PLUGIN_VOLUME_PATH, HEADLAMP_DEPLOY) with the deploy script's
expected interface from PR #59.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: deploy plugin via temporary pod instead of assuming local PVC mount

The deploy script assumed the PVC was mounted on the CI runner at
/mnt/headlamp-plugins, but the runner pod doesn't have that mount.
Fix by using a temporary pod (kubectl run) that mounts the PVC,
receives the plugin tarball via stdin, and extracts it.

Also adds missing workflow steps to create the PVC and upgrade
Headlamp with the shared volume helm values before deploying.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: add kubectl, helm, and helm repo setup steps to e2e workflow

The self-hosted runner doesn't have kubectl or helm pre-installed.
Add setup steps using azure/setup-kubectl and azure/setup-helm
actions, and add the Headlamp helm repo before the upgrade step.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: update Headlamp Helm repo URL from headlamp-k8s to kubernetes-sigs

The Headlamp project moved to the kubernetes-sigs org. The old Helm chart
repository URL (headlamp-k8s.github.io) returns 404, causing E2E workflow
failure at the `helm repo add` step.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* chore: add RBAC manifest for E2E CI runner

Documents the Role and RoleBinding applied to the cluster for the ARC
runner service account. Grants permissions in kube-system needed for
shared volume plugin deployment (PVCs, pods, Helm resources).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: remove .github/workflows/e2e.yaml changes from PR

The workflow changes should be handled separately by Hugh Hackman
per PRI-215. This PR should only contain deployment manifests and
scripts, not CI workflow modifications.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* ci: add shared volume plugin deployment to E2E workflow

Adds the build, Helm, PVC, and plugin deploy steps needed for the
shared volume E2E approach. Uses the correct kubernetes-sigs Helm repo
URL and overrides config.sessionTTL=0 to avoid schema validation error.

This is the workflow counterpart to the deployment manifests and scripts
already in this PR (PVC, values overlay, deploy script).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): set sessionTTL=1 to satisfy Helm schema minimum

The Headlamp Helm chart schema enforces a minimum of 1 for
config.sessionTTL. Setting it to 0 caused helm upgrade to fail
with a schema validation error.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): add cluster-scoped RBAC for CI runner

The Headlamp Helm chart manages ClusterRole and ClusterRoleBinding
resources. The CI runner SA needs cluster-level permissions to
get/update these during helm upgrade. Added ClusterRole and
ClusterRoleBinding alongside the existing namespace-scoped Role.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): replace helm upgrade with kubectl patch to avoid cluster RBAC

The CI runner SA cannot access cluster-scoped resources (ClusterRole,
ClusterRoleBinding) needed by helm upgrade's 3-way merge. Replace the
helm upgrade step with kubectl patch commands that add the shared volume
mount directly to the Headlamp deployment.

This eliminates the need for cluster-admin intervention:
- kubectl patch adds PVC volume + volumeMount to the deployment
- kubectl set env configures the plugins directory
- kubectl rollout status waits for the update

Also removes the now-unnecessary ClusterRole/ClusterRoleBinding from the
RBAC manifest — only namespace-scoped Role/RoleBinding is needed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): improve volume mount idempotency check

Check for existing volume mount by mountPath and PVC claimName, not
just by volume name. A prior helm upgrade may have created mounts
with different names but the same path, causing kubectl patch to fail
with "mountPath must be unique".

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): schedule deploy pod on same node as Headlamp

The headlamp-plugins PVC is ReadWriteOnce, so the temporary deploy
pod must run on the same node as the Headlamp pod to mount it.
Look up the Headlamp pod's node and set nodeName in the pod spec.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use Job with base64 tarball instead of kubectl run stdin

The kubectl run --rm -i stdin pipe times out in the ARC runner
environment. Replace with a Kubernetes Job that receives the plugin
tarball as base64-encoded data in the container command. This avoids
the unreliable attach/stdin mechanism entirely.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use ConfigMap for tarball instead of inline base64

Embedding base64 data in the YAML spec broke parsing. Store the plugin
tarball in a ConfigMap via --from-file and mount it in the deploy Job.
This avoids both the stdin pipe issue and the YAML escaping issue.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use temp file for Job YAML to avoid heredoc escaping

Variable expansion inside heredocs breaks YAML parsing when values
contain colons and quotes (like nodeName). Write the Job manifest to
a temp file with literal YAML, then sed-substitute the dynamic values.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use Pod instead of Job for plugin deploy

The CI runner SA has permission to create Pods but not Jobs in
kube-system. Switch from a Job to a plain Pod with restartPolicy:Never.
Use ConfigMap mount for tarball data (no stdin piping needed).

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align registerPluginSettings name with deployed plugin directory

The plugin is deployed to the 'polaris' directory but was registered with
'headlamp-polaris', causing Headlamp to not match the settings component
with the loaded plugin. This fixes all 5 failing E2E settings tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: use package name for registerPluginSettings, not directory name

Headlamp identifies plugins by their package.json name (headlamp-polaris),
not the deploy directory name (polaris). The previous commit incorrectly
changed this to 'polaris', causing the settings component to never render
in the plugin settings page — breaking all 5 E2E settings tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align registerPluginSettings name with deploy directory 'polaris'

The shared volume deploy script places the plugin at /headlamp/plugins/polaris/,
so Headlamp matches settings by directory name 'polaris', not the package.json
name 'headlamp-polaris'. This reverts commit b9d718b which incorrectly changed
the registration name back to 'headlamp-polaris'.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align plugin deploy dir with package.json name, clean stale dirs

The PVC had a stale headlamp-polaris directory from a previous install.
Headlamp loads plugins by scanning the plugins dir and reading package.json
from each subdirectory — it was loading the old build from headlamp-polaris/
while the deploy script was writing to polaris/. The settings registration
name needs to match the plugin name Headlamp identifies.

Changes:
- Deploy script now uses headlamp-polaris as the directory name (matching
  package.json name field)
- Deploy pod cleans up both polaris/ and headlamp-polaris/ before deploying
  to ensure no stale copies remain
- registerPluginSettings uses headlamp-polaris to match Headlamp's plugin
  identifier

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix: align registerPluginSettings and E2E test with package.json name

Headlamp identifies plugins by reading package.json from the plugin
directory. Since package.json name is 'headlamp-polaris', both the
registerPluginSettings call and the E2E settings test must use
'headlamp-polaris', not 'polaris'.

- registerPluginSettings('polaris') → registerPluginSettings('headlamp-polaris')
- E2E test locator: text=polaris → text=headlamp-polaris

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): load main page before settings to ensure plugin list is populated

Headlamp's PluginSettings component initializes its state from
localStorage on mount and never syncs when props.plugins updates later.
If the settings page loads before fetchAndExecutePlugins completes,
the plugin list stays empty and the test can't find "headlamp-polaris".

Fix: navigate to the main page first, wait for the Polaris sidebar
entry to confirm the plugin is loaded (which populates localStorage),
then navigate to the settings page.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use client-side routing for settings navigation

The PluginSettings component reads the plugin registry once on mount
and never re-renders when new plugins register. Using page.goto() for
the settings URL re-initializes the SPA, causing PluginSettings to
mount before async plugin scripts finish calling registerPluginSettings().

Replace page.goto() with pushState + popstate to do client-side routing.
This preserves the already-loaded plugin registrations from the main
page, so PluginSettings sees the plugin immediately on mount.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(e2e): use correct HOME-context URL for plugin settings page

The settings page is at /settings/plugins (HOME sidebar context), not
/c/main/settings/plugins (in-cluster context). The in-cluster URL
doesn't match any route, so PluginSettings never mounted and the
plugin entry was never visible.

With the correct URL, no preloading or client-side routing hacks are
needed — PluginSettings uses useTypedSelector on the Redux plugin store,
so it re-renders automatically when registerPluginSettings() fires.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Gandalf the Greybeard <gandalf@privilegedescalation.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Hugh Hackman <hugh@privilegedescalation.com>
Co-authored-by: Hugh Hackman <hugh-hackman[bot]@users.noreply.github.com>
2026-03-18 02:42:42 +00:00
2026-03-17 17:00:48 +00:00

Headlamp Polaris Plugin

Artifact Hub CI E2E Tests License: Apache-2.0

A Headlamp plugin that surfaces Fairwinds Polaris audit results directly in the Headlamp UI.

Documentation | Installation | Security | Development

What It Does

Adds a Polaris top-level sidebar section to Headlamp with comprehensive security, reliability, and efficiency audit integration:

Main Views

  • Overview Dashboard -- cluster score with percentage gauge, check distribution charts, top 10 most common failing checks across the cluster, cluster statistics, and last audit time with manual refresh button
  • Namespaces -- table of all namespaces with per-namespace score and check counts; click a namespace to open a detailed side panel (1000px wide, theme-aware)
  • Namespace Detail Panel -- per-namespace score, check counts, resource-level audit results, external Polaris dashboard link, and exemption management

Integrated Features

  • App Bar Score Badge -- cluster Polaris score displayed as a colored chip in the top navigation bar (green ≥80%, yellow ≥50%, red <50%); click to navigate to overview
  • Inline Resource Audits -- Polaris audit results automatically injected into detail views for Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs; shows compact score, failing checks table, and link to full report
  • Exemption Management -- add or remove Polaris exemptions via annotation patches directly from the UI; supports per-check exemptions or exempt-all
  • Configurable Dashboard URL -- supports both Kubernetes service proxy URLs and full HTTP/HTTPS URLs for external Polaris deployments
  • Connection Testing -- test button in settings to verify Polaris dashboard connectivity and show version info
  • Dark Mode Support -- full theme adaptation using MUI useTheme() API; drawer, settings, and all UI elements respect system/Headlamp theme

Data & Refresh

Data is fetched from the Polaris dashboard API through the Kubernetes service proxy (/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json) or custom URLs. The plugin is primarily read-only; it only writes when explicitly applying exemption annotations.

Results are refreshed on a user-configurable interval (1 / 5 / 10 / 30 minutes, default 5). Settings are available in Settings > Plugins > Polaris and persist in browser localStorage.

Error states are handled explicitly with context-specific messages: RBAC denied (403), Polaris not installed (404/503), malformed JSON, network failures, and CORS issues.

Prerequisites

Requirement Minimum version
Headlamp v0.26+
Polaris (with dashboard enabled) Any recent release
Kubernetes v1.24+

Polaris must be deployed in the polaris namespace with the dashboard component enabled (dashboard.enabled: true in the Helm chart, which is the default). The plugin reads from the polaris-dashboard ClusterIP service on port 80.

Installing

The plugin is published on Artifact Hub. Install via the Headlamp UI:

  1. Go to Settings → Plugins
  2. Click Catalog tab
  3. Search for "Polaris"
  4. Click Install

Or configure Headlamp via Helm:

config:
  pluginsDir: /headlamp/plugins

pluginsManager:
  sources:
    - name: headlamp-polaris-plugin
      url: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz

RBAC / Security Setup

The plugin fetches audit data through the Kubernetes API server's service proxy sub-resource. The identity making the request (Headlamp's service account, or the user's own token in token-auth mode) must be granted:

Verb API Group Resource Resource Name Namespace
get "" (core) services/proxy polaris-dashboard polaris

Minimal RBAC manifests

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: polaris-proxy-reader
  namespace: polaris
rules:
  - apiGroups: ['']
    resources: ['services/proxy']
    resourceNames: ['polaris-dashboard']
    verbs: ['get']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: headlamp-polaris-proxy
  namespace: polaris
subjects:
  - kind: ServiceAccount
    name: headlamp # adjust to match your Headlamp service account
    namespace: kube-system # adjust to match the namespace Headlamp runs in
roleRef:
  kind: Role
  name: polaris-proxy-reader
  apiGroup: rbac.authorization.k8s.io

Apply with kubectl apply -f polaris-rbac.yaml.

Token-auth mode

When Headlamp is configured for user-supplied tokens (rather than a fixed service account), each user must have the RoleBinding above attached to their own identity. A 403 error in the plugin means the currently logged-in user lacks this binding.

NetworkPolicy

If the polaris namespace enforces network policies, ensure ingress is allowed from the Kubernetes API server (which performs the proxy hop) to polaris-dashboard on port 80.

Read-only access

The plugin only performs GET requests through the service proxy. No create, update, delete, or patch verbs are required. Do not grant broader access than get on services/proxy.

Audit logging

Every proxied request is recorded in Kubernetes API audit logs as a get on services/proxy in the polaris namespace. If the auto-refresh interval generates more audit volume than desired, increase the refresh interval in the plugin settings or adjust your audit policy.

Documentation

Complete Documentation - Documentation hub with all guides

Comprehensive Guides

Guide Description
Architecture System architecture, data flow, component hierarchy, design decisions
Deployment Production deployment with Helm, Kubernetes, FluxCD
Security Security model, RBAC requirements, vulnerability reporting
Testing Unit tests, E2E tests, CI/CD, best practices
Contributing Development workflow, branching strategy, PR process
Changelog Complete release history (v0.0.1 to current)

Troubleshooting

For comprehensive troubleshooting, see docs/troubleshooting/README.md.

Quick reference:

Symptom Likely Cause Quick Fix
Plugin not in sidebar Plugin not installed or needs browser refresh Hard refresh browser (Cmd+Shift+R / Ctrl+Shift+F5)
403 Access Denied Missing RBAC binding for services/proxy Apply Role + RoleBinding from RBAC section
404 or 503 Polaris not installed, or dashboard disabled Install Polaris with dashboard.enabled: true in polaris namespace
Dark mode white backgrounds Old plugin version Upgrade to v0.6.0+ and hard refresh browser
Settings page empty Old plugin version Upgrade to v0.3.3+
No data / infinite spinner Network policy or Polaris pod down Check network policies and kubectl get pods -n polaris

Development

For detailed development guide, see CONTRIBUTING.md.

Quick Start

# Clone repository
git clone https://github.com/privilegedescalation/headlamp-polaris-plugin.git
cd headlamp-polaris-plugin

# Install dependencies
npm install

# Run with hot reload
npm start  # Opens Headlamp at http://localhost:4466

# Build for production
npm run build        # outputs dist/main.js
npm run package      # creates headlamp-polaris-plugin-<version>.tar.gz

# Run tests
npm test             # unit tests
npm run e2e          # E2E tests (requires Headlamp instance)

# Code quality
npm run lint         # eslint
npm run tsc          # type-check
npm run format       # prettier format

Running Tests

# Unit tests (Vitest)
npm test
npm run test:watch

# E2E tests (Playwright)
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
npm run e2e
npm run e2e:headed   # see browser

For complete testing guide including CI/CD integration, see docs/TESTING.md.

Project Structure

src/
  index.tsx                           -- Entry point. Registers sidebar entries, routes, and error boundaries.
  test-utils.tsx                      -- Shared test fixtures (makeResult, makeAuditData).
  api/
    polaris.ts                        -- TypeScript types (AuditData schema), usePolarisData hook,
                                         countResults utilities, refresh interval settings.
    checkMapping.ts                   -- Polaris check ID → human-readable name mapping.
    topIssues.ts                      -- Top failing checks aggregation logic.
    PolarisDataContext.tsx             -- React context provider; shared data fetch across views.
  components/
    DashboardView.tsx                 -- Overview page (score, check summary with skipped, cluster info).
    NamespacesListView.tsx            -- Namespace list with scores; MUI Drawer detail panel.
    InlineAuditSection.tsx            -- Inline audit for Deployment/StatefulSet/DaemonSet/Job/CronJob detail views.
    ExemptionManager.tsx              -- Polaris exemption annotation management.
    AppBarScoreBadge.tsx              -- App bar cluster score chip.
    PolarisSettings.tsx               -- Plugin settings page (refresh interval, dashboard URL).
vitest.config.mts                     -- Vitest configuration (jsdom environment).

Data Source

The plugin fetches live audit results from the Polaris dashboard HTTP API via the Kubernetes service proxy:

GET /api/v1/namespaces/polaris/services/polaris-dashboard/proxy/results.json

This endpoint is served by the polaris-dashboard ClusterIP service, which is created by the Polaris Helm chart when dashboard.enabled: true. The JSON response matches Polaris's AuditData schema (pkg/validator/output.go):

AuditData
  ClusterInfo      -- nodes, pods, namespaces, controllers
  Results[]        -- per-workload results
    Results{}      -- top-level check results (ResultSet)
    PodResult
      Results{}    -- pod-level check results
      ContainerResults[]
        Results{}  -- container-level check results

Each check in a ResultSet has Success (bool) and Severity ("warning", "danger", or "ignore"). Checks with Severity: "ignore" and Success: false are counted as skipped. The cluster score is computed client-side as pass / total * 100.

Known Limitations

Skipped Count and Annotation-Based Exemptions

The Skipped count shown in the plugin only reflects checks with Severity: "ignore" in the Polaris API response. It does not include annotation-based exemptions (e.g., polaris.fairwinds.com/privilegeEscalationAllowed-exempt: "true").

Why? Polaris completely omits exempted checks from the results.json endpoint. The native Polaris dashboard UI computes the "skipped" count client-side by:

  1. Querying Kubernetes resources (Deployments, DaemonSets, StatefulSets, Pods) directly
  2. Parsing their annotations for polaris.fairwinds.com/*-exempt keys
  3. Counting how many checks were exempted

This plugin only has access to the processed audit results via the service proxy and does not query raw Kubernetes resources. To show accurate exemption counts, the plugin would need to:

  • Request cluster-wide read access to all workload types (requires additional RBAC grants beyond services/proxy)
  • Parse annotations on every workload in every namespace
  • Cross-reference with the Polaris check catalog to count exemptions

This is a significant architectural change and is not currently implemented. Hover over the "Skipped" count in the UI to see a tooltip explaining this limitation.

Workaround: Use the "View in Polaris Dashboard" link from any namespace detail view to see the full exemption count in the native dashboard.

Releasing

Releases are automated via GitHub Actions. To cut a release:

# 1. Update CHANGELOG.md with new version
# 2. Bump version in package.json and artifacthub-pkg.yml:
git add package.json artifacthub-pkg.yml CHANGELOG.md
git commit -m "chore: bump version to X.Y.Z"
git push origin main

# 3. Create and push tag:
git tag vX.Y.Z
git push origin vX.Y.Z

This triggers the GitHub Actions release workflow (.github/workflows/release.yaml):

  1. Build the plugin in a node:20 container
  2. Package a .tar.gz tarball
  3. Create a GitHub release with the tarball attached
  4. Calculate SHA256 checksum
  5. Update artifacthub-pkg.yml checksum on main branch
  6. Force-move the tag to include checksum update

A guard step prevents infinite loops: if the release tarball checksum already matches the metadata, the workflow is skipped.

ArtifactHub Sync

ArtifactHub pulls plugin metadata from GitHub every 30 minutes. After creating a release:

For complete release process and version numbering guidelines, see CONTRIBUTING.md#release-process.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for:

  • Development workflow
  • Branching strategy (feature branches required for code changes)
  • Commit message conventions (Conventional Commits)
  • PR process and review checklist
  • Code style guidelines
  • Testing requirements

License

Apache-2.0 License - see LICENSE file for details.

S
Description
Headlamp plugin for Fairwinds Polaris security and best-practices auditing
Readme Apache-2.0 2.8 MiB
v1.0.1 Latest
2026-05-20 22:51:19 +00:00
Languages
TypeScript 99.8%
Dockerfile 0.2%