Remove all references to the incorrect `config.watchPlugins: false` requirement that was believed necessary for Headlamp v0.39.0+. Investigation revealed that plugins work correctly with the default `watchPlugins: true` setting. The earlier documentation was based on a misunderstanding of the plugin loading mechanism. Changes: - Remove watchPlugins: false from all YAML configuration examples - Remove warning sections about watchPlugins requirement - Update troubleshooting guides to focus on actual issues - Simplify installation instructions by removing unnecessary config Files updated: - README.md (main installation docs and troubleshooting table) - docs/DEPLOYMENT.md - docs/TROUBLESHOOTING.md - docs/getting-started/* (quick-start, installation, prerequisites) - docs/deployment/* (helm, production) - docs/troubleshooting/* (common-issues, README) - Multiple other doc files formatted by prettier This cleanup ensures ArtifactHub and GitHub documentation show correct, simplified installation instructions. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
20 KiB
Architecture
This document describes the architecture, design decisions, and data flow of the Headlamp Polaris Plugin.
Table of Contents
- Overview
- System Architecture
- Data Flow
- Component Hierarchy
- State Management
- Design Decisions
- Integration Points
- Known Limitations
Overview
The Headlamp Polaris Plugin is a read-only dashboard that surfaces Fairwinds Polaris audit results within the Headlamp UI. It fetches data from the Polaris dashboard API via the Kubernetes service proxy and presents it in a hierarchical navigation structure.
Key Characteristics:
- Read-only: No write operations to cluster or Polaris
- Service proxy based: Uses K8s API server proxy to reach Polaris
- React Context for state: Shared data fetch across components
- Headlamp plugin API: Integrates via official plugin system
- Type-safe: Full TypeScript with strict mode
System Architecture
┌─────────────────────────────────────────────────────────────┐
│ Headlamp UI (React) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ App Bar │ │ Sidebar │ │ Routes │ │
│ │ (Badge) │ │ (Navigation)│ │ (Views) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Plugin Registry │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────▼──────────────┐ │
│ │ Polaris Plugin │ │
│ ├────────────────────────────┤ │
│ │ • registerSidebarEntry │ │
│ │ • registerRoute │ │
│ │ • registerAppBarAction │ │
│ │ • registerPluginSettings │ │
│ │ • registerDetailsViewSection│ │
│ └─────────────┬──────────────┘ │
│ │ │
│ ┌─────────────▼──────────────┐ │
│ │ PolarisDataContext │ │
│ │ (React Context Provider) │ │
│ └─────────────┬──────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ┌────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │Dashboard │ │ Namespaces │ │ Namespace │ │
│ │View │ │ ListView │ │ Detail │ │
│ └──────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│
┌───────▼────────┐
│ ApiProxy │
│ (Headlamp) │
└───────┬────────┘
│
┌───────▼────────┐
│ Kubernetes │
│ API Server │
└───────┬────────┘
│
┌───────▼────────┐
│ Service Proxy │
│ /api/v1/ns/ │
│ polaris/svcs/ │
│ polaris- │
│ dashboard/ │
│ proxy/ │
└───────┬────────┘
│
┌───────▼────────┐
│ Polaris │
│ Dashboard │
│ (ClusterIP) │
└───────┬────────┘
│
┌───────▼────────┐
│ results.json │
│ (AuditData) │
└────────────────┘
Data Flow
1. Initial Load
User loads Headlamp
↓
Headlamp loads plugins
↓
Plugin registers routes, sidebar, app bar actions
↓
User navigates to /polaris
↓
DashboardView mounts
↓
PolarisDataContext.Provider wraps component
↓
usePolarisDataContext() hook triggers fetch
↓
ApiProxy.request() → K8s API → Service Proxy → Polaris
↓
AuditData returned and cached in Context
↓
Components receive data and render
2. Data Refresh
User clicks "Refresh" button or auto-refresh interval elapses
↓
refresh() function called in Context
↓
setRefreshKey() increments (forces re-fetch)
↓
useEffect dependency triggers new fetch
↓
ApiProxy.request() → Polaris Dashboard
↓
Context state updated with new data
↓
All consuming components re-render automatically
3. Navigation Flow
User clicks "Polaris" in sidebar
↓
Route: /c/main/polaris (DashboardView)
↓
Display cluster score, check distribution
↓
User clicks "Namespaces" submenu
↓
Route: /c/main/polaris/namespaces (NamespacesListView)
↓
Display table of namespaces with scores
↓
User clicks namespace button in table
↓
Drawer opens, URL hash updates (#namespace-name)
↓
NamespaceDetailView renders in drawer
↓
Display namespace score + resource table
Component Hierarchy
Plugin Entry Point
src/index.tsx
- Registers sidebar entries (Polaris → Overview, Namespaces)
- Registers routes (
/polaris,/polaris/namespaces) - Registers app bar action (score badge)
- Registers plugin settings page
- Registers details view section (inline audit)
Data Layer
src/api/PolarisDataContext.tsx
- React Context Provider for shared data
- Fetches AuditData from Polaris dashboard
- Handles auto-refresh based on user settings
- Provides
{ data, loading, error, refresh }to consumers
src/api/polaris.ts
- TypeScript types for AuditData schema
- Utility functions:
countResults(),computeScore() - Settings management:
getRefreshInterval(),setRefreshInterval() - Constants:
DASHBOARD_URL_DEFAULT,INTERVAL_OPTIONS
src/api/checkMapping.ts
- Maps Polaris check IDs to human-readable names
- Used for display in UI (e.g., "hostIPCSet" → "Host IPC")
src/api/topIssues.ts
- Aggregates failing checks across cluster
- Groups by check ID and severity
- Used for top issues dashboard
View Components
src/components/DashboardView.tsx
- Route:
/polaris - Purpose: Cluster-wide overview
- Features:
- Cluster score (percentage)
- Check distribution (pass/warning/danger/skipped)
- Cluster info (Polaris version, last audit time)
- Refresh button
- Data: Uses
usePolarisDataContext()
src/components/NamespacesListView.tsx
- Route:
/polaris/namespaces - Purpose: List all namespaces with scores
- Features:
- Table with namespace, score, pass/warning/danger counts
- Clickable namespace buttons (opens drawer)
- Sorted by score (lowest first)
- Data: Uses
usePolarisDataContext(), aggregates by namespace
src/components/NamespaceDetailView.tsx
- Route: Drawer on
/polaris/namespaces#<namespace> - Purpose: Namespace-level drill-down
- Features:
- Namespace score
- Resource table (kind, name, score, counts)
- URL hash navigation
- Keyboard shortcuts (Escape to close)
- Data: Filters
usePolarisDataContext()by namespace
UI Components
src/components/AppBarScoreBadge.tsx
- Location: Headlamp app bar (top-right)
- Purpose: Quick cluster score visibility
- Features:
- Color-coded badge (green ≥80%, orange ≥50%, red <50%)
- Clickable (navigates to
/polaris) - Shield emoji icon
- Data: Uses
usePolarisDataContext()
src/components/PolarisSettings.tsx
- Location: Settings → Plugins → Polaris
- Purpose: Plugin configuration
- Features:
- Refresh interval selector (1 min to 30 min)
- Dashboard URL input (custom Polaris instances)
- Connection test button
- Data: localStorage for persistence
src/components/InlineAuditSection.tsx
- Location: Resource detail pages (Deployment, StatefulSet, etc.)
- Purpose: Show Polaris audit inline
- Features:
- Pass/warning/danger counts
- Check details with messages
- Severity badges
- Data: Uses
usePolarisDataContext(), filters by resource
src/components/ExemptionManager.tsx
- Location: (Planned feature, UI exists but not fully integrated)
- Purpose: Manage Polaris exemptions via annotations
- Features:
- View current exemptions
- Add exemptions for failing checks
- Remove exemptions
State Management
Why React Context?
Decision: Use React Context instead of Redux/Zustand
Rationale:
- Simple state: Single AuditData object shared across views
- Read-only: No complex mutations or transactions
- Headlamp constraints: Plugin cannot add dependencies (Redux not bundled)
- Performance: Data changes infrequently (refresh interval 1-30 min)
Context Structure
interface PolarisDataContextValue {
data: AuditData | null; // Audit results or null if loading/error
loading: boolean; // True during initial fetch
error: string | null; // Error message if fetch failed
refresh: () => void; // Manual refresh function
}
Data Fetching Strategy
- Initial fetch: On first mount of any component using the context
- Auto-refresh: Based on user setting (default 5 minutes)
- Manual refresh: Via refresh button in UI
- Caching: Data persists in context until refresh (no per-route refetch)
localStorage Usage
Settings persisted in localStorage:
polaris-plugin-refresh-interval: Number (seconds), default 300polaris-plugin-dashboard-url: String, default service proxy path
No sensitive data stored in localStorage.
Design Decisions
1. Service Proxy vs. Direct Access
Decision: Use Kubernetes service proxy, not direct ClusterIP access
Rationale:
- Headlamp already has K8s API credentials (service account or user token)
- Service proxy leverages existing RBAC (no new credentials needed)
- Works with Headlamp's token auth and OIDC
- Simpler deployment (no additional network policies for plugin)
Trade-off:
- Requires
getpermission onservices/proxyresource - Path is longer:
/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
2. Two-Level Sidebar Nesting
Decision: Sidebar has "Polaris" → "Overview" and "Namespaces" (2 levels max)
Rationale:
- Headlamp sidebar supports 2-level nesting maximum
- Deeper nesting (e.g., Polaris → Namespaces → ) doesn't work
- Sidebar Collapse component is route-based, not click-to-toggle
Alternative Considered:
- Dynamic sidebar with namespace entries → rejected (Headlamp limitation)
Current Solution:
- Use table in NamespacesListView with clickable namespace buttons
- Namespace detail opens in drawer (not new route)
3. Drawer Navigation Instead of Routes
Decision: Namespace detail uses drawer, not dedicated route
Rationale:
- Better UX (drawer overlays table, no navigation loss)
- URL hash preserves navigation state (
#namespace-name) - Keyboard shortcuts (Escape to close)
- Sidebar doesn't support 3-level nesting for per-namespace routes
Implementation:
- URL:
/polaris/namespaces#kube-system - Drawer controlled by hash presence
useEffectwatches hash changes
4. No MUI Direct Imports
Decision: Never import from @mui/material or @mui/icons-material
Rationale:
- Headlamp plugin environment doesn't provide full MUI library
- Importing MUI causes
createSvgIcon undefinederror - Plugins must use Headlamp CommonComponents only
Alternative:
- Use standard HTML elements with inline styles
- Use theme-aware CSS variables (
--mui-palette-*)
5. TypeScript Strict Mode
Decision: Enable all TypeScript strict checks
Rationale:
- Catch errors at compile time
- Better IDE support and autocomplete
- Enforces type safety (no
any, no implicit unknowns)
Impact:
- More verbose code (explicit types required)
- Better maintainability and refactorability
6. Auto-Refresh Default: 5 Minutes
Decision: Default refresh interval is 5 minutes (configurable)
Rationale:
- Polaris audits typically run every 10-30 minutes
- Balance between data freshness and API load
- User can configure from 1 minute to 30 minutes
Considered:
- WebSocket/SSE for real-time updates → rejected (Polaris dashboard doesn't support)
- Shorter default → rejected (unnecessary API calls)
Integration Points
Headlamp Plugin API
Version: ≥ v0.13.0
Registration Functions Used:
// Sidebar navigation
registerSidebarEntry({ parent, name, label, url, icon });
// Routes
registerRoute({ path, sidebar, name, exact, component });
// App bar actions
registerAppBarAction(component);
// Plugin settings
registerPluginSettings(name, component, displaySaveButton);
// Resource detail sections
registerDetailsViewSection(component);
Key Changes in v0.13.0:
registerDetailsViewSectionnow takes 1 argument (component), not 2 (name, component)registerAppBarActionnow takes 1 argument (component), not 2 (name, component)
Headlamp CommonComponents
Used Components:
SectionBox- Card-like container with titleSectionHeader- Page header with titleStatusLabel- Color-coded status badgesNameValueTable- Key-value table layoutSimpleTable- Data table with sortingDrawer- Right-side overlay panelLoader- Loading spinner
Router:
Router.createRouteURL()- Generate plugin route URLs- React Router's
useHistory(),useParams(),useLocation()
Kubernetes API (via ApiProxy)
Used for:
- Fetching Polaris results:
ApiProxy.request(dashboardUrl + 'results.json') - No direct K8s API calls (all data from Polaris dashboard)
RBAC Required:
getonservices/proxyforpolaris-dashboardinpolarisnamespace
Known Limitations
1. Sidebar Nesting Depth
Limitation: Headlamp sidebar supports only 2 levels
Impact: Cannot have dynamic per-namespace sidebar entries
Workaround: Use table with drawer navigation
2. Skipped Checks Visibility
Limitation: Skipped checks (severity "ignore") counted but details not shown in dashboard
Reason: Polaris API groups skipped checks but doesn't provide per-check details
Impact: Users see skipped count but can't drill down to specific skipped checks
Documented: README, tooltip on skipped count
3. No Write Operations
Limitation: Plugin cannot modify Polaris configuration or exemptions
Reason: Read-only by design (service proxy only has get permission)
Impact: Exemption manager UI exists but requires manual annotation edits
Future: Could add PATCH permission to enable exemption annotations via UI
4. No Real-Time Updates
Limitation: Data refreshes on interval (1-30 minutes), not real-time
Reason: Polaris dashboard doesn't support WebSocket/SSE
Impact: Users may see stale data between refreshes
Workaround: Manual refresh button, configurable interval
5. MUI Import Restrictions
Limitation: Cannot import MUI components directly
Reason: Headlamp plugin environment doesn't provide full MUI bundle
Impact: Must use Headlamp CommonComponents or HTML elements
Documented: CLAUDE.md, CONTRIBUTING.md
6. Single Cluster Support
Limitation: Plugin shows data for current cluster only
Reason: Headlamp's multi-cluster support is route-based (/c/<cluster>/...)
Impact: Users must switch clusters in Headlamp to see different cluster's Polaris data
Future: Could enhance to aggregate multi-cluster if Headlamp API supports it
Performance Considerations
Bundle Size
- Current: ~27 KB minified (gzip: ~7.6 KB)
- Target: Keep under 50 KB to ensure fast loading
- Strategy: No heavy dependencies, tree-shaking enabled
Data Fetching
- Lazy loading: Data not fetched until user navigates to plugin
- Caching: Single fetch shared across all views (React Context)
- Refresh strategy: User-controlled interval prevents excessive API calls
Rendering
- React.memo: Not needed (data changes infrequently)
- Virtual scrolling: Not needed (namespace/resource lists typically <100 items)
- Component splitting: Lazy load views if bundle grows significantly
Future Architecture Enhancements
Potential Improvements
-
WebWorker for data processing
- Offload
countResults()aggregation for large clusters - Keep UI responsive during heavy computation
- Offload
-
IndexedDB caching
- Cache audit data offline
- Show stale data + "refresh available" indicator
-
GraphQL/REST API abstraction
- Decouple from Polaris dashboard JSON format
- Support multiple backend sources
-
Plugin-to-plugin communication
- Integrate with other Headlamp plugins (e.g., policy enforcement)
- Shared state between plugins
-
Incremental updates
- Fetch only changed namespaces/resources
- Reduce bandwidth and processing