Files
Chris Farhood 24033ca977 docs: remove incorrect watchPlugins: false references
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>
2026-02-13 09:54:15 -05:00

9.1 KiB

Data Flow

Detailed data flow sequences for the Headlamp Polaris Plugin.

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

4. Error Handling Flow

ApiProxy.request() called
    ↓
Fetch fails with HTTP error
    ↓
Error caught in usePolarisData hook
    ↓
Error status code checked (403, 404, 503, etc.)
    ↓
Context-specific error message set:
  • 403: RBAC permission denied
  • 404/503: Polaris not installed
  • Other: Generic network error
    ↓
Error state propagated to consuming components
    ↓
Components render error UI with StatusLabel
    ↓
User sees error message with actionable guidance

5. Service Proxy Request Flow

Plugin code: ApiProxy.request(path)
    ↓
Headlamp backend proxies request
    ↓
HTTP GET to Kubernetes API server
    ↓
API server authenticates request (service account or user token)
    ↓
API server checks RBAC:
  • Verb: get
  • Resource: services/proxy
  • ResourceName: polaris-dashboard
  • Namespace: polaris
    ↓
If authorized:
  API server proxies to Polaris service
    ↓
Polaris dashboard returns results.json
    ↓
Response flows back to plugin
    ↓
If denied (403):
  RBAC error returned to plugin
    ↓
Plugin displays error with RBAC guidance

6. Settings Persistence Flow

User navigates to Settings → Plugins → Polaris
    ↓
PolarisSettings component mounts
    ↓
Component reads localStorage:
  • polaris-plugin-refresh-interval
  • polaris-plugin-dashboard-url
    ↓
Form populated with current values
    ↓
User modifies settings (refresh interval, dashboard URL)
    ↓
User clicks "Save"
    ↓
Settings written to localStorage:
  localStorage.setItem('polaris-plugin-refresh-interval', value)
  localStorage.setItem('polaris-plugin-dashboard-url', url)
    ↓
Success message displayed
    ↓
Context refreshes data with new interval
    ↓
All plugin views use new settings immediately

7. App Bar Badge Flow

Headlamp renders app bar
    ↓
Plugin's registerAppBarAction called
    ↓
AppBarScoreBadge component rendered in app bar
    ↓
Component uses usePolarisDataContext()
    ↓
Data fetched from Polaris (shared with views)
    ↓
Score computed: (pass / total) * 100
    ↓
Badge color determined:
  • Green: score ≥ 80
  • Yellow: score 50-79
  • Red: score < 50
    ↓
Badge rendered with score and shield icon
    ↓
User clicks badge
    ↓
Navigate to /polaris (overview page)

8. Inline Audit Section Flow

User views Deployment/StatefulSet detail page
    ↓
Headlamp calls registered details view sections
    ↓
Plugin's InlineAuditSection component rendered
    ↓
Component receives resource metadata from Headlamp
    ↓
Component uses usePolarisDataContext()
    ↓
Filters audit results by:
  • Namespace === resource.namespace
  • Kind === resource.kind
  • Name === resource.name
    ↓
If matching audit result found:
  Extract check counts (pass/warning/danger)
    ↓
Render compact audit section:
  • Score badge
  • Check counts
  • Link to full Polaris report
    ↓
If no match found:
  Render "No audit data available" message

Data Structures

AuditData Schema

interface AuditData {
  PolarisOutputVersion: string; // "1.0"
  AuditTime: string; // ISO 8601 timestamp
  SourceType: string; // "Cluster"
  SourceName: string; // Cluster identifier
  DisplayName: string; // Human-readable name
  ClusterInfo: {
    Version: string; // K8s version
    Nodes: number;
    Pods: number;
    Namespaces: number;
    Controllers: number;
  };
  Results: Result[]; // Array of resource audit results
}

interface Result {
  Name: string; // Resource name
  Namespace: string; // Kubernetes namespace
  Kind: string; // "Deployment", "StatefulSet", etc.
  Results: ResultSet; // Resource-level checks
  PodResult?: {
    Name: string;
    Results: ResultSet; // Pod-level checks
    ContainerResults: {
      Name: string;
      Results: ResultSet; // Container-level checks
    }[];
  };
  CreatedTime: string; // ISO 8601 timestamp
}

type ResultSet = Record<string, ResultMessage>;

interface ResultMessage {
  ID: string; // Check ID (e.g., "cpuLimitsMissing")
  Message: string; // Human-readable message
  Details: string[]; // Additional context
  Success: boolean; // true = passed, false = failed
  Severity: 'ignore' | 'warning' | 'danger';
  Category: string; // "Security", "Efficiency", etc.
}

Result Counts

interface ResultCounts {
  total: number; // Total checks performed
  pass: number; // Checks that passed (Success: true)
  warning: number; // Failed checks with Severity: "warning"
  danger: number; // Failed checks with Severity: "danger"
  skipped: number; // Failed checks with Severity: "ignore"
}

Data Transformations

1. Aggregating Counts

// Input: AuditData.Results[]
// Output: ResultCounts

function countResults(data: AuditData): ResultCounts {
  const counts = { total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 };

  for (const result of data.Results) {
    // Count resource-level checks
    countResultSet(result.Results, counts);

    // Count pod-level checks
    if (result.PodResult) {
      countResultSet(result.PodResult.Results, counts);

      // Count container-level checks
      for (const container of result.PodResult.ContainerResults) {
        countResultSet(container.Results, counts);
      }
    }
  }

  return counts;
}

function countResultSet(rs: ResultSet, counts: ResultCounts): void {
  for (const key in rs) {
    const msg = rs[key];
    counts.total++;
    if (msg.Success) {
      counts.pass++;
    } else if (msg.Severity === 'ignore') {
      counts.skipped++;
    } else if (msg.Severity === 'warning') {
      counts.warning++;
    } else if (msg.Severity === 'danger') {
      counts.danger++;
    }
  }
}

2. Computing Score

// Input: ResultCounts
// Output: Score (0-100)

function computeScore(counts: ResultCounts): number {
  if (counts.total === 0) return 0;
  return Math.round((counts.pass / counts.total) * 100);
}

// Examples:
// { total: 100, pass: 90, ... } → 90
// { total: 100, pass: 50, ... } → 50
// { total: 0, pass: 0, ... } → 0

3. Filtering by Namespace

// Input: AuditData, namespace string
// Output: Result[] for that namespace

function filterResultsByNamespace(data: AuditData, namespace: string): Result[] {
  return data.Results.filter(r => r.Namespace === namespace);
}

4. Extracting Namespaces

// Input: AuditData
// Output: Sorted array of unique namespace names

function getNamespaces(data: AuditData): string[] {
  const namespaces = new Set<string>();
  for (const result of data.Results) {
    if (result.Namespace) {
      namespaces.add(result.Namespace);
    }
  }
  return Array.from(namespaces).sort();
}

Caching Strategy

Current Implementation:

  • Data fetched once and stored in React Context
  • Shared across all plugin views (no duplicate fetches)
  • Cached until manual refresh or auto-refresh interval

Cache Invalidation:

  • Manual refresh button click
  • Auto-refresh interval elapses
  • Settings change (dashboard URL)

No Persistence:

  • Data NOT persisted to localStorage
  • Each browser session fetches fresh data
  • No offline mode

Future Enhancement:

  • IndexedDB caching for offline access
  • Incremental updates (fetch only changed namespaces)
  • Service Worker for background refresh

Next Steps

References