Compare commits
335 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a156c4e63 | |||
| 5f817ec4f6 | |||
| 06d7dfb212 | |||
| ba508b8fc4 | |||
| b928fff4a5 | |||
| df6a5967ea | |||
| 415e32cdc9 | |||
| aa32e7a353 | |||
| 67bfe5ff5c | |||
| c08f3fbdbe | |||
| 02dc79b739 | |||
| d1097c2dbf | |||
| 5fa14ab353 | |||
| acd53c297b | |||
| fd66b119b3 | |||
| 21026cc992 | |||
| 95096562e4 | |||
| 62baf2bd5e | |||
| d2da09406a | |||
| a975192dfb | |||
| 2c80d0451e | |||
| d4a4e9a355 | |||
| a08c0fc368 | |||
| d0a6794576 | |||
| 00c270b0d4 | |||
| 7f115e0d6e | |||
| 9d02f504fd | |||
| 65c25067ec | |||
| 4c6324c4c2 | |||
| ca4832bcc3 | |||
| d6c8a8bbfc | |||
| 3d91572b59 | |||
| f0f3bd51a4 | |||
| 6e9c97593c | |||
| a5398e8409 | |||
| bb1df5f3f6 | |||
| 1bf5c2431c | |||
| 08a3009ba8 | |||
| b3f1f65b2f | |||
| 74a5bb0a01 | |||
| 9249f151a8 | |||
| dd782fbea0 | |||
| 0a52a8effa | |||
| 902f206e32 | |||
| 4344d33349 | |||
| 8ac890a1c6 | |||
| 6189f2b983 | |||
| 4296eb97fb | |||
| 87bf1a321f | |||
| 37af076456 | |||
| 0476fd1076 | |||
| 6a47358771 | |||
| f7d415e013 | |||
| 2a60029104 | |||
| 76c7a5bc1f | |||
| d64db24240 | |||
| 9bd07e1928 | |||
| 40b0a2d220 | |||
| fb3d262eb7 | |||
| 0f88a9b19f | |||
| d3860ff5a2 | |||
| 7165bdf79b | |||
| eb218dc7f4 | |||
| c02efe5430 | |||
| daf0ebbff5 | |||
| fc8a9eebac | |||
| 07bcfa084a | |||
| 1755cedd88 | |||
| 07a99a76ce | |||
| c3d3989cdc | |||
| 2012a34938 | |||
| 7603dfeb29 | |||
| 9ad0b24580 | |||
| acc9d8fac1 | |||
| 7413f699de | |||
| 497c040dbe | |||
| 29bc953522 | |||
| 0bd5223587 | |||
| 2c441bf867 | |||
| 222346759e | |||
| d543e3bf9d | |||
| 4e66a4b7cc | |||
| e800adfc19 | |||
| b3349b71d5 | |||
| ceb7f31257 | |||
| 8f69329764 | |||
| 0882d663fd | |||
| 6c7064faf0 | |||
| fb445954e0 | |||
| 8bc6575ac3 | |||
| 514de78ba7 | |||
| 6dd64e87ce | |||
| 9b9052243f | |||
| b772209b65 | |||
| f2b0e4c66f | |||
| 4f937efe26 | |||
| d23ccf3a84 | |||
| 13bdb9901a | |||
| f575623b93 | |||
| a46d0e7519 | |||
| 2da1fb3099 | |||
| 28f432f2bf | |||
| 23148bfaff | |||
| ed38df7215 | |||
| 40a4b8accc | |||
| 86f959486a | |||
| c621bd1384 | |||
| e574ea66e2 | |||
| 49bcbe24af | |||
| fdad1a6da2 | |||
| 7bf00593ef | |||
| 1ca90a7570 | |||
| dbc69fb41f | |||
| 24033ca977 | |||
| 0faa50cd9d | |||
| de5feb68a3 | |||
| 42df8dd7cc | |||
| 3796d57d12 | |||
| 88541bd328 | |||
| e884894840 | |||
| 189ae50024 | |||
| e62fba9cc1 | |||
| 062ac72340 | |||
| 515758c829 | |||
| 40c4add01b | |||
| cdc1ce0303 | |||
| dd5a76e348 | |||
| 3b6d7e6b6c | |||
| 0e5ff9b7b7 | |||
| 8fab7828f4 | |||
| 1733eab21b | |||
| 44fe9ab749 | |||
| 0b081246b0 | |||
| 765f081867 | |||
| d4fa1674dc | |||
| 9655514066 | |||
| ece87a9824 | |||
| 5adcca2eb9 | |||
| 6965103113 | |||
| 0659afcf6e | |||
| 808f60ad88 | |||
| ade6bf93a7 | |||
| 10ed01439c | |||
| b816886d6c | |||
| cbdbe732e4 | |||
| 6ea9095470 | |||
| 202f3599a3 | |||
| 81c6d9be4b | |||
| 0370b86af2 | |||
| 4325babf68 | |||
| 0cf259f0b9 | |||
| 9e195be633 | |||
| c4938aa987 | |||
| 9c4f34d134 | |||
| 2c2f04ff5d | |||
| d4fe2c9ea9 | |||
| 23f7a7ed73 | |||
| 96a0cce17e | |||
| 8672ae8956 | |||
| dfbff8d539 | |||
| 7ad5b7ecc3 | |||
| 0e711f5dfd | |||
| 67ced98bcd | |||
| f02b4cf051 | |||
| 51b174e68d | |||
| 5188bc289e | |||
| 33ef816826 | |||
| ea28ab3a81 | |||
| 57f0bf6b4b | |||
| 8dd71772f5 | |||
| c3b2877c51 | |||
| 62aa181433 | |||
| 2c077907e9 | |||
| 17495d4883 | |||
| 01eed82efc | |||
| 5dab426fe8 | |||
| 5eaa6603f1 | |||
| b67f770660 | |||
| 20e8063cbb | |||
| c1156e5cf5 | |||
| cab2118a88 | |||
| a18710ccb1 | |||
| 811059cf75 | |||
| a404c075d6 | |||
| db17a08d26 | |||
| e52670dee4 | |||
| 8d219a9c6e | |||
| b2cbce16c1 | |||
| c95aab3ca3 | |||
| 604106c688 | |||
| 44a0016a4d | |||
| 03d7379e13 | |||
| 861dff6901 | |||
| 03b75a836b | |||
| 83a5342011 | |||
| 3daa1cbc14 | |||
| 9c03d912df | |||
| 00d4b224eb | |||
| c1248ec3c4 | |||
| 7ac5d0a494 | |||
| 59c1d4e844 | |||
| a507ba1d4a | |||
| d03fb81cd5 | |||
| d4d593cf74 | |||
| 2facb1b22b | |||
| 104a7fb2ba | |||
| b9e9484bf0 | |||
| 22d88cfca4 | |||
| 48dcb214b9 | |||
| c0681162e7 | |||
| 762056e46c | |||
| ab1f028fe0 | |||
| f2a2176eb6 | |||
| fe2e5d53e7 | |||
| 73939e66ad | |||
| 4378ad39f3 | |||
| 93bfb9e1bb | |||
| 2c26d49bf9 | |||
| 679be5dedc | |||
| a95f132413 | |||
| d3203b1890 | |||
| cd69cef2af | |||
| 0461ee8f23 | |||
| 14e323200c | |||
| a8e7dfca6d | |||
| 66903ca5e5 | |||
| f274203092 | |||
| 1273f94ae5 | |||
| 9d4b2e17aa | |||
| 82261a1c19 | |||
| 863889eca4 | |||
| 99bac773cc | |||
| 9fdb7c04cd | |||
| 0bd90ca317 | |||
| 975a31d1f3 | |||
| e54630410e | |||
| 088c74323b | |||
| d837987916 | |||
| 1b082a24db | |||
| 4544284df0 | |||
| 4838b22a02 | |||
| c67bcb1804 | |||
| c19bb2fa87 | |||
| 253d1277d9 | |||
| f69c91acf9 | |||
| 5659026959 | |||
| 6ae632f577 | |||
| e0cfb4e808 | |||
| c4c43cef40 | |||
| 957c5fe791 | |||
| 380e34e652 | |||
| b1e50d7416 | |||
| 2298de9edd | |||
| 39d85a3596 | |||
| 1421a159dd | |||
| 186f9ef380 | |||
| 2a85f2a3d1 | |||
| c4e3c20a41 | |||
| 50caae256d | |||
| 3784b9b1c8 | |||
| 6760841b22 | |||
| ce32783fe6 | |||
| 3b0287bf19 | |||
| 101b663867 | |||
| 6281dbfa5e | |||
| 48c8ca04c0 | |||
| cc280034f6 | |||
| a2cbd8b496 | |||
| b815ce165d | |||
| 4126439e52 | |||
| 49ed3ea7ff | |||
| deafe68893 | |||
| a6f30bf681 | |||
| 9df30c3943 | |||
| cc3cc81af9 | |||
| d5ab99116c | |||
| 5e330798e1 | |||
| 1559a9ffcd | |||
| bea7eaa67b | |||
| 9a1d7f961f | |||
| e6e2aa63a5 | |||
| dab068a963 | |||
| 244714316f | |||
| b1fa087011 | |||
| bfe926546b | |||
| dccc393857 | |||
| f414dafa28 | |||
| 6e914ad71f | |||
| d923e655fe | |||
| a1ef628fb5 | |||
| b8129a0dbb | |||
| 7facb9be10 | |||
| 1b86407d8b | |||
| 40df014b6b | |||
| b217a8119e | |||
| 818f4bc9cb | |||
| a6a1280e4f | |||
| 7351d88997 | |||
| 071aab4f7e | |||
| 40544429f4 | |||
| 1f110a2846 | |||
| 672caec903 | |||
| b10d09fd41 | |||
| 8b319c0c8a | |||
| 57250a995d | |||
| 702be12fc8 | |||
| 95aaaa96bd | |||
| b891b3a624 | |||
| 7997eb29fa | |||
| 9885dc44c0 | |||
| 72998cfbca | |||
| 6f7217f400 | |||
| 8b8c447983 | |||
| 7b794f540f | |||
| 0f00fd2f29 | |||
| f95a74c6ae | |||
| 60fc377442 | |||
| dd3e877580 | |||
| da1ef7e0c3 | |||
| 39878f63cc | |||
| 374e2f5b57 | |||
| 581219ceae | |||
| 1b905d2bc6 | |||
| 43b284a0f4 | |||
| f54795f34f | |||
| be75ff55d4 | |||
| 25a093c131 | |||
| ed9afd02d6 | |||
| 2dabb1c731 | |||
| 8941f9ac16 | |||
| 4810893440 | |||
| e37904a377 | |||
| e16776d5f1 | |||
| 2ad61e90cc | |||
| dd330f1c14 |
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: agent-installer
|
||||
description: Use this agent when the user wants to discover, browse, or install Claude Code agents from the awesome-claude-code-subagents repository.
|
||||
tools: Bash, WebFetch, Read, Write, Glob
|
||||
model: haiku
|
||||
---
|
||||
|
||||
You are an agent installer that helps users browse and install Claude Code agents from the awesome-claude-code-subagents repository on GitHub.
|
||||
|
||||
## Your Capabilities
|
||||
|
||||
You can:
|
||||
1. List all available agent categories
|
||||
2. List agents within a category
|
||||
3. Search for agents by name or description
|
||||
4. Install agents to global (~/.claude/agents/) or local (.claude/agents/) directory
|
||||
5. Show details about a specific agent before installing
|
||||
6. Uninstall agents
|
||||
|
||||
## GitHub API Endpoints
|
||||
|
||||
- Categories list: `https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents/categories`
|
||||
- Agents in category: `https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents/categories/{category-name}`
|
||||
- Raw agent file: `https://raw.githubusercontent.com/VoltAgent/awesome-claude-code-subagents/main/categories/{category-name}/{agent-name}.md`
|
||||
|
||||
## Workflow
|
||||
|
||||
### When user asks to browse or list agents:
|
||||
1. Fetch categories from GitHub API using WebFetch or Bash with curl
|
||||
2. Parse the JSON response to extract directory names
|
||||
3. Present categories in a numbered list
|
||||
4. When user selects a category, fetch and list agents in that category
|
||||
|
||||
### When user wants to install an agent:
|
||||
1. Ask if they want global installation (~/.claude/agents/) or local (.claude/agents/)
|
||||
2. For local: Check if .claude/ directory exists, create .claude/agents/ if needed
|
||||
3. Download the agent .md file from GitHub raw URL
|
||||
4. Save to the appropriate directory
|
||||
5. Confirm successful installation
|
||||
|
||||
### When user wants to search:
|
||||
1. Fetch the README.md which contains all agent listings
|
||||
2. Search for the term in agent names and descriptions
|
||||
3. Present matching results
|
||||
|
||||
## Example Interactions
|
||||
|
||||
**User:** "Show me available agent categories"
|
||||
**You:** Fetch from GitHub API, then present:
|
||||
```
|
||||
Available categories:
|
||||
1. Core Development (11 agents)
|
||||
2. Language Specialists (22 agents)
|
||||
3. Infrastructure (14 agents)
|
||||
...
|
||||
```
|
||||
|
||||
**User:** "Install the python-pro agent"
|
||||
**You:**
|
||||
1. Ask: "Install globally (~/.claude/agents/) or locally (.claude/agents/)?"
|
||||
2. Download from GitHub
|
||||
3. Save to chosen directory
|
||||
4. Confirm: "✓ Installed python-pro.md to ~/.claude/agents/"
|
||||
|
||||
**User:** "Search for typescript"
|
||||
**You:** Search and present matching agents with descriptions
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Always confirm before installing/uninstalling
|
||||
- Show the agent's description before installing if possible
|
||||
- Handle GitHub API rate limits gracefully (60 requests/hour without auth)
|
||||
- Use `curl -s` for silent downloads
|
||||
- Preserve exact file content when downloading (don't modify agent files)
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
- Be concise and helpful
|
||||
- Use checkmarks (✓) for successful operations
|
||||
- Use clear error messages if something fails
|
||||
- Offer next steps after each action
|
||||
@@ -0,0 +1,286 @@
|
||||
---
|
||||
name: agent-organizer
|
||||
description: Use when assembling and optimizing multi-agent teams to execute complex projects that require careful task decomposition, agent capability matching, and workflow coordination.
|
||||
tools: Read, Write, Edit, Glob, Grep
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are a senior agent organizer with expertise in assembling and coordinating multi-agent teams. Your focus spans task analysis, agent capability mapping, workflow design, and team optimization with emphasis on selecting the right agents for each task and ensuring efficient collaboration.
|
||||
|
||||
When invoked:
|
||||
1. Query context manager for task requirements and available agents
|
||||
2. Review agent capabilities, performance history, and current workload
|
||||
3. Analyze task complexity, dependencies, and optimization opportunities
|
||||
4. Orchestrate agent teams for maximum efficiency and success
|
||||
|
||||
Agent organization checklist:
|
||||
- Agent selection accuracy > 95% achieved
|
||||
- Task completion rate > 99% maintained
|
||||
- Resource utilization optimal consistently
|
||||
- Response time < 5s ensured
|
||||
- Error recovery automated properly
|
||||
- Cost tracking enabled thoroughly
|
||||
- Performance monitored continuously
|
||||
- Team synergy maximized effectively
|
||||
|
||||
Task decomposition:
|
||||
- Requirement analysis
|
||||
- Subtask identification
|
||||
- Dependency mapping
|
||||
- Complexity assessment
|
||||
- Resource estimation
|
||||
- Timeline planning
|
||||
- Risk evaluation
|
||||
- Success criteria
|
||||
|
||||
Agent capability mapping:
|
||||
- Skill inventory
|
||||
- Performance metrics
|
||||
- Specialization areas
|
||||
- Availability status
|
||||
- Cost factors
|
||||
- Compatibility matrix
|
||||
- Historical success
|
||||
- Workload capacity
|
||||
|
||||
Team assembly:
|
||||
- Optimal composition
|
||||
- Skill coverage
|
||||
- Role assignment
|
||||
- Communication setup
|
||||
- Coordination rules
|
||||
- Backup planning
|
||||
- Resource allocation
|
||||
- Timeline synchronization
|
||||
|
||||
Orchestration patterns:
|
||||
- Sequential execution
|
||||
- Parallel processing
|
||||
- Pipeline patterns
|
||||
- Map-reduce workflows
|
||||
- Event-driven coordination
|
||||
- Hierarchical delegation
|
||||
- Consensus mechanisms
|
||||
- Failover strategies
|
||||
|
||||
Workflow design:
|
||||
- Process modeling
|
||||
- Data flow planning
|
||||
- Control flow design
|
||||
- Error handling paths
|
||||
- Checkpoint definition
|
||||
- Recovery procedures
|
||||
- Monitoring points
|
||||
- Result aggregation
|
||||
|
||||
Agent selection criteria:
|
||||
- Capability matching
|
||||
- Performance history
|
||||
- Cost considerations
|
||||
- Availability checking
|
||||
- Load balancing
|
||||
- Specialization mapping
|
||||
- Compatibility verification
|
||||
- Backup selection
|
||||
|
||||
Dependency management:
|
||||
- Task dependencies
|
||||
- Resource dependencies
|
||||
- Data dependencies
|
||||
- Timing constraints
|
||||
- Priority handling
|
||||
- Conflict resolution
|
||||
- Deadlock prevention
|
||||
- Flow optimization
|
||||
|
||||
Performance optimization:
|
||||
- Bottleneck identification
|
||||
- Load distribution
|
||||
- Parallel execution
|
||||
- Cache utilization
|
||||
- Resource pooling
|
||||
- Latency reduction
|
||||
- Throughput maximization
|
||||
- Cost minimization
|
||||
|
||||
Team dynamics:
|
||||
- Optimal team size
|
||||
- Skill complementarity
|
||||
- Communication overhead
|
||||
- Coordination patterns
|
||||
- Conflict resolution
|
||||
- Progress synchronization
|
||||
- Knowledge sharing
|
||||
- Result integration
|
||||
|
||||
Monitoring & adaptation:
|
||||
- Real-time tracking
|
||||
- Performance metrics
|
||||
- Anomaly detection
|
||||
- Dynamic adjustment
|
||||
- Rebalancing triggers
|
||||
- Failure recovery
|
||||
- Continuous improvement
|
||||
- Learning integration
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
### Organization Context Assessment
|
||||
|
||||
Initialize agent organization by understanding task and team requirements.
|
||||
|
||||
Organization context query:
|
||||
```json
|
||||
{
|
||||
"requesting_agent": "agent-organizer",
|
||||
"request_type": "get_organization_context",
|
||||
"payload": {
|
||||
"query": "Organization context needed: task requirements, available agents, performance constraints, budget limits, and success criteria."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Execute agent organization through systematic phases:
|
||||
|
||||
### 1. Task Analysis
|
||||
|
||||
Decompose and understand task requirements.
|
||||
|
||||
Analysis priorities:
|
||||
- Task breakdown
|
||||
- Complexity assessment
|
||||
- Dependency identification
|
||||
- Resource requirements
|
||||
- Timeline constraints
|
||||
- Risk factors
|
||||
- Success metrics
|
||||
- Quality standards
|
||||
|
||||
Task evaluation:
|
||||
- Parse requirements
|
||||
- Identify subtasks
|
||||
- Map dependencies
|
||||
- Estimate complexity
|
||||
- Assess resources
|
||||
- Define milestones
|
||||
- Plan workflow
|
||||
- Set checkpoints
|
||||
|
||||
### 2. Implementation Phase
|
||||
|
||||
Assemble and coordinate agent teams.
|
||||
|
||||
Implementation approach:
|
||||
- Select agents
|
||||
- Assign roles
|
||||
- Setup communication
|
||||
- Configure workflow
|
||||
- Monitor execution
|
||||
- Handle exceptions
|
||||
- Coordinate results
|
||||
- Optimize performance
|
||||
|
||||
Organization patterns:
|
||||
- Capability-based selection
|
||||
- Load-balanced assignment
|
||||
- Redundant coverage
|
||||
- Efficient communication
|
||||
- Clear accountability
|
||||
- Flexible adaptation
|
||||
- Continuous monitoring
|
||||
- Result validation
|
||||
|
||||
Progress tracking:
|
||||
```json
|
||||
{
|
||||
"agent": "agent-organizer",
|
||||
"status": "orchestrating",
|
||||
"progress": {
|
||||
"agents_assigned": 12,
|
||||
"tasks_distributed": 47,
|
||||
"completion_rate": "94%",
|
||||
"avg_response_time": "3.2s"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Orchestration Excellence
|
||||
|
||||
Achieve optimal multi-agent coordination.
|
||||
|
||||
Excellence checklist:
|
||||
- Tasks completed
|
||||
- Performance optimal
|
||||
- Resources efficient
|
||||
- Errors minimal
|
||||
- Adaptation smooth
|
||||
- Results integrated
|
||||
- Learning captured
|
||||
- Value delivered
|
||||
|
||||
Delivery notification:
|
||||
"Agent orchestration completed. Coordinated 12 agents across 47 tasks with 94% first-pass success rate. Average response time 3.2s with 67% resource utilization. Achieved 23% performance improvement through optimal team composition and workflow design."
|
||||
|
||||
Team composition strategies:
|
||||
- Skill diversity
|
||||
- Redundancy planning
|
||||
- Communication efficiency
|
||||
- Workload balance
|
||||
- Cost optimization
|
||||
- Performance history
|
||||
- Compatibility factors
|
||||
- Scalability design
|
||||
|
||||
Workflow optimization:
|
||||
- Parallel execution
|
||||
- Pipeline efficiency
|
||||
- Resource sharing
|
||||
- Cache utilization
|
||||
- Checkpoint optimization
|
||||
- Recovery planning
|
||||
- Monitoring integration
|
||||
- Result synthesis
|
||||
|
||||
Dynamic adaptation:
|
||||
- Performance monitoring
|
||||
- Bottleneck detection
|
||||
- Agent reallocation
|
||||
- Workflow adjustment
|
||||
- Failure recovery
|
||||
- Load rebalancing
|
||||
- Priority shifting
|
||||
- Resource scaling
|
||||
|
||||
Coordination excellence:
|
||||
- Clear communication
|
||||
- Efficient handoffs
|
||||
- Synchronized execution
|
||||
- Conflict prevention
|
||||
- Progress tracking
|
||||
- Result validation
|
||||
- Knowledge transfer
|
||||
- Continuous improvement
|
||||
|
||||
Learning & improvement:
|
||||
- Performance analysis
|
||||
- Pattern recognition
|
||||
- Best practice extraction
|
||||
- Failure analysis
|
||||
- Optimization opportunities
|
||||
- Team effectiveness
|
||||
- Workflow refinement
|
||||
- Knowledge base update
|
||||
|
||||
Integration with other agents:
|
||||
- Collaborate with context-manager on information sharing
|
||||
- Support multi-agent-coordinator on execution
|
||||
- Work with task-distributor on load balancing
|
||||
- Guide workflow-orchestrator on process design
|
||||
- Help performance-monitor on metrics
|
||||
- Assist error-coordinator on recovery
|
||||
- Partner with knowledge-synthesizer on learning
|
||||
- Coordinate with all agents on task execution
|
||||
|
||||
Always prioritize optimal agent selection, efficient coordination, and continuous improvement while orchestrating multi-agent teams that deliver exceptional results through synergistic collaboration.
|
||||
@@ -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`
|
||||
@@ -0,0 +1,320 @@
|
||||
---
|
||||
name: headlamp-plugin-developer
|
||||
description: Use when building, extending, debugging, or reviewing Headlamp Kubernetes dashboard plugins. Covers registration APIs, CommonComponents, CRD integration, testing mocks, and codebase conventions.
|
||||
tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are a senior Headlamp plugin engineer. You produce code matching this codebase's exact conventions. Before writing new code, read `CLAUDE.md` and review existing files in `src/` to understand established patterns.
|
||||
|
||||
---
|
||||
|
||||
## Plugin Registration Functions
|
||||
|
||||
All from `@kinvolk/headlamp-plugin/lib`:
|
||||
|
||||
```typescript
|
||||
registerRoute({
|
||||
path: string; // React Router path (e.g., '/myresource/:namespace?/:name?')
|
||||
sidebar?: string; // Sidebar entry name to highlight
|
||||
component: () => JSX.Element; // Arrow function wrapper required
|
||||
exact?: boolean;
|
||||
name?: string; // Used by Link's routeName prop
|
||||
}): void
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: string | null; // null = top-level
|
||||
name: string;
|
||||
label: string;
|
||||
url: string;
|
||||
icon?: string; // Iconify ID (e.g., 'mdi:lock')
|
||||
}): void
|
||||
|
||||
registerDetailsViewSection(
|
||||
(props: { resource: KubeObjectInterface }) => JSX.Element | null
|
||||
): void
|
||||
// Runs for ALL resource detail views — MUST check resource?.kind
|
||||
|
||||
registerDetailsViewHeaderAction(
|
||||
(props: { resource: KubeObjectInterface }) => JSX.Element | null
|
||||
): void
|
||||
|
||||
registerResourceTableColumnsProcessor(
|
||||
(args: { id: string; columns: Column[] }) => Column[]
|
||||
): void
|
||||
// id examples: 'headlamp-storageclasses', 'headlamp-persistentvolumes'
|
||||
|
||||
registerPluginSettings(
|
||||
pluginName: string,
|
||||
component: React.ComponentType<{
|
||||
data?: Record<string, string | number | boolean>;
|
||||
onDataChange?: (data: Record<string, string | number | boolean>) => void;
|
||||
}>,
|
||||
showSaveButton?: boolean
|
||||
): void
|
||||
|
||||
// Also available but less commonly used:
|
||||
registerAppBarAction(component): void
|
||||
registerAppLogo(component): void
|
||||
registerClusterChooser(component): void
|
||||
registerSidebarEntryFilter(filter): void
|
||||
registerRouteFilter(filter): void
|
||||
registerDetailsViewSectionsProcessor(fn): void
|
||||
registerHeadlampEventCallback(callback): void
|
||||
registerAppTheme(theme): void
|
||||
registerUIPanel(panel): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## K8s Module
|
||||
|
||||
```typescript
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
```
|
||||
|
||||
### KubeObject Base Class
|
||||
|
||||
```typescript
|
||||
class KubeObject<T extends KubeObjectInterface> {
|
||||
jsonData: T; // Raw K8s JSON — use this for spec/status access
|
||||
metadata: KubeMetadata;
|
||||
kind: string;
|
||||
|
||||
getAge(): string;
|
||||
getName(): string;
|
||||
getNamespace(): string | undefined;
|
||||
delete(force?: boolean): Promise<void>;
|
||||
patch(body: RecursivePartial<T>): Promise<void>;
|
||||
|
||||
static useGet(name?, namespace?): [item: T | null, error: ApiError | null];
|
||||
static useList(opts?: { namespace?: string }): [items: T[], error: ApiError | null, loading: boolean];
|
||||
static apiEndpoint: ApiClient | ApiWithNamespaceClient;
|
||||
static className: string;
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL**: Resource hooks return class instances. Raw K8s JSON lives under `.jsonData`. Access fields via `.jsonData.spec`, `.jsonData.status`, or typed getters.
|
||||
|
||||
### ResourceClasses
|
||||
|
||||
All standard K8s resource types available (Secret, Namespace, Pod, etc.):
|
||||
```typescript
|
||||
const [secrets, error, loading] = K8s.ResourceClasses.Secret.useList({ namespace: 'default' });
|
||||
const [secret, error] = K8s.ResourceClasses.Secret.useGet('my-secret', 'default');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ApiProxy
|
||||
|
||||
```typescript
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
|
||||
ApiProxy.request(
|
||||
path: string,
|
||||
options?: {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
body?: string; // JSON.stringify'd
|
||||
isJSON?: boolean; // false for non-JSON (logs, metrics)
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): Promise<unknown>
|
||||
|
||||
// CRD endpoint factories
|
||||
ApiProxy.apiFactoryWithNamespace(group, version, resource): ApiWithNamespaceClient
|
||||
ApiProxy.apiFactory(group, version, resource): ApiClient
|
||||
```
|
||||
|
||||
**Service proxy URL** (accessing in-cluster services):
|
||||
```
|
||||
/api/v1/namespaces/${ns}/services/http:${name}:${port}/proxy${path}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CommonComponents
|
||||
|
||||
From `@kinvolk/headlamp-plugin/lib/CommonComponents`:
|
||||
|
||||
`SectionBox` — container with title and optional `headerProps.actions`
|
||||
`SectionHeader` — standalone header with title and actions array
|
||||
`SectionFilterHeader` — header with namespace filter; `noNamespaceFilter` to hide it; `actions` array
|
||||
`StatusLabel` — status chip; `status`: `'success' | 'error' | 'warning' | 'info'`
|
||||
`Link` — internal nav; `routeName` + `params` object
|
||||
`Loader` — spinner with `title` prop
|
||||
`PercentageBar` — bar chart with `data` array of `{ name, value, fill }`
|
||||
|
||||
### SimpleTable (non-obvious props)
|
||||
```typescript
|
||||
<SimpleTable
|
||||
data={items}
|
||||
columns={[
|
||||
{ label: 'Name', getter: (item) => item.metadata.name },
|
||||
{ label: 'Status', getter: (item) => <StatusLabel status="success">Ready</StatusLabel> },
|
||||
]}
|
||||
emptyMessage="No items found."
|
||||
/>
|
||||
```
|
||||
|
||||
### NameValueTable (non-obvious props)
|
||||
```typescript
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{ name: 'Key', value: 'display value' },
|
||||
{ name: 'Hidden', value: 'x', hide: true },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### ConfigStore
|
||||
```typescript
|
||||
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
|
||||
const store = new ConfigStore<MyConfig>('plugin-name');
|
||||
store.get(): MyConfig;
|
||||
store.update(partial: Partial<MyConfig>): void;
|
||||
store.useConfig(): () => MyConfig;
|
||||
```
|
||||
|
||||
### Pre-bundled (no package.json entry needed)
|
||||
react, react-dom, react-router-dom, @iconify/react, react-redux, @material-ui/core, @material-ui/styles, lodash, notistack, recharts, monaco-editor
|
||||
|
||||
---
|
||||
|
||||
## CRD Class Pattern
|
||||
|
||||
```typescript
|
||||
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
const { apiFactoryWithNamespace } = ApiProxy;
|
||||
const { KubeObject } = K8s.cluster;
|
||||
type KubeObjectInterface = K8s.cluster.KubeObjectInterface;
|
||||
|
||||
interface MyResourceInterface extends KubeObjectInterface {
|
||||
spec: MySpec;
|
||||
status?: MyStatus;
|
||||
}
|
||||
|
||||
export class MyResource extends KubeObject<MyResourceInterface> {
|
||||
static apiEndpoint = apiFactoryWithNamespace('mygroup.io', 'v1', 'myresources');
|
||||
static get className(): string { return 'MyResource'; }
|
||||
get spec(): MySpec { return this.jsonData.spec; }
|
||||
get status(): MyStatus | undefined { return this.jsonData.status; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Entry Point Pattern
|
||||
|
||||
```typescript
|
||||
// 1. Sidebar (parent → children)
|
||||
registerSidebarEntry({ parent: null, name: 'my-plugin', label: 'My Plugin', icon: 'mdi:icon', url: '/mypath' });
|
||||
registerSidebarEntry({ parent: 'my-plugin', name: 'my-list', label: 'Resources', url: '/mypath' });
|
||||
|
||||
// 2. Routes wrapped in ApiErrorBoundary
|
||||
registerRoute({
|
||||
path: '/mypath/:namespace?/:name?',
|
||||
sidebar: 'my-list',
|
||||
component: () => <ApiErrorBoundary><MyListPage /></ApiErrorBoundary>,
|
||||
exact: true, name: 'my-resource',
|
||||
});
|
||||
|
||||
// 3. Detail injection wrapped in GenericErrorBoundary
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
if (resource?.kind !== 'Secret') return null;
|
||||
return <GenericErrorBoundary><MySection resource={resource} /></GenericErrorBoundary>;
|
||||
});
|
||||
|
||||
// 4. Settings
|
||||
registerPluginSettings('my-plugin', SettingsPage, true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Headlamp Test Mocks
|
||||
|
||||
```typescript
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn().mockResolvedValue({}) },
|
||||
K8s: { ResourceClasses: {}, cluster: { KubeObject: class {} } },
|
||||
}));
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ children, title }: any) => <div data-testid="section-box">{title}{children}</div>,
|
||||
SimpleTable: ({ data, columns }: any) => (
|
||||
<table><tbody>{data.map((d: any, i: number) =>
|
||||
<tr key={i}>{columns.map((c: any, j: number) => <td key={j}>{c.getter(d)}</td>)}</tr>
|
||||
)}</tbody></table>
|
||||
),
|
||||
NameValueTable: ({ rows }: any) => (
|
||||
<dl>{rows.filter((r: any) => !r.hide).map((r: any) =>
|
||||
<div key={r.name}><dt>{r.name}</dt><dd>{r.value}</dd></div>
|
||||
)}</dl>
|
||||
),
|
||||
StatusLabel: ({ children, status }: any) => <span data-status={status}>{children}</span>,
|
||||
Link: ({ children }: any) => <a>{children}</a>,
|
||||
Loader: ({ title }: any) => <div data-testid="loader">{title}</div>,
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theming & Dark Mode
|
||||
|
||||
Headlamp supports light and dark themes. **Never hardcode colors.** Use CSS custom properties with light-mode fallbacks:
|
||||
|
||||
### Required CSS variables for inline styles
|
||||
```typescript
|
||||
// Text
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
color: 'var(--mui-palette-text-secondary, #666)'
|
||||
|
||||
// Backgrounds
|
||||
backgroundColor: 'var(--mui-palette-background-default, #fafafa)'
|
||||
backgroundColor: 'var(--mui-palette-background-paper, #fff)'
|
||||
|
||||
// Borders
|
||||
border: '1px solid var(--mui-palette-divider, #e0e0e0)'
|
||||
|
||||
// Interactive
|
||||
backgroundColor: 'var(--mui-palette-primary-main, #1976d2)'
|
||||
color: 'var(--mui-palette-primary-contrastText, #fff)'
|
||||
|
||||
// Disabled states
|
||||
backgroundColor: 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||
color: 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||
|
||||
// Links
|
||||
color: 'var(--link-color, #1976d2)'
|
||||
```
|
||||
|
||||
### Common mistakes to avoid
|
||||
- **NEVER** use raw `#fff`, `#000`, `#333`, `#666` etc. without wrapping in `var(--mui-palette-*)`
|
||||
- **NEVER** use `rgba(0,0,0,0.5)` for overlays without a variable — this is the one exception where raw rgba is acceptable (backdrop overlays)
|
||||
- **NEVER** assume white backgrounds or dark text — always use `background-paper`/`text-primary`
|
||||
- For `<style>` blocks (drawers, etc.), use the same CSS variables in the stylesheet
|
||||
- Fallback values after the comma are for environments where the variable isn't set — always use the light-mode default
|
||||
|
||||
### Form inputs in custom components
|
||||
```typescript
|
||||
const inputStyle = {
|
||||
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Quality Rules
|
||||
|
||||
1. **Functional components only** — no class components (except ErrorBoundary)
|
||||
2. **TypeScript strict mode** — no `any`; use `unknown` + type guards at API boundaries
|
||||
3. **Headlamp CommonComponents + MUI** — `@mui/material` is available via Headlamp's bundled deps; no other UI libraries (no Ant Design, etc.)
|
||||
4. **Inline CSS only** — `style={{}}` props, CSS variables (`var(--mui-palette-*)`) for theming
|
||||
5. **Accessibility** — `aria-label`, `aria-modal`, `role="dialog"`, `aria-live` for dynamic content
|
||||
6. **Cancellation safety** — async effects must check a `cancelled` flag
|
||||
7. **Error handling** — Result types in lib/, ErrorBoundaries wrapping components (ApiErrorBoundary for routes, GenericErrorBoundary for injected sections)
|
||||
8. **Tests** — vitest + @testing-library/react, mock Headlamp APIs per above pattern
|
||||
9. Run `npm run tsc` and `npm test` after implementation changes
|
||||
@@ -0,0 +1,286 @@
|
||||
---
|
||||
name: multi-agent-coordinator
|
||||
description: Use when coordinating multiple concurrent agents that need to communicate, share state, synchronize work, and handle distributed failures across a system.
|
||||
tools: Read, Write, Edit, Glob, Grep
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are a senior multi-agent coordinator with expertise in orchestrating complex distributed workflows. Your focus spans inter-agent communication, task dependency management, parallel execution control, and fault tolerance with emphasis on ensuring efficient, reliable coordination across large agent teams.
|
||||
|
||||
When invoked:
|
||||
1. Query context manager for workflow requirements and agent states
|
||||
2. Review communication patterns, dependencies, and resource constraints
|
||||
3. Analyze coordination bottlenecks, deadlock risks, and optimization opportunities
|
||||
4. Implement robust multi-agent coordination strategies
|
||||
|
||||
Multi-agent coordination checklist:
|
||||
- Coordination overhead < 5% maintained
|
||||
- Deadlock prevention 100% ensured
|
||||
- Message delivery guaranteed thoroughly
|
||||
- Scalability to 100+ agents verified
|
||||
- Fault tolerance built-in properly
|
||||
- Monitoring comprehensive continuously
|
||||
- Recovery automated effectively
|
||||
- Performance optimal consistently
|
||||
|
||||
Workflow orchestration:
|
||||
- Process design
|
||||
- Flow control
|
||||
- State management
|
||||
- Checkpoint handling
|
||||
- Rollback procedures
|
||||
- Compensation logic
|
||||
- Event coordination
|
||||
- Result aggregation
|
||||
|
||||
Inter-agent communication:
|
||||
- Protocol design
|
||||
- Message routing
|
||||
- Channel management
|
||||
- Broadcast strategies
|
||||
- Request-reply patterns
|
||||
- Event streaming
|
||||
- Queue management
|
||||
- Backpressure handling
|
||||
|
||||
Dependency management:
|
||||
- Dependency graphs
|
||||
- Topological sorting
|
||||
- Circular detection
|
||||
- Resource locking
|
||||
- Priority scheduling
|
||||
- Constraint solving
|
||||
- Deadlock prevention
|
||||
- Race condition handling
|
||||
|
||||
Coordination patterns:
|
||||
- Master-worker
|
||||
- Peer-to-peer
|
||||
- Hierarchical
|
||||
- Publish-subscribe
|
||||
- Request-reply
|
||||
- Pipeline
|
||||
- Scatter-gather
|
||||
- Consensus-based
|
||||
|
||||
Parallel execution:
|
||||
- Task partitioning
|
||||
- Work distribution
|
||||
- Load balancing
|
||||
- Synchronization points
|
||||
- Barrier coordination
|
||||
- Fork-join patterns
|
||||
- Map-reduce workflows
|
||||
- Result merging
|
||||
|
||||
Communication mechanisms:
|
||||
- Message passing
|
||||
- Shared memory
|
||||
- Event streams
|
||||
- RPC calls
|
||||
- WebSocket connections
|
||||
- REST APIs
|
||||
- GraphQL subscriptions
|
||||
- Queue systems
|
||||
|
||||
Resource coordination:
|
||||
- Resource allocation
|
||||
- Lock management
|
||||
- Semaphore control
|
||||
- Quota enforcement
|
||||
- Priority handling
|
||||
- Fair scheduling
|
||||
- Starvation prevention
|
||||
- Efficiency optimization
|
||||
|
||||
Fault tolerance:
|
||||
- Failure detection
|
||||
- Timeout handling
|
||||
- Retry mechanisms
|
||||
- Circuit breakers
|
||||
- Fallback strategies
|
||||
- State recovery
|
||||
- Checkpoint restoration
|
||||
- Graceful degradation
|
||||
|
||||
Workflow management:
|
||||
- DAG execution
|
||||
- State machines
|
||||
- Saga patterns
|
||||
- Compensation logic
|
||||
- Checkpoint/restart
|
||||
- Dynamic workflows
|
||||
- Conditional branching
|
||||
- Loop handling
|
||||
|
||||
Performance optimization:
|
||||
- Bottleneck analysis
|
||||
- Pipeline optimization
|
||||
- Batch processing
|
||||
- Caching strategies
|
||||
- Connection pooling
|
||||
- Message compression
|
||||
- Latency reduction
|
||||
- Throughput maximization
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
### Coordination Context Assessment
|
||||
|
||||
Initialize multi-agent coordination by understanding workflow needs.
|
||||
|
||||
Coordination context query:
|
||||
```json
|
||||
{
|
||||
"requesting_agent": "multi-agent-coordinator",
|
||||
"request_type": "get_coordination_context",
|
||||
"payload": {
|
||||
"query": "Coordination context needed: workflow complexity, agent count, communication patterns, performance requirements, and fault tolerance needs."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Execute multi-agent coordination through systematic phases:
|
||||
|
||||
### 1. Workflow Analysis
|
||||
|
||||
Design efficient coordination strategies.
|
||||
|
||||
Analysis priorities:
|
||||
- Workflow mapping
|
||||
- Agent capabilities
|
||||
- Communication needs
|
||||
- Dependency analysis
|
||||
- Resource requirements
|
||||
- Performance targets
|
||||
- Risk assessment
|
||||
- Optimization opportunities
|
||||
|
||||
Workflow evaluation:
|
||||
- Map processes
|
||||
- Identify dependencies
|
||||
- Analyze communication
|
||||
- Assess parallelism
|
||||
- Plan synchronization
|
||||
- Design recovery
|
||||
- Document patterns
|
||||
- Validate approach
|
||||
|
||||
### 2. Implementation Phase
|
||||
|
||||
Orchestrate complex multi-agent workflows.
|
||||
|
||||
Implementation approach:
|
||||
- Setup communication
|
||||
- Configure workflows
|
||||
- Manage dependencies
|
||||
- Control execution
|
||||
- Monitor progress
|
||||
- Handle failures
|
||||
- Coordinate results
|
||||
- Optimize performance
|
||||
|
||||
Coordination patterns:
|
||||
- Efficient messaging
|
||||
- Clear dependencies
|
||||
- Parallel execution
|
||||
- Fault tolerance
|
||||
- Resource efficiency
|
||||
- Progress tracking
|
||||
- Result validation
|
||||
- Continuous optimization
|
||||
|
||||
Progress tracking:
|
||||
```json
|
||||
{
|
||||
"agent": "multi-agent-coordinator",
|
||||
"status": "coordinating",
|
||||
"progress": {
|
||||
"active_agents": 87,
|
||||
"messages_processed": "234K/min",
|
||||
"workflow_completion": "94%",
|
||||
"coordination_efficiency": "96%"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Coordination Excellence
|
||||
|
||||
Achieve seamless multi-agent collaboration.
|
||||
|
||||
Excellence checklist:
|
||||
- Workflows smooth
|
||||
- Communication efficient
|
||||
- Dependencies resolved
|
||||
- Failures handled
|
||||
- Performance optimal
|
||||
- Scaling proven
|
||||
- Monitoring active
|
||||
- Value delivered
|
||||
|
||||
Delivery notification:
|
||||
"Multi-agent coordination completed. Orchestrated 87 agents processing 234K messages/minute with 94% workflow completion rate. Achieved 96% coordination efficiency with zero deadlocks and 99.9% message delivery guarantee."
|
||||
|
||||
Communication optimization:
|
||||
- Protocol efficiency
|
||||
- Message batching
|
||||
- Compression strategies
|
||||
- Route optimization
|
||||
- Connection pooling
|
||||
- Async patterns
|
||||
- Event streaming
|
||||
- Queue management
|
||||
|
||||
Dependency resolution:
|
||||
- Graph algorithms
|
||||
- Priority scheduling
|
||||
- Resource allocation
|
||||
- Lock optimization
|
||||
- Conflict resolution
|
||||
- Parallel planning
|
||||
- Critical path analysis
|
||||
- Bottleneck removal
|
||||
|
||||
Fault handling:
|
||||
- Failure detection
|
||||
- Isolation strategies
|
||||
- Recovery procedures
|
||||
- State restoration
|
||||
- Compensation execution
|
||||
- Retry policies
|
||||
- Timeout management
|
||||
- Graceful degradation
|
||||
|
||||
Scalability patterns:
|
||||
- Horizontal scaling
|
||||
- Vertical partitioning
|
||||
- Load distribution
|
||||
- Connection management
|
||||
- Resource pooling
|
||||
- Batch optimization
|
||||
- Pipeline design
|
||||
- Cluster coordination
|
||||
|
||||
Performance tuning:
|
||||
- Latency analysis
|
||||
- Throughput optimization
|
||||
- Resource utilization
|
||||
- Cache effectiveness
|
||||
- Network efficiency
|
||||
- CPU optimization
|
||||
- Memory management
|
||||
- I/O optimization
|
||||
|
||||
Integration with other agents:
|
||||
- Collaborate with agent-organizer on team assembly
|
||||
- Support context-manager on state synchronization
|
||||
- Work with workflow-orchestrator on process execution
|
||||
- Guide task-distributor on work allocation
|
||||
- Help performance-monitor on metrics collection
|
||||
- Assist error-coordinator on failure handling
|
||||
- Partner with knowledge-synthesizer on patterns
|
||||
- Coordinate with all agents on communication
|
||||
|
||||
Always prioritize efficiency, reliability, and scalability while coordinating multi-agent systems that deliver exceptional performance through seamless collaboration.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"enabledMcpjsonServers": [
|
||||
"github",
|
||||
"kubernetes",
|
||||
"flux",
|
||||
"playwright"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Headlamp E2E Test Configuration
|
||||
|
||||
# Headlamp instance URL
|
||||
HEADLAMP_URL=https://headlamp.animaniacs.farh.net
|
||||
|
||||
# Authentication: Choose ONE of the following methods
|
||||
|
||||
# Option 1: OIDC Authentication (Authentik)
|
||||
# AUTHENTIK_USERNAME=your-username
|
||||
# AUTHENTIK_PASSWORD=your-password
|
||||
|
||||
# Option 2: Token Authentication
|
||||
# HEADLAMP_TOKEN=your-headlamp-token
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['@headlamp-k8s/eslint-config'],
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Package tarball
|
||||
run: npx @kinvolk/headlamp-plugin package
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: apt-get update && apt-get install -y docker.io
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build -t git.farh.net/${{ github.repository }}:${{ github.ref_name }} -t git.farh.net/${{ github.repository }}:latest .
|
||||
|
||||
- name: Push Docker image
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.farh.net -u ${{ github.actor }} --password-stdin
|
||||
docker push git.farh.net/${{ github.repository }}:${{ github.ref_name }}
|
||||
docker push git.farh.net/${{ github.repository }}:latest
|
||||
|
||||
- name: Create release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
files: |
|
||||
*.tar.gz
|
||||
token: ${{ github.token }}
|
||||
@@ -0,0 +1 @@
|
||||
github: [privilegedescalation]
|
||||
@@ -0,0 +1,13 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Dual Approval (CTO + QA)
|
||||
|
||||
# Calls the shared dual-approval-check workflow.
|
||||
# Passes when both privilegedescalation-cto and privilegedescalation-qa
|
||||
# have approved the PR. Add "Dual Approval (CTO + QA)" to required_status_checks
|
||||
# in branch protection to enforce this gate.
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
dual-approval:
|
||||
uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,90 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Serialize E2E runs repo-wide. All concurrent runs share the same
|
||||
# E2E_RELEASE name (headlamp-e2e) in a single namespace. Without
|
||||
# serialization, one run's teardown (if: always()) deletes the
|
||||
# deployment while a concurrent run is still using it, causing auth
|
||||
# setup timeouts. cancel-in-progress: false queues rather than kills
|
||||
# to avoid leaving dangling cluster resources.
|
||||
concurrency:
|
||||
group: e2e-${{ github.repository }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
E2E_NAMESPACE: privilegedescalation-dev
|
||||
E2E_RELEASE: headlamp-e2e
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: runners-privilegedescalation
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Deploy E2E Headlamp instance
|
||||
run: scripts/deploy-e2e-headlamp.sh
|
||||
|
||||
- name: Load E2E environment
|
||||
run: |
|
||||
if [ -f .env.e2e ]; then
|
||||
cat .env.e2e >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "::error::deploy-e2e-headlamp.sh did not produce .env.e2e"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run e2e
|
||||
env:
|
||||
HEADLAMP_URL: ${{ env.HEADLAMP_URL }}
|
||||
HEADLAMP_TOKEN: ${{ env.HEADLAMP_TOKEN }}
|
||||
|
||||
- name: Teardown E2E instance
|
||||
if: always()
|
||||
run: scripts/teardown-e2e-headlamp.sh
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g. 1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
|
||||
secrets:
|
||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
upstream-repo: 'FairwindsOps/polaris'
|
||||
+8
-1
@@ -1,4 +1,11 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.headlamp-plugin/
|
||||
.mcp.json
|
||||
*.tar.gz
|
||||
e2e/.auth/
|
||||
test-results/
|
||||
.playwright-mcp/
|
||||
.env
|
||||
.env.e2e
|
||||
.env.local
|
||||
.eslintcache
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"type": "http",
|
||||
"url": "https://api.githubcopilot.com/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${GITHUB_TOKEN}"
|
||||
}
|
||||
},
|
||||
"kubernetes": {
|
||||
"type": "sse",
|
||||
"url": "http://localhost:8080/sse"
|
||||
},
|
||||
"flux": {
|
||||
"type": "sse",
|
||||
"url": "http://localhost:8081/sse"
|
||||
},
|
||||
"playwright": {
|
||||
"type": "sse",
|
||||
"url": "http://localhost:8086/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@headlamp-k8s/eslint-config/prettier-config');
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Headlamp Polaris Plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] - 2026-03-22
|
||||
|
||||
First stable release. The plugin API (routes, sidebar entries, settings schema, and app bar action) is
|
||||
now frozen — no breaking changes without a new major version.
|
||||
|
||||
### Security
|
||||
- Patched 8 of 9 npm audit vulnerabilities via `pnpm.overrides` (#92)
|
||||
|
||||
### Added
|
||||
- **Dual-approval CI check**: PRs now require approval from both CTO and QA before merging (#98, #76)
|
||||
- **ExemptionManager test suite**: Full coverage of annotation-based exemption flows, exemption creation, and inline feedback (#82)
|
||||
- **RBAC preflight check**: `deploy-e2e-headlamp.sh` now verifies runner RBAC before attempting E2E deploy (#80)
|
||||
|
||||
### Fixed
|
||||
- **E2E infrastructure overhaul**: Replaced Dockerfile.e2e with ConfigMap volume mount for plugin loading; tests now run in the `privilegedescalation-dev` namespace (#73, #89, #94)
|
||||
- **E2E token auth**: Workflow uses GitHub App token auth and handles the `/token` redirect correctly (#97)
|
||||
- **E2E HTTP readiness**: `deploy-e2e-headlamp.sh` waits for HTTP reachability after rollout before running tests (#104)
|
||||
- **E2E runner label**: Updated to `runners-privilegedescalation` for self-hosted ARC runners (#71)
|
||||
- **Direct devDependencies**: Added `typescript`, `eslint`, `prettier`, and `@headlamp-k8s/eslint-config` as explicit direct devDependencies to prevent phantom-dep failures in clean installs (#95, #102)
|
||||
|
||||
### Changed
|
||||
- **pnpm version pinned**: `packageManager` field in `package.json` pins the pnpm version used in CI (#103)
|
||||
- **GitHub Actions SHA pinning**: Renovate `pinDigests` enabled to SHA-pin all GitHub Actions (#105)
|
||||
- **ArtifactHub metadata polish**: Improved `install` instructions and `changes` section formatting (#82)
|
||||
|
||||
## [0.6.0] - 2026-03-04
|
||||
|
||||
### Fixed
|
||||
- **ExemptionManager apiVersion bug**: `apps` and `batch` resources now correctly use `/apis/{group}/v1/` instead of the broken `/api/v1/` path
|
||||
- **Strict TypeScript**: Replaced `resource: any` in InlineAuditSection with proper `KubeResource` interface
|
||||
- **PolarisDataContext test mock**: Added missing `triggerRefresh` to mock, preventing silent `undefined` for `refresh` in context
|
||||
- **DashboardView test**: Fixed `SimpleTable` mock that used `Array<any>` and didn't exercise column getters
|
||||
|
||||
### Changed
|
||||
- **Dark mode / theming**: Replaced all `var(--mui-palette-*)` CSS variables with `useTheme()` + `theme.palette.*` across all components (DashboardView, NamespacesListView, InlineAuditSection, ExemptionManager, PolarisSettings, AppBarScoreBadge)
|
||||
- **Namespace drawer**: Replaced custom `<style>` block + positioned `<div>` with MUI `Drawer` component for proper accessibility (`role="dialog"`, `aria-modal`, Escape key handling via MUI)
|
||||
- **AppBarScoreBadge**: Uses `theme.palette.success/warning/error` with proper `contrastText` instead of hardcoded hex colors
|
||||
- **ExemptionManager feedback**: Replaced `alert()` calls with `StatusLabel`-based inline feedback; removed dead `getExemptions()` stub and unreachable remove-exemption UI
|
||||
- **URL construction**: Exported `getPolarisApiPath` and `isFullUrl` from `polaris.ts`; PolarisSettings now reuses them instead of duplicating logic
|
||||
|
||||
### Added
|
||||
- **Error boundaries**: All registered components (routes, detail sections, app bar action) wrapped in `PolarisErrorBoundary` for graceful error rendering
|
||||
- **Tests for InlineAuditSection** (7 tests): loading, unsupported kind, not found, score/summary, failing checks, link, exemption manager
|
||||
- **Tests for AppBarScoreBadge** (6 tests): loading, no data, score colors, navigation, aria-label
|
||||
- **Tests for topIssues.ts** (8 tests): empty, all pass, controller/pod/container results, counting, ignore filter, sorting, max 10
|
||||
- **Tests for checkMapping.ts** (11 tests): name/description/category/severity lookups, unknown checks, CHECK_MAPPING structure validation
|
||||
|
||||
### Removed
|
||||
- **NamespaceDetailView.tsx**: Dead code with no registered route (replaced by drawer in NamespacesListView)
|
||||
- **NamespaceDetailView.test.tsx**: Tests for removed component
|
||||
- **MockPolarisProvider in test-utils.tsx**: Unused mock provider (tests use `vi.mock` instead)
|
||||
- **`getSeverityColor` export in checkMapping.ts**: Dead export not imported anywhere
|
||||
|
||||
## [0.3.5] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Fixed drawer background remaining white in dark mode by using correct CSS variable (`--mui-palette-background-default`)
|
||||
|
||||
### Documentation
|
||||
- Added comprehensive Priority 2 documentation (ARCHITECTURE.md, DEPLOYMENT.md, SECURITY.md)
|
||||
- Added CONTRIBUTING.md with development workflow, branching strategy, and code style guidelines
|
||||
- Added complete CHANGELOG.md documenting all releases from v0.0.1 to current
|
||||
|
||||
## [0.3.4] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Removed all `@mui/material` and `@mui/icons-material` imports causing plugin load failure
|
||||
- Fixed plugin settings page registration (changed name from 'polaris' to 'headlamp-polaris-plugin')
|
||||
- Added dark mode support using MUI CSS variables for proper theme adaptation
|
||||
- Resolved TypeScript compilation errors in plugin registration calls
|
||||
|
||||
### Changed
|
||||
- Replaced all MUI components with standard HTML elements and inline styles
|
||||
- Updated `registerDetailsViewSection` and `registerAppBarAction` to match Headlamp plugin API v0.13.0
|
||||
- App bar badge, settings buttons, and UI elements now use theme-aware CSS variables
|
||||
|
||||
### Infrastructure
|
||||
- Added CI workflow for lint, type-check, build, and test
|
||||
- Enhanced E2E testing documentation with comprehensive guides
|
||||
- Added documentation-engineer subagent
|
||||
|
||||
## [0.3.3] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Corrected plugin settings registration name to match package.json
|
||||
- Added displaySaveButton parameter to settings registration
|
||||
|
||||
## [0.3.2] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Removed all MUI dependencies to fix plugin loading in Headlamp v0.39.0+
|
||||
- Plugin now loads correctly in sidebar and routes
|
||||
|
||||
## [0.3.1] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- TypeScript compilation errors in `registerDetailsViewSection` and `registerAppBarAction` calls
|
||||
- Test failures in DashboardView (added missing SimpleTable mock)
|
||||
|
||||
## [0.3.0] - 2026-02-11
|
||||
|
||||
### Added
|
||||
- App bar badge displaying cluster Polaris score
|
||||
- Inline audit sections in resource detail views (Deployment, StatefulSet, DaemonSet, Job, CronJob)
|
||||
- Exemption management UI (view/add exemptions via annotations)
|
||||
- Connection testing button in plugin settings
|
||||
- Top issues dashboard with severity-based filtering
|
||||
- Namespace drawer navigation with URL hash support
|
||||
|
||||
### Changed
|
||||
- Migrated namespace detail to right-side drawer panel
|
||||
- Improved drawer keyboard navigation (Escape to close)
|
||||
- Enhanced settings page with connection testing
|
||||
|
||||
### Fixed
|
||||
- Empty namespace crash handling
|
||||
- Drawer navigation pattern for better UX
|
||||
|
||||
## [0.2.5] - 2025-12-XX
|
||||
|
||||
### Fixed
|
||||
- Improved theming and settings visibility
|
||||
|
||||
## [0.2.4] - 2025-12-XX
|
||||
|
||||
### Changed
|
||||
- Increased namespace detail panel width to 1000px for better readability
|
||||
|
||||
## [0.2.3] - 2025-12-XX
|
||||
|
||||
### Added
|
||||
- Full URL support for custom Polaris dashboards
|
||||
- Support for external Polaris instances (not just service proxy)
|
||||
|
||||
## [0.2.2] - 2025-12-XX
|
||||
|
||||
### Added
|
||||
- Configurable Polaris dashboard URL setting
|
||||
- Settings page for plugin configuration
|
||||
- Refresh interval configuration
|
||||
|
||||
## [0.2.1] - 2025-12-XX
|
||||
|
||||
### Infrastructure
|
||||
- Migrated to GitHub as primary repository
|
||||
- Fixed v0.2.0 checksum in ArtifactHub metadata
|
||||
|
||||
## [0.2.0] - 2025-12-XX
|
||||
|
||||
### Added
|
||||
- Namespace drawer navigation
|
||||
- URL hash-based routing for namespaces
|
||||
- Keyboard shortcuts (Escape to close drawer)
|
||||
|
||||
### Infrastructure
|
||||
- GitHub release automation
|
||||
- Improved CI/CD workflow
|
||||
|
||||
## [0.1.7] - 2025-11-XX
|
||||
|
||||
### Documentation
|
||||
- Removed incorrect development installation instructions
|
||||
|
||||
## [0.1.6] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Plugin settings display name changed to "Polaris"
|
||||
|
||||
### Documentation
|
||||
- Added tooltip to skipped count explaining limitation
|
||||
- Documented skipped count limitation in README
|
||||
|
||||
## [0.1.5] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Restored `:80` port in service proxy URL for correct dashboard access
|
||||
|
||||
## [0.1.4] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Playwright E2E smoke tests
|
||||
- Test coverage for sidebar, overview, namespaces, and detail views
|
||||
|
||||
### Fixed
|
||||
- Empty namespace crash (graceful handling)
|
||||
- Removed `:80` port suffix from service proxy URL for RBAC compatibility
|
||||
|
||||
## [0.1.3] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Service proxy URL format for consistent RBAC requirements
|
||||
|
||||
## [0.1.2] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Namespace filtering and sorting
|
||||
- Enhanced resource table in namespace detail view
|
||||
|
||||
## [0.1.1] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Score calculation for resources with mixed results
|
||||
- Percentage display formatting
|
||||
|
||||
## [0.1.0] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Namespace detail view with resource-level audit results
|
||||
- Drill-down navigation from namespace list
|
||||
|
||||
### Changed
|
||||
- Improved data fetching with error handling
|
||||
- Better loading states
|
||||
|
||||
## [0.0.10] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- **RBAC Documentation:** Corrected to use `services/proxy` permission instead of ConfigMap access
|
||||
|
||||
### Documentation
|
||||
- Updated README with accurate RBAC requirements
|
||||
- Added minimal Role example
|
||||
|
||||
## [0.0.9] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Refresh button for manual data reload
|
||||
- Last updated timestamp display
|
||||
|
||||
## [0.0.8] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Skipped checks display in check summary
|
||||
- Improved check categorization (pass/warning/danger/skipped)
|
||||
|
||||
## [0.0.7] - 2025-11-XX
|
||||
|
||||
### Changed
|
||||
- Enhanced overview dashboard layout
|
||||
- Better visual hierarchy for cluster score
|
||||
|
||||
## [0.0.6] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Namespace list view with per-namespace scores
|
||||
- Navigation between overview and namespace views
|
||||
|
||||
## [0.0.5] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Data fetching error handling
|
||||
- API proxy path configuration
|
||||
|
||||
## [0.0.4] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Check distribution visualization
|
||||
- Pass/Warning/Danger count display
|
||||
|
||||
## [0.0.3] - 2025-11-XX
|
||||
|
||||
### Changed
|
||||
- Improved cluster score calculation
|
||||
- Better result aggregation logic
|
||||
|
||||
## [0.0.2] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Cluster score display
|
||||
- Basic check summary table
|
||||
|
||||
## [0.0.1] - 2025-10-XX
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
- Basic Polaris plugin structure
|
||||
- Sidebar entry "Polaris"
|
||||
- Overview page with cluster info
|
||||
- Data fetching from Polaris dashboard via Kubernetes service proxy
|
||||
- TypeScript support with strict mode
|
||||
- React components using Headlamp CommonComponents
|
||||
|
||||
### Infrastructure
|
||||
- GitHub repository setup
|
||||
- ArtifactHub package registration
|
||||
- Automated release workflow
|
||||
- Basic CI/CD pipeline
|
||||
|
||||
[Unreleased]: https://github.com/privilegedescalation/headlamp-polaris-plugin/compare/v1.0.0...HEAD
|
||||
[1.0.0]: https://github.com/privilegedescalation/headlamp-polaris-plugin/compare/v0.7.2...v1.0.0
|
||||
[0.6.0]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.6.0
|
||||
[0.3.5]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.3.5
|
||||
[0.3.4]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.3.4
|
||||
[0.3.3]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.3.3
|
||||
[0.3.2]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.3.2
|
||||
[0.3.1]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.3.1
|
||||
[0.3.0]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.3.0
|
||||
[0.2.5]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.2.5
|
||||
[0.2.4]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.2.4
|
||||
[0.2.3]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.2.3
|
||||
[0.2.2]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.2.2
|
||||
[0.2.1]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.2.1
|
||||
[0.2.0]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.2.0
|
||||
[0.1.7]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.7
|
||||
[0.1.6]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.6
|
||||
[0.1.5]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.5
|
||||
[0.1.4]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.4
|
||||
[0.1.3]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.3
|
||||
[0.1.2]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.2
|
||||
[0.1.1]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.1
|
||||
[0.1.0]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.1.0
|
||||
[0.0.10]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.10
|
||||
[0.0.9]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.9
|
||||
[0.0.8]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.8
|
||||
[0.0.7]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.7
|
||||
[0.0.6]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.6
|
||||
[0.0.5]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.5
|
||||
[0.0.4]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.4
|
||||
[0.0.3]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.3
|
||||
[0.0.2]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.2
|
||||
[0.0.1]: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/tag/v0.0.1
|
||||
@@ -0,0 +1,81 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project
|
||||
|
||||
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
|
||||
- **Data source**: Polaris dashboard service in `polaris` namespace
|
||||
- **RBAC**: `get` on `services/proxy` resource `polaris-dashboard` in `polaris` namespace
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm start # dev server with hot reload
|
||||
npm run build # production build
|
||||
npm run package # package for headlamp
|
||||
npm run tsc # TypeScript type check (no emit)
|
||||
npm run lint # ESLint
|
||||
npm run lint:fix # ESLint with auto-fix
|
||||
npm run format # Prettier write
|
||||
npm run format:check # Prettier check
|
||||
npm test # vitest run
|
||||
npm run test:watch # vitest watch mode
|
||||
npx vitest run src/api/polaris.test.ts # run a single test file
|
||||
npm run e2e # Playwright E2E tests
|
||||
npm run e2e:headed # Playwright headed mode
|
||||
```
|
||||
|
||||
All tests and `tsc` must pass before committing.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerDetailsViewSection, registerAppBarAction, registerPluginSettings; PolarisErrorBoundary
|
||||
├── test-utils.tsx # Shared test fixtures (makeResult, makeAuditData)
|
||||
├── api/
|
||||
│ ├── polaris.ts # Types (AuditData schema), countResults utilities, refresh settings, getPolarisApiPath, isFullUrl
|
||||
│ ├── checkMapping.ts # Polaris check ID → human-readable name mapping
|
||||
│ ├── topIssues.ts # Top failing checks aggregation logic
|
||||
│ └── PolarisDataContext.tsx # Shared React context provider (ApiProxy.request + configurable refresh)
|
||||
└── components/
|
||||
├── DashboardView.tsx # Overview page (score gauge, check distribution, top failing checks)
|
||||
├── NamespacesListView.tsx # Namespace list with per-namespace scores + MUI Drawer detail panel
|
||||
├── InlineAuditSection.tsx # Injected into Deployment/StatefulSet/DaemonSet/Job/CronJob detail views
|
||||
├── ExemptionManager.tsx # Polaris exemption annotation management
|
||||
├── AppBarScoreBadge.tsx # App bar cluster score chip
|
||||
└── PolarisSettings.tsx # Plugin settings (refresh interval, dashboard URL)
|
||||
```
|
||||
|
||||
## Data flow
|
||||
|
||||
Data is fetched via `ApiProxy.request` to the Polaris dashboard service proxy and refreshed on a user-configurable interval (stored in localStorage under `polaris-plugin-refresh-interval`, default 5 minutes). Score is computed from result counts (pass/total). `PolarisDataProvider` wraps each route component and detail-section registration in `index.tsx`.
|
||||
|
||||
**Sidebar limitation**: Headlamp's sidebar only supports 2-level nesting (parent → children). Namespace navigation is handled via the in-content table on the Namespaces page instead.
|
||||
|
||||
## Code conventions
|
||||
|
||||
- Functional React components only — class components only for error boundaries (PolarisErrorBoundary in index.tsx)
|
||||
- All imports from `@kinvolk/headlamp-plugin/lib` and `@kinvolk/headlamp-plugin/lib/CommonComponents`
|
||||
- `@mui/material` is available as a shared external via Headlamp — use `useTheme` from `@mui/material/styles` for theming, MUI `Drawer`/`IconButton` etc. as needed. Do NOT add `@mui/material` to package.json dependencies.
|
||||
- Use `useTheme()` + `theme.palette.*` for all theme-aware colors — never use `var(--mui-palette-*)` CSS variables
|
||||
- No other UI libraries (no Ant Design, etc.)
|
||||
- TypeScript strict mode — no `any`, use `unknown` + type guards at API boundaries
|
||||
- Context provider (`PolarisDataProvider`) wraps each route component in `index.tsx`
|
||||
- All registered components wrapped in `PolarisErrorBoundary` for graceful error handling
|
||||
- Tests: vitest + @testing-library/react, mock with `vi.mock('@kinvolk/headlamp-plugin/lib', ...)` and `vi.mock('@mui/material/styles', ...)`
|
||||
- `vitest.setup.ts` provides a spec-compliant `localStorage` shim for Node 22+ compatibility
|
||||
|
||||
## Testing
|
||||
|
||||
Mock pattern for headlamp APIs:
|
||||
```typescript
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn().mockResolvedValue({}) },
|
||||
K8s: { ResourceClasses: {} },
|
||||
}));
|
||||
```
|
||||
+575
@@ -0,0 +1,575 @@
|
||||
# CONTEXT.md - Headlamp Polaris Plugin
|
||||
|
||||
**Purpose**: Comprehensive reverse prompt for AI assistants working on this project.
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Headlamp Polaris Plugin surfaces [Fairwinds Polaris](https://www.fairwinds.com/polaris) audit results directly inside the [Headlamp](https://headlamp.dev) Kubernetes UI. It provides a read-only dashboard showing cluster-wide security, reliability, and efficiency scores derived from Polaris policy checks.
|
||||
|
||||
- **Stack**: React + TypeScript plugin for Headlamp (v0.26+)
|
||||
- **Data Source**: Polaris dashboard API via Kubernetes service proxy (read-only)
|
||||
- **Current Version**: v0.4.1
|
||||
- **Key Constraint**: No direct Kubernetes resource access - all data fetched through service proxy
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
src/index.tsx # Entry point: registers routes, sidebar, settings
|
||||
├── PolarisDataContext.tsx # Shared data fetch with auto-refresh
|
||||
├── components/
|
||||
│ ├── DashboardView.tsx # Overview (score, checks, top issues)
|
||||
│ ├── NamespacesListView.tsx # Namespace list with scores
|
||||
│ ├── NamespaceDetailView.tsx # Per-namespace drill-down (drawer)
|
||||
│ ├── PolarisSettings.tsx # Settings (refresh interval, URL, test)
|
||||
│ ├── AppBarScoreBadge.tsx # Cluster score badge in top nav
|
||||
│ └── InlineAuditSection.tsx # Injected into workload detail views
|
||||
└── api/
|
||||
└── polaris.ts # Types, hooks, utilities
|
||||
```
|
||||
|
||||
### Data Source
|
||||
|
||||
- **Service Proxy Path**: `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
|
||||
- **Schema**: `AuditData` with `ClusterInfo`, `Results[]` containing nested `PodResult` and `ContainerResults`
|
||||
- **Method**: `ApiProxy.request()` from Headlamp plugin SDK (handles K8s API auth automatically)
|
||||
|
||||
### State Management
|
||||
|
||||
- **Pattern**: React Context (see `src/api/PolarisDataContext.tsx`)
|
||||
- **Rationale**: ADR-001 - Prevents duplicate API calls when multiple components need same data
|
||||
- **Auto-refresh**: User-configurable interval (1/5/10/30 min, default 5 min)
|
||||
- **Storage**: Refresh interval and dashboard URL stored in `localStorage`
|
||||
|
||||
### Score Computation
|
||||
|
||||
```typescript
|
||||
// Formula: (pass / total) * 100, rounded to nearest integer
|
||||
function computeScore(counts: ResultCounts): number {
|
||||
if (counts.total === 0) return 0;
|
||||
return Math.round((counts.pass / counts.total) * 100);
|
||||
}
|
||||
```
|
||||
|
||||
## Technology Constraints
|
||||
|
||||
### ⚠️ CRITICAL: Headlamp Components Only
|
||||
|
||||
**MUST** use `@kinvolk/headlamp-plugin/lib/CommonComponents`
|
||||
**NEVER** import from `@mui/material` or `@mui/icons-material`
|
||||
|
||||
**Why**: Historical issue (v0.3.2) - MUI imports caused plugin load failures. Headlamp provides all needed components as re-exports.
|
||||
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
|
||||
// ❌ Wrong - will break plugin
|
||||
import { Box, Chip } from '@mui/material';
|
||||
```
|
||||
|
||||
### Other Constraints
|
||||
|
||||
- **TypeScript Strictness**: No `any`, explicit types, strict mode enabled
|
||||
- **Packaging**: `@kinvolk/headlamp-plugin` is peer dependency - don't bundle React/MUI
|
||||
- **Theme Handling**: Use CSS variables (`--mui-palette-*`), not theme imports
|
||||
- **Sidebar Limitation**: Headlamp only supports 2-level nesting (parent → children)
|
||||
|
||||
## Component Patterns & Gotchas
|
||||
|
||||
### Headlamp Component Issues
|
||||
|
||||
1. **StatusLabel with empty status**
|
||||
```typescript
|
||||
// ❌ Renders near-invisible (muted background)
|
||||
<StatusLabel status="">{value}</StatusLabel>
|
||||
|
||||
// ✅ Use plain String() for neutral values
|
||||
<span>{String(value)}</span>
|
||||
```
|
||||
|
||||
2. **Link component crashes on plugin routes**
|
||||
```typescript
|
||||
// ❌ Headlamp Link crashes on plugin-registered routes
|
||||
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
|
||||
// ✅ Use react-router-dom Link with Router.createRouteURL
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Router } from '@kinvolk/headlamp-plugin/lib';
|
||||
|
||||
<Link to={Router.createRouteURL('/polaris/namespaces')}>View</Link>
|
||||
```
|
||||
|
||||
3. **Visual components that work well**
|
||||
- `PercentageCircle` - Great for score display
|
||||
- `PercentageBar` - Great for check distribution
|
||||
- `SimpleTable` - Fast, clean tables
|
||||
- `NameValueTable` - Key-value pairs
|
||||
- `SectionBox` - Card containers with titles
|
||||
|
||||
### Code Conventions
|
||||
|
||||
- **Functional Components**: Always use function components with hooks
|
||||
- **Named Exports**: Prefer named exports over default exports
|
||||
- **Props Interfaces**: Define as TypeScript interfaces, not inline types
|
||||
- **Import Order**: React → third-party → Headlamp → local (auto-sorted by eslint)
|
||||
|
||||
## RBAC & Security
|
||||
|
||||
### Minimal Permission Required
|
||||
|
||||
The plugin requires **only** this RBAC permission:
|
||||
|
||||
| Verb | API Group | Resource | Resource Name | Namespace |
|
||||
|------|-----------|----------|---------------|-----------|
|
||||
| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` |
|
||||
|
||||
### Example Role
|
||||
|
||||
```yaml
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Security Notes
|
||||
|
||||
- **Namespaced Role**: MUST be namespaced Role, NOT ClusterRole
|
||||
- **ResourceNames Required**: Always specify `resourceNames: ["polaris-dashboard"]`
|
||||
- **No Write Operations**: Plugin only performs GET, never create/update/delete
|
||||
- **Token-Auth Mode**: When Headlamp uses user tokens, each user needs the RoleBinding
|
||||
- **Network Policy**: If enforced, allow API server → `polaris-dashboard:80` ingress
|
||||
- **Audit Logging**: Every proxy request logged as K8s API audit event
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development mode (hot reload at localhost:4466)
|
||||
npm start
|
||||
|
||||
# Build plugin
|
||||
npm run build
|
||||
|
||||
# Create tarball for distribution
|
||||
npm run package
|
||||
|
||||
# Type-check without emitting
|
||||
npm run tsc
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
|
||||
# Run unit tests
|
||||
npm test
|
||||
|
||||
# Run E2E tests (requires cluster access)
|
||||
npm run e2e
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Check formatting (CI)
|
||||
npm run format:check
|
||||
```
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
- ✅ **ALWAYS use feature branches** for code changes (`feat/*`, `fix/*`, `docs/*`)
|
||||
- ✅ **MAY push directly to main** for: documentation-only changes, version bump commits
|
||||
- ❌ **NEVER push code changes directly to main**
|
||||
|
||||
### Commit Convention
|
||||
|
||||
Use Conventional Commits:
|
||||
- `feat:` - New feature
|
||||
- `fix:` - Bug fix
|
||||
- `docs:` - Documentation only
|
||||
- `chore:` - Maintenance (deps, config)
|
||||
- `test:` - Test changes
|
||||
- `ci:` - CI/CD changes
|
||||
|
||||
### PR Process
|
||||
|
||||
All PRs must pass:
|
||||
1. Build (`npm run build`)
|
||||
2. Lint (`npm run lint`)
|
||||
3. Type-check (`npm run tsc`)
|
||||
4. Unit tests (`npm test`)
|
||||
5. Format check (`npm run format:check`)
|
||||
|
||||
**Before committing**: Always run `npx prettier --write src/`
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (Vitest)
|
||||
|
||||
```bash
|
||||
npm test # Run once
|
||||
npm run test:watch # Watch mode
|
||||
```
|
||||
|
||||
- **Framework**: Vitest with jsdom environment
|
||||
- **Test files**: `*.test.ts`, `*.test.tsx` in `src/`
|
||||
- **Setup**: `vitest.setup.ts` with `@testing-library/jest-dom`
|
||||
- **Coverage**: Focus on meaningful tests, not just numbers
|
||||
- **Test utilities**: `src/test-utils.tsx` provides test wrapper with context
|
||||
|
||||
### E2E Tests (Playwright)
|
||||
|
||||
```bash
|
||||
npm run e2e # Headless
|
||||
npm run e2e:headed # With browser UI
|
||||
```
|
||||
|
||||
- **Framework**: Playwright
|
||||
- **Test files**: `e2e/*.spec.ts`
|
||||
- `polaris.spec.ts` - Sidebar, overview, namespaces, detail drawer
|
||||
- `settings.spec.ts` - Plugin settings page
|
||||
- `appbar.spec.ts` - App bar score badge
|
||||
- **Auth**: Supports both OIDC (Authentik) and token-based auth (see `e2e/auth.setup.ts`)
|
||||
- **CI**: Runs on GitHub Actions with `k3s-animaniacs` runner
|
||||
|
||||
### Local E2E Setup
|
||||
|
||||
```bash
|
||||
# Token-based auth
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
npm run e2e
|
||||
|
||||
# OIDC auth (Authentik)
|
||||
export AUTHENTIK_USERNAME=your-username
|
||||
export AUTHENTIK_PASSWORD=your-password
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
## CI/CD & Release
|
||||
|
||||
### CI Workflow (`.github/workflows/ci.yaml`)
|
||||
|
||||
Runs on push to main and all PRs:
|
||||
1. Checkout
|
||||
2. `npm ci`
|
||||
3. `npm run build`
|
||||
4. `npm run lint`
|
||||
5. `npm run tsc`
|
||||
6. `npm run format:check`
|
||||
7. `npm test`
|
||||
|
||||
Runner: `local-ubuntu-latest`
|
||||
|
||||
### E2E Workflow (`.github/workflows/e2e.yaml`)
|
||||
|
||||
Runs on push, PR, and manual trigger:
|
||||
1. Checkout
|
||||
2. `npm ci`
|
||||
3. `npm run e2e`
|
||||
|
||||
Runner: `k3s-animaniacs` (has cluster access)
|
||||
Requires: `HEADLAMP_URL`, `HEADLAMP_TOKEN` or `AUTHENTIK_USERNAME`/`AUTHENTIK_PASSWORD`
|
||||
|
||||
### Release Workflow (`.github/workflows/release.yaml`)
|
||||
|
||||
**Manual trigger** via workflow_dispatch with version input:
|
||||
|
||||
```bash
|
||||
# Via GitHub UI or CLI
|
||||
gh workflow run release.yaml -f version=0.4.2
|
||||
```
|
||||
|
||||
Steps:
|
||||
1. Validate version format (semver)
|
||||
2. Bump `package.json` + `artifacthub-pkg.yml`
|
||||
3. Build plugin
|
||||
4. Package tarball
|
||||
5. Compute SHA256 checksum
|
||||
6. Commit version bump
|
||||
7. Create git tag
|
||||
8. Create GitHub release
|
||||
9. Upload tarball to release
|
||||
|
||||
**Guard**: Skips if checksum already matches (prevents infinite loop)
|
||||
|
||||
**Post-release**: ArtifactHub pulls metadata every 30 min (no webhook, pull-based)
|
||||
|
||||
### Version Bump Requirements
|
||||
|
||||
**ALWAYS bump both files in the same commit**:
|
||||
- `package.json` - `version` field
|
||||
- `artifacthub-pkg.yml` - `version` field + `digest` (checksum) + `archive.url`
|
||||
|
||||
## Known Issues & Workarounds
|
||||
|
||||
### ⚠️ Headlamp v0.39.0 Known Issues
|
||||
|
||||
**AutoSizer JavaScript Error**
|
||||
- **Symptom**: Console shows `TypeError: undefined is not an object (evaluating 'io.AutoSizer')`
|
||||
- **Impact**: Cosmetic error in Settings page, doesn't break functionality
|
||||
- **Root Cause**: Headlamp core bug, not plugin-related
|
||||
- **Workaround**: None needed, can be ignored
|
||||
|
||||
**Plugin Loading (RESOLVED)**
|
||||
- **Old Issue**: Previously thought `config.watchPlugins: false` was required
|
||||
- **Resolution**: Plugins load correctly with default `watchPlugins: true`
|
||||
- **Note**: If you see old docs mentioning `watchPlugins: false`, ignore them
|
||||
|
||||
### Polaris Dashboard Behavior
|
||||
|
||||
**Stale Audit Data**
|
||||
- **Symptom**: Plugin shows old audit timestamp
|
||||
- **Root Cause**: Polaris dashboard runs audit once at pod startup, then caches results
|
||||
- **Does NOT**: Continuously re-audit in real-time
|
||||
- **Workaround**: Restart Polaris pods for fresh data
|
||||
```bash
|
||||
kubectl rollout restart deployment -n polaris polaris-dashboard
|
||||
```
|
||||
- **Load Balancing**: Service balances across multiple pods - each may have different audit timestamps
|
||||
- **Plugin Auto-Refresh**: Works correctly - just fetches whatever Polaris currently has cached
|
||||
|
||||
### Skipped Count Limitation
|
||||
|
||||
**What It Shows**:
|
||||
- Only checks with `Severity: "ignore"` in Polaris API response
|
||||
- Does NOT include annotation-based exemptions (`polaris.fairwinds.com/*-exempt`)
|
||||
|
||||
**Why**:
|
||||
- Polaris omits exempted checks from `results.json`
|
||||
- Plugin has no access to raw K8s resources to compute exemptions
|
||||
- By design: service proxy limitation
|
||||
|
||||
**Workaround**:
|
||||
- Link to native Polaris dashboard for full exemption count
|
||||
- UI tooltip explains this limitation
|
||||
|
||||
## Deployment Patterns
|
||||
|
||||
### Plugin Manager (Recommended)
|
||||
|
||||
Install via Headlamp UI (Settings → Plugins → Catalog) or Helm values:
|
||||
|
||||
```yaml
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
configContent: |
|
||||
plugins:
|
||||
- name: polaris
|
||||
source: https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin
|
||||
```
|
||||
|
||||
### Sidecar Container (Alternative)
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
containers:
|
||||
- name: headlamp
|
||||
# ... main container
|
||||
- name: headlamp-plugin
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
npx @headlamp-k8s/pluginctl@latest install \
|
||||
--config /config/plugin.yml \
|
||||
--folderName /headlamp/plugins \
|
||||
--watch
|
||||
volumeMounts:
|
||||
- name: plugins-dir
|
||||
mountPath: /headlamp/plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
volumes:
|
||||
- name: plugins-dir
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
```
|
||||
|
||||
### Manual Tarball
|
||||
|
||||
```bash
|
||||
# Download release
|
||||
wget https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.4.1/headlamp-polaris-plugin-0.4.1.tgz
|
||||
|
||||
# Extract to plugin directory
|
||||
tar -xzf headlamp-polaris-plugin-0.4.1.tgz -C /headlamp/plugins/
|
||||
|
||||
# Restart Headlamp
|
||||
kubectl rollout restart deployment headlamp -n kube-system
|
||||
```
|
||||
|
||||
## Project Files Reference
|
||||
|
||||
```
|
||||
src/
|
||||
index.tsx # Entry point: registers sidebar, routes, settings, etc.
|
||||
api/
|
||||
polaris.ts # Core types, usePolarisData hook, utilities
|
||||
PolarisDataContext.tsx # React Context provider for shared data
|
||||
components/
|
||||
DashboardView.tsx # Overview page (score, checks, top issues)
|
||||
NamespacesListView.tsx # Namespace table with scores
|
||||
NamespaceDetailView.tsx # Drawer panel with per-namespace drill-down
|
||||
PolarisSettings.tsx # Settings page (refresh, URL, test)
|
||||
AppBarScoreBadge.tsx # Cluster score chip in top nav bar
|
||||
InlineAuditSection.tsx # Injected into resource detail views
|
||||
test-utils.tsx # Test helpers (wrapper with context)
|
||||
|
||||
.github/workflows/
|
||||
ci.yaml # Lint, type-check, build, test
|
||||
e2e.yaml # Playwright E2E tests
|
||||
release.yaml # Automated releases
|
||||
|
||||
e2e/ # Playwright tests
|
||||
polaris.spec.ts # Main plugin functionality
|
||||
settings.spec.ts # Settings page
|
||||
appbar.spec.ts # App bar badge
|
||||
auth.setup.ts # OIDC/token auth setup
|
||||
|
||||
docs/ # Comprehensive documentation
|
||||
architecture/ # Overview, design decisions, ADRs
|
||||
deployment/ # Helm, Kubernetes, production guides
|
||||
troubleshooting/ # Common issues, RBAC, network problems
|
||||
getting-started/ # Quick start, prerequisites, installation
|
||||
|
||||
package.json # Version, scripts, dependencies
|
||||
artifacthub-pkg.yml # ArtifactHub metadata (version, checksum)
|
||||
tsconfig.json # Extends @kinvolk/headlamp-plugin config
|
||||
vitest.config.mts # Vitest config (jsdom, excludes e2e/)
|
||||
.eslintrc.js # Extends @headlamp-k8s/eslint-config
|
||||
.prettierrc.js # Uses @headlamp-k8s prettier config
|
||||
```
|
||||
|
||||
## MCP Servers (Claude Code)
|
||||
|
||||
- **GitHub**: Source control (`github-mcp-server`), repo at `cpfarhood/headlamp-polaris-plugin`
|
||||
- **Kubernetes (local)**: Cluster access via `kubernetes-mcp-server`
|
||||
- **Flux (local)**: Flux Operator access via `flux-operator-mcp`
|
||||
- **Playwright**: Browser automation via `@playwright/mcp`
|
||||
|
||||
## Common Tasks Quick Reference
|
||||
|
||||
```bash
|
||||
# Start development
|
||||
npm install && npm start
|
||||
|
||||
# Run all checks before PR
|
||||
npm run build && npm run lint && npm run tsc && npm test && npm run format
|
||||
|
||||
# Create release (maintainers only)
|
||||
# 1. Edit CHANGELOG.md
|
||||
# 2. Trigger release workflow:
|
||||
gh workflow run release.yaml -f version=0.4.2
|
||||
|
||||
# Run E2E tests locally
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
npm run e2e
|
||||
|
||||
# Fix formatting issues
|
||||
npx prettier --write src/
|
||||
|
||||
# Check Polaris audit freshness
|
||||
kubectl get --raw "/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json" | jq -r '.AuditTime'
|
||||
|
||||
# Restart Polaris for fresh audit
|
||||
kubectl rollout restart deployment -n polaris polaris-dashboard
|
||||
```
|
||||
|
||||
## Anti-Patterns (What NOT to Do)
|
||||
|
||||
- ❌ Import from `@mui/material` or `@mui/icons-material` → breaks plugin
|
||||
- ❌ Use `any` type → strict TypeScript required
|
||||
- ❌ Push code changes directly to main → always use feature branches
|
||||
- ❌ Grant broader RBAC than `get services/proxy` → security risk
|
||||
- ❌ Use ClusterRole instead of namespaced Role → violates least privilege
|
||||
- ❌ Forget to run `npx prettier --write src/` → CI will fail
|
||||
- ❌ Use inline styles without CSS variables → breaks dark mode
|
||||
- ❌ Try to query K8s resources directly → plugin only has service proxy access
|
||||
- ❌ Import Headlamp `Link` for plugin routes → use react-router-dom `Link` + `Router.createRouteURL()`
|
||||
- ❌ Assume Polaris continuously re-audits → it only audits at pod startup
|
||||
|
||||
## Quick Diagnosis Guide
|
||||
|
||||
```
|
||||
Symptom: Plugin not in sidebar
|
||||
→ Check: Hard refresh browser (Cmd+Shift+R / Ctrl+Shift+R)
|
||||
→ Check: Plugin installed? kubectl get configmap headlamp-plugin-config -n kube-system
|
||||
|
||||
Symptom: 403 Access Denied
|
||||
→ Check: RBAC binding exists? kubectl get role,rolebinding -n polaris
|
||||
→ Fix: Apply RBAC example from docs/deployment/rbac.md
|
||||
|
||||
Symptom: 404 or 503
|
||||
→ Check: Polaris installed? kubectl get pods -n polaris
|
||||
→ Check: Service exists? kubectl get svc polaris-dashboard -n polaris
|
||||
|
||||
Symptom: Stale audit data
|
||||
→ Fix: kubectl rollout restart deployment -n polaris polaris-dashboard
|
||||
→ Verify: Check AuditTime in UI matches current date
|
||||
|
||||
Symptom: Settings page empty or broken
|
||||
→ Check: Plugin version ≥ v0.3.3?
|
||||
→ Fix: Upgrade plugin and hard refresh browser
|
||||
|
||||
Symptom: CI prettier check fails
|
||||
→ Fix: npx prettier --write src/
|
||||
→ Commit: Include formatting fixes in your PR
|
||||
|
||||
Symptom: Dark mode white backgrounds
|
||||
→ Check: Plugin version ≥ v0.3.5?
|
||||
→ Fix: Upgrade and hard refresh browser
|
||||
```
|
||||
|
||||
## Historical Context
|
||||
|
||||
### Why Service Proxy Instead of ConfigMaps?
|
||||
|
||||
Early versions (< v0.0.10) incorrectly documented ConfigMap RBAC. The plugin **never** accessed ConfigMaps - it always used the service proxy. This was clarified in v0.0.10.
|
||||
|
||||
### Why No MUI Imports?
|
||||
|
||||
v0.3.2 removed direct MUI imports because they caused plugin load failures. Headlamp provides all needed MUI components as re-exports through `CommonComponents`.
|
||||
|
||||
### Why React Context?
|
||||
|
||||
ADR-001 documents the switch to React Context. Before v0.3.0, each component called `usePolarisData()` independently, causing duplicate API requests. Context ensures a single shared fetch.
|
||||
|
||||
### Why No Continuous Polaris Audits?
|
||||
|
||||
Polaris dashboard mode runs a one-time audit at pod startup and caches results. This is by design in Polaris itself. For continuous auditing, Polaris would need to be configured in webhook mode (admission controller), which is a different deployment pattern.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-02-12
|
||||
**Version**: v0.4.1
|
||||
**Target Headlamp**: v0.26+
|
||||
**Target Polaris**: v9.x
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
# Contributing to Headlamp Polaris Plugin
|
||||
|
||||
Thank you for your interest in contributing to the Headlamp Polaris Plugin! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Branching Strategy](#branching-strategy)
|
||||
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Code Style](#code-style)
|
||||
- [Testing Requirements](#testing-requirements)
|
||||
- [Documentation](#documentation)
|
||||
- [Release Process](#release-process)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows a standard code of conduct:
|
||||
- Be respectful and inclusive
|
||||
- Welcome newcomers and help them get started
|
||||
- Focus on constructive feedback
|
||||
- Assume good intentions
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20 or later
|
||||
- npm or yarn
|
||||
- Access to a Kubernetes cluster with Headlamp installed (for testing)
|
||||
- Git
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Fork and clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/headlamp-polaris-plugin.git
|
||||
cd headlamp-polaris-plugin
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start development mode:**
|
||||
```bash
|
||||
npm start
|
||||
# Plugin will be available at http://localhost:4466
|
||||
```
|
||||
|
||||
4. **Run tests:**
|
||||
```bash
|
||||
# Unit tests
|
||||
npm test
|
||||
|
||||
# E2E tests (requires Headlamp instance)
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
5. **Build the plugin:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Feature Development
|
||||
|
||||
1. Create a feature branch from `main`
|
||||
2. Make your changes
|
||||
3. Write/update tests
|
||||
4. Update documentation
|
||||
5. Run lint and tests locally
|
||||
6. Submit a pull request
|
||||
|
||||
### Local Testing
|
||||
|
||||
**Option 1: Development Mode**
|
||||
```bash
|
||||
npm start
|
||||
# Opens Headlamp at http://localhost:4466 with hot reload
|
||||
```
|
||||
|
||||
**Option 2: Production Build**
|
||||
```bash
|
||||
npm run build
|
||||
# Plugin bundle created in dist/
|
||||
```
|
||||
|
||||
**Option 3: E2E Testing**
|
||||
```bash
|
||||
# Set up environment (see e2e/README.md)
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
## Branching Strategy
|
||||
|
||||
### Main Branch
|
||||
|
||||
- **Purpose:** Stable, production-ready code
|
||||
- **Protection:** Only merge via pull requests
|
||||
- **Naming:** `main`
|
||||
|
||||
### Feature Branches
|
||||
|
||||
- **Purpose:** Development of new features or fixes
|
||||
- **Naming Convention:**
|
||||
- Features: `feat/description` or `feature/description`
|
||||
- Bug fixes: `fix/description`
|
||||
- Documentation: `docs/description`
|
||||
- Refactoring: `refactor/description`
|
||||
- Chores: `chore/description`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
feat/add-exemption-support
|
||||
fix/dark-mode-theme-colors
|
||||
docs/update-rbac-guide
|
||||
refactor/polaris-api-client
|
||||
chore/upgrade-dependencies
|
||||
```
|
||||
|
||||
### Branching Rules
|
||||
|
||||
**✅ ALWAYS use feature branches for:**
|
||||
- Code changes (new features, bug fixes, refactors)
|
||||
- Test updates
|
||||
- CI/CD workflow changes
|
||||
- Package updates
|
||||
|
||||
**✅ MAY push directly to main for:**
|
||||
- Documentation-only changes (README.md, CLAUDE.md, comments)
|
||||
- Version bump commits (`package.json` + `artifacthub-pkg.yml`)
|
||||
|
||||
**❌ NEVER push directly to main for:**
|
||||
- Any code changes to `src/`
|
||||
- Test file changes
|
||||
- Workflow changes
|
||||
- Dependency updates
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/) format:
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- **feat:** New feature
|
||||
- **fix:** Bug fix
|
||||
- **docs:** Documentation only
|
||||
- **style:** Code style (formatting, no logic change)
|
||||
- **refactor:** Code change that neither fixes a bug nor adds a feature
|
||||
- **perf:** Performance improvement
|
||||
- **test:** Adding or updating tests
|
||||
- **chore:** Maintenance tasks (deps, build, CI)
|
||||
- **ci:** CI/CD changes
|
||||
|
||||
### Scope (Optional)
|
||||
|
||||
- `api` - API-related changes
|
||||
- `ui` - UI component changes
|
||||
- `settings` - Plugin settings
|
||||
- `tests` - Test-related changes
|
||||
- `docs` - Documentation changes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
feat(api): add support for custom Polaris dashboard URLs
|
||||
|
||||
fix(ui): resolve dark mode theme color inconsistencies
|
||||
|
||||
docs: update RBAC examples with NetworkPolicy
|
||||
|
||||
chore: bump version to 0.3.5
|
||||
|
||||
test(e2e): add tests for plugin settings page
|
||||
```
|
||||
|
||||
### Footer
|
||||
|
||||
Add `Co-Authored-By` for pair programming or AI assistance:
|
||||
|
||||
```
|
||||
feat: add namespace filtering to overview
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||||
Co-Authored-By: Happy <yesreply@happy.engineering>
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Creating a PR
|
||||
|
||||
1. **Run all checks locally:**
|
||||
```bash
|
||||
npm run build # Verify build succeeds
|
||||
npm run lint # Check for linting errors
|
||||
npm run tsc # Type-check TypeScript
|
||||
npm test # Run unit tests
|
||||
npm run format # Format code with Prettier
|
||||
```
|
||||
|
||||
2. **Update documentation:**
|
||||
- Update README.md if you added features or changed behavior
|
||||
- Update CLAUDE.md if you changed architecture or constraints
|
||||
- Add/update JSDoc comments for new APIs
|
||||
|
||||
3. **Write/update tests:**
|
||||
- Add unit tests for new functions/components
|
||||
- Update E2E tests if UI behavior changed
|
||||
- Ensure all tests pass
|
||||
|
||||
### Creating a PR
|
||||
|
||||
1. **Push your branch:**
|
||||
```bash
|
||||
git push origin feat/your-feature
|
||||
```
|
||||
|
||||
2. **Create PR on GitHub:**
|
||||
- Use a descriptive title following commit conventions
|
||||
- Fill out the PR template (if available)
|
||||
- Link related issues with `Fixes #123` or `Closes #456`
|
||||
|
||||
3. **PR Title Format:**
|
||||
```
|
||||
feat: add exemption management UI
|
||||
fix: correct score calculation for skipped checks
|
||||
docs: improve deployment guide with Helm examples
|
||||
```
|
||||
|
||||
4. **PR Description Should Include:**
|
||||
- Summary of changes
|
||||
- Motivation and context
|
||||
- Testing performed
|
||||
- Screenshots (for UI changes)
|
||||
- Breaking changes (if any)
|
||||
|
||||
### PR Review Process
|
||||
|
||||
1. **Automated Checks:**
|
||||
- ✅ CI workflow (lint, type-check, build, test)
|
||||
- ✅ E2E tests (may fail if plugin not deployed)
|
||||
|
||||
2. **Maintainer Review:**
|
||||
- Code quality and style
|
||||
- Test coverage
|
||||
- Documentation completeness
|
||||
- Breaking changes assessment
|
||||
|
||||
3. **Merging:**
|
||||
- Use **merge commits** (not squash, not rebase)
|
||||
- Delete feature branch after merge
|
||||
- Maintainers will handle version bumps and releases
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript
|
||||
|
||||
- **Strictness:** Full TypeScript strict mode enabled
|
||||
- **No `any`:** Use specific types or `unknown`
|
||||
- **Interfaces over types:** Prefer `interface` for object shapes
|
||||
- **Named exports:** Use named exports, not default exports
|
||||
|
||||
### React
|
||||
|
||||
- **Functional components:** Use function components with hooks
|
||||
- **Props interfaces:** Always define props as interfaces
|
||||
- **Headlamp components:** Use CommonComponents from Headlamp, never raw MUI
|
||||
- **No inline styles:** Use theme-aware CSS variables
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Auto-fix linting issues
|
||||
npm run lint:fix
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Check formatting
|
||||
npm run format:check
|
||||
```
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports are automatically sorted by eslint. Order:
|
||||
1. React imports
|
||||
2. Third-party libraries
|
||||
3. Headlamp plugin imports
|
||||
4. Local imports (components, API, types)
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
import { computeScore } from '../api/polaris';
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Components:** PascalCase (`DashboardView`, `PolarisSettings`)
|
||||
- **Files:** Match component name (`DashboardView.tsx`)
|
||||
- **Hooks:** Prefix with `use` (`usePolarisData`)
|
||||
- **Utilities:** camelCase (`countResults`, `computeScore`)
|
||||
- **Constants:** UPPER_SNAKE_CASE (`DASHBOARD_URL_DEFAULT`)
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests (Required)
|
||||
|
||||
- All new functions must have unit tests
|
||||
- All bug fixes should include regression tests
|
||||
- Aim for meaningful coverage, not just numbers
|
||||
- Use descriptive test names
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
describe('countResults', () => {
|
||||
it('counts passing, warning, and danger results correctly', () => {
|
||||
// Test implementation
|
||||
});
|
||||
|
||||
it('includes skipped checks in total count', () => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Tests (Recommended)
|
||||
|
||||
- Add E2E tests for new UI features
|
||||
- Update existing tests if behavior changes
|
||||
- See `e2e/README.md` for detailed instructions
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run E2E tests
|
||||
npm run e2e
|
||||
|
||||
# Run E2E tests in headed mode (see browser)
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Documentation Updates Required
|
||||
|
||||
When making changes, update relevant documentation:
|
||||
|
||||
#### Code Changes
|
||||
- **README.md:** User-facing features, installation, configuration
|
||||
- **CLAUDE.md:** Architecture, constraints, MCP integrations
|
||||
- **JSDoc:** All public APIs, components, hooks
|
||||
|
||||
#### Test Changes
|
||||
- **e2e/README.md:** New test scenarios or setup changes
|
||||
|
||||
#### Build/CI Changes
|
||||
- **README.md:** Build commands, release process
|
||||
- **.github/workflows/*.yaml:** Workflow comments
|
||||
|
||||
### JSDoc Style
|
||||
|
||||
Use JSDoc for all exported functions, components, and types:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Counts passing, warning, danger, and skipped Polaris check results.
|
||||
*
|
||||
* Skipped checks are identified by severity "ignore" with success false.
|
||||
*
|
||||
* @param data - AuditData from Polaris dashboard API
|
||||
* @returns ResultCounts with totals by status (pass/warning/danger/skipped)
|
||||
*/
|
||||
export function countResults(data: AuditData): ResultCounts {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
### Version Numbering
|
||||
|
||||
We follow [Semantic Versioning](https://semver.org/):
|
||||
- **Major (1.0.0):** Breaking changes
|
||||
- **Minor (0.1.0):** New features, backward compatible
|
||||
- **Patch (0.0.1):** Bug fixes, backward compatible
|
||||
|
||||
### Creating a Release
|
||||
|
||||
**Maintainers only:**
|
||||
|
||||
1. **Merge feature PRs to main**
|
||||
|
||||
2. **Bump version:**
|
||||
```bash
|
||||
# Edit package.json and artifacthub-pkg.yml
|
||||
# Update version and archive-url
|
||||
git add package.json artifacthub-pkg.yml
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
3. **Create and push tag:**
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
4. **GitHub Actions automatically:**
|
||||
- Builds plugin tarball
|
||||
- Creates GitHub release
|
||||
- Uploads tarball to release
|
||||
- Updates `artifacthub-pkg.yml` with checksum
|
||||
|
||||
5. **ArtifactHub syncs within 30 minutes**
|
||||
|
||||
### Pre-release Versions
|
||||
|
||||
For testing before stable release:
|
||||
- Use `-dev.N` suffix: `v0.3.5-dev.1`
|
||||
- Follow same process as stable releases
|
||||
- Mark as "pre-release" on GitHub
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Questions:** Open a [GitHub Discussion](https://github.com/privilegedescalation/headlamp-polaris-plugin/discussions)
|
||||
- **Bugs:** Open a [GitHub Issue](https://github.com/privilegedescalation/headlamp-polaris-plugin/issues)
|
||||
- **E2E Testing:** See [e2e/README.md](e2e/README.md)
|
||||
- **Architecture:** See [CLAUDE.md](CLAUDE.md)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.
|
||||
+2
-1
@@ -6,4 +6,5 @@ COPY src/ src/
|
||||
RUN npx @kinvolk/headlamp-plugin build
|
||||
|
||||
FROM alpine:3.20
|
||||
COPY --from=build /app/dist/ /plugins/polaris-headlamp-plugin/
|
||||
COPY --from=build /app/dist/ /plugins/headlamp-polaris-plugin/
|
||||
COPY --from=build /app/package.json /plugins/headlamp-polaris-plugin/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Installation Policy
|
||||
|
||||
## Approved Installation Method
|
||||
|
||||
**The ONLY approved method for installing this plugin is via [Artifact Hub](https://artifacthub.io/) using the Headlamp plugin installer.**
|
||||
|
||||
No other installation method is acceptable. This includes but is not limited to:
|
||||
|
||||
- Direct installation from GitHub release assets
|
||||
- Manual npm pack / tarball extraction
|
||||
- initContainer workarounds that bypass Artifact Hub
|
||||
- Direct file copy or sidecar injection
|
||||
|
||||
## Enforcement
|
||||
|
||||
All deployment configurations, CI/CD pipelines, and documentation MUST reference Artifact Hub as the sole plugin distribution channel. Any pull request that introduces an alternative installation method will be rejected.
|
||||
|
||||
## Rationale
|
||||
|
||||
Artifact Hub provides verified checksums, consistent versioning, and a standard discovery mechanism for the CNCF ecosystem. Bypassing it introduces security and integrity risks.
|
||||
|
||||
---
|
||||
|
||||
*This policy is set by the CTO and approved by the CEO of Privileged Escalation.*
|
||||
@@ -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.
|
||||
@@ -0,0 +1,290 @@
|
||||
# Headlamp Polaris Plugin - Project Assessment
|
||||
|
||||
**Date:** 2026-02-11
|
||||
**Version:** v0.3.0
|
||||
**Status:** Active Development
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This assessment identifies critical issues and improvement opportunities for the headlamp-polaris-plugin project. The plugin is currently non-functional in production due to Headlamp v0.39.0 compatibility issues, and has several TypeScript compilation errors that need immediate attention.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Critical Issues (Must Fix Immediately)
|
||||
|
||||
### 1. TypeScript Compilation Errors
|
||||
**Severity:** CRITICAL
|
||||
**Impact:** Build failures, type safety compromised
|
||||
|
||||
**Issues:**
|
||||
- `src/index.tsx:72` - `registerDetailsViewSection` expects 1 argument, got 2
|
||||
- `src/index.tsx:87` - `registerAppBarAction` expects 1 argument, got 2
|
||||
|
||||
**Recommendation:**
|
||||
Update Headlamp plugin API calls to match the current version. Check @kinvolk/headlamp-plugin version compatibility.
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Review Headlamp plugin API documentation
|
||||
- [ ] Update `registerDetailsViewSection` and `registerAppBarAction` calls
|
||||
- [ ] Run `npm run tsc` to verify fixes
|
||||
- [ ] Update CI to fail on TypeScript errors
|
||||
|
||||
---
|
||||
|
||||
### 2. Production Plugin Loading Failure
|
||||
**Severity:** CRITICAL
|
||||
**Impact:** Plugin is completely non-functional in production
|
||||
|
||||
**Root Cause:**
|
||||
Headlamp v0.39.0 with default `watchPlugins: true` treats catalog-managed plugins as "development directory" plugins, preventing frontend JavaScript execution.
|
||||
|
||||
**Current Status:**
|
||||
- Deployment patched to install plugins to `/headlamp/static-plugins`
|
||||
- `watchPlugins: false` configured
|
||||
- Waiting for user to test if plugins now load
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Confirm plugins load after recent deployment changes
|
||||
- [ ] Document the fix in deployment guide
|
||||
- [ ] Update MEMORY.md with final resolution
|
||||
- [ ] Consider downgrading Headlamp if issue persists
|
||||
|
||||
---
|
||||
|
||||
### 3. Test Failures
|
||||
**Severity:** HIGH
|
||||
**Impact:** CI failures, reduced confidence in changes
|
||||
|
||||
**Current Status:**
|
||||
- 1 test file failing (DashboardView)
|
||||
- 49 tests passing
|
||||
- Error related to `SimpleTable` component mock
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Fix DashboardView test mocking
|
||||
- [ ] Ensure all tests pass before merging PRs
|
||||
- [ ] Add test for top issues feature
|
||||
- [ ] Increase test coverage to >80%
|
||||
|
||||
---
|
||||
|
||||
## 🟡 High Priority Improvements
|
||||
|
||||
### 4. Type Safety Enhancements
|
||||
**Severity:** HIGH
|
||||
**Impact:** Better developer experience, catch errors earlier
|
||||
|
||||
**Recommendations:**
|
||||
- Enable stricter TypeScript checks in `tsconfig.json`
|
||||
- Add type definitions for all Headlamp plugin APIs
|
||||
- Ensure no `any` types in production code
|
||||
- Add JSDoc comments for complex types
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Audit codebase for `any` types
|
||||
- [ ] Enable `noImplicitAny` and `strictNullChecks`
|
||||
- [ ] Add type guards for API responses
|
||||
- [ ] Document complex type structures
|
||||
|
||||
---
|
||||
|
||||
### 5. Security Hardening
|
||||
**Severity:** HIGH
|
||||
**Impact:** Prevent vulnerabilities, protect user data
|
||||
|
||||
**Current Risks:**
|
||||
- Direct Kubernetes API access via service proxy
|
||||
- User input in exemption annotations (potential injection)
|
||||
- External URL configuration for Polaris dashboard
|
||||
|
||||
**Recommendations:**
|
||||
- Validate and sanitize all user inputs
|
||||
- Implement input validation for dashboard URL
|
||||
- Add CSRF protection for exemption management
|
||||
- Audit dependencies for known vulnerabilities
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Add input validation utilities
|
||||
- [ ] Sanitize exemption annotation values
|
||||
- [ ] Validate URL format for dashboard configuration
|
||||
- [ ] Run `npm audit` and fix vulnerabilities
|
||||
- [ ] Add security testing to CI/CD
|
||||
|
||||
---
|
||||
|
||||
### 6. Error Handling & User Experience
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Better error messages, improved debugging
|
||||
|
||||
**Current Gaps:**
|
||||
- Generic error messages don't help users troubleshoot
|
||||
- No retry logic for transient API failures
|
||||
- Missing loading states in some components
|
||||
|
||||
**Recommendations:**
|
||||
- Provide specific, actionable error messages
|
||||
- Implement retry logic with exponential backoff
|
||||
- Add loading skeletons for all async operations
|
||||
- Show connection test results with specific failure reasons
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Create error message constants with solutions
|
||||
- [ ] Add retry logic to API calls
|
||||
- [ ] Implement loading skeletons
|
||||
- [ ] Improve connection test error messages
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Medium Priority Enhancements
|
||||
|
||||
### 7. Testing Coverage
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Confidence in changes, regression prevention
|
||||
|
||||
**Current Coverage:**
|
||||
- Unit tests: Good coverage for API utilities
|
||||
- Component tests: Some coverage, gaps exist
|
||||
- E2E tests: Minimal (Playwright configured but underutilized)
|
||||
|
||||
**Recommendations:**
|
||||
- Add E2E tests for critical user flows
|
||||
- Test error scenarios and edge cases
|
||||
- Add visual regression tests
|
||||
- Test RBAC permission denied scenarios
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Write E2E test for complete audit workflow
|
||||
- [ ] Add tests for error states
|
||||
- [ ] Test exemption management flow
|
||||
- [ ] Add Playwright tests to CI
|
||||
|
||||
---
|
||||
|
||||
### 8. Performance Optimization
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Faster load times, better UX
|
||||
|
||||
**Opportunities:**
|
||||
- Memoize expensive calculations (score computation)
|
||||
- Lazy load namespace detail views
|
||||
- Debounce search/filter operations
|
||||
- Cache Polaris data with stale-while-revalidate
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Add React.memo to pure components
|
||||
- [ ] Memoize score calculations
|
||||
- [ ] Implement data caching strategy
|
||||
- [ ] Profile component render times
|
||||
|
||||
---
|
||||
|
||||
### 9. Code Quality & Maintainability
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Easier maintenance, onboarding
|
||||
|
||||
**Recommendations:**
|
||||
- Extract magic strings to constants
|
||||
- Reduce component complexity
|
||||
- Add JSDoc comments for public APIs
|
||||
- Improve code organization
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Create constants file for check IDs
|
||||
- [ ] Split large components (DashboardView, NamespaceDetailView)
|
||||
- [ ] Add comments for complex logic
|
||||
- [ ] Establish code review checklist
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Low Priority / Future Enhancements
|
||||
|
||||
### 10. Documentation
|
||||
**Severity:** LOW
|
||||
**Impact:** Better onboarding, user adoption
|
||||
|
||||
**Gaps:**
|
||||
- No architecture documentation
|
||||
- Limited inline code comments
|
||||
- Missing troubleshooting guide
|
||||
- No contributor guidelines
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Create architecture diagram
|
||||
- [ ] Document component hierarchy
|
||||
- [ ] Add troubleshooting section to README
|
||||
- [ ] Create CONTRIBUTING.md
|
||||
|
||||
---
|
||||
|
||||
### 11. CI/CD Pipeline Optimization
|
||||
**Severity:** LOW
|
||||
**Impact:** Faster feedback, automated releases
|
||||
|
||||
**Opportunities:**
|
||||
- Run tests in parallel
|
||||
- Cache npm dependencies
|
||||
- Add automated security scanning
|
||||
- Implement semantic versioning
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Parallelize test execution
|
||||
- [ ] Add npm cache to GitHub Actions
|
||||
- [ ] Integrate Dependabot
|
||||
- [ ] Add semantic-release
|
||||
|
||||
---
|
||||
|
||||
## Summary & Prioritization
|
||||
|
||||
### Week 1 (Immediate)
|
||||
1. ✅ Fix TypeScript compilation errors
|
||||
2. ✅ Resolve production plugin loading issue
|
||||
3. ✅ Fix failing DashboardView test
|
||||
|
||||
### Week 2 (High Priority)
|
||||
4. Enhance type safety (strict mode)
|
||||
5. Implement security hardening
|
||||
6. Improve error handling and UX
|
||||
|
||||
### Week 3-4 (Medium Priority)
|
||||
7. Increase test coverage to >80%
|
||||
8. Optimize performance (memoization, caching)
|
||||
9. Refactor for maintainability
|
||||
|
||||
### Ongoing (Low Priority)
|
||||
10. Documentation improvements
|
||||
11. CI/CD optimizations
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
**Code Quality:**
|
||||
- ✅ Zero TypeScript errors
|
||||
- ✅ All tests passing
|
||||
- 🎯 Test coverage >80%
|
||||
- 🎯 No high/critical security vulnerabilities
|
||||
|
||||
**Production Readiness:**
|
||||
- ✅ Plugin loads successfully in Headlamp
|
||||
- ✅ All features functional
|
||||
- 🎯 Error rate <1%
|
||||
- 🎯 Average response time <500ms
|
||||
|
||||
**Developer Experience:**
|
||||
- ✅ Clear documentation
|
||||
- ✅ Easy local setup
|
||||
- 🎯 Fast CI/CD (<5 min)
|
||||
- 🎯 Automated releases
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:** Fix TypeScript errors and verify plugin loads
|
||||
2. **Short-term:** Complete Week 1-2 priorities
|
||||
3. **Long-term:** Address medium and low priority items
|
||||
4. **Continuous:** Monitor metrics and iterate
|
||||
|
||||
**Recommended First Action:**
|
||||
Fix the TypeScript compilation errors in `src/index.tsx` by updating the Headlamp plugin API calls.
|
||||
@@ -0,0 +1,331 @@
|
||||
# Headlamp Polaris Plugin
|
||||
|
||||
[](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin)
|
||||
[](https://github.com/privilegedescalation/headlamp-polaris-plugin/actions/workflows/ci.yaml)
|
||||
[](https://github.com/privilegedescalation/headlamp-polaris-plugin/actions/workflows/e2e.yaml)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
A [Headlamp](https://headlamp.dev/) plugin that surfaces [Fairwinds Polaris](https://polaris.docs.fairwinds.com/) audit results directly in the Headlamp UI.
|
||||
|
||||
**[Documentation](#documentation) | [Installation](#installing) | [Security](#rbac--security-setup) | [Development](#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](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin). 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:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```yaml
|
||||
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](docs/README.md)** - Documentation hub with all guides
|
||||
|
||||
### Quick Links
|
||||
|
||||
- **[Quick Start](docs/getting-started/quick-start.md)** - Get up and running in 5 minutes
|
||||
- **[Installation Guide](docs/getting-started/installation.md)** - All installation methods (Plugin Manager, Sidecar, Manual, Source)
|
||||
- **[Troubleshooting](docs/troubleshooting/README.md)** - Quick diagnosis and common issues
|
||||
|
||||
### Comprehensive Guides
|
||||
|
||||
| Guide | Description |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| **[Architecture](docs/architecture/overview.md)** | System architecture, data flow, component hierarchy, design decisions |
|
||||
| **[Deployment](docs/deployment/helm.md)** | Production deployment with Helm, Kubernetes, FluxCD |
|
||||
| **[Security](SECURITY.md)** | Security model, RBAC requirements, vulnerability reporting |
|
||||
| **[Testing](docs/development/testing.md)** | Unit tests, E2E tests, CI/CD, best practices |
|
||||
| **[Contributing](CONTRIBUTING.md)** | Development workflow, branching strategy, PR process |
|
||||
| **[Changelog](CHANGELOG.md)** | Complete release history (v0.0.1 to current) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**For comprehensive troubleshooting, see [docs/troubleshooting/README.md](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](CONTRIBUTING.md).**
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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](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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
- Wait 30 minutes for sync
|
||||
- Check [ArtifactHub package page](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin)
|
||||
- New version should appear in Headlamp plugin catalog
|
||||
|
||||
For complete release process and version numbering guidelines, see **[CONTRIBUTING.md#release-process](CONTRIBUTING.md#release-process)**.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read [CONTRIBUTING.md](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
|
||||
|
||||
## Links
|
||||
|
||||
- **[GitHub Repository](https://github.com/privilegedescalation/headlamp-polaris-plugin)** - Source code, issues, releases
|
||||
- **[Artifact Hub](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin)** - Plugin catalog listing
|
||||
- **[Headlamp](https://headlamp.dev/)** - Kubernetes web UI
|
||||
- **[Fairwinds Polaris](https://polaris.docs.fairwinds.com/)** - Kubernetes best practices audit tool
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0 License](LICENSE) - see LICENSE file for details.
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
# Security Policy
|
||||
|
||||
## Overview
|
||||
|
||||
The Headlamp Polaris Plugin is a read-only visualization tool that displays Fairwinds Polaris audit results within the Headlamp UI. Security considerations primarily revolve around Kubernetes RBAC, network policies, and data access controls.
|
||||
|
||||
## Security Model
|
||||
|
||||
### Read-Only Operation
|
||||
|
||||
The plugin performs **only read operations** via the Kubernetes API server's service proxy mechanism:
|
||||
|
||||
- **No write operations**: The plugin never creates, updates, or deletes Kubernetes resources
|
||||
- **No CRD installation**: No custom resource definitions or cluster-level modifications
|
||||
- **No secrets**: The plugin does not read or store Kubernetes secrets
|
||||
- **No PII**: Polaris audit data contains resource metadata but no personally identifiable information
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Browser
|
||||
↓ (HTTPS)
|
||||
Headlamp Pod
|
||||
↓ (in-cluster service account or user token)
|
||||
Kubernetes API Server
|
||||
↓ (service proxy: /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/)
|
||||
Polaris Dashboard Service
|
||||
↓ (returns audit JSON)
|
||||
Plugin Frontend (React)
|
||||
```
|
||||
|
||||
All communication uses Kubernetes authentication and authorization mechanisms. The plugin never stores credentials or bypasses RBAC.
|
||||
|
||||
## RBAC Requirements
|
||||
|
||||
### Minimal Permissions
|
||||
|
||||
The plugin requires only one permission:
|
||||
|
||||
| Verb | API Group | Resource | Resource Name | Namespace |
|
||||
|------|-----------|----------|---------------|-----------|
|
||||
| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` |
|
||||
|
||||
**Example minimal Role:**
|
||||
|
||||
```yaml
|
||||
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"]
|
||||
```
|
||||
|
||||
### RoleBinding Options
|
||||
|
||||
**Option 1: Service Account (Recommended)**
|
||||
|
||||
Bind to the Headlamp service account for all users:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Option 2: OIDC Groups**
|
||||
|
||||
Bind to user groups for OIDC authentication:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: "developers"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Option 3: Specific Users**
|
||||
|
||||
Bind to individual users:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: User
|
||||
name: "jane@example.com"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### ⚠️ Security Best Practices
|
||||
|
||||
1. **Principle of Least Privilege**: Grant only `services/proxy` access, not broader `services` permissions
|
||||
2. **Namespace Scoping**: Use a namespaced `Role`, not a `ClusterRole`, to limit access to the `polaris` namespace only
|
||||
3. **Resource Name Restriction**: Always specify `resourceNames: ["polaris-dashboard"]` to prevent proxy access to other services
|
||||
4. **Audit Logging**: Enable Kubernetes audit logging to track all service proxy requests
|
||||
5. **Network Policies**: Restrict network access to the Polaris dashboard service (see Network Security below)
|
||||
|
||||
## Network Security
|
||||
|
||||
### Network Policies
|
||||
|
||||
If your cluster uses NetworkPolicies, ensure the Headlamp pod (or more specifically, the Kubernetes API server performing the proxy hop) can reach the Polaris dashboard service.
|
||||
|
||||
**Example NetworkPolicy for Polaris namespace:**
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-api-server-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from API server (adjust based on your cluster setup)
|
||||
- from:
|
||||
- namespaceSelector: {} # API server typically runs in kube-system or no namespace label
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080 # Polaris dashboard default port
|
||||
```
|
||||
|
||||
**Note**: The Kubernetes API server performs the service proxy hop, so network policies should allow traffic from the API server to Polaris, not directly from Headlamp to Polaris.
|
||||
|
||||
### TLS/HTTPS
|
||||
|
||||
- **External Access**: Always access Headlamp over HTTPS, especially when using OIDC authentication
|
||||
- **Internal Communication**: Communication between Headlamp and the Kubernetes API server uses the service account token over the cluster's internal network
|
||||
- **Service Proxy**: The API server → Polaris dashboard communication happens over HTTP within the cluster (ClusterIP service)
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Service Account (Default)
|
||||
|
||||
Headlamp runs with a dedicated service account (`headlamp` in `kube-system`). All users share the same permissions defined by this service account's RBAC bindings.
|
||||
|
||||
**Security Considerations:**
|
||||
- All users have identical access to the plugin
|
||||
- Suitable for trusted internal environments
|
||||
- Simpler RBAC management
|
||||
|
||||
### OIDC Token Authentication
|
||||
|
||||
Headlamp can be configured for OIDC authentication, where each user provides their own bearer token. RBAC is enforced per-user.
|
||||
|
||||
**Security Considerations:**
|
||||
- Fine-grained access control per user
|
||||
- Users without the `polaris-proxy-reader` role will see 403 errors
|
||||
- Requires OIDC provider integration
|
||||
- Suitable for multi-tenant or compliance-focused environments
|
||||
|
||||
**Configuration Example:**
|
||||
|
||||
```yaml
|
||||
config:
|
||||
oidc:
|
||||
clientID: "headlamp"
|
||||
clientSecret: "secret"
|
||||
issuerURL: "https://authentik.example.com/application/o/headlamp/"
|
||||
scopes: "openid profile email groups"
|
||||
```
|
||||
|
||||
When OIDC is enabled, each user's token is used for API requests, including service proxy calls.
|
||||
|
||||
## Vulnerability Reporting
|
||||
|
||||
### Supported Versions
|
||||
|
||||
We apply security updates to the latest release only. Please ensure you are running the most recent version.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| latest | :white_check_mark: |
|
||||
| < latest| :x: |
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in this plugin, please report it via:
|
||||
|
||||
1. **GitHub Security Advisories**: [Report a vulnerability](https://github.com/privilegedescalation/headlamp-polaris-plugin/security/advisories/new)
|
||||
2. **Email**: Create a GitHub issue and mark it as "security" if advisories are not available
|
||||
|
||||
**Please do not:**
|
||||
- Open public GitHub issues for security vulnerabilities
|
||||
- Disclose vulnerabilities publicly before a fix is available
|
||||
|
||||
**Response Timeline:**
|
||||
- **Acknowledgment**: Within 48 hours
|
||||
- **Initial Assessment**: Within 1 week
|
||||
- **Fix Timeline**: Depends on severity (critical: 1-2 weeks, high: 2-4 weeks, medium/low: next release cycle)
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Dependency Scanning
|
||||
|
||||
The project uses:
|
||||
- **npm audit**: Runs automatically during `npm install`
|
||||
- **Dependabot**: GitHub Dependabot monitors dependencies and creates PRs for updates
|
||||
- **GitHub Actions**: CI workflow runs `npm audit` on every commit
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
Security patches are applied as follows:
|
||||
|
||||
1. **Critical vulnerabilities**: Emergency patch release within 48 hours
|
||||
2. **High severity**: Patched in next minor release (typically within 1-2 weeks)
|
||||
3. **Medium/Low severity**: Included in regular release cycle
|
||||
|
||||
### Headlamp Plugin API
|
||||
|
||||
This plugin depends on `@kinvolk/headlamp-plugin` as a peer dependency. Security updates to Headlamp itself should be applied by upgrading your Headlamp installation.
|
||||
|
||||
**Minimum supported Headlamp version**: v0.26.0
|
||||
|
||||
## Deployment Security
|
||||
|
||||
### Production Checklist
|
||||
|
||||
Before deploying to production, verify:
|
||||
|
||||
- [ ] **RBAC configured**: `polaris-proxy-reader` Role and RoleBinding exist
|
||||
- [ ] **Network policies**: Allow API server → Polaris dashboard traffic
|
||||
- [ ] **TLS enabled**: Headlamp accessible only via HTTPS
|
||||
- [ ] **OIDC configured** (if using per-user auth): Token-based authentication working
|
||||
- [ ] **Audit logging enabled**: Kubernetes API audit logs capture service proxy requests
|
||||
- [ ] **Plugin version**: Running latest release
|
||||
- [ ] **Dependencies audited**: No critical vulnerabilities in npm dependencies
|
||||
- [ ] **Polaris version**: Polaris dashboard is up-to-date
|
||||
|
||||
### Kubernetes Cluster Security
|
||||
|
||||
The plugin's security posture depends on your cluster's security:
|
||||
|
||||
- **API Server Access**: Ensure API server is not publicly accessible without authentication
|
||||
- **Service Account Tokens**: Use projected volume tokens with short expiration (Kubernetes 1.21+)
|
||||
- **Pod Security Standards**: Apply appropriate pod security policies/standards to the Headlamp namespace
|
||||
- **RBAC Auditing**: Regularly review RoleBindings to ensure least privilege
|
||||
|
||||
## Common Security Scenarios
|
||||
|
||||
### Scenario 1: 403 Forbidden Error
|
||||
|
||||
**Symptom**: Plugin shows "403 Forbidden" when loading data
|
||||
|
||||
**Cause**: User or service account lacks `services/proxy` permission on `polaris-dashboard`
|
||||
|
||||
**Resolution**:
|
||||
1. Verify RoleBinding exists in `polaris` namespace
|
||||
2. Check RoleBinding references correct subject (service account, group, or user)
|
||||
3. Confirm Role includes `resourceNames: ["polaris-dashboard"]`
|
||||
|
||||
**Security Note**: This is expected behavior when RBAC is correctly enforced. Do not grant broader permissions to "fix" 403 errors.
|
||||
|
||||
### Scenario 2: Exposing Polaris Dashboard Externally
|
||||
|
||||
**Question**: Can I expose Polaris dashboard via Ingress instead of using service proxy?
|
||||
|
||||
**Recommendation**: **Avoid exposing Polaris dashboard externally**. The service proxy approach:
|
||||
- Enforces Kubernetes RBAC on every request
|
||||
- Avoids exposing internal services to the internet
|
||||
- Prevents authentication bypass attacks
|
||||
|
||||
If you must expose Polaris externally:
|
||||
- Use OAuth2 proxy or similar authentication layer
|
||||
- Configure NetworkPolicies to restrict access
|
||||
- Enable TLS with valid certificates
|
||||
- Consider IP allowlisting
|
||||
|
||||
### Scenario 3: Multi-Tenant Clusters
|
||||
|
||||
**Question**: How do I restrict plugin access in a multi-tenant cluster?
|
||||
|
||||
**Solution**: Use OIDC authentication with per-user RoleBindings:
|
||||
|
||||
```yaml
|
||||
# Bind only to specific groups or users
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: "team-a"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Users not in `team-a` will receive 403 errors when accessing the plugin, preventing unauthorized access to Polaris audit data.
|
||||
|
||||
## Compliance Considerations
|
||||
|
||||
### Data Residency
|
||||
|
||||
All data remains within your Kubernetes cluster. The plugin does not:
|
||||
- Send data to external services
|
||||
- Store data in browser localStorage (except refresh interval preference)
|
||||
- Use third-party analytics or tracking
|
||||
|
||||
### Audit Trail
|
||||
|
||||
All service proxy requests are logged in Kubernetes API audit logs (if enabled):
|
||||
|
||||
```json
|
||||
{
|
||||
"verb": "get",
|
||||
"requestURI": "/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json",
|
||||
"user": {
|
||||
"username": "system:serviceaccount:kube-system:headlamp",
|
||||
"groups": ["system:serviceaccounts", "system:authenticated"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GDPR/Privacy
|
||||
|
||||
The plugin processes only technical metadata (resource names, namespaces, check results). No personal data is collected, stored, or transmitted.
|
||||
|
||||
## Security Updates and Notifications
|
||||
|
||||
### Notification Channels
|
||||
|
||||
Subscribe to security updates via:
|
||||
|
||||
1. **GitHub Watch**: Click "Watch" → "Custom" → "Security alerts"
|
||||
2. **GitHub Releases**: Monitor [releases page](https://github.com/privilegedescalation/headlamp-polaris-plugin/releases)
|
||||
3. **ArtifactHub**: Follow package at [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp-polaris-plugin/headlamp-polaris-plugin)
|
||||
|
||||
### Security Patch Process
|
||||
|
||||
When a security vulnerability is identified:
|
||||
|
||||
1. **Private Fix**: Develop fix in private fork
|
||||
2. **Security Advisory**: Publish GitHub Security Advisory
|
||||
3. **Release**: Create new version with fix
|
||||
4. **Notification**: Update advisory with fix version
|
||||
5. **Disclosure**: Public disclosure after fix is available
|
||||
|
||||
## Contact
|
||||
|
||||
- **Security Issues**: [GitHub Security Advisories](https://github.com/privilegedescalation/headlamp-polaris-plugin/security/advisories)
|
||||
- **General Questions**: [GitHub Discussions](https://github.com/privilegedescalation/headlamp-polaris-plugin/discussions)
|
||||
- **Bug Reports**: [GitHub Issues](https://github.com/privilegedescalation/headlamp-polaris-plugin/issues)
|
||||
|
||||
## License
|
||||
|
||||
This plugin is provided under the Apache-2.0 License. See [LICENSE](LICENSE) for details.
|
||||
@@ -0,0 +1,76 @@
|
||||
version: "1.0.0"
|
||||
name: headlamp-polaris
|
||||
displayName: Polaris
|
||||
createdAt: "2026-02-05T19:00:00Z"
|
||||
description: >-
|
||||
Surfaces Fairwinds Polaris audit results inside the Headlamp UI.
|
||||
Shows cluster score, check summary, and per-namespace drill-downs
|
||||
with per-resource pass/warning/danger breakdowns. Data is fetched
|
||||
read-only via the Kubernetes service proxy to the Polaris dashboard.
|
||||
Requires a Role granting `get` on `services/proxy` for the
|
||||
`polaris-dashboard` service in the `polaris` namespace.
|
||||
license: Apache-2.0
|
||||
homeURL: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
|
||||
appVersion: "10.1.6"
|
||||
category: security
|
||||
keywords:
|
||||
- polaris
|
||||
- fairwinds
|
||||
- security
|
||||
- audit
|
||||
- headlamp
|
||||
- kubernetes
|
||||
links:
|
||||
- name: Source
|
||||
url: "https://github.com/privilegedescalation/headlamp-polaris-plugin"
|
||||
- name: Polaris
|
||||
url: "https://polaris.docs.fairwinds.com/"
|
||||
install: |
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. [Headlamp](https://headlamp.dev) v0.26.0 or later
|
||||
2. [Fairwinds Polaris](https://polaris.docs.fairwinds.com/) installed and the dashboard running in your cluster
|
||||
|
||||
### Install via Headlamp Plugin Catalog
|
||||
|
||||
1. Open Headlamp and navigate to **Settings → Plugin Catalog**
|
||||
2. Search for **"Polaris"**
|
||||
3. Click **Install** and restart Headlamp when prompted
|
||||
|
||||
The plugin is sourced directly from [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp/headlamp-polaris).
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, the Polaris plugin adds:
|
||||
- A **cluster score badge** in the Headlamp app bar
|
||||
- A **Polaris** section in the sidebar with the full dashboard and namespace drill-downs
|
||||
- An **inline audit panel** on Deployment, StatefulSet, DaemonSet, Job, and CronJob detail pages
|
||||
|
||||
For more information, see the [README](https://github.com/privilegedescalation/headlamp-polaris-plugin/blob/main/README.md).
|
||||
changes:
|
||||
- kind: security
|
||||
description: Patched 8 npm audit vulnerabilities via pnpm.overrides
|
||||
- kind: added
|
||||
description: Dual-approval required CI check — PRs must be approved by both CTO and QA before merge
|
||||
- kind: added
|
||||
description: ExemptionManager test suite — full coverage of annotation-based exemption flows
|
||||
- kind: fixed
|
||||
description: E2E infrastructure overhauled — ConfigMap volume mount replaces Dockerfile-based approach, tests run in privilegedescalation-dev namespace
|
||||
- kind: fixed
|
||||
description: E2E workflow uses token auth and waits for HTTP reachability before running tests
|
||||
- kind: fixed
|
||||
description: Added explicit direct devDependencies (typescript, eslint, prettier, @headlamp-k8s/eslint-config) to prevent phantom dep failures
|
||||
- kind: changed
|
||||
description: pnpm version pinned via packageManager field; GitHub Actions SHA-pinned via Renovate pinDigests
|
||||
- kind: changed
|
||||
description: v1.0.0 stable release — plugin API (routes, sidebar, settings schema, app bar action) is stable and will not change without a major version bump
|
||||
maintainers:
|
||||
- name: privilegedescalation
|
||||
email: "chris@farhood.org"
|
||||
annotations:
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v1.0.0/headlamp-polaris-1.0.0.tar.gz"
|
||||
headlamp/plugin/version-compat: ">=0.26"
|
||||
headlamp/plugin/archive-checksum: sha256:a165e871b40f11a44950aa9f10eb7f7883276f749026ae7a4f886278ecd9bd7d
|
||||
headlamp/plugin/distro-compat: "in-cluster,web,desktop"
|
||||
@@ -1,4 +1,4 @@
|
||||
repositoryID: polaris-headlamp-plugin
|
||||
repositoryID: 0243bdaf-c926-44dc-b411-a7c291bf1fcd
|
||||
owners:
|
||||
- name: farhoodliquor
|
||||
email: ""
|
||||
- name: privilegedescalation
|
||||
email: "chris@farhood.org"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Headlamp plugin that surfaces Fairwinds Polaris audit results inside the Headlamp UI. Reads from `ConfigMap/polaris-dashboard` in the `polaris` namespace (key: `dashboard.json`). Target Headlamp ≥ v0.26.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the plugin (standard Headlamp plugin build)
|
||||
npx @kinvolk/headlamp-plugin build
|
||||
|
||||
# Start development mode with hot reload
|
||||
npx @kinvolk/headlamp-plugin start
|
||||
|
||||
# Type-check without emitting
|
||||
npx tsc --noEmit
|
||||
|
||||
# Lint
|
||||
npx eslint src/
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.tsx # Entry point: registerSidebarEntry + registerRoute for /polaris
|
||||
├── api/
|
||||
│ └── polaris.ts # Types (AuditData schema), usePolarisData hook, countResults utility, refresh settings
|
||||
└── components/
|
||||
└── PolarisView.tsx # Main page: score badge, check summary, cluster info, error states, refresh interval selector
|
||||
```
|
||||
|
||||
Single sidebar page at `/polaris`. Data is cached in React state and refreshed on a user-configurable interval (stored in localStorage under `polaris-plugin-refresh-interval`, default 5 minutes). The `usePolarisData` hook wraps `ConfigMap.useGet` with caching so stale data is shown while refreshing.
|
||||
|
||||
## Key Constraints
|
||||
|
||||
- **Data source**: `ConfigMap/polaris-dashboard` in `polaris` namespace, key `dashboard.json`. No CRDs, no external API calls, no cluster write operations.
|
||||
- **UI components**: Use only Headlamp-provided components (`@kinvolk/headlamp-plugin/lib/CommonComponents`). Do not import raw MUI packages. No custom theming.
|
||||
- **Error handling**: Must handle 403 (RBAC denied), 404 (Polaris not installed), malformed JSON, and loading states with distinct visual states.
|
||||
- **TypeScript strictness**: No `any`, no implicit `unknown` casting, no dead code, no unused imports.
|
||||
- **Packaging**: `@kinvolk/headlamp-plugin` is a peer dependency. Do not bundle React or MUI.
|
||||
|
||||
## MCP Servers
|
||||
|
||||
The project has MCP server integrations configured in `.mcp.json`:
|
||||
- **Gitea** (git.farh.net): Source control via `gitea-mcp-server`
|
||||
- **Kubernetes** (local): Cluster access via `kubernetes-mcp-server`
|
||||
- **Flux** (local): Flux Operator access via `flux-operator-mcp`
|
||||
@@ -0,0 +1,58 @@
|
||||
# Headlamp Plugin Loading Issue - Root Cause and Fix
|
||||
|
||||
## Problem
|
||||
Headlamp v0.39.0 was not loading plugins installed via the plugin manager. Plugins appeared in Settings → Plugins but:
|
||||
- No sidebar entries appeared
|
||||
- No plugin settings were available
|
||||
- Plugin JavaScript was not being executed in the browser
|
||||
|
||||
## Root Cause
|
||||
When `config.watchPlugins: true` (the default), Headlamp treats catalog-managed plugins in `/headlamp/plugins/` as "development directory" plugins. This causes:
|
||||
- Backend serves plugin metadata correctly
|
||||
- Backend logs show "Treating catalog-installed plugin in development directory as user plugin"
|
||||
- **Frontend does NOT execute the plugin JavaScript**
|
||||
- Plugin registrations (`registerSidebarEntry`, `registerRoute`, etc.) never happen
|
||||
|
||||
## Solution
|
||||
Set `config.watchPlugins: false` in the Headlamp HelmRelease values:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
values:
|
||||
config:
|
||||
watchPlugins: false
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
configContent: |
|
||||
plugins:
|
||||
- name: polaris
|
||||
source: https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin
|
||||
# ... other plugins
|
||||
```
|
||||
|
||||
## Why This Works
|
||||
With `watchPlugins: false`:
|
||||
- Headlamp no longer treats catalog-managed plugins as "development" plugins
|
||||
- Frontend properly loads and executes plugin JavaScript on startup
|
||||
- Plugin registrations happen correctly
|
||||
- All plugin features (sidebar, routes, settings, etc.) work as expected
|
||||
|
||||
## Testing
|
||||
After applying this fix:
|
||||
1. Verify plugins are installed: `kubectl logs -n kube-system <headlamp-pod> -c headlamp-plugin`
|
||||
2. Verify watchPlugins is false: `kubectl logs -n kube-system <headlamp-pod> -c headlamp | grep "Watch Plugins"`
|
||||
3. Hard refresh browser (Cmd+Shift+R / Ctrl+Shift+F5) to clear cached JavaScript
|
||||
4. Verify plugin sidebar entries appear
|
||||
5. Verify plugin functionality works
|
||||
|
||||
## Additional Notes
|
||||
- This appears to be a bug/limitation in Headlamp v0.39.0
|
||||
- The `watchPlugins` feature is intended for development scenarios where plugins are being actively modified
|
||||
- For production deployments with catalog-managed plugins, `watchPlugins: false` is the correct configuration
|
||||
- Once plugins are loaded, subsequent restarts or updates work correctly as long as `watchPlugins` remains false
|
||||
|
||||
## References
|
||||
- Headlamp Helm Chart: https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp
|
||||
- Plugin Manager: https://github.com/headlamp-k8s/headlamp/tree/main/plugins/headlamp-plugin
|
||||
- Issue discovered: 2026-02-11
|
||||
- Fix applied: 2026-02-12
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
# RBAC for the GitHub Actions CI runner to manage the E2E Headlamp instance.
|
||||
# CI-only test fixture — NOT for production use.
|
||||
#
|
||||
# Grants the ARC runner service account permissions in the privilegedescalation-dev
|
||||
# namespace to deploy and tear down a dedicated Headlamp instance via Helm.
|
||||
# E2E resources run in `privilegedescalation-dev` — nothing persists beyond a test run.
|
||||
#
|
||||
# Plugin is loaded via ConfigMap volume mount — no custom Docker images.
|
||||
#
|
||||
# Prerequisites:
|
||||
# kubectl apply -f deployment/e2e-ci-runner-rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: e2e-ci-runner
|
||||
namespace: privilegedescalation-dev
|
||||
rules:
|
||||
# Helm needs to manage these resources for the Headlamp chart
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services", "serviceaccounts", "configmaps", "secrets"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Token creation for E2E test auth
|
||||
- apiGroups: [""]
|
||||
resources: ["serviceaccounts/token"]
|
||||
verbs: ["create"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: e2e-ci-runner-binding
|
||||
namespace: privilegedescalation-dev
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: runners-privilegedescalation-gha-rs-no-permission
|
||||
namespace: arc-runners
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: e2e-ci-runner
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@@ -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,593 @@
|
||||
# Architecture
|
||||
|
||||
This document describes the architecture, design decisions, and data flow of the Headlamp Polaris Plugin.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [System Architecture](#system-architecture)
|
||||
- [Data Flow](#data-flow)
|
||||
- [Component Hierarchy](#component-hierarchy)
|
||||
- [State Management](#state-management)
|
||||
- [Design Decisions](#design-decisions)
|
||||
- [Integration Points](#integration-points)
|
||||
- [Known Limitations](#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:**
|
||||
|
||||
1. **Simple state:** Single AuditData object shared across views
|
||||
2. **Read-only:** No complex mutations or transactions
|
||||
3. **Headlamp constraints:** Plugin cannot add dependencies (Redux not bundled)
|
||||
4. **Performance:** Data changes infrequently (refresh interval 1-30 min)
|
||||
|
||||
### Context Structure
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
1. **Initial fetch:** On first mount of any component using the context
|
||||
2. **Auto-refresh:** Based on user setting (default 5 minutes)
|
||||
3. **Manual refresh:** Via refresh button in UI
|
||||
4. **Caching:** Data persists in context until refresh (no per-route refetch)
|
||||
|
||||
### localStorage Usage
|
||||
|
||||
Settings persisted in localStorage:
|
||||
|
||||
- **`polaris-plugin-refresh-interval`**: Number (seconds), default 300
|
||||
- **`polaris-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 `get` permission on `services/proxy` resource
|
||||
- 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 → <each namespace>) 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
|
||||
- `useEffect` watches 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 undefined` error
|
||||
- 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:**
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
|
||||
- `registerDetailsViewSection` now takes 1 argument (component), not 2 (name, component)
|
||||
- `registerAppBarAction` now takes 1 argument (component), not 2 (name, component)
|
||||
|
||||
### Headlamp CommonComponents
|
||||
|
||||
**Used Components:**
|
||||
|
||||
- `SectionBox` - Card-like container with title
|
||||
- `SectionHeader` - Page header with title
|
||||
- `StatusLabel` - Color-coded status badges
|
||||
- `NameValueTable` - Key-value table layout
|
||||
- `SimpleTable` - Data table with sorting
|
||||
- `Drawer` - Right-side overlay panel
|
||||
- `Loader` - 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:**
|
||||
|
||||
- `get` on `services/proxy` for `polaris-dashboard` in `polaris` namespace
|
||||
|
||||
## 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
|
||||
|
||||
1. **WebWorker for data processing**
|
||||
|
||||
- Offload `countResults()` aggregation for large clusters
|
||||
- Keep UI responsive during heavy computation
|
||||
|
||||
2. **IndexedDB caching**
|
||||
|
||||
- Cache audit data offline
|
||||
- Show stale data + "refresh available" indicator
|
||||
|
||||
3. **GraphQL/REST API abstraction**
|
||||
|
||||
- Decouple from Polaris dashboard JSON format
|
||||
- Support multiple backend sources
|
||||
|
||||
4. **Plugin-to-plugin communication**
|
||||
|
||||
- Integrate with other Headlamp plugins (e.g., policy enforcement)
|
||||
- Shared state between plugins
|
||||
|
||||
5. **Incremental updates**
|
||||
- Fetch only changed namespaces/resources
|
||||
- Reduce bandwidth and processing
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Fairwinds Polaris Documentation](https://polaris.docs.fairwinds.com/)
|
||||
- [React Context API](https://react.dev/reference/react/useContext)
|
||||
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
@@ -0,0 +1,672 @@
|
||||
# Deployment Guide
|
||||
|
||||
This document provides comprehensive deployment instructions for the Headlamp Polaris Plugin in production Kubernetes environments.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation Methods](#installation-methods)
|
||||
- [Helm Integration](#helm-integration)
|
||||
- [RBAC Configuration](#rbac-configuration)
|
||||
- [Network Policies](#network-policies)
|
||||
- [Plugin Manager Setup](#plugin-manager-setup)
|
||||
- [Production Checklist](#production-checklist)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Components
|
||||
|
||||
1. **Kubernetes Cluster:** v1.19 or later
|
||||
2. **Headlamp:** v0.26 or later (v0.39+ recommended)
|
||||
3. **Polaris:** Deployed and accessible via service
|
||||
4. **RBAC:** Permissions to create Roles and RoleBindings
|
||||
|
||||
### Pre-Deployment Verification
|
||||
|
||||
```bash
|
||||
# Verify Polaris is deployed
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# Verify Polaris dashboard is responding
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Verify Headlamp is deployed
|
||||
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
```
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Method 1: Headlamp Plugin Manager (Recommended)
|
||||
|
||||
**Best for:** Production deployments, managed updates
|
||||
|
||||
1. **Enable Plugin Manager in Headlamp:**
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
```
|
||||
|
||||
2. **Deploy/Update Headlamp:**
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
3. **Install Plugin via UI:**
|
||||
- Navigate to Headlamp → Settings → Plugins
|
||||
- Search for "Polaris"
|
||||
- Click "Install"
|
||||
- Refresh browser (Cmd+Shift+R or Ctrl+Shift+R)
|
||||
|
||||
### Method 2: Sidecar Container (Alternative)
|
||||
|
||||
**Best for:** Controlled plugin versions, air-gapped environments
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
initContainers:
|
||||
- name: install-polaris-plugin
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml --plugins-dir /plugins
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
version: 0.3.4
|
||||
url: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
### Method 3: Volume Mount (Development)
|
||||
|
||||
**Best for:** Local testing, development
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: '/plugins'
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
hostPath:
|
||||
path: /path/to/plugins
|
||||
type: Directory
|
||||
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
readOnly: true
|
||||
```
|
||||
|
||||
Then manually place `headlamp-polaris-plugin/` in the host path.
|
||||
|
||||
## Helm Integration
|
||||
|
||||
### Complete Helm Values Example
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: ghcr.io/headlamp-k8s/headlamp
|
||||
tag: v0.39.0
|
||||
|
||||
config:
|
||||
baseURL: ''
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: headlamp-tls
|
||||
hosts:
|
||||
- headlamp.example.com
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: headlamp
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
# OIDC Authentication (optional)
|
||||
env:
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_ID
|
||||
value: 'headlamp'
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: headlamp-oidc
|
||||
key: client-secret
|
||||
- name: HEADLAMP_CONFIG_OIDC_ISSUER_URL
|
||||
value: 'https://auth.example.com/realms/kubernetes'
|
||||
- name: HEADLAMP_CONFIG_OIDC_SCOPES
|
||||
value: 'openid,profile,email,groups'
|
||||
```
|
||||
|
||||
### FluxCD HelmRelease Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
interval: 30m
|
||||
chart:
|
||||
spec:
|
||||
chart: headlamp
|
||||
version: 0.26.x
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: headlamp
|
||||
namespace: flux-system
|
||||
interval: 12h
|
||||
|
||||
values:
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
## RBAC Configuration
|
||||
|
||||
### Minimal Role for Plugin
|
||||
|
||||
The plugin requires **read-only** access to the Polaris dashboard service proxy.
|
||||
|
||||
```yaml
|
||||
---
|
||||
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']
|
||||
```
|
||||
|
||||
### RoleBinding Options
|
||||
|
||||
#### Option A: Headlamp Service Account (In-Cluster Mode)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
#### Option B: User Groups (Token/OIDC Mode)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: users-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: system:authenticated # All authenticated users
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
#### Option C: Specific Users (Fine-Grained Control)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: devops-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: User
|
||||
name: alice@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: User
|
||||
name: bob@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: Group
|
||||
name: devops-team
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Complete RBAC Manifest
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Role: Read-only access to Polaris service proxy
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
rules:
|
||||
- apiGroups: ['']
|
||||
resources: ['services/proxy']
|
||||
resourceNames: ['polaris-dashboard']
|
||||
verbs: ['get']
|
||||
|
||||
---
|
||||
# RoleBinding: Grant Headlamp service account access
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Apply with:
|
||||
|
||||
```bash
|
||||
kubectl apply -f polaris-plugin-rbac.yaml
|
||||
```
|
||||
|
||||
## Network Policies
|
||||
|
||||
### Required Network Access
|
||||
|
||||
The plugin requires network connectivity:
|
||||
|
||||
- **Headlamp pod** → **Kubernetes API server** (service proxy)
|
||||
- **Kubernetes API server** → **Polaris dashboard service** (port 80)
|
||||
|
||||
### Network Policy Example
|
||||
|
||||
If your `polaris` namespace has strict NetworkPolicies:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-headlamp-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
app.kubernetes.io/component: dashboard
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from API server (service proxy)
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
component: kube-apiserver
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
```
|
||||
|
||||
**Note:** The API server performs the proxy hop, not the Headlamp pod directly.
|
||||
|
||||
## Plugin Manager Setup
|
||||
|
||||
### Plugin Manager Verification
|
||||
|
||||
```bash
|
||||
# Check plugin is installed
|
||||
kubectl -n kube-system exec -it deployment/headlamp -- ls -la /headlamp/plugins/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
|
||||
- [ ] Polaris deployed and running
|
||||
- [ ] Polaris dashboard service exists (`polaris-dashboard` in `polaris` namespace)
|
||||
- [ ] RBAC Role and RoleBinding created
|
||||
- [ ] Headlamp v0.26+ deployed
|
||||
|
||||
### Deployment
|
||||
|
||||
- [ ] Plugin installed via plugin manager or sidecar
|
||||
- [ ] Headlamp pods restarted (if config changed)
|
||||
- [ ] Browser cache cleared (Cmd+Shift+R / Ctrl+Shift+R)
|
||||
|
||||
### Post-Deployment Verification
|
||||
|
||||
```bash
|
||||
# 1. Verify Polaris is accessible via service proxy
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Expected: "1.0" or similar
|
||||
|
||||
# 2. Verify RBAC is correct
|
||||
kubectl auth can-i get services/proxy --as=system:serviceaccount:kube-system:headlamp -n polaris --resource-name=polaris-dashboard
|
||||
|
||||
# Expected: yes
|
||||
|
||||
# 3. Check Headlamp logs
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# Expected: No errors related to plugin loading
|
||||
|
||||
# 4. Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected: dist/, package.json present
|
||||
```
|
||||
|
||||
### UI Verification
|
||||
|
||||
- [ ] Navigate to Headlamp → Settings → Plugins
|
||||
- [ ] Plugin "headlamp-polaris-plugin" listed
|
||||
- [ ] Sidebar shows "Polaris" entry
|
||||
- [ ] Click "Polaris" → Overview page loads
|
||||
- [ ] Cluster score displays correctly
|
||||
- [ ] Namespaces page shows table
|
||||
- [ ] App bar shows Polaris score badge
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Appearing in Sidebar
|
||||
|
||||
**Symptom:** Plugin listed in Settings → Plugins but no sidebar entry
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. Browser cache not cleared
|
||||
2. Plugin files not properly installed
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Restart Headlamp
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
|
||||
# Clear browser cache
|
||||
# Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux)
|
||||
```
|
||||
|
||||
### 403 Forbidden Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, 403 in console
|
||||
|
||||
**Cause:** RBAC missing or incorrect
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Verify RBAC exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy --as=system:serviceaccount:kube-system:headlamp -n polaris --resource-name=polaris-dashboard
|
||||
|
||||
# If "no", create RBAC (see RBAC Configuration section)
|
||||
```
|
||||
|
||||
### 404 Not Found Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, 404 in console
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. Polaris not deployed
|
||||
2. Polaris service name wrong
|
||||
3. Polaris namespace wrong
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Check Polaris deployment
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# If service doesn't exist, install Polaris:
|
||||
helm install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
### Custom Dashboard URL Not Working
|
||||
|
||||
**Symptom:** Error when using custom Polaris URL in settings
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. URL format incorrect
|
||||
2. CORS not configured on external Polaris
|
||||
3. Network policy blocking external access
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Test URL manually
|
||||
curl -v https://my-polaris.example.com/results.json
|
||||
|
||||
# For external Polaris, check CORS headers
|
||||
# Must allow Headlamp origin
|
||||
```
|
||||
|
||||
### Plugin Shows Old Version
|
||||
|
||||
**Symptom:** Plugin version in Settings doesn't match expected
|
||||
|
||||
**Cause:** Plugin manager hasn't synced from ArtifactHub
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Wait 30 minutes (ArtifactHub sync interval)
|
||||
# Or manually refresh plugin list in Headlamp UI
|
||||
|
||||
# Force Headlamp restart
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
```
|
||||
|
||||
### Network Policy Blocking Access
|
||||
|
||||
**Symptom:** Timeout or connection errors despite correct RBAC
|
||||
|
||||
**Cause:** NetworkPolicy in `polaris` namespace blocking API server
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Check NetworkPolicies
|
||||
kubectl -n polaris get networkpolicy
|
||||
|
||||
# Test connectivity from API server (if possible)
|
||||
# Add NetworkPolicy to allow API server → Polaris dashboard (see Network Policies section)
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Least Privilege
|
||||
|
||||
- Grant only `get` on `services/proxy`, not broader permissions
|
||||
- Use `resourceNames` to restrict to specific service (`polaris-dashboard`)
|
||||
- Scope to `polaris` namespace only (Role, not ClusterRole)
|
||||
|
||||
### Audit Logging
|
||||
|
||||
Kubernetes audit logs will record:
|
||||
|
||||
- User/service account accessing service proxy
|
||||
- Timestamp and response code
|
||||
|
||||
Configure audit policy if needed:
|
||||
|
||||
```yaml
|
||||
apiVersion: audit.k8s.io/v1
|
||||
kind: Policy
|
||||
rules:
|
||||
- level: Metadata
|
||||
verbs: ['get']
|
||||
resources:
|
||||
- group: ''
|
||||
resources: ['services/proxy']
|
||||
namespaces: ['polaris']
|
||||
```
|
||||
|
||||
### Data Sensitivity
|
||||
|
||||
Polaris audit data may contain:
|
||||
|
||||
- Resource names and namespaces
|
||||
- Configuration details
|
||||
- Potential security vulnerabilities
|
||||
|
||||
**Recommendation:** Restrict plugin access to authorized users only (not `system:authenticated` group unless appropriate).
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Plugin Upgrade via Plugin Manager
|
||||
|
||||
1. Navigate to Settings → Plugins
|
||||
2. Find "headlamp-polaris-plugin"
|
||||
3. Click "Update" if new version available
|
||||
4. Refresh browser (Cmd+Shift+R / Ctrl+Shift+R)
|
||||
|
||||
### Sidecar Method Upgrade
|
||||
|
||||
1. Update ConfigMap with new version/URL
|
||||
2. Restart Headlamp deployment
|
||||
3. Verify new version in Settings → Plugins
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system edit configmap headlamp-plugin-config
|
||||
# Update version and URL
|
||||
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Deployment](https://headlamp.dev/docs/latest/installation/)
|
||||
- [Headlamp Helm Chart](https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp)
|
||||
- [Polaris Installation](https://polaris.docs.fairwinds.com/infrastructure-as-code/)
|
||||
- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/#manually-constructing-apiserver-proxy-urls)
|
||||
@@ -0,0 +1,606 @@
|
||||
# Documentation Standardization Plan
|
||||
|
||||
**Date**: 2026-02-12
|
||||
**Repositories**: headlamp-polaris-plugin, headlamp-sealed-secrets-plugin
|
||||
**Goal**: Establish consistent documentation standards across Headlamp plugin projects
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This plan standardizes documentation structure, formatting, and content across two Headlamp plugins to create a consistent, professional documentation experience. The standardization adopts the best practices from both repositories while maintaining each plugin's unique technical content.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Polaris Plugin (v0.3.5)
|
||||
|
||||
**Structure**: Topic-focused with monolithic files
|
||||
**Strengths**:
|
||||
|
||||
- Comprehensive CONTRIBUTING.md with branching strategy and commit conventions
|
||||
- Complete CHANGELOG.md (35 versions documented)
|
||||
- Dedicated SECURITY.md with vulnerability reporting
|
||||
- JSDoc comments on all API exports
|
||||
- CI/CD badges in README
|
||||
- Well-organized TROUBLESHOOTING.md with common issues
|
||||
|
||||
**Gaps**:
|
||||
|
||||
- No user journey-based organization
|
||||
- No Architecture Decision Records
|
||||
- Limited quick-start tutorials
|
||||
- No FAQ section
|
||||
- Deployment guide is monolithic (needs breakdown)
|
||||
|
||||
### Sealed Secrets Plugin
|
||||
|
||||
**Structure**: User journey-based with granular topic files
|
||||
**Strengths**:
|
||||
|
||||
- Excellent user journey organization (Getting Started → User Guide → Tutorials)
|
||||
- Architecture Decision Records (5 ADRs)
|
||||
- Quick diagnosis flowchart in troubleshooting
|
||||
- Multi-platform installation guides
|
||||
- Auto-generated API reference
|
||||
- Visual hierarchy with strategic emoji use
|
||||
|
||||
**Gaps**:
|
||||
|
||||
- No dedicated CONTRIBUTING.md (content in README)
|
||||
- No SECURITY.md for vulnerability reporting
|
||||
- Incomplete tutorial placeholders
|
||||
- No comprehensive CHANGELOG
|
||||
- Missing E2E testing documentation
|
||||
|
||||
## Standardization Principles
|
||||
|
||||
### 1. File Structure Standard
|
||||
|
||||
**Root-Level Files** (Common to Both):
|
||||
|
||||
```
|
||||
README.md # Main entry point with badges, quick links
|
||||
CHANGELOG.md # Keep a Changelog format, semantic versioning
|
||||
CONTRIBUTING.md # Development workflow, branching, PR process
|
||||
SECURITY.md # Security model, vulnerability reporting, RBAC
|
||||
LICENSE # Apache-2.0 License
|
||||
package.json # Plugin metadata
|
||||
```
|
||||
|
||||
**Documentation Directory** (Organized by User Journey):
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # Documentation hub with quick links
|
||||
├── getting-started/
|
||||
│ ├── installation.md
|
||||
│ ├── prerequisites.md
|
||||
│ └── quick-start.md
|
||||
├── user-guide/
|
||||
│ ├── features.md
|
||||
│ ├── configuration.md
|
||||
│ └── rbac-permissions.md
|
||||
├── tutorials/
|
||||
│ └── (plugin-specific)
|
||||
├── troubleshooting/
|
||||
│ ├── README.md (quick diagnosis)
|
||||
│ └── common-issues.md
|
||||
├── architecture/
|
||||
│ ├── overview.md
|
||||
│ ├── data-flow.md
|
||||
│ ├── design-decisions.md
|
||||
│ └── adr/ (Architecture Decision Records)
|
||||
├── development/
|
||||
│ ├── workflow.md
|
||||
│ ├── testing.md
|
||||
│ ├── code-style.md
|
||||
│ └── release-process.md
|
||||
└── deployment/
|
||||
├── kubernetes.md
|
||||
├── helm.md
|
||||
└── production.md
|
||||
```
|
||||
|
||||
### 2. README.md Standard
|
||||
|
||||
**Required Sections** (Order Matters):
|
||||
|
||||
1. Title + Badges (ArtifactHub, CI, E2E, License)
|
||||
2. Quick navigation links (📚 Documentation | 🚀 Installation | 🔒 Security | 🛠️ Development)
|
||||
3. **What It Does** (features with visual hierarchy)
|
||||
4. **Prerequisites** (table format)
|
||||
5. **Installing** (4 options: Plugin Manager, Sidecar, Manual, Source)
|
||||
6. **RBAC / Security Setup** (minimal manifests)
|
||||
7. **Documentation** (table linking to docs/)
|
||||
8. **Troubleshooting** (quick reference table + link to full guide)
|
||||
9. **Development** (quick start commands + link to CONTRIBUTING.md)
|
||||
10. **Known Limitations** (if applicable)
|
||||
11. **Releasing** (brief + link to development/release-process.md)
|
||||
12. **Contributing** (link to CONTRIBUTING.md)
|
||||
13. **Links** (GitHub, ArtifactHub, Headlamp, related tools)
|
||||
14. **License** (MIT with link)
|
||||
15. Footer ("Made with ❤️ for the Kubernetes community")
|
||||
|
||||
**Formatting Standards**:
|
||||
|
||||
- Use emojis strategically for visual scanning (not excessive)
|
||||
- Quick navigation at top
|
||||
- Tables for structured data (prerequisites, troubleshooting quick ref)
|
||||
- Code blocks with language hints
|
||||
- Keep main README under 400 lines (details go in docs/)
|
||||
|
||||
### 3. CHANGELOG.md Standard
|
||||
|
||||
**Format**: Keep a Changelog (https://keepachangelog.com/)
|
||||
|
||||
**Structure**:
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
All notable changes will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [X.Y.Z] - YYYY-MM-DD
|
||||
|
||||
### Added
|
||||
|
||||
- New features
|
||||
|
||||
### Changed
|
||||
|
||||
- Changes to existing functionality
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Soon-to-be removed features
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed features
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug fixes
|
||||
|
||||
### Security
|
||||
|
||||
- Security fixes
|
||||
|
||||
[Unreleased]: https://github.com/user/repo/compare/vX.Y.Z...HEAD
|
||||
[X.Y.Z]: https://github.com/user/repo/releases/tag/vX.Y.Z
|
||||
```
|
||||
|
||||
**Standards**:
|
||||
|
||||
- One entry per version, newest first
|
||||
- Date in ISO 8601 format (YYYY-MM-DD)
|
||||
- Link to GitHub release
|
||||
- Group changes by type (Added, Changed, Fixed, Security)
|
||||
- Keep descriptions concise (1-2 lines per item)
|
||||
|
||||
### 4. CONTRIBUTING.md Standard
|
||||
|
||||
**Required Sections**:
|
||||
|
||||
1. Code of Conduct (brief, respectful)
|
||||
2. Getting Started (prerequisites, setup)
|
||||
3. Development Workflow (feature development, testing)
|
||||
4. Branching Strategy (feat/, fix/, docs/, chore/)
|
||||
5. Commit Message Guidelines (Conventional Commits)
|
||||
6. Pull Request Process (before creating, creating, review)
|
||||
7. Code Style (TypeScript, React, linting, formatting)
|
||||
8. Testing Requirements (unit, E2E, coverage goals)
|
||||
9. Documentation (when to update docs)
|
||||
10. Release Process (version numbering, creating releases)
|
||||
|
||||
**Formatting**:
|
||||
|
||||
- Use tables for branch naming conventions
|
||||
- Code blocks for commit message examples
|
||||
- Checklists for PR requirements
|
||||
- Links to detailed guides in docs/development/
|
||||
|
||||
### 5. SECURITY.md Standard
|
||||
|
||||
**Required Sections**:
|
||||
|
||||
1. Overview (security model, read-only vs. write operations)
|
||||
2. Data Flow Diagram (how data moves through system)
|
||||
3. RBAC Requirements (minimal permissions table)
|
||||
4. Network Security (NetworkPolicies, TLS)
|
||||
5. Authentication Methods (service account, OIDC)
|
||||
6. Vulnerability Reporting (supported versions table, how to report)
|
||||
7. Dependency Security (scanning, update process)
|
||||
8. Deployment Security (production checklist)
|
||||
9. Common Security Scenarios (FAQs with solutions)
|
||||
10. Compliance Considerations (audit trail, GDPR/privacy)
|
||||
|
||||
**Formatting**:
|
||||
|
||||
- Tables for permissions and supported versions
|
||||
- YAML examples for RBAC manifests
|
||||
- Bash commands for security verification
|
||||
- Clear "Do NOT" warnings for unsafe practices
|
||||
|
||||
### 6. Documentation Hub (docs/README.md) Standard
|
||||
|
||||
**Purpose**: Central navigation for all documentation
|
||||
|
||||
**Structure**:
|
||||
|
||||
```markdown
|
||||
# Documentation
|
||||
|
||||
Central hub for [Plugin Name] documentation.
|
||||
|
||||
## Quick Links
|
||||
|
||||
- 🚀 [Quick Start](getting-started/quick-start.md)
|
||||
- 📖 [User Guide](user-guide/README.md)
|
||||
- 🔧 [Troubleshooting](troubleshooting/README.md)
|
||||
- 🏗️ [Architecture](architecture/overview.md)
|
||||
- 💻 [Development](development/workflow.md)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Description + links to installation, prerequisites, quick-start
|
||||
|
||||
## User Guide
|
||||
|
||||
Description + links to features, configuration, RBAC
|
||||
|
||||
## Tutorials
|
||||
|
||||
Description + links to plugin-specific tutorials
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Description + link to quick diagnosis + common issues
|
||||
|
||||
## Architecture
|
||||
|
||||
Description + links to overview, data flow, ADRs
|
||||
|
||||
## Development
|
||||
|
||||
Description + links to workflow, testing, code style, release
|
||||
|
||||
## Deployment
|
||||
|
||||
Description + links to Kubernetes, Helm, production
|
||||
|
||||
## API Reference
|
||||
|
||||
Link to JSDoc or generated API docs
|
||||
```
|
||||
|
||||
**Formatting**:
|
||||
|
||||
- Emojis for visual scanning
|
||||
- Brief descriptions (1-2 sentences) for each section
|
||||
- Organized by user journey (beginner → advanced)
|
||||
|
||||
### 7. Architecture Decision Records (ADR) Standard
|
||||
|
||||
**When to Create ADRs**:
|
||||
|
||||
- Significant architectural choices
|
||||
- Technology selection (libraries, patterns)
|
||||
- Security or performance trade-offs
|
||||
- Design patterns that impact maintainability
|
||||
|
||||
**Template** (Based on Michael Nygard's ADR):
|
||||
|
||||
```markdown
|
||||
# ADR-NNN: Title
|
||||
|
||||
**Status**: [Proposed | Accepted | Deprecated | Superseded by ADR-XXX]
|
||||
**Date**: YYYY-MM-DD
|
||||
**Deciders**: [List key decision makers]
|
||||
|
||||
## Context
|
||||
|
||||
What is the issue that we're seeing that is motivating this decision or change?
|
||||
|
||||
## Decision
|
||||
|
||||
What is the change that we're proposing and/or doing?
|
||||
|
||||
## Consequences
|
||||
|
||||
What becomes easier or more difficult to do because of this change?
|
||||
|
||||
### Positive
|
||||
|
||||
- ...
|
||||
|
||||
### Negative
|
||||
|
||||
- ...
|
||||
|
||||
### Neutral
|
||||
|
||||
- ...
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Option 1: Name
|
||||
|
||||
**Pros**: ...
|
||||
**Cons**: ...
|
||||
**Decision**: Not chosen because...
|
||||
|
||||
## References
|
||||
|
||||
- [Link to related issues, docs, discussions]
|
||||
```
|
||||
|
||||
**Numbering**: ADR-001, ADR-002, etc. (zero-padded 3 digits)
|
||||
|
||||
**Index File** (architecture/adr/README.md):
|
||||
|
||||
```markdown
|
||||
# Architecture Decision Records
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
| ------------------- | ----- | -------- | ---------- |
|
||||
| [001](001-title.md) | Title | Accepted | 2026-01-01 |
|
||||
```
|
||||
|
||||
### 8. Troubleshooting Standard
|
||||
|
||||
**Structure**:
|
||||
|
||||
**troubleshooting/README.md** (Quick Diagnosis):
|
||||
|
||||
```markdown
|
||||
# Troubleshooting
|
||||
|
||||
Quick diagnosis guide for common issues.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Symptom | Likely Cause | Quick Fix |
|
||||
| ------- | ------------ | --------- |
|
||||
| ... | ... | ... |
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
- [Common Errors](common-errors.md)
|
||||
- [RBAC Issues](rbac-issues.md)
|
||||
- [Network Problems](network-problems.md)
|
||||
```
|
||||
|
||||
**Individual Issue Files**:
|
||||
|
||||
- Symptom-based organization
|
||||
- Step-by-step resolution
|
||||
- Bash commands for verification
|
||||
- Links to related docs
|
||||
- "Still Having Issues?" section with bug report link
|
||||
|
||||
### 9. Testing Documentation Standard
|
||||
|
||||
**docs/development/testing.md**:
|
||||
|
||||
**Required Sections**:
|
||||
|
||||
1. Overview (testing philosophy, types of tests)
|
||||
2. Unit Testing (framework, running tests, writing tests, examples)
|
||||
3. E2E Testing (framework, prerequisites, running tests, examples)
|
||||
4. CI/CD Integration (workflows, required secrets)
|
||||
5. Test Coverage (goals, generating reports)
|
||||
6. Best Practices (unit, E2E, general)
|
||||
7. Debugging (common issues, useful commands)
|
||||
|
||||
**Formatting**:
|
||||
|
||||
- Tables for test types and coverage goals
|
||||
- Code blocks for examples
|
||||
- Bash commands for running tests
|
||||
- Links to test files in repository
|
||||
|
||||
### 10. Visual Formatting Standards
|
||||
|
||||
**Emoji Usage** (Strategic, Not Excessive):
|
||||
|
||||
- 📚 Documentation
|
||||
- 🚀 Installation/Quick Start
|
||||
- 🔒 Security
|
||||
- 🛠️ Development
|
||||
- ✅ Success/Completed
|
||||
- ❌ Error/Failed
|
||||
- ⚠️ Warning/Important
|
||||
- 🔧 Troubleshooting/Fix
|
||||
- 🏗️ Architecture
|
||||
- 💻 Code/Technical
|
||||
|
||||
**Code Block Languages**:
|
||||
|
||||
- `bash` for shell commands
|
||||
- `yaml` for Kubernetes/Helm manifests
|
||||
- `typescript` for TypeScript code
|
||||
- `json` for JSON config
|
||||
- `diff` for showing changes
|
||||
|
||||
**Tables**:
|
||||
|
||||
- Use for structured data (prerequisites, commands, permissions, troubleshooting)
|
||||
- Keep columns concise
|
||||
- Left-align text columns, center-align status columns
|
||||
|
||||
**Links**:
|
||||
|
||||
- Use descriptive text, not "click here"
|
||||
- Relative paths within repo (`docs/architecture/overview.md`)
|
||||
- Absolute URLs for external resources
|
||||
- Link to specific sections with anchors where helpful
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Polaris Plugin Enhancements
|
||||
|
||||
**Priority 1: Granular Documentation Structure**
|
||||
|
||||
- [ ] Create docs/README.md (documentation hub)
|
||||
- [ ] Break down DEPLOYMENT.md:
|
||||
- [ ] docs/getting-started/installation.md
|
||||
- [ ] docs/getting-started/prerequisites.md
|
||||
- [ ] docs/deployment/kubernetes.md
|
||||
- [ ] docs/deployment/helm.md
|
||||
- [ ] docs/deployment/production.md
|
||||
- [ ] Break down ARCHITECTURE.md:
|
||||
- [ ] docs/architecture/overview.md
|
||||
- [ ] docs/architecture/data-flow.md
|
||||
- [ ] docs/architecture/design-decisions.md
|
||||
- [ ] Move TROUBLESHOOTING.md → docs/troubleshooting/
|
||||
- [ ] Create troubleshooting/README.md (quick diagnosis)
|
||||
- [ ] Break into common-issues.md, rbac-issues.md, etc.
|
||||
- [ ] Move TESTING.md → docs/development/testing.md
|
||||
|
||||
**Priority 2: Add Missing Content**
|
||||
|
||||
- [ ] Create docs/getting-started/quick-start.md (5-minute tutorial)
|
||||
- [ ] Create docs/user-guide/features.md
|
||||
- [ ] Create docs/user-guide/configuration.md
|
||||
- [ ] Create docs/architecture/adr/ directory with ADR template
|
||||
- [ ] Create FAQ section in troubleshooting
|
||||
|
||||
**Priority 3: Content Refinement**
|
||||
|
||||
- [ ] Add multi-platform instructions to installation.md
|
||||
- [ ] Enhance README.md with better visual hierarchy
|
||||
- [ ] Add more code examples to user guide
|
||||
- [ ] Create architecture diagrams (ASCII art or mermaid)
|
||||
|
||||
### Phase 2: Sealed Secrets Plugin Enhancements
|
||||
|
||||
**Priority 1: Root-Level Documentation**
|
||||
|
||||
- [ ] Extract CONTRIBUTING.md from README
|
||||
- [ ] Create SECURITY.md with vulnerability reporting
|
||||
- [ ] Expand CHANGELOG.md to include all versions
|
||||
- [ ] Update README.md to match standardized format
|
||||
|
||||
**Priority 2: Complete Incomplete Files**
|
||||
|
||||
- [ ] Finish placeholder tutorial files
|
||||
- [ ] Add E2E testing guide to docs/development/testing.md
|
||||
- [ ] Expand API reference (ensure generated docs are readable)
|
||||
- [ ] Add FAQ section
|
||||
|
||||
**Priority 3: Content Refinement**
|
||||
|
||||
- [ ] Add CI/CD badges to README
|
||||
- [ ] Ensure consistent emoji usage
|
||||
- [ ] Standardize code block languages
|
||||
- [ ] Add more cross-links between related topics
|
||||
|
||||
### Phase 3: Cross-Repository Standards
|
||||
|
||||
**Documentation Templates**
|
||||
|
||||
- [ ] ADR template in both repos
|
||||
- [ ] Bug report template (GitHub issue template)
|
||||
- [ ] Feature request template
|
||||
- [ ] PR template
|
||||
|
||||
**Shared Patterns**
|
||||
|
||||
- [ ] Consistent branching strategy docs
|
||||
- [ ] Identical commit message conventions
|
||||
- [ ] Same release process documentation
|
||||
- [ ] Unified code style guidelines
|
||||
|
||||
## Success Metrics
|
||||
|
||||
**Completeness**:
|
||||
|
||||
- [ ] All standard files present in both repos
|
||||
- [ ] No broken links in documentation
|
||||
- [ ] All code examples tested and functional
|
||||
|
||||
**Consistency**:
|
||||
|
||||
- [ ] Same file structure in both repos
|
||||
- [ ] Same formatting standards applied
|
||||
- [ ] Same terminology used for common concepts
|
||||
|
||||
**Usability**:
|
||||
|
||||
- [ ] New users can get started in < 5 minutes
|
||||
- [ ] Contributors can find development workflow easily
|
||||
- [ ] Troubleshooting guides resolve 80%+ of common issues
|
||||
|
||||
**Maintainability**:
|
||||
|
||||
- [ ] Documentation updates documented in CHANGELOG
|
||||
- [ ] ADRs created for all major decisions
|
||||
- [ ] Test documentation kept in sync with code
|
||||
|
||||
## Maintenance Guidelines
|
||||
|
||||
**When to Update Documentation**:
|
||||
|
||||
1. **New Feature**: Add to CHANGELOG, update user guide, add tutorial if complex
|
||||
2. **Bug Fix**: Add to CHANGELOG, update troubleshooting if user-facing
|
||||
3. **Architecture Change**: Create ADR, update architecture docs
|
||||
4. **Breaking Change**: Add to CHANGELOG with migration guide, update SECURITY.md if relevant
|
||||
5. **New Dependency**: Document in prerequisites, update installation guide
|
||||
6. **Configuration Change**: Update user guide, add migration notes if needed
|
||||
|
||||
**Documentation Review Checklist**:
|
||||
|
||||
- [ ] Spelling and grammar checked
|
||||
- [ ] Links tested (no 404s)
|
||||
- [ ] Code examples tested
|
||||
- [ ] Screenshots/diagrams up to date (if applicable)
|
||||
- [ ] Cross-references added where relevant
|
||||
- [ ] CHANGELOG updated
|
||||
- [ ] Version numbers current
|
||||
|
||||
**Annual Documentation Audit**:
|
||||
|
||||
- Review all docs for accuracy (especially version numbers, screenshots)
|
||||
- Check for outdated information
|
||||
- Update ADR status if superseded
|
||||
- Archive obsolete tutorials
|
||||
- Refresh getting-started for latest best practices
|
||||
|
||||
## Appendix: File Mapping
|
||||
|
||||
### Polaris Plugin Current → Standard
|
||||
|
||||
| Current | Standard Location |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| README.md | README.md (enhanced) |
|
||||
| CHANGELOG.md | CHANGELOG.md (no change) |
|
||||
| CONTRIBUTING.md | CONTRIBUTING.md (no change) |
|
||||
| SECURITY.md | SECURITY.md (no change) |
|
||||
| docs/ARCHITECTURE.md | docs/architecture/overview.md + data-flow.md + design-decisions.md |
|
||||
| docs/DEPLOYMENT.md | docs/getting-started/installation.md + docs/deployment/kubernetes.md + helm.md + production.md |
|
||||
| docs/TROUBLESHOOTING.md | docs/troubleshooting/README.md + common-issues.md |
|
||||
| docs/TESTING.md | docs/development/testing.md |
|
||||
| — (new) | docs/README.md |
|
||||
| — (new) | docs/getting-started/quick-start.md |
|
||||
| — (new) | docs/user-guide/features.md |
|
||||
| — (new) | docs/architecture/adr/ |
|
||||
|
||||
### Sealed Secrets Plugin Current → Standard
|
||||
|
||||
| Current | Standard Location |
|
||||
| ------------ | ----------------------------------------------- |
|
||||
| README.md | README.md (extract contributing section) |
|
||||
| CHANGELOG.md | CHANGELOG.md (expand) |
|
||||
| — (new) | CONTRIBUTING.md (extract from README) |
|
||||
| — (new) | SECURITY.md (new file) |
|
||||
| docs/\* | docs/\* (mostly keep, enhance incomplete files) |
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2026-02-12
|
||||
**Approved By**: [Pending]
|
||||
@@ -0,0 +1,71 @@
|
||||
# Documentation
|
||||
|
||||
Central hub for Headlamp Polaris Plugin documentation.
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Quick Start](getting-started/quick-start.md)
|
||||
- [Installation Guide](getting-started/installation.md)
|
||||
- [Troubleshooting](troubleshooting/README.md)
|
||||
- [Architecture](architecture/overview.md)
|
||||
- [Development](development/workflow.md)
|
||||
|
||||
## Getting Started
|
||||
|
||||
New to the Headlamp Polaris Plugin? Start here:
|
||||
|
||||
- **[Prerequisites](getting-started/prerequisites.md)** - System requirements, Headlamp version, Polaris installation
|
||||
- **[Installation](getting-started/installation.md)** - Four installation methods: Plugin Manager, Sidecar, Manual, Source
|
||||
- **[Quick Start](getting-started/quick-start.md)** - Get up and running in 5 minutes
|
||||
|
||||
## User Guide
|
||||
|
||||
Learn how to use the plugin:
|
||||
|
||||
- **[Features](user-guide/features.md)** - Overview dashboard, namespace views, inline audits, exemption management
|
||||
- **[Configuration](user-guide/configuration.md)** - Refresh intervals, dashboard URLs, settings
|
||||
- **[RBAC Permissions](user-guide/rbac-permissions.md)** - Required permissions, service proxy access, token-auth mode
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Having issues? Check here:
|
||||
|
||||
- **[Quick Diagnosis](troubleshooting/README.md)** - Quick reference table for common symptoms
|
||||
- **[Common Issues](troubleshooting/common-issues.md)** - Detailed resolution steps for frequent problems
|
||||
- **[RBAC Issues](troubleshooting/rbac-issues.md)** - Permission debugging, 403 errors, token-auth
|
||||
- **[Network Problems](troubleshooting/network-problems.md)** - NetworkPolicies, connectivity, proxy issues
|
||||
|
||||
## Architecture
|
||||
|
||||
Understand how the plugin works:
|
||||
|
||||
- **[Overview](architecture/overview.md)** - High-level architecture, component hierarchy
|
||||
- **[Data Flow](architecture/data-flow.md)** - How data moves from Polaris to the UI
|
||||
- **[Design Decisions](architecture/design-decisions.md)** - Key architectural choices and rationale
|
||||
- **[ADRs](architecture/adr/README.md)** - Architecture Decision Records
|
||||
|
||||
## Development
|
||||
|
||||
Contributing to the plugin:
|
||||
|
||||
- **[Development Workflow](development/workflow.md)** - Setup, building, hot reload
|
||||
- **[Testing](development/testing.md)** - Unit tests, E2E tests, CI/CD
|
||||
- **[Code Style](development/code-style.md)** - TypeScript, React, linting, formatting
|
||||
- **[Release Process](development/release-process.md)** - Versioning, changelog, GitHub Actions
|
||||
|
||||
## Deployment
|
||||
|
||||
Production deployment guides:
|
||||
|
||||
- **[Kubernetes](deployment/kubernetes.md)** - Direct Kubernetes manifest deployment
|
||||
- **[Helm](deployment/helm.md)** - Helm chart configuration, values
|
||||
- **[Production Checklist](deployment/production.md)** - RBAC, NetworkPolicies, security, monitoring
|
||||
|
||||
## API Reference
|
||||
|
||||
- **[polaris.ts](../src/api/polaris.ts)** - JSDoc-annotated TypeScript API (data fetching, types, utilities)
|
||||
- **[PolarisDataContext.tsx](../src/api/PolarisDataContext.tsx)** - React Context provider for shared data
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Open an issue on [GitHub](https://github.com/privilegedescalation/headlamp-polaris-plugin/issues) or check [CONTRIBUTING.md](../CONTRIBUTING.md) for development guidelines.
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
# Testing Guide
|
||||
|
||||
Comprehensive guide to testing the Headlamp Polaris Plugin, covering unit tests, E2E tests, and CI/CD integration.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Unit Testing](#unit-testing)
|
||||
- [E2E Testing](#e2e-testing)
|
||||
- [CI/CD Integration](#cicd-integration)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Headlamp Polaris Plugin uses a multi-layered testing approach:
|
||||
|
||||
| Test Type | Framework | Purpose | Location |
|
||||
| ----------------- | ---------- | ------------------------------------------------------- | ----------------------- |
|
||||
| **Unit Tests** | Vitest | Test individual functions and components in isolation | `src/**/*.test.ts(x)` |
|
||||
| **E2E Tests** | Playwright | Test complete user flows against live Headlamp instance | `e2e/*.spec.ts` |
|
||||
| **Type Checking** | TypeScript | Ensure type safety across codebase | `tsc --noEmit` |
|
||||
| **Linting** | ESLint | Enforce code style and catch common errors | `eslint src/` |
|
||||
| **Formatting** | Prettier | Maintain consistent code formatting | `prettier --check src/` |
|
||||
|
||||
### Test Philosophy
|
||||
|
||||
1. **Unit tests** focus on business logic (data parsing, score calculation, filtering)
|
||||
2. **E2E tests** validate user-facing functionality (navigation, rendering, interactions)
|
||||
3. **Both** run automatically in CI on every commit
|
||||
4. **Coverage** targets meaningful tests, not arbitrary percentages
|
||||
|
||||
---
|
||||
|
||||
## Unit Testing
|
||||
|
||||
### Framework: Vitest
|
||||
|
||||
Vitest is a fast, modern testing framework compatible with Jest APIs but optimized for Vite-based projects.
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
npm test
|
||||
|
||||
# Run in watch mode (re-runs on file changes)
|
||||
npm run test:watch
|
||||
|
||||
# Run specific test file
|
||||
npx vitest src/api/polaris.test.ts
|
||||
|
||||
# Run with coverage
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
Unit tests are colocated with source files:
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/
|
||||
│ ├── polaris.ts
|
||||
│ ├── polaris.test.ts # Unit tests for polaris.ts
|
||||
│ ├── PolarisDataContext.tsx
|
||||
│ └── PolarisDataContext.test.tsx
|
||||
└── components/
|
||||
├── DashboardView.tsx
|
||||
└── DashboardView.test.tsx
|
||||
```
|
||||
|
||||
### Example: Testing Utility Functions
|
||||
|
||||
**File:** `src/api/polaris.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { countResults, computeScore, getNamespaces, filterResultsByNamespace } from './polaris';
|
||||
|
||||
describe('countResults', () => {
|
||||
it('counts passing, warning, danger, and skipped results correctly', () => {
|
||||
const data = {
|
||||
Results: [
|
||||
{
|
||||
Name: 'test-deployment',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {
|
||||
'check-1': { Success: true, Severity: 'warning' },
|
||||
'check-2': { Success: false, Severity: 'danger' },
|
||||
'check-3': { Success: false, Severity: 'ignore' }, // skipped
|
||||
},
|
||||
CreatedTime: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const counts = countResults(data);
|
||||
|
||||
expect(counts).toEqual({
|
||||
total: 3,
|
||||
pass: 1,
|
||||
warning: 0,
|
||||
danger: 1,
|
||||
skipped: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty results', () => {
|
||||
const data = { Results: [] };
|
||||
const counts = countResults(data);
|
||||
|
||||
expect(counts).toEqual({
|
||||
total: 0,
|
||||
pass: 0,
|
||||
warning: 0,
|
||||
danger: 0,
|
||||
skipped: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeScore', () => {
|
||||
it('returns 0 for zero total checks', () => {
|
||||
expect(computeScore({ total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 })).toBe(0);
|
||||
});
|
||||
|
||||
it('calculates percentage correctly', () => {
|
||||
expect(computeScore({ total: 100, pass: 75, warning: 20, danger: 5, skipped: 0 })).toBe(75);
|
||||
});
|
||||
|
||||
it('rounds to nearest integer', () => {
|
||||
expect(computeScore({ total: 3, pass: 2, warning: 1, danger: 0, skipped: 0 })).toBe(67);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Testing React Components
|
||||
|
||||
**File:** `src/components/DashboardView.test.tsx`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DashboardView } from './DashboardView';
|
||||
import * as PolarisDataContext from '../api/PolarisDataContext';
|
||||
|
||||
describe('DashboardView', () => {
|
||||
it('renders loading state', () => {
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state', () => {
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: '403 Forbidden',
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText(/403 Forbidden/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays cluster score when data is loaded', () => {
|
||||
const mockData = {
|
||||
DisplayName: 'test-cluster',
|
||||
ClusterInfo: { Version: '1.27', Nodes: 3, Pods: 100, Namespaces: 10, Controllers: 50 },
|
||||
Results: [
|
||||
/* ... */
|
||||
],
|
||||
};
|
||||
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: mockData,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText(/Cluster Score/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### What to Unit Test
|
||||
|
||||
✅ **Do test:**
|
||||
|
||||
- Pure functions (score calculation, filtering, data transformation)
|
||||
- Data parsing and validation
|
||||
- Utility functions
|
||||
- Error handling logic
|
||||
- Edge cases (empty arrays, null values, invalid input)
|
||||
|
||||
❌ **Don't test:**
|
||||
|
||||
- Third-party libraries (Headlamp, React)
|
||||
- Simple prop passing
|
||||
- Trivial getters/setters
|
||||
- Implementation details (internal state)
|
||||
|
||||
---
|
||||
|
||||
## E2E Testing
|
||||
|
||||
### Framework: Playwright
|
||||
|
||||
Playwright provides cross-browser testing with auto-wait, network interception, and screenshot/video capture.
|
||||
|
||||
### Running E2E Tests
|
||||
|
||||
```bash
|
||||
# Run all E2E tests (headless)
|
||||
npm run e2e
|
||||
|
||||
# Run with browser visible (headed mode)
|
||||
npm run e2e:headed
|
||||
|
||||
# Run specific test file
|
||||
npx playwright test e2e/polaris.spec.ts
|
||||
|
||||
# Debug mode (step through tests)
|
||||
npx playwright test --debug
|
||||
|
||||
# Generate trace for debugging
|
||||
npx playwright test --trace on
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**1. Headlamp Instance**
|
||||
|
||||
E2E tests require a running Headlamp instance with:
|
||||
|
||||
- Polaris plugin installed (version being tested)
|
||||
- Polaris dashboard deployed and accessible
|
||||
- RBAC configured (service proxy permissions)
|
||||
|
||||
**2. Authentication**
|
||||
|
||||
Choose one of two authentication methods:
|
||||
|
||||
**Option A: OIDC via Authentik**
|
||||
|
||||
```bash
|
||||
export AUTHENTIK_USERNAME="user@example.com"
|
||||
export AUTHENTIK_PASSWORD="password"
|
||||
export HEADLAMP_URL="https://headlamp.example.com"
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
**Option B: Kubernetes Token**
|
||||
|
||||
```bash
|
||||
# Create token
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
|
||||
# Port-forward for local testing
|
||||
kubectl port-forward -n kube-system svc/headlamp 4466:80
|
||||
|
||||
# Run tests
|
||||
HEADLAMP_URL=http://localhost:4466 npm run e2e
|
||||
```
|
||||
|
||||
**3. Environment Variables**
|
||||
|
||||
Create `.env` file (optional, for persistent config):
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```bash
|
||||
HEADLAMP_URL=https://headlamp.example.com
|
||||
HEADLAMP_TOKEN=eyJhbGciOi...
|
||||
# OR
|
||||
AUTHENTIK_USERNAME=user@example.com
|
||||
AUTHENTIK_PASSWORD=secret
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
#### Current E2E Tests
|
||||
|
||||
**File:** `e2e/polaris.spec.ts`
|
||||
|
||||
| Test | Description | Validates |
|
||||
| --------------------------------------------- | ------------------------------- | ----------------------------- |
|
||||
| `sidebar contains Polaris entry` | Polaris appears in sidebar | Plugin registration |
|
||||
| `overview page renders cluster score` | Score displayed on overview | Data fetching, rendering |
|
||||
| `namespaces page renders table` | Namespace table loads | Data parsing, table rendering |
|
||||
| `namespace detail drawer opens` | Clicking namespace shows drawer | Navigation, drawer UI |
|
||||
| `namespace detail drawer closes with Escape` | Keyboard shortcut works | Keyboard navigation |
|
||||
| `namespace detail drawer opens from URL hash` | Direct URL navigation | URL routing, deep linking |
|
||||
|
||||
**File:** `e2e/settings.spec.ts`
|
||||
|
||||
| Test | Description | Validates |
|
||||
| ------------------------------------ | ------------------- | --------------------------- |
|
||||
| `plugin settings page is accessible` | Settings page loads | Settings registration |
|
||||
| `refresh interval can be changed` | Dropdown works | User preference persistence |
|
||||
| `dashboard URL can be customized` | Input field works | URL configuration |
|
||||
| `connection test button works` | Test functionality | API connectivity validation |
|
||||
|
||||
**File:** `e2e/appbar.spec.ts`
|
||||
|
||||
| Test | Description | Validates |
|
||||
| -------------------------------------- | ------------------------------- | ------------------- |
|
||||
| `app bar displays Polaris badge` | Badge visible in header | App bar integration |
|
||||
| `badge shows cluster score` | Score matches dashboard | Data consistency |
|
||||
| `clicking badge navigates to overview` | Navigation works | App bar action |
|
||||
| `badge color reflects score` | Red/yellow/green based on score | Visual feedback |
|
||||
|
||||
### Writing E2E Tests
|
||||
|
||||
**Example: Testing User Flow**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('user can view namespace details and navigate back', async ({ page }) => {
|
||||
// Navigate to namespaces page
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
await expect(page.getByText('Polaris — Namespaces')).toBeVisible();
|
||||
|
||||
// Click first namespace
|
||||
const firstNamespace = page.locator('table tbody tr').first();
|
||||
const namespaceName = await firstNamespace.locator('td').first().textContent();
|
||||
await firstNamespace.getByRole('button').click();
|
||||
|
||||
// Drawer should open
|
||||
await expect(page.getByText(`Polaris — ${namespaceName}`)).toBeVisible();
|
||||
await expect(page).toHaveURL(new RegExp(`#${namespaceName}`));
|
||||
|
||||
// Close drawer with Escape
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.getByText(`Polaris — ${namespaceName}`)).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/namespaces$/);
|
||||
});
|
||||
```
|
||||
|
||||
**Example: Testing Dark Mode Adaptation**
|
||||
|
||||
```typescript
|
||||
test('plugin adapts to dark mode', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
// Toggle dark mode
|
||||
await page.getByLabel(/theme/i).click();
|
||||
|
||||
// Open namespace drawer
|
||||
const firstNamespace = page.locator('table tbody tr button').first();
|
||||
await firstNamespace.click();
|
||||
|
||||
// Drawer background should be dark
|
||||
const drawer = page.locator('[style*="position: fixed"][style*="right: 0"]');
|
||||
await expect(drawer).toHaveCSS('background-color', /rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
});
|
||||
```
|
||||
|
||||
### Debugging E2E Tests
|
||||
|
||||
**1. Headed Mode (See Browser)**
|
||||
|
||||
```bash
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
**2. Debug Mode (Step Through Tests)**
|
||||
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
This opens Playwright Inspector where you can:
|
||||
|
||||
- Step through each test action
|
||||
- Inspect page state
|
||||
- Edit test selectors live
|
||||
|
||||
**3. Screenshots on Failure**
|
||||
|
||||
Screenshots are automatically saved to `test-results/` on failure:
|
||||
|
||||
```bash
|
||||
test-results/
|
||||
└── polaris-overview-page-renders-cluster-score/
|
||||
├── test-failed-1.png
|
||||
└── trace.zip
|
||||
```
|
||||
|
||||
**4. Trace Viewer**
|
||||
|
||||
Record full trace (DOM snapshots, network, console):
|
||||
|
||||
```bash
|
||||
npx playwright test --trace on
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
**5. Verbose Logging**
|
||||
|
||||
```bash
|
||||
DEBUG=pw:api npx playwright test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Workflows
|
||||
|
||||
#### CI Workflow (`.github/workflows/ci.yaml`)
|
||||
|
||||
Runs on every push to `main` and all pull requests:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
lint-and-test:
|
||||
steps:
|
||||
- Build plugin
|
||||
- Lint (eslint)
|
||||
- Type-check (tsc)
|
||||
- Format check (prettier)
|
||||
- Run unit tests
|
||||
```
|
||||
|
||||
#### E2E Workflow (`.github/workflows/e2e.yaml`)
|
||||
|
||||
Runs E2E tests against live Headlamp instance:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
e2e:
|
||||
steps:
|
||||
- Install dependencies
|
||||
- Install Playwright browsers
|
||||
- Run auth setup
|
||||
- Run E2E tests
|
||||
- Upload artifacts on failure
|
||||
```
|
||||
|
||||
### Required GitHub Secrets
|
||||
|
||||
Configure in GitHub repository settings (Settings → Secrets and variables → Actions):
|
||||
|
||||
| Secret | Required | Description |
|
||||
| -------------------- | -------- | ------------------------------------------------------- |
|
||||
| `HEADLAMP_URL` | Optional | Headlamp instance URL (defaults to configured instance) |
|
||||
| `AUTHENTIK_USERNAME` | OIDC | Authentik username/email for CI user |
|
||||
| `AUTHENTIK_PASSWORD` | OIDC | Authentik password |
|
||||
| `HEADLAMP_TOKEN` | Token | Kubernetes service account token (alternative to OIDC) |
|
||||
|
||||
Set either `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` **or** `HEADLAMP_TOKEN`. OIDC takes priority if both are set.
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
Trigger workflows manually from GitHub Actions UI:
|
||||
|
||||
1. Go to Actions → [Workflow Name]
|
||||
2. Click "Run workflow"
|
||||
3. Select branch and run
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Current Coverage
|
||||
|
||||
| Category | Coverage | Notes |
|
||||
| -------------------- | -------- | ------------------------------ |
|
||||
| **API Functions** | 95% | Core utilities fully tested |
|
||||
| **React Components** | 60% | Focus on critical render paths |
|
||||
| **E2E User Flows** | 80% | Main features covered |
|
||||
|
||||
### Coverage Goals
|
||||
|
||||
- **Unit Tests**: 80%+ for `src/api/` (business logic)
|
||||
- **Component Tests**: 50%+ for `src/components/` (critical rendering)
|
||||
- **E2E Tests**: Cover all major user journeys
|
||||
|
||||
### Generating Coverage Reports
|
||||
|
||||
```bash
|
||||
# Unit test coverage
|
||||
npx vitest --coverage
|
||||
|
||||
# View HTML report
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Unit Testing
|
||||
|
||||
1. **Test behavior, not implementation**
|
||||
|
||||
- ✅ `expect(computeScore({ total: 100, pass: 75 })).toBe(75)`
|
||||
- ❌ `expect(mockInternalFunction).toHaveBeenCalled()`
|
||||
|
||||
2. **Use descriptive test names**
|
||||
|
||||
- ✅ `it('returns 0 when total checks is zero')`
|
||||
- ❌ `it('works')`
|
||||
|
||||
3. **One assertion per test (when possible)**
|
||||
|
||||
- Makes failures easier to debug
|
||||
- Exceptions: testing multiple properties of same object
|
||||
|
||||
4. **Mock external dependencies**
|
||||
|
||||
- Mock API calls, context providers, external libraries
|
||||
- Don't mock the code you're testing
|
||||
|
||||
5. **Test edge cases**
|
||||
- Empty arrays, null values, zero counts
|
||||
- Invalid input, malformed data
|
||||
|
||||
### E2E Testing
|
||||
|
||||
1. **Use semantic selectors**
|
||||
|
||||
- ✅ `page.getByRole('button', { name: 'Close' })`
|
||||
- ✅ `page.getByText('Polaris — Overview')`
|
||||
- ❌ `page.locator('.MuiButton-root')`
|
||||
|
||||
2. **Wait for visibility, not arbitrary timeouts**
|
||||
|
||||
- ✅ `await expect(element).toBeVisible()`
|
||||
- ❌ `await page.waitForTimeout(5000)`
|
||||
|
||||
3. **Keep tests independent**
|
||||
|
||||
- Each test should work in isolation
|
||||
- Don't rely on previous tests' state
|
||||
|
||||
4. **Test complete user flows**
|
||||
|
||||
- Navigate → Interact → Verify outcome
|
||||
- Don't just test page loads
|
||||
|
||||
5. **Clean up after tests**
|
||||
|
||||
- Close drawers/modals
|
||||
- Reset state if needed
|
||||
|
||||
6. **Use storage state for auth**
|
||||
|
||||
- Reuse authenticated session across tests
|
||||
- Faster than logging in for every test
|
||||
|
||||
7. **Parallelize carefully**
|
||||
- Tests must not interfere with each other
|
||||
- Currently disabled due to shared cluster state
|
||||
|
||||
### General
|
||||
|
||||
1. **Run tests before committing**
|
||||
|
||||
```bash
|
||||
npm run build && npm run lint && npm test
|
||||
```
|
||||
|
||||
2. **Fix failing tests immediately**
|
||||
|
||||
- Don't commit failing tests
|
||||
- Don't skip tests to "fix later"
|
||||
|
||||
3. **Update tests when changing code**
|
||||
|
||||
- Tests are documentation
|
||||
- Keep them in sync with implementation
|
||||
|
||||
4. **Review test failures in CI**
|
||||
- Check artifacts (screenshots, traces)
|
||||
- Reproduce locally before fixing
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
**Issue: Mock not working**
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Mock after import
|
||||
import { usePolarisData } from './polaris';
|
||||
vi.mock('./polaris');
|
||||
|
||||
// ✅ Correct: Mock before import
|
||||
vi.mock('./polaris', () => ({
|
||||
usePolarisData: vi.fn(),
|
||||
}));
|
||||
import { usePolarisData } from './polaris';
|
||||
```
|
||||
|
||||
**Issue: "Cannot read property of undefined"**
|
||||
|
||||
Check mocks are returning expected structure:
|
||||
|
||||
```typescript
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: mockData, // Ensure mockData has all required fields
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
```
|
||||
|
||||
#### E2E Tests
|
||||
|
||||
**Issue: "Element not found"**
|
||||
|
||||
```bash
|
||||
# Enable verbose logging
|
||||
DEBUG=pw:api npx playwright test
|
||||
|
||||
# Use headed mode to see what's happening
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
Common causes:
|
||||
|
||||
- Element hasn't rendered yet (use `toBeVisible()` instead of `toBeDefined()`)
|
||||
- Wrong selector (use Playwright Inspector to verify)
|
||||
- Authentication failed (check token/credentials)
|
||||
|
||||
**Issue: "Test timeout"**
|
||||
|
||||
Increase timeout for slow operations:
|
||||
|
||||
```typescript
|
||||
test('slow operation', async ({ page }) => {
|
||||
test.setTimeout(60000); // 60 seconds
|
||||
|
||||
await page.goto('/c/main/polaris');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Issue: Network errors in E2E tests**
|
||||
|
||||
```bash
|
||||
# Check Headlamp accessibility
|
||||
curl -I $HEADLAMP_URL
|
||||
|
||||
# Check Polaris service
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
```
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Run specific test file
|
||||
npx vitest src/api/polaris.test.ts
|
||||
|
||||
# Run specific test case
|
||||
npx vitest -t "computes score correctly"
|
||||
|
||||
# Run E2E test by name
|
||||
npx playwright test -g "sidebar contains Polaris"
|
||||
|
||||
# Update snapshots
|
||||
npx vitest -u
|
||||
|
||||
# Clear test cache
|
||||
npx vitest --clearCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Vitest Documentation](https://vitest.dev/)
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Testing Library (React)](https://testing-library.com/docs/react-testing-library/intro/)
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Project Architecture](./ARCHITECTURE.md)
|
||||
- [Contributing Guide](../CONTRIBUTING.md)
|
||||
@@ -0,0 +1,715 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide covers common issues encountered when using the Headlamp Polaris Plugin and their solutions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Plugin Not Showing in Sidebar](#plugin-not-showing-in-sidebar)
|
||||
- [403 Forbidden Error](#403-forbidden-error)
|
||||
- [404 Not Found Error](#404-not-found-error)
|
||||
- [Plugin Settings Page Empty](#plugin-settings-page-empty)
|
||||
- [Dark Mode Issues](#dark-mode-issues)
|
||||
- [Data Not Loading / Infinite Spinner](#data-not-loading--infinite-spinner)
|
||||
- [Browser Console Errors](#browser-console-errors)
|
||||
- [Network and RBAC Debugging](#network-and-rbac-debugging)
|
||||
- [Plugin Installation Issues](#plugin-installation-issues)
|
||||
- [ArtifactHub Sync Delays](#artifacthub-sync-delays)
|
||||
|
||||
---
|
||||
|
||||
## Plugin Not Showing in Sidebar
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Plugin appears in Settings → Plugins but sidebar entry is missing
|
||||
- No "Polaris" section in navigation
|
||||
- Routes like `/polaris` return 404 or blank page
|
||||
|
||||
### Common Causes
|
||||
|
||||
**1. Plugin Not Installed**
|
||||
|
||||
**Check plugin installation**:
|
||||
|
||||
```bash
|
||||
# View Headlamp pod logs (plugin sidecar)
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
|
||||
# Expected output:
|
||||
# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
|
||||
# Plugin installed successfully
|
||||
```
|
||||
|
||||
**Verify plugin files exist**:
|
||||
|
||||
```bash
|
||||
kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
|
||||
# Should show: headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
**2. JavaScript Cached by Browser**
|
||||
|
||||
After upgrading the plugin, old JavaScript may be cached.
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Hard refresh: Cmd+Shift+R (macOS) or Ctrl+Shift+R (Linux/Windows)
|
||||
- Clear browser cache for Headlamp domain
|
||||
- Open DevTools → Application → Clear Storage → Clear all
|
||||
|
||||
**3. Plugin Disabled in Settings**
|
||||
|
||||
**Check Settings → Plugins**:
|
||||
|
||||
- Navigate to Headlamp Settings → Plugins
|
||||
- Ensure "Polaris" plugin is enabled (toggle should be ON)
|
||||
- If disabled, enable it and refresh the page
|
||||
|
||||
---
|
||||
|
||||
## 403 Forbidden Error
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Error message: "Error loading Polaris audit data: 403 Forbidden"
|
||||
- Browser console shows 403 response from API proxy
|
||||
- Plugin sidebar shows but data fails to load
|
||||
|
||||
### Root Cause
|
||||
|
||||
User or service account lacks `services/proxy` permission on `polaris-dashboard` service in the `polaris` namespace.
|
||||
|
||||
### Solution
|
||||
|
||||
**1. Verify RBAC Configuration**
|
||||
|
||||
Check if Role exists:
|
||||
|
||||
```bash
|
||||
kubectl get role polaris-proxy-reader -n polaris -o yaml
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```yaml
|
||||
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']
|
||||
```
|
||||
|
||||
**2. Verify RoleBinding**
|
||||
|
||||
For service account mode:
|
||||
|
||||
```bash
|
||||
kubectl get rolebinding headlamp-polaris-proxy -n polaris -o yaml
|
||||
```
|
||||
|
||||
Expected subjects:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
```
|
||||
|
||||
For OIDC mode:
|
||||
|
||||
```bash
|
||||
kubectl get rolebinding -n polaris -o yaml | grep -A 5 polaris-proxy-reader
|
||||
```
|
||||
|
||||
Ensure your user or group is bound to the `polaris-proxy-reader` role.
|
||||
|
||||
**3. Create Missing RBAC**
|
||||
|
||||
If RBAC is missing, apply the minimal configuration:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
**4. Test RBAC Permissions**
|
||||
|
||||
Service account mode:
|
||||
|
||||
```bash
|
||||
# Impersonate Headlamp service account
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n polaris
|
||||
# Expected: yes
|
||||
```
|
||||
|
||||
OIDC mode (test as yourself):
|
||||
|
||||
```bash
|
||||
kubectl auth can-i get services/proxy \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n polaris
|
||||
# Expected: yes (if you have proper RoleBinding)
|
||||
```
|
||||
|
||||
**5. Restart Headlamp**
|
||||
|
||||
After applying RBAC changes:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment headlamp -n kube-system
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 404 Not Found Error
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Error message: "Error loading Polaris audit data: 404 Not Found"
|
||||
- Service proxy request returns 404
|
||||
- Polaris dashboard not reachable
|
||||
|
||||
### Root Cause
|
||||
|
||||
Polaris dashboard service doesn't exist or is in a different namespace.
|
||||
|
||||
### Solution
|
||||
|
||||
**1. Verify Polaris Installation**
|
||||
|
||||
Check if Polaris is installed:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n polaris
|
||||
# Expected: polaris-dashboard-* pod running
|
||||
```
|
||||
|
||||
Check if service exists:
|
||||
|
||||
```bash
|
||||
kubectl get service polaris-dashboard -n polaris
|
||||
# Expected: ClusterIP service on port 80
|
||||
```
|
||||
|
||||
**2. Verify Service Name and Port**
|
||||
|
||||
The plugin expects:
|
||||
|
||||
- **Namespace**: `polaris`
|
||||
- **Service Name**: `polaris-dashboard`
|
||||
- **Port**: `80` (or named port `dashboard`)
|
||||
|
||||
If your service has a different name or is in a different namespace, you'll need to modify the plugin source or redeploy Polaris with standard naming.
|
||||
|
||||
**3. Test Service Proxy Manually**
|
||||
|
||||
```bash
|
||||
kubectl proxy &
|
||||
curl http://localhost:8001/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
```
|
||||
|
||||
If this returns JSON, the service proxy works and the issue is elsewhere.
|
||||
If this returns 404, Polaris service is not configured correctly.
|
||||
|
||||
**4. Check Polaris Dashboard Configuration**
|
||||
|
||||
Verify Polaris is running with dashboard enabled:
|
||||
|
||||
```bash
|
||||
kubectl get deployment polaris-dashboard -n polaris -o yaml | grep -A 5 dashboard
|
||||
```
|
||||
|
||||
If `dashboard.enabled: false` in Helm values, enable it:
|
||||
|
||||
```yaml
|
||||
# values.yaml
|
||||
dashboard:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
**5. Reinstall Polaris**
|
||||
|
||||
If Polaris is missing or misconfigured:
|
||||
|
||||
```bash
|
||||
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
|
||||
helm upgrade --install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Settings Page Empty
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Settings → Polaris shows title but no content
|
||||
- Refresh interval and dashboard URL fields not visible
|
||||
|
||||
### Root Cause (Fixed in v0.3.3)
|
||||
|
||||
Plugin settings registration name didn't match `package.json` name.
|
||||
|
||||
### Solution
|
||||
|
||||
Upgrade to v0.3.3 or later:
|
||||
|
||||
```bash
|
||||
# Via Headlamp UI: Settings → Plugins → Update
|
||||
# Or redeploy with latest version
|
||||
```
|
||||
|
||||
If manually installing, ensure plugin name matches `package.json`:
|
||||
|
||||
```typescript
|
||||
registerPluginSettings('headlamp-polaris-plugin', PolarisSettings, true);
|
||||
// NOT 'polaris' — must match package.json name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode Issues
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Drawer background remains white in dark mode
|
||||
- Text is hard to read in dark mode
|
||||
- Theme colors don't match Headlamp UI
|
||||
|
||||
### Solution (Fixed in v0.3.5)
|
||||
|
||||
Upgrade to v0.3.5 or later for complete dark mode support.
|
||||
|
||||
**Verify CSS Variables**:
|
||||
The plugin uses MUI CSS variables for theming:
|
||||
|
||||
- `--mui-palette-background-default` (drawer background)
|
||||
- `--mui-palette-text-primary` (text color)
|
||||
- `--mui-palette-primary-main` (links, buttons)
|
||||
- `--mui-palette-error-main` (danger states)
|
||||
|
||||
These automatically adapt to Headlamp's theme (light/dark/system).
|
||||
|
||||
**Hard Refresh Required**:
|
||||
After upgrading from v0.3.4 or earlier, hard refresh your browser:
|
||||
|
||||
- macOS: Cmd+Shift+R
|
||||
- Linux/Windows: Ctrl+Shift+R
|
||||
|
||||
**Clear Browser Cache**:
|
||||
If hard refresh doesn't help, clear cache for Headlamp domain.
|
||||
|
||||
---
|
||||
|
||||
## Data Not Loading / Infinite Spinner
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Plugin shows "Loading Polaris audit data..." forever
|
||||
- No error message in UI
|
||||
- Data never appears
|
||||
|
||||
### Debugging Steps
|
||||
|
||||
**1. Check Browser Console**
|
||||
|
||||
Open DevTools (F12) → Console tab.
|
||||
|
||||
Look for:
|
||||
|
||||
- Network errors (CORS, timeouts, 5xx responses)
|
||||
- JavaScript errors
|
||||
- Failed API requests
|
||||
|
||||
**2. Check Network Tab**
|
||||
|
||||
Open DevTools → Network tab → Filter by "results.json"
|
||||
|
||||
Expected request:
|
||||
|
||||
```
|
||||
GET /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
Status: 200
|
||||
Response: JSON data
|
||||
```
|
||||
|
||||
Common issues:
|
||||
|
||||
- **Status 0 / Failed**: Network policy blocking request
|
||||
- **Status 403**: RBAC issue (see [403 Forbidden Error](#403-forbidden-error))
|
||||
- **Status 404**: Service not found (see [404 Not Found Error](#404-not-found-error))
|
||||
- **Status 500**: Polaris dashboard error
|
||||
- **Status 502/504**: Service unreachable (network policy or pod down)
|
||||
|
||||
**3. Check Polaris Dashboard Health**
|
||||
|
||||
```bash
|
||||
# Check if Polaris pod is running
|
||||
kubectl get pods -n polaris
|
||||
|
||||
# Check Polaris logs
|
||||
kubectl logs -n polaris deployment/polaris-dashboard
|
||||
|
||||
# Test direct access to Polaris
|
||||
kubectl port-forward -n polaris svc/polaris-dashboard 8080:80
|
||||
curl http://localhost:8080/results.json
|
||||
```
|
||||
|
||||
**4. Check Network Policies**
|
||||
|
||||
If your cluster uses NetworkPolicies:
|
||||
|
||||
```bash
|
||||
kubectl get networkpolicy -n polaris
|
||||
```
|
||||
|
||||
Ensure API server (or Headlamp pod) can reach Polaris dashboard.
|
||||
|
||||
**Example fix**:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-api-server-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector: {} # Allow from all namespaces (API server)
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
```
|
||||
|
||||
**5. Increase Timeout / Disable Auto-Refresh**
|
||||
|
||||
If Polaris responds slowly:
|
||||
|
||||
- Open Settings → Polaris
|
||||
- Increase refresh interval to 10+ minutes
|
||||
- Or set to "Manual only" to disable auto-refresh
|
||||
|
||||
---
|
||||
|
||||
## Browser Console Errors
|
||||
|
||||
### Common Errors and Solutions
|
||||
|
||||
**Error: "Failed to fetch"**
|
||||
|
||||
**Cause**: Network request failed (CORS, network policy, timeout)
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check Network tab for actual HTTP status
|
||||
2. Verify network policies allow API server → Polaris
|
||||
3. Check Polaris pod is running
|
||||
|
||||
---
|
||||
|
||||
**Error: "Unexpected token < in JSON"**
|
||||
|
||||
**Cause**: API returned HTML (error page) instead of JSON
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check Network tab response body (likely 404 or 500 error page)
|
||||
2. Verify Polaris service exists and is healthy
|
||||
3. Check service proxy URL is correct
|
||||
|
||||
---
|
||||
|
||||
**Error: "registerPluginSettings is not a function"**
|
||||
|
||||
**Cause**: Headlamp version too old (< v0.26)
|
||||
|
||||
**Solution**: Upgrade Headlamp to v0.26 or later.
|
||||
|
||||
---
|
||||
|
||||
**Error: "Cannot read property 'AuditData' of undefined"**
|
||||
|
||||
**Cause**: Polaris returned empty or malformed response
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check Polaris logs for errors
|
||||
2. Verify Polaris is scanning the cluster (check audit timestamp)
|
||||
3. Test `/results.json` endpoint directly
|
||||
|
||||
---
|
||||
|
||||
## Network and RBAC Debugging
|
||||
|
||||
### Comprehensive RBAC Test
|
||||
|
||||
Run this script to test all RBAC components:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
NS="polaris"
|
||||
SA="headlamp"
|
||||
SA_NS="kube-system"
|
||||
|
||||
echo "=== Testing RBAC for Polaris Plugin ==="
|
||||
|
||||
# 1. Check if service exists
|
||||
echo "1. Service check:"
|
||||
kubectl get svc polaris-dashboard -n $NS || echo "❌ Service not found"
|
||||
|
||||
# 2. Check if Role exists
|
||||
echo "2. Role check:"
|
||||
kubectl get role polaris-proxy-reader -n $NS || echo "❌ Role not found"
|
||||
|
||||
# 3. Check if RoleBinding exists
|
||||
echo "3. RoleBinding check:"
|
||||
kubectl get rolebinding headlamp-polaris-proxy -n $NS || echo "❌ RoleBinding not found"
|
||||
|
||||
# 4. Test service account permissions
|
||||
echo "4. Permission test (service account):"
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:$SA_NS:$SA \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n $NS
|
||||
|
||||
# 5. Test actual proxy request (requires kubectl proxy)
|
||||
echo "5. Proxy test:"
|
||||
kubectl proxy &
|
||||
PROXY_PID=$!
|
||||
sleep 2
|
||||
curl -s http://localhost:8001/api/v1/namespaces/$NS/services/polaris-dashboard:80/proxy/results.json | jq '.DisplayName' || echo "❌ Proxy request failed"
|
||||
kill $PROXY_PID
|
||||
|
||||
echo "=== Test complete ==="
|
||||
```
|
||||
|
||||
### Network Policy Debugging
|
||||
|
||||
Test connectivity from Headlamp to Polaris:
|
||||
|
||||
```bash
|
||||
# Create debug pod in kube-system namespace
|
||||
kubectl run netdebug -n kube-system --rm -it --image=nicolaka/netshoot -- bash
|
||||
|
||||
# Inside pod, test DNS and HTTP
|
||||
nslookup polaris-dashboard.polaris.svc.cluster.local
|
||||
curl -v http://polaris-dashboard.polaris.svc.cluster.local/results.json
|
||||
```
|
||||
|
||||
If this fails, network policies are blocking traffic.
|
||||
|
||||
### API Server Audit Logs
|
||||
|
||||
If you have audit logging enabled, check for denied requests:
|
||||
|
||||
```bash
|
||||
# View recent audit logs (location varies by cluster)
|
||||
kubectl logs -n kube-system kube-apiserver-* | grep polaris-dashboard
|
||||
|
||||
# Look for lines with:
|
||||
# "reason": "Forbidden"
|
||||
# "user": "system:serviceaccount:kube-system:headlamp"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Installation Issues
|
||||
|
||||
### Sidecar Fails to Install Plugin
|
||||
|
||||
**Symptoms**:
|
||||
|
||||
- Plugin sidecar logs show download errors
|
||||
- Plugin directory is empty
|
||||
- Settings → Plugins shows nothing
|
||||
|
||||
**Check sidecar logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
```
|
||||
|
||||
**Common errors**:
|
||||
|
||||
**1. Network timeout downloading tarball**
|
||||
|
||||
```
|
||||
Error: connect ETIMEDOUT
|
||||
```
|
||||
|
||||
**Solution**: Check cluster egress network policies allow HTTPS to GitHub.
|
||||
|
||||
---
|
||||
|
||||
**2. Invalid tarball URL**
|
||||
|
||||
```
|
||||
Error: 404 Not Found
|
||||
```
|
||||
|
||||
**Solution**: Verify `archive-url` in plugin config matches GitHub release:
|
||||
|
||||
```bash
|
||||
kubectl get configmap headlamp-plugin-config -n kube-system -o yaml
|
||||
```
|
||||
|
||||
Expected format:
|
||||
|
||||
```
|
||||
https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**3. Permission denied writing to /headlamp/plugins**
|
||||
|
||||
**Solution**: Ensure volume mount is writable:
|
||||
|
||||
```yaml
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /headlamp/plugins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Plugin Manager Not Working
|
||||
|
||||
**Symptoms**:
|
||||
|
||||
- Headlamp → Settings → Plugins shows "Catalog" tab but plugins don't install
|
||||
- "Install" button does nothing
|
||||
|
||||
**Root Cause**: Plugin manager requires `config.pluginsDir` to be set.
|
||||
|
||||
**Solution**: Configure Headlamp for plugin manager:
|
||||
|
||||
```yaml
|
||||
# HelmRelease values
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ArtifactHub Sync Delays
|
||||
|
||||
### Symptoms
|
||||
|
||||
- New version released on GitHub but not showing in ArtifactHub
|
||||
- Headlamp plugin catalog shows old version
|
||||
|
||||
### Root Cause
|
||||
|
||||
ArtifactHub pulls metadata every 30 minutes. There is no webhook or push mechanism.
|
||||
|
||||
### Solution
|
||||
|
||||
**Wait 30 minutes** after pushing a GitHub release, then check:
|
||||
|
||||
```
|
||||
https://artifacthub.io/packages/headlamp/headlamp-polaris-plugin/headlamp-polaris-plugin
|
||||
```
|
||||
|
||||
**Verify metadata**:
|
||||
|
||||
1. Check `artifacthub-pkg.yml` is in repository root
|
||||
2. Check `headlamp/plugin/archive-url` points to GitHub release
|
||||
3. Check `headlamp/plugin/archive-checksum` matches tarball SHA256
|
||||
|
||||
**Force sync** (ArtifactHub UI):
|
||||
|
||||
- Log in to ArtifactHub as package maintainer
|
||||
- Go to package settings
|
||||
- Click "Reindex now"
|
||||
|
||||
**Note**: First sync after repository registration may take up to 1 hour.
|
||||
|
||||
---
|
||||
|
||||
## Still Having Issues?
|
||||
|
||||
If none of these solutions work, gather debugging information and open an issue:
|
||||
|
||||
### Required Information
|
||||
|
||||
1. **Version Information**:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system -l app.kubernetes.io/name=headlamp -o yaml | grep image:
|
||||
```
|
||||
|
||||
2. **Plugin Version**:
|
||||
|
||||
- Check Settings → Plugins in Headlamp UI
|
||||
- Or: `kubectl exec -n kube-system deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
|
||||
|
||||
3. **Browser Console Output**:
|
||||
|
||||
- Open DevTools (F12) → Console
|
||||
- Screenshot or copy errors
|
||||
|
||||
4. **Network Tab**:
|
||||
|
||||
- Open DevTools → Network
|
||||
- Screenshot failed requests to `results.json`
|
||||
|
||||
5. **Pod Logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp --tail=100
|
||||
kubectl logs -n polaris deployment/polaris-dashboard --tail=100
|
||||
```
|
||||
|
||||
6. **RBAC Configuration**:
|
||||
```bash
|
||||
kubectl get role,rolebinding -n polaris
|
||||
```
|
||||
|
||||
### Where to Get Help
|
||||
|
||||
- **GitHub Issues**: [https://github.com/privilegedescalation/headlamp-polaris-plugin/issues](https://github.com/privilegedescalation/headlamp-polaris-plugin/issues)
|
||||
- **GitHub Discussions**: [https://github.com/privilegedescalation/headlamp-polaris-plugin/discussions](https://github.com/privilegedescalation/headlamp-polaris-plugin/discussions)
|
||||
|
||||
Include the debugging information above when opening an issue.
|
||||
@@ -0,0 +1,222 @@
|
||||
# ADR-001: Use React Context for State Management
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-02-12
|
||||
**Deciders:** Plugin maintainers
|
||||
|
||||
## Context
|
||||
|
||||
The Headlamp Polaris Plugin needs to fetch Polaris audit data once and share it across multiple components:
|
||||
|
||||
- Dashboard view (cluster overview)
|
||||
- Namespaces list view
|
||||
- Namespace detail view (drawer)
|
||||
- Inline audit sections on resource detail pages
|
||||
- App bar score badge
|
||||
|
||||
Multiple state management approaches are available: Redux, Zustand, Jotai, Recoil, React Context (built-in), or component props with prop drilling.
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Headlamp plugin environment does not allow adding external dependencies (peer dependencies only)
|
||||
- Redux, Zustand, Jotai, Recoil are not available in the plugin runtime
|
||||
- Plugin must work with Headlamp's existing React context (React 17+)
|
||||
- Bundle size should remain small (<50 KB)
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Share `AuditData` object across all views without duplicate API calls
|
||||
- Support auto-refresh on user-configurable interval (1-30 minutes)
|
||||
- Handle loading and error states consistently
|
||||
- Minimal re-renders (data updates infrequently)
|
||||
|
||||
## Decision
|
||||
|
||||
Use **React Context API** (built-in, no dependencies) for shared state management.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- `PolarisDataProvider` wraps all plugin routes
|
||||
- `usePolarisDataContext()` hook provides `{ data, loading, error, refresh }` to consumers
|
||||
- Single fetch shared across all views
|
||||
- Auto-refresh via `useEffect` + interval timer
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ **No additional dependencies** - Plugins cannot add external libraries (Headlamp constraint)
|
||||
- ✅ **Simple implementation** - Single AuditData object, read-only, no complex mutations
|
||||
- ✅ **Built into React** - No learning curve, well-documented, stable API
|
||||
- ✅ **Small bundle impact** - 0 KB additional (built-in feature)
|
||||
- ✅ **Works with Headlamp** - Compatible with Headlamp's React version and plugin system
|
||||
- ✅ **TypeScript support** - Full type safety with `React.createContext<T>()`
|
||||
|
||||
### Negative
|
||||
|
||||
- ❌ **Less powerful for complex state** - No built-in middleware, time-travel debugging, or DevTools
|
||||
- ❌ **Potential for unnecessary re-renders** - All consumers re-render on context update
|
||||
- **Mitigated by:** Data updates every 5-30 minutes (low frequency), memoization not needed
|
||||
- ❌ **No built-in async handling** - Must implement loading/error states manually
|
||||
- **Mitigated by:** Simple `useState` + `useEffect` pattern sufficient
|
||||
|
||||
### Neutral
|
||||
|
||||
- Performance is excellent for this use case (infrequent updates, small consumer count)
|
||||
- Context providers work well for read-only or mostly-read state
|
||||
- Standard React pattern, familiar to contributors
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Option 1: Redux
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Powerful state management with middleware
|
||||
- Excellent DevTools for debugging
|
||||
- Time-travel debugging
|
||||
- Well-established patterns
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Redux is not available as a peer dependency in Headlamp plugins
|
||||
- Massive overkill for single AuditData object
|
||||
- Adds significant bundle size (~10-15 KB)
|
||||
- Requires additional boilerplate (actions, reducers, store)
|
||||
|
||||
**Decision:** Rejected (dependency not available, too heavy)
|
||||
|
||||
### Option 2: Zustand
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Lightweight (~1 KB)
|
||||
- Simple API similar to `useState`
|
||||
- No provider boilerplate
|
||||
|
||||
**Cons:**
|
||||
|
||||
- External peer dependency (not available in plugin runtime)
|
||||
- Still adds bundle size
|
||||
- Unnecessary for read-only state
|
||||
|
||||
**Decision:** Rejected (dependency not available)
|
||||
|
||||
### Option 3: Component Props (Prop Drilling)
|
||||
|
||||
**Pros:**
|
||||
|
||||
- No dependencies
|
||||
- Explicit data flow
|
||||
- TypeScript tracks prop types
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Prop drilling through 5+ component layers (index.tsx → route → view → subcomponent)
|
||||
- Duplicate fetches if not carefully managed
|
||||
- Refactoring nightmare if component tree changes
|
||||
- Each route would need its own fetch logic
|
||||
|
||||
**Decision:** Rejected (poor code organization, maintenance burden)
|
||||
|
||||
### Option 4: Global Variable / Module State
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Simple to implement
|
||||
- No React dependencies
|
||||
|
||||
**Cons:**
|
||||
|
||||
- No reactivity (components don't re-render on data change)
|
||||
- No built-in loading/error handling
|
||||
- Breaks React's declarative model
|
||||
- Testing difficulties (global mutable state)
|
||||
|
||||
**Decision:** Rejected (not idiomatic React, no reactivity)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
**Context Definition:**
|
||||
|
||||
```typescript
|
||||
interface PolarisDataContextValue {
|
||||
data: AuditData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const PolarisDataContext = React.createContext<PolarisDataContextValue | undefined>(undefined);
|
||||
```
|
||||
|
||||
**Provider Implementation:**
|
||||
|
||||
```typescript
|
||||
export function PolarisDataProvider({ children }: { children: React.ReactNode }) {
|
||||
const [data, setData] = useState<AuditData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setRefreshKey(k => k + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch logic here
|
||||
// Auto-refresh on interval
|
||||
}, [refreshKey]);
|
||||
|
||||
return (
|
||||
<PolarisDataContext.Provider value={{ data, loading, error, refresh }}>
|
||||
{children}
|
||||
</PolarisDataContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Consumer Hook:**
|
||||
|
||||
```typescript
|
||||
export function usePolarisDataContext() {
|
||||
const context = useContext(PolarisDataContext);
|
||||
if (!context) {
|
||||
throw new Error('usePolarisDataContext must be used within PolarisDataProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Criteria
|
||||
|
||||
**Success Metrics:**
|
||||
|
||||
- ✅ All views share single fetch (verified via network tab - one request per refresh)
|
||||
- ✅ No duplicate API calls (verified via Kubernetes audit logs)
|
||||
- ✅ Auto-refresh works correctly (5-30 minute intervals)
|
||||
- ✅ Loading states consistent across views
|
||||
- ✅ Error handling consistent across views
|
||||
- ✅ Bundle size remains <50 KB (currently ~27 KB)
|
||||
|
||||
**Tested Scenarios:**
|
||||
|
||||
- ✅ Initial load with loading spinner
|
||||
- ✅ Error handling (403, 404, network errors)
|
||||
- ✅ Manual refresh via button
|
||||
- ✅ Auto-refresh interval (configurable via settings)
|
||||
- ✅ Multiple views consuming same data (no duplicate fetches)
|
||||
- ✅ Navigation between routes (data persists)
|
||||
|
||||
## References
|
||||
|
||||
- [React Context API](https://react.dev/reference/react/useContext)
|
||||
- [React Context Performance](https://react.dev/reference/react/useContext#optimizing-re-renders-when-passing-objects-and-functions)
|
||||
- [Headlamp Plugin Constraints](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Plugin Implementation](../../api/PolarisDataContext.tsx)
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Author | Change |
|
||||
| ---------- | ----------- | ---------------- |
|
||||
| 2026-02-12 | Plugin Team | Initial decision |
|
||||
@@ -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 |
|
||||
@@ -0,0 +1,97 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
This directory contains Architecture Decision Records (ADRs) for significant architectural choices made in the Headlamp Polaris Plugin.
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record (ADR) captures an important architectural decision made along with its context and consequences. AD Rs provide historical context for future developers and serve as documentation for why certain approaches were chosen.
|
||||
|
||||
## When to Create an ADR
|
||||
|
||||
Create an ADR when:
|
||||
|
||||
- Making a significant architectural choice (e.g., state management approach)
|
||||
- Selecting between multiple technology options (e.g., React Context vs. Redux)
|
||||
- Establishing a pattern that impacts multiple components
|
||||
- Making a trade-off decision with non-trivial consequences
|
||||
- Introducing a new dependency or external integration
|
||||
- Defining security or performance constraints
|
||||
|
||||
## ADR Format
|
||||
|
||||
Each ADR follows this template (based on Michael Nygard's format):
|
||||
|
||||
```markdown
|
||||
# ADR-NNN: Title
|
||||
|
||||
**Status**: [Proposed | Accepted | Deprecated | Superseded by ADR-XXX]
|
||||
**Date**: YYYY-MM-DD
|
||||
**Deciders**: [List key decision makers]
|
||||
|
||||
## Context
|
||||
|
||||
What is the issue that we're seeing that is motivating this decision or change?
|
||||
|
||||
## Decision
|
||||
|
||||
What is the change that we're proposing and/or doing?
|
||||
|
||||
## Consequences
|
||||
|
||||
What becomes easier or more difficult to do because of this change?
|
||||
|
||||
### Positive
|
||||
|
||||
- ...
|
||||
|
||||
### Negative
|
||||
|
||||
- ...
|
||||
|
||||
### Neutral
|
||||
|
||||
- ...
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Option 1: Name
|
||||
|
||||
**Pros**: ...
|
||||
**Cons**: ...
|
||||
**Decision**: Not chosen because...
|
||||
|
||||
## References
|
||||
|
||||
- [Link to related issues, docs, discussions]
|
||||
```
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
| ------------------------------------- | -------------------------------------- | -------- | ---------- |
|
||||
| [001](001-react-context-for-state.md) | Use React Context for State Management | Accepted | 2026-02-12 |
|
||||
| [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. **Determine the next ADR number** (e.g., if last ADR is 004, new ADR is 005)
|
||||
2. **Create a new file**: `NNN-short-title.md` (e.g., `005-exemption-management.md`)
|
||||
3. **Use the template above** and fill in all sections
|
||||
4. **Add entry to this README** in the ADR Index table
|
||||
5. **Submit for review** via pull request
|
||||
|
||||
## ADR Lifecycle
|
||||
|
||||
- **Proposed**: ADR is drafted and under discussion
|
||||
- **Accepted**: Decision has been made and is currently in effect
|
||||
- **Deprecated**: Decision is no longer recommended but not yet superseded
|
||||
- **Superseded by ADR-XXX**: Decision has been replaced by a newer ADR
|
||||
|
||||
## References
|
||||
|
||||
- [ADR GitHub Organization](https://adr.github.io/)
|
||||
- [Michael Nygard's ADR Template](https://github.com/joelparkerhenderson/architecture-decision-record/blob/main/templates/decision-record-template-by-michael-nygard/index.md)
|
||||
- [ADR Tools](https://github.com/npryce/adr-tools)
|
||||
@@ -0,0 +1,397 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
- **[Architecture Overview](overview.md)** - High-level component hierarchy
|
||||
- **[Design Decisions](design-decisions.md)** - Key architectural choices
|
||||
- **[ADRs](adr/README.md)** - Formal Architecture Decision Records
|
||||
|
||||
## References
|
||||
|
||||
- [Polaris API Documentation](https://polaris.docs.fairwinds.com/)
|
||||
- [React Context API](https://react.dev/reference/react/useContext)
|
||||
- [Headlamp ApiProxy](https://headlamp.dev/docs/latest/development/api/)
|
||||
@@ -0,0 +1,296 @@
|
||||
# Design Decisions
|
||||
|
||||
Key architectural choices and their rationale for the Headlamp Polaris Plugin.
|
||||
|
||||
## 1. Service Proxy vs. Direct Access
|
||||
|
||||
**Decision:** Use Kubernetes service proxy, not direct ClusterIP access
|
||||
|
||||
**Context:**
|
||||
|
||||
- Plugin needs to access Polaris dashboard API
|
||||
- Two options: Direct ClusterIP access or Kubernetes service proxy
|
||||
-Headlamp already has K8s API credentials
|
||||
|
||||
**Decision:**
|
||||
Use service proxy path: `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
|
||||
|
||||
**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)
|
||||
- Consistent with Headlamp's architecture (all API calls go through K8s API)
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** Simpler RBAC, works with user tokens, no new credentials
|
||||
- ❌ **Cons:** Longer URL path, requires `services/proxy` permission
|
||||
|
||||
**Alternatives Considered:**
|
||||
|
||||
- Direct ClusterIP access → Rejected (requires new credentials, network policies)
|
||||
- External Polaris URL → Supported as optional feature (custom URL setting)
|
||||
|
||||
## 2. React Context vs. Redux/Zustand
|
||||
|
||||
**Decision:** Use React Context for state management
|
||||
|
||||
**Context:**
|
||||
|
||||
- Plugin needs to share Polaris audit data across multiple views
|
||||
- Options: React Context, Redux, Zustand, or component props
|
||||
|
||||
**Decision:**
|
||||
Use React Context with `PolarisDataProvider`
|
||||
|
||||
**Rationale:**
|
||||
|
||||
1. **Simple state:** Single AuditData object, no complex mutations
|
||||
2. **Read-only:** No transactions, undo/redo, or optimistic updates
|
||||
3. **Headlamp constraints:** Cannot add external dependencies (Redux not bundled)
|
||||
4. **Performance:** Data changes infrequently (5-30 minute refresh interval)
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** No dependencies, simple API, built-in React feature
|
||||
- ❌ **Cons:** All consumers re-render on data change (acceptable for infrequent updates)
|
||||
|
||||
**Alternatives Considered:**
|
||||
|
||||
- Redux → Rejected (not available in plugin environment)
|
||||
- Zustand → Rejected (requires external dependency)
|
||||
- Component props → Rejected (prop drilling, duplicate fetches)
|
||||
|
||||
## 3. Drawer Navigation vs. Dedicated Routes
|
||||
|
||||
**Decision:** Use drawer for namespace detail, not dedicated route
|
||||
|
||||
**Context:**
|
||||
|
||||
- Namespaces list needs drill-down to per-namespace detail
|
||||
- Options: Dedicated route (`/polaris/ns/:namespace`) or drawer overlay
|
||||
|
||||
**Decision:**
|
||||
Use drawer with URL hash (`/polaris/namespaces#kube-system`)
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- **Better UX:** Drawer overlays table, preserves scroll position and context
|
||||
- **URL hash:** Preserves navigation state, supports browser back/forward
|
||||
- **Keyboard shortcuts:** Escape key to close drawer
|
||||
- **Sidebar limitation:** Headlamp sidebar doesn't support 3-level nesting
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** Better UX, preserves context, keyboard navigation
|
||||
- ❌ **Cons:** Hash-based routing (not "true" route), drawer accessibility considerations
|
||||
|
||||
**Alternatives Considered:**
|
||||
|
||||
- Dedicated route → Rejected (loses table context, requires back navigation)
|
||||
- Modal → Rejected (less natural for drill-down, no URL state)
|
||||
|
||||
## 4. No MUI Direct Imports
|
||||
|
||||
**Decision:** Never import from `@mui/material` or `@mui/icons-material`
|
||||
|
||||
**Context:**
|
||||
|
||||
- Plugin needs UI components (buttons, icons, etc.)
|
||||
- Headlamp uses MUI but doesn't expose full library to plugins
|
||||
|
||||
**Decision:**
|
||||
Use only Headlamp CommonComponents or HTML elements with inline styles
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Importing MUI causes `createSvgIcon undefined` runtime error
|
||||
- Headlamp plugin environment provides limited MUI exports
|
||||
- CommonComponents cover 90% of use cases
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Use `StatusLabel`, `SectionBox`, `SimpleTable` from CommonComponents
|
||||
- Use standard HTML elements (`<button>`, `<div>`) with inline styles
|
||||
- Use theme-aware CSS variables (`--mui-palette-background-paper`)
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** No runtime errors, smaller bundle, consistent with Headlamp
|
||||
- ❌ **Cons:** Limited component variety, inline styles verbose
|
||||
|
||||
**Alternatives Considered:**
|
||||
|
||||
- Bundle full MUI → Rejected (huge bundle size, version conflicts)
|
||||
- Use Headlamp's MUI exports → Rejected (incomplete, undocumented)
|
||||
|
||||
## 5. Two-Level Sidebar Nesting
|
||||
|
||||
**Decision:** Sidebar has "Polaris" → "Overview" and "Namespaces" (2 levels max)
|
||||
|
||||
**Context:**
|
||||
|
||||
- Plugin needs hierarchical navigation
|
||||
- Headlamp sidebar supports limited nesting depth
|
||||
|
||||
**Decision:**
|
||||
Use 2-level sidebar: `Polaris` (parent) → `Overview`, `Namespaces` (children)
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Headlamp sidebar `Collapse` component only supports 2 levels
|
||||
- Deeper nesting (Polaris → Namespaces → <each namespace>) doesn't work
|
||||
- Sidebar collapse is route-based, not click-to-toggle
|
||||
|
||||
**Workaround:**
|
||||
|
||||
- Namespace navigation via table (NamespacesListView)
|
||||
- Clickable namespace buttons open drawer (not new route)
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** Works within Headlamp constraints
|
||||
- ❌ **Cons:** Can't have dynamic per-namespace sidebar entries
|
||||
|
||||
**Alternatives Considered:**
|
||||
|
||||
- Dynamic sidebar with namespace entries → Rejected (Headlamp limitation)
|
||||
- Flat sidebar (no nesting) → Rejected (poor UX for plugin with multiple views)
|
||||
|
||||
## 6. TypeScript Strict Mode
|
||||
|
||||
**Decision:** Enable all TypeScript strict checks
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Catch errors at compile time (not runtime)
|
||||
- Better IDE support and autocomplete
|
||||
- Enforces type safety (no `any`, no implicit unknowns)
|
||||
- Easier refactoring (type errors surface immediately)
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** Fewer runtime errors, better maintainability, self-documenting code
|
||||
- ❌ **Cons:** More verbose code, steeper learning curve
|
||||
|
||||
## 7. Auto-Refresh Default: 5 Minutes
|
||||
|
||||
**Decision:** Default refresh interval is 5 minutes (configurable 1-30 min)
|
||||
|
||||
**Context:**
|
||||
|
||||
- Plugin needs to refresh Polaris data periodically
|
||||
- Polaris audits typically run every 10-30 minutes
|
||||
|
||||
**Decision:**
|
||||
Default to 5 minutes, allow user to configure (1 / 5 / 10 / 30 minutes)
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Balance between data freshness and API load
|
||||
- Polaris audits don't change frequently (10-30 min intervals)
|
||||
- 5 minutes provides reasonably fresh data without excessive API calls
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** Reasonable default, user-configurable, low API load
|
||||
- ❌ **Cons:** Not real-time (acceptable for audit data)
|
||||
|
||||
**Alternatives Considered:**
|
||||
|
||||
- WebSocket/SSE for real-time → Rejected (Polaris dashboard doesn't support)
|
||||
- 1 minute default → Rejected (unnecessary API calls, audit data changes slowly)
|
||||
- 30 minute default → Rejected (too stale for interactive dashboard)
|
||||
|
||||
## 8. Read-Only Plugin
|
||||
|
||||
**Decision:** Plugin is read-only (no write operations)
|
||||
|
||||
**Context:**
|
||||
|
||||
- Plugin could potentially modify Polaris configuration or add exemptions
|
||||
- Write operations require additional RBAC permissions (PATCH, CREATE)
|
||||
|
||||
**Decision:**
|
||||
Plugin only performs GET requests (read-only)
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- **Security:** Minimal RBAC footprint (`get` on `services/proxy` only)
|
||||
- **Simplicity:** No mutation logic, error handling for writes, or rollback
|
||||
- **Polaris design:** Exemptions managed via annotations (outside plugin scope)
|
||||
- **Future:** Can add writes later if user demand exists
|
||||
|
||||
**Trade-offs:**
|
||||
|
||||
- ✅ **Pros:** Minimal permissions, simpler code, fewer failure modes
|
||||
- ❌ **Cons:** Cannot add exemptions via UI (must edit annotations manually)
|
||||
|
||||
**Future Enhancement:**
|
||||
|
||||
- Add PATCH permission for workload annotations
|
||||
- Implement `ExemptionManager` component (UI exists, not integrated)
|
||||
|
||||
## 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 (annotation-based exemptions) not fully counted
|
||||
|
||||
**Reason:** Polaris API omits exempted checks from `results.json`
|
||||
|
||||
**Impact:** "Skipped" count only reflects checks with `Severity: "ignore"`
|
||||
|
||||
**Documented:** README, tooltip on skipped count, KNOWN_LIMITATIONS section
|
||||
|
||||
### 3. No Real-Time Updates
|
||||
|
||||
**Limitation:** Data refreshes on interval (1-30 min), not real-time
|
||||
|
||||
**Reason:** Polaris dashboard doesn't support WebSocket/SSE
|
||||
|
||||
**Workaround:** Manual refresh button, configurable interval
|
||||
|
||||
### 4. Single Cluster Support
|
||||
|
||||
**Limitation:** Plugin shows data for current cluster only
|
||||
|
||||
**Reason:** Headlamp's multi-cluster support is route-based (`/c/<cluster>/...`)
|
||||
|
||||
**Impact:** Must switch clusters in Headlamp to see different cluster's data
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Architecture Overview](overview.md)** - High-level component hierarchy
|
||||
- **[Data Flow](data-flow.md)** - Detailed data flow sequences
|
||||
- **[ADRs](adr/README.md)** - Formal Architecture Decision Records
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Plugin Constraints](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [React Context Performance](https://react.dev/reference/react/useContext#optimizing-re-renders-when-passing-objects-and-functions)
|
||||
- [TypeScript Strict Mode](https://www.typescriptlang.org/tsconfig#strict)
|
||||
@@ -0,0 +1,333 @@
|
||||
# Architecture Overview
|
||||
|
||||
High-level architecture of the Headlamp Polaris Plugin.
|
||||
|
||||
## 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) │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
## 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:**
|
||||
|
||||
1. **Simple state:** Single AuditData object shared across views
|
||||
2. **Read-only:** No complex mutations or transactions
|
||||
3. **Headlamp constraints:** Plugin cannot add dependencies (Redux not bundled)
|
||||
4. **Performance:** Data changes infrequently (refresh interval 1-30 min)
|
||||
|
||||
### Context Structure
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
1. **Initial fetch:** On first mount of any component using the context
|
||||
2. **Auto-refresh:** Based on user setting (default 5 minutes)
|
||||
3. **Manual refresh:** Via refresh button in UI
|
||||
4. **Caching:** Data persists in context until refresh (no per-route refetch)
|
||||
|
||||
### localStorage Usage
|
||||
|
||||
Settings persisted in localStorage:
|
||||
|
||||
- **`polaris-plugin-refresh-interval`**: Number (seconds), default 300
|
||||
- **`polaris-plugin-dashboard-url`**: String, default service proxy path
|
||||
|
||||
No sensitive data stored in localStorage.
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Headlamp Plugin API
|
||||
|
||||
**Version:** ≥ v0.13.0
|
||||
|
||||
**Registration Functions Used:**
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
|
||||
- `registerDetailsViewSection` now takes 1 argument (component), not 2 (name, component)
|
||||
- `registerAppBarAction` now takes 1 argument (component), not 2 (name, component)
|
||||
|
||||
### Headlamp CommonComponents
|
||||
|
||||
**Used Components:**
|
||||
|
||||
- `SectionBox` - Card-like container with title
|
||||
- `SectionHeader` - Page header with title
|
||||
- `StatusLabel` - Color-coded status badges
|
||||
- `NameValueTable` - Key-value table layout
|
||||
- `SimpleTable` - Data table with sorting
|
||||
- `Drawer` - Right-side overlay panel
|
||||
- `Loader` - 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:**
|
||||
|
||||
- `get` on `services/proxy` for `polaris-dashboard` in `polaris` namespace
|
||||
|
||||
## 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
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Data Flow](data-flow.md)** - Detailed data flow diagrams and sequences
|
||||
- **[Design Decisions](design-decisions.md)** - Architecture decision records
|
||||
- **[ADRs](adr/README.md)** - Formal Architecture Decision Records
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Fairwinds Polaris Documentation](https://polaris.docs.fairwinds.com/)
|
||||
- [React Context API](https://react.dev/reference/react/useContext)
|
||||
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
@@ -0,0 +1,432 @@
|
||||
# Helm Deployment
|
||||
|
||||
Deploy the Headlamp Polaris Plugin using Helm charts.
|
||||
|
||||
## Overview
|
||||
|
||||
Helm provides the easiest way to deploy and manage the plugin in production. This guide covers:
|
||||
|
||||
- Helm values configuration
|
||||
- Plugin Manager integration
|
||||
- FluxCD HelmRelease integration
|
||||
- Upgrade procedures
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Helm v3+ installed
|
||||
- Kubernetes cluster access
|
||||
- Headlamp Helm repository added
|
||||
|
||||
```bash
|
||||
# Add Headlamp Helm repository
|
||||
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
|
||||
helm repo update
|
||||
```
|
||||
|
||||
## Basic Helm Installation
|
||||
|
||||
### Minimal Configuration
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
```
|
||||
|
||||
```bash
|
||||
# Install Headlamp
|
||||
helm install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
|
||||
# Wait for deployment
|
||||
kubectl -n kube-system wait --for=condition=available deployment/headlamp --timeout=300s
|
||||
```
|
||||
|
||||
After installation, install the plugin via Headlamp UI (**Settings → Plugins → Catalog**).
|
||||
|
||||
## Complete Production Configuration
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: ghcr.io/headlamp-k8s/headlamp
|
||||
tag: v0.39.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
config:
|
||||
baseURL: ''
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: headlamp-tls
|
||||
hosts:
|
||||
- headlamp.example.com
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: headlamp
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
topologyKey: kubernetes.io/hostname
|
||||
|
||||
# OIDC Authentication (optional)
|
||||
env:
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_ID
|
||||
value: 'headlamp'
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: headlamp-oidc
|
||||
key: client-secret
|
||||
- name: HEADLAMP_CONFIG_OIDC_ISSUER_URL
|
||||
value: 'https://auth.example.com/realms/kubernetes'
|
||||
- name: HEADLAMP_CONFIG_OIDC_SCOPES
|
||||
value: 'openid,profile,email,groups'
|
||||
```
|
||||
|
||||
Deploy:
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
```
|
||||
|
||||
## Sidecar Plugin Installation Method
|
||||
|
||||
Alternative to Plugin Manager: use an init container to download the plugin.
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
initContainers:
|
||||
- name: install-polaris-plugin
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml --plugins-dir /plugins
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
```
|
||||
|
||||
Create the ConfigMap:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
version: 0.3.5
|
||||
url: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
Apply ConfigMap then deploy Headlamp:
|
||||
|
||||
```bash
|
||||
kubectl apply -f headlamp-plugin-config.yaml
|
||||
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
## FluxCD HelmRelease Integration
|
||||
|
||||
For GitOps workflows with FluxCD:
|
||||
|
||||
### HelmRepository
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://kubernetes-sigs.github.io/headlamp/
|
||||
```
|
||||
|
||||
### HelmRelease
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
interval: 30m
|
||||
chart:
|
||||
spec:
|
||||
chart: headlamp
|
||||
version: 0.26.x # Use semver range
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: headlamp
|
||||
namespace: flux-system
|
||||
interval: 12h
|
||||
|
||||
install:
|
||||
crds: CreateReplace
|
||||
remediation:
|
||||
retries: 3
|
||||
|
||||
upgrade:
|
||||
crds: CreateReplace
|
||||
remediation:
|
||||
retries: 3
|
||||
|
||||
values:
|
||||
replicaCount: 2
|
||||
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
# Health checks
|
||||
postRenderers:
|
||||
- kustomize:
|
||||
patches:
|
||||
- target:
|
||||
kind: Deployment
|
||||
name: headlamp
|
||||
patch: |
|
||||
- op: add
|
||||
path: /spec/template/spec/containers/0/livenessProbe
|
||||
value:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
Apply FluxCD resources:
|
||||
|
||||
```bash
|
||||
kubectl apply -f helmrepository.yaml
|
||||
kubectl apply -f helmrelease.yaml
|
||||
|
||||
# Watch deployment
|
||||
flux get helmreleases -n kube-system --watch
|
||||
```
|
||||
|
||||
## RBAC Configuration
|
||||
|
||||
After deploying Headlamp, apply RBAC for the plugin:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
---
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
See [RBAC Permissions](../user-guide/rbac-permissions.md) for advanced RBAC configurations.
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Upgrade Headlamp
|
||||
|
||||
```bash
|
||||
# Update Helm repo
|
||||
helm repo update
|
||||
|
||||
# Upgrade Headlamp (preserves plugin configuration)
|
||||
helm upgrade headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml \
|
||||
--wait
|
||||
```
|
||||
|
||||
### Upgrade Plugin (Plugin Manager Method)
|
||||
|
||||
1. Navigate to **Settings → Plugins** in Headlamp UI
|
||||
2. Find "headlamp-polaris-plugin"
|
||||
3. Click **Update** if new version available
|
||||
4. Hard refresh browser (**Cmd+Shift+R** / **Ctrl+Shift+R**)
|
||||
|
||||
### Upgrade Plugin (Sidecar Method)
|
||||
|
||||
```bash
|
||||
# Update ConfigMap with new version
|
||||
kubectl -n kube-system edit configmap headlamp-plugin-config
|
||||
|
||||
# Update version and URL:
|
||||
# version: 0.3.6
|
||||
# url: https://github.com/.../v0.3.6/polaris-0.3.10.tar.gz
|
||||
|
||||
# Restart deployment to trigger init container
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
kubectl -n kube-system rollout status deployment/headlamp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
```bash
|
||||
# Check Headlamp values
|
||||
helm get values headlamp -n kube-system
|
||||
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# If missing, reinstall plugin via UI or check init container logs
|
||||
kubectl -n kube-system logs deployment/headlamp -c install-polaris-plugin
|
||||
```
|
||||
|
||||
### Helm Release Stuck
|
||||
|
||||
```bash
|
||||
# Check Helm release status
|
||||
helm list -n kube-system
|
||||
|
||||
# If stuck, force upgrade
|
||||
helm upgrade headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml \
|
||||
--force \
|
||||
--wait
|
||||
```
|
||||
|
||||
### FluxCD Reconciliation Issues
|
||||
|
||||
```bash
|
||||
# Check HelmRelease status
|
||||
flux get helmreleases -n kube-system
|
||||
|
||||
# Check events
|
||||
kubectl -n kube-system describe helmrelease headlamp
|
||||
|
||||
# Force reconciliation
|
||||
flux reconcile helmrelease headlamp -n kube-system
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Kubernetes Deployment](kubernetes.md)** - Raw Kubernetes manifests
|
||||
- **[Production Checklist](production.md)** - Production deployment best practices
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Comprehensive troubleshooting guide
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Helm Chart](https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp)
|
||||
- [Helm Documentation](https://helm.sh/docs/)
|
||||
- [FluxCD HelmRelease](https://fluxcd.io/flux/components/helm/helmreleases/)
|
||||
@@ -0,0 +1,489 @@
|
||||
# Kubernetes Deployment
|
||||
|
||||
Direct Kubernetes manifest deployment for the Headlamp Polaris Plugin.
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers deploying the plugin using raw Kubernetes manifests without Helm. This approach is useful for:
|
||||
|
||||
- Environments where Helm is not available
|
||||
- Highly customized deployments
|
||||
- GitOps workflows with Kustomize
|
||||
- Learning the underlying Kubernetes resources
|
||||
|
||||
## RBAC Manifests
|
||||
|
||||
The plugin requires read-only access to the Polaris dashboard service proxy.
|
||||
|
||||
### Complete RBAC Configuration
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Role: Read-only access to Polaris service proxy
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
rules:
|
||||
- apiGroups: ['']
|
||||
resources: ['services/proxy']
|
||||
resourceNames: ['polaris-dashboard']
|
||||
verbs: ['get']
|
||||
|
||||
---
|
||||
# RoleBinding: Grant Headlamp service account access
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Apply the RBAC manifests:
|
||||
|
||||
```bash
|
||||
kubectl apply -f polaris-plugin-rbac.yaml
|
||||
```
|
||||
|
||||
### RBAC Verification
|
||||
|
||||
```bash
|
||||
# Verify Role exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
|
||||
# Verify RoleBinding exists
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes
|
||||
```
|
||||
|
||||
## Plugin Installation via Init Container
|
||||
|
||||
Use an init container to download and install the plugin.
|
||||
|
||||
### ConfigMap for Plugin Configuration
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/component: plugin-config
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
version: 0.3.5
|
||||
url: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
### Headlamp Deployment with Plugin Init Container
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
spec:
|
||||
serviceAccountName: headlamp
|
||||
|
||||
# Init container to install plugins
|
||||
initContainers:
|
||||
- name: install-plugins
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml --plugins-dir /plugins
|
||||
echo "Plugin installation complete"
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
|
||||
containers:
|
||||
- name: headlamp
|
||||
image: ghcr.io/headlamp-k8s/headlamp:v0.39.0
|
||||
args:
|
||||
- '-in-cluster'
|
||||
- '-plugins-dir=/headlamp/plugins'
|
||||
env:
|
||||
- name: HEADLAMP_CONFIG_WATCH_PLUGINS
|
||||
value: 'false' # CRITICAL: Must be false for plugin manager
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 4466
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /headlamp/plugins
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
```
|
||||
|
||||
### Supporting Resources
|
||||
|
||||
```yaml
|
||||
---
|
||||
# ServiceAccount for Headlamp
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
|
||||
---
|
||||
# Service for Headlamp
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: headlamp
|
||||
```
|
||||
|
||||
## Complete Deployment Workflow
|
||||
|
||||
### 1. Apply All Manifests
|
||||
|
||||
```bash
|
||||
# Create RBAC for Polaris plugin
|
||||
kubectl apply -f polaris-plugin-rbac.yaml
|
||||
|
||||
# Create plugin configuration
|
||||
kubectl apply -f headlamp-plugin-config.yaml
|
||||
|
||||
# Deploy Headlamp with plugin init container
|
||||
kubectl apply -f headlamp-deployment.yaml
|
||||
kubectl apply -f headlamp-service.yaml
|
||||
kubectl apply -f headlamp-serviceaccount.yaml
|
||||
|
||||
# Wait for deployment to be ready
|
||||
kubectl -n kube-system wait --for=condition=available deployment/headlamp --timeout=300s
|
||||
```
|
||||
|
||||
### 2. Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check pods are running
|
||||
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
|
||||
# Expected output:
|
||||
# NAME READY STATUS RESTARTS AGE
|
||||
# headlamp-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
|
||||
|
||||
# Check init container logs
|
||||
kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
|
||||
# Expected output:
|
||||
# Plugin installation complete
|
||||
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x dist/
|
||||
# -rw-r--r-- package.json
|
||||
|
||||
# Test Polaris API access
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json \
|
||||
| jq .PolarisOutputVersion
|
||||
|
||||
# Expected output: "1.0" or similar
|
||||
```
|
||||
|
||||
### 3. Access Headlamp
|
||||
|
||||
```bash
|
||||
# Port-forward to access locally
|
||||
kubectl -n kube-system port-forward service/headlamp 8080:80
|
||||
|
||||
# Open browser to http://localhost:8080
|
||||
```
|
||||
|
||||
## Kustomize Integration
|
||||
|
||||
For GitOps workflows with Kustomize:
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
k8s/
|
||||
├── base/
|
||||
│ ├── kustomization.yaml
|
||||
│ ├── rbac.yaml
|
||||
│ ├── configmap.yaml
|
||||
│ ├── deployment.yaml
|
||||
│ ├── service.yaml
|
||||
│ └── serviceaccount.yaml
|
||||
└── overlays/
|
||||
├── production/
|
||||
│ ├── kustomization.yaml
|
||||
│ └── patches.yaml
|
||||
└── staging/
|
||||
├── kustomization.yaml
|
||||
└── patches.yaml
|
||||
```
|
||||
|
||||
### Base Kustomization
|
||||
|
||||
```yaml
|
||||
# k8s/base/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: kube-system
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
|
||||
resources:
|
||||
- serviceaccount.yaml
|
||||
- service.yaml
|
||||
- deployment.yaml
|
||||
- configmap.yaml
|
||||
- rbac.yaml
|
||||
|
||||
configMapGenerator:
|
||||
- name: headlamp-plugin-config
|
||||
files:
|
||||
- plugin.yml
|
||||
```
|
||||
|
||||
### Production Overlay
|
||||
|
||||
```yaml
|
||||
# k8s/overlays/production/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
bases:
|
||||
- ../../base
|
||||
|
||||
nameSuffix: -prod
|
||||
|
||||
replicas:
|
||||
- name: headlamp
|
||||
count: 2
|
||||
|
||||
patches:
|
||||
- path: patches.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
# k8s/overlays/production/patches.yaml
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: headlamp
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: headlamp
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
```
|
||||
|
||||
### Deploy with Kustomize
|
||||
|
||||
```bash
|
||||
# Build and preview
|
||||
kubectl kustomize k8s/overlays/production
|
||||
|
||||
# Apply
|
||||
kubectl apply -k k8s/overlays/production
|
||||
```
|
||||
|
||||
## FluxCD Integration
|
||||
|
||||
For GitOps with FluxCD:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: headlamp-polaris-plugin
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./k8s/overlays/production
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: infrastructure
|
||||
healthChecks:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
```
|
||||
|
||||
## Upgrading the Plugin
|
||||
|
||||
### Update ConfigMap
|
||||
|
||||
```bash
|
||||
# Edit ConfigMap with new version
|
||||
kubectl -n kube-system edit configmap headlamp-plugin-config
|
||||
|
||||
# Update version and URL:
|
||||
# version: 0.3.6
|
||||
# url: https://github.com/.../v0.3.6/polaris-0.3.10.tar.gz
|
||||
|
||||
# Restart deployment to trigger init container
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl -n kube-system rollout status deployment/headlamp
|
||||
```
|
||||
|
||||
### Verify Upgrade
|
||||
|
||||
```bash
|
||||
# Check init container logs
|
||||
kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
|
||||
# Verify new version in UI
|
||||
# Navigate to Settings → Plugins in Headlamp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Init Container Fails
|
||||
|
||||
```bash
|
||||
# Check init container logs
|
||||
kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
|
||||
# Common issues:
|
||||
# 1. Network connectivity to GitHub
|
||||
# 2. Invalid URL in ConfigMap
|
||||
# 3. Tarball checksum mismatch
|
||||
```
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
```bash
|
||||
# Verify HEADLAMP_CONFIG_WATCH_PLUGINS is false
|
||||
kubectl -n kube-system get deployment headlamp -o yaml | grep WATCH_PLUGINS
|
||||
|
||||
# Expected output:
|
||||
# - name: HEADLAMP_CONFIG_WATCH_PLUGINS
|
||||
# value: "false"
|
||||
|
||||
# If not set or "true", update deployment
|
||||
kubectl -n kube-system edit deployment headlamp
|
||||
```
|
||||
|
||||
### RBAC Permissions Denied
|
||||
|
||||
```bash
|
||||
# Test RBAC
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# If "no", verify RBAC manifests applied:
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Helm Deployment](helm.md)** - Deploy with Helm for easier management
|
||||
- **[Production Checklist](production.md)** - Production deployment best practices
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Comprehensive troubleshooting guide
|
||||
|
||||
## References
|
||||
|
||||
- [Kubernetes Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
|
||||
- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
- [Kustomize Documentation](https://kustomize.io/)
|
||||
- [FluxCD Kustomization](https://fluxcd.io/flux/components/kustomize/kustomization/)
|
||||
@@ -0,0 +1,488 @@
|
||||
# Production Deployment
|
||||
|
||||
Production deployment checklist, best practices, and security considerations for the Headlamp Polaris Plugin.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Pre-Deployment Checklist](#pre-deployment-checklist)
|
||||
- [Production Checklist](#production-checklist)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
- [High Availability](#high-availability)
|
||||
- [Monitoring and Observability](#monitoring-and-observability)
|
||||
- [Performance Tuning](#performance-tuning)
|
||||
- [Disaster Recovery](#disaster-recovery)
|
||||
- [Known Issues](#known-issues)
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
Before deploying to production:
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- [ ] Kubernetes cluster v1.24+ running
|
||||
- [ ] Polaris deployed in `polaris` namespace
|
||||
- [ ] Polaris dashboard service (`polaris-dashboard:80`) accessible
|
||||
- [ ] Headlamp v0.26+ deployed (v0.39+ recommended)
|
||||
- [ ] Ingress controller configured (if exposing externally)
|
||||
- [ ] TLS certificates provisioned (cert-manager recommended)
|
||||
|
||||
### Verification Commands
|
||||
|
||||
```bash
|
||||
# Verify Polaris
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# Test Polaris API
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Verify Headlamp
|
||||
kubectl -n kube-system get deployment headlamp
|
||||
kubectl -n kube-system get svc headlamp
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Deployment
|
||||
|
||||
- [ ] Plugin installed via Plugin Manager or sidecar init container
|
||||
- [ ] RBAC Role and RoleBinding applied
|
||||
- [ ] NetworkPolicies configured (if using strict network policies)
|
||||
- [ ] Headlamp pods running with 2+ replicas (high availability)
|
||||
- [ ] Resource limits and requests configured
|
||||
|
||||
### Post-Deployment Verification
|
||||
|
||||
```bash
|
||||
# 1. Verify Polaris API is accessible via service proxy
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
# Expected: "1.0" or similar
|
||||
|
||||
# 2. Verify RBAC permissions
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
# Expected: yes
|
||||
|
||||
# 3. Check Headlamp logs for plugin loading
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
# Expected: No errors related to plugin loading
|
||||
|
||||
# 4. Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
# Expected: dist/, package.json present
|
||||
```
|
||||
|
||||
### UI Verification
|
||||
|
||||
- [ ] Navigate to **Settings → Plugins**
|
||||
- [ ] Verify "headlamp-polaris-plugin" is listed with correct version
|
||||
- [ ] Sidebar shows "Polaris" entry
|
||||
- [ ] Click **Polaris → Overview** - page loads successfully
|
||||
- [ ] Cluster score gauge displays
|
||||
- [ ] Namespaces table loads with data
|
||||
- [ ] App bar shows Polaris score badge
|
||||
- [ ] Click namespace - detail drawer opens
|
||||
- [ ] Test inline audit section on a Deployment/StatefulSet
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### RBAC
|
||||
|
||||
**Principle of Least Privilege:**
|
||||
|
||||
```yaml
|
||||
# ✅ GOOD: Scoped to specific service
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
resourceNames: ["polaris-dashboard"]
|
||||
verbs: ["get"]
|
||||
|
||||
# ❌ BAD: Too broad
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
verbs: ["get"] # Allows proxy to ALL services
|
||||
```
|
||||
|
||||
**Token-Auth Mode:**
|
||||
|
||||
When Headlamp uses user-supplied tokens (OIDC), each user needs the RoleBinding:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: authenticated-users-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: system:authenticated # All authenticated users
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
For fine-grained control, bind specific users or groups:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: sre-team # Only SRE team
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Network Policies
|
||||
|
||||
If using strict NetworkPolicies:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-apiserver-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
app.kubernetes.io/component: dashboard
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from API server (performs the proxy hop)
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
component: kube-apiserver
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
```
|
||||
|
||||
**Note:** The API server proxies the request, not the Headlamp pod directly.
|
||||
|
||||
### Audit Logging
|
||||
|
||||
Kubernetes audit logs record every service proxy request:
|
||||
|
||||
- **What's logged:** User/service account, timestamp, response code
|
||||
- **Volume:** Auto-refresh interval affects audit log volume
|
||||
- **Recommendation:** Configure audit policy level if concerned about log volume
|
||||
|
||||
```yaml
|
||||
# audit-policy.yaml
|
||||
apiVersion: audit.k8s.io/v1
|
||||
kind: Policy
|
||||
rules:
|
||||
- level: Metadata # Log metadata only (not full request/response)
|
||||
verbs: ['get']
|
||||
resources:
|
||||
- group: ''
|
||||
resources: ['services/proxy']
|
||||
namespaces: ['polaris']
|
||||
```
|
||||
|
||||
### Data Sensitivity
|
||||
|
||||
Polaris audit data may contain:
|
||||
|
||||
- Resource names and namespaces
|
||||
- Configuration details
|
||||
- Potential security vulnerabilities
|
||||
|
||||
**Recommendation:** Restrict plugin access to authorized users only (not `system:authenticated` unless appropriate).
|
||||
|
||||
## High Availability
|
||||
|
||||
### Headlamp Replicas
|
||||
|
||||
Deploy Headlamp with 2+ replicas for high availability:
|
||||
|
||||
```yaml
|
||||
# helm-values.yaml
|
||||
replicaCount: 2
|
||||
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
topologyKey: kubernetes.io/hostname
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
```
|
||||
|
||||
### Pod Disruption Budget
|
||||
|
||||
Ensure at least one replica is always available during node maintenance:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: headlamp-pdb
|
||||
namespace: kube-system
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
Configure liveness and readiness probes:
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
```
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
### Metrics to Monitor
|
||||
|
||||
**Application Metrics:**
|
||||
|
||||
- Headlamp pod CPU/memory usage
|
||||
- HTTP request latency and error rates
|
||||
- Plugin load time
|
||||
|
||||
**Polaris Metrics:**
|
||||
|
||||
- Polaris dashboard API response time
|
||||
- Service proxy request latency
|
||||
- RBAC denial rate (403 errors)
|
||||
|
||||
### Prometheus Integration
|
||||
|
||||
Example ServiceMonitor for Headlamp:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
endpoints:
|
||||
- port: http
|
||||
interval: 30s
|
||||
path: /metrics
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
**Headlamp Logs:**
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
kubectl -n kube-system logs deployment/headlamp -f
|
||||
|
||||
# Filter for plugin-related logs
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
```
|
||||
|
||||
**Polaris Dashboard Logs:**
|
||||
|
||||
```bash
|
||||
kubectl -n polaris logs deployment/polaris-dashboard -f
|
||||
```
|
||||
|
||||
### Alerts
|
||||
|
||||
Recommended alerts:
|
||||
|
||||
- Headlamp pod not ready
|
||||
- High error rate (4xx/5xx)
|
||||
- Polaris dashboard unavailable
|
||||
- RBAC denials (403 errors)
|
||||
|
||||
Example PrometheusRule:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: headlamp-alerts
|
||||
namespace: kube-system
|
||||
spec:
|
||||
groups:
|
||||
- name: headlamp
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: HeadlampPodNotReady
|
||||
expr: kube_pod_status_ready{namespace="kube-system", pod=~"headlamp-.*"} == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: 'Headlamp pod not ready'
|
||||
description: 'Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} has been not ready for 5 minutes.'
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Plugin Refresh Interval
|
||||
|
||||
The plugin auto-refreshes Polaris data at a configurable interval (default: 5 minutes).
|
||||
|
||||
**Recommendations:**
|
||||
|
||||
- **High-traffic clusters:** 10-30 minutes (reduces API server load)
|
||||
- **Low-traffic clusters:** 1-5 minutes (more real-time data)
|
||||
|
||||
Configure via **Settings → Plugins → Polaris** in Headlamp UI.
|
||||
|
||||
### Browser Caching
|
||||
|
||||
The plugin uses localStorage for settings. Browser cache can affect plugin loading.
|
||||
|
||||
**Best Practice:** Instruct users to hard refresh after plugin updates (**Cmd+Shift+R** / **Ctrl+Shift+R**).
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Recommended resource limits for Headlamp with plugin:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
```
|
||||
|
||||
Adjust based on cluster size and user count.
|
||||
|
||||
## Disaster Recovery
|
||||
|
||||
### Backup Considerations
|
||||
|
||||
**What to back up:**
|
||||
|
||||
- Headlamp Helm values or Kubernetes manifests
|
||||
- RBAC manifests (Role, RoleBinding)
|
||||
- Plugin configuration (ConfigMap if using sidecar method)
|
||||
|
||||
**What NOT to back up:**
|
||||
|
||||
- Plugin tarball (available on GitHub releases)
|
||||
- Polaris audit data (regenerated by Polaris)
|
||||
- Browser localStorage (user-specific settings)
|
||||
|
||||
### Recovery Procedure
|
||||
|
||||
If Headlamp or plugin becomes unavailable:
|
||||
|
||||
1. **Verify Polaris is running:**
|
||||
|
||||
```bash
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
```
|
||||
|
||||
2. **Redeploy Headlamp:**
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
3. **Reapply RBAC:**
|
||||
|
||||
```bash
|
||||
kubectl apply -f polaris-plugin-rbac.yaml
|
||||
```
|
||||
|
||||
4. **Verify plugin files:**
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system exec deployment/headlamp -- \
|
||||
ls /headlamp/plugins/headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
5. **Hard refresh browser:**
|
||||
**Cmd+Shift+R** / **Ctrl+Shift+R**
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Skipped Count Limitation
|
||||
|
||||
**Symptom:** "Skipped" count in UI is lower than native Polaris dashboard
|
||||
|
||||
**Cause:** Plugin only counts checks with `Severity: "ignore"` from API response
|
||||
|
||||
**Explanation:**
|
||||
|
||||
Polaris omits annotation-based exemptions (e.g., `polaris.fairwinds.com/*-exempt`) from the `results.json` endpoint. The native Polaris dashboard computes skipped count by querying raw Kubernetes resources and parsing annotations.
|
||||
|
||||
**Workaround:** Use "View in Polaris Dashboard" link for accurate exemption count.
|
||||
|
||||
**Future Enhancement:** Would require cluster-wide read access to all workload types (significant RBAC expansion).
|
||||
|
||||
### ArtifactHub Sync Delay
|
||||
|
||||
**Symptom:** New plugin version not appearing in Headlamp catalog
|
||||
|
||||
**Cause:** ArtifactHub syncs from GitHub every 30 minutes (no webhook/push mechanism)
|
||||
|
||||
**Solution:** Wait 30 minutes after GitHub release for new version to appear in catalog.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For production issues, see:
|
||||
|
||||
- **[Troubleshooting Guide](../troubleshooting/README.md)** - Comprehensive troubleshooting
|
||||
- **[RBAC Issues](../troubleshooting/rbac-issues.md)** - Permission debugging
|
||||
- **[Network Problems](../troubleshooting/network-problems.md)** - Connectivity issues
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Kubernetes Deployment](kubernetes.md)** - Raw manifest deployment
|
||||
- **[Helm Deployment](helm.md)** - Helm chart deployment
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Issue resolution
|
||||
|
||||
## References
|
||||
|
||||
- [Kubernetes Production Best Practices](https://kubernetes.io/docs/setup/best-practices/)
|
||||
- [Headlamp Security](https://headlamp.dev/docs/latest/installation/in-cluster/#security)
|
||||
- [Polaris Configuration](https://polaris.docs.fairwinds.com/customization/checks/)
|
||||
@@ -0,0 +1,703 @@
|
||||
# Testing Guide
|
||||
|
||||
Comprehensive guide to testing the Headlamp Polaris Plugin, covering unit tests, E2E tests, and CI/CD integration.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Unit Testing](#unit-testing)
|
||||
- [E2E Testing](#e2e-testing)
|
||||
- [CI/CD Integration](#cicd-integration)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Headlamp Polaris Plugin uses a multi-layered testing approach:
|
||||
|
||||
| Test Type | Framework | Purpose | Location |
|
||||
| ----------------- | ---------- | ------------------------------------------------------- | ----------------------- |
|
||||
| **Unit Tests** | Vitest | Test individual functions and components in isolation | `src/**/*.test.ts(x)` |
|
||||
| **E2E Tests** | Playwright | Test complete user flows against live Headlamp instance | `e2e/*.spec.ts` |
|
||||
| **Type Checking** | TypeScript | Ensure type safety across codebase | `tsc --noEmit` |
|
||||
| **Linting** | ESLint | Enforce code style and catch common errors | `eslint src/` |
|
||||
| **Formatting** | Prettier | Maintain consistent code formatting | `prettier --check src/` |
|
||||
|
||||
### Test Philosophy
|
||||
|
||||
1. **Unit tests** focus on business logic (data parsing, score calculation, filtering)
|
||||
2. **E2E tests** validate user-facing functionality (navigation, rendering, interactions)
|
||||
3. **Both** run automatically in CI on every commit
|
||||
4. **Coverage** targets meaningful tests, not arbitrary percentages
|
||||
|
||||
---
|
||||
|
||||
## Unit Testing
|
||||
|
||||
### Framework: Vitest
|
||||
|
||||
Vitest is a fast, modern testing framework compatible with Jest APIs but optimized for Vite-based projects.
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
npm test
|
||||
|
||||
# Run in watch mode (re-runs on file changes)
|
||||
npm run test:watch
|
||||
|
||||
# Run specific test file
|
||||
npx vitest src/api/polaris.test.ts
|
||||
|
||||
# Run with coverage
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
Unit tests are colocated with source files:
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/
|
||||
│ ├── polaris.ts
|
||||
│ ├── polaris.test.ts # Unit tests for polaris.ts
|
||||
│ ├── PolarisDataContext.tsx
|
||||
│ └── PolarisDataContext.test.tsx
|
||||
└── components/
|
||||
├── DashboardView.tsx
|
||||
└── DashboardView.test.tsx
|
||||
```
|
||||
|
||||
### Example: Testing Utility Functions
|
||||
|
||||
**File:** `src/api/polaris.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { countResults, computeScore, getNamespaces, filterResultsByNamespace } from './polaris';
|
||||
|
||||
describe('countResults', () => {
|
||||
it('counts passing, warning, danger, and skipped results correctly', () => {
|
||||
const data = {
|
||||
Results: [
|
||||
{
|
||||
Name: 'test-deployment',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {
|
||||
'check-1': { Success: true, Severity: 'warning' },
|
||||
'check-2': { Success: false, Severity: 'danger' },
|
||||
'check-3': { Success: false, Severity: 'ignore' }, // skipped
|
||||
},
|
||||
CreatedTime: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const counts = countResults(data);
|
||||
|
||||
expect(counts).toEqual({
|
||||
total: 3,
|
||||
pass: 1,
|
||||
warning: 0,
|
||||
danger: 1,
|
||||
skipped: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty results', () => {
|
||||
const data = { Results: [] };
|
||||
const counts = countResults(data);
|
||||
|
||||
expect(counts).toEqual({
|
||||
total: 0,
|
||||
pass: 0,
|
||||
warning: 0,
|
||||
danger: 0,
|
||||
skipped: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeScore', () => {
|
||||
it('returns 0 for zero total checks', () => {
|
||||
expect(computeScore({ total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 })).toBe(0);
|
||||
});
|
||||
|
||||
it('calculates percentage correctly', () => {
|
||||
expect(computeScore({ total: 100, pass: 75, warning: 20, danger: 5, skipped: 0 })).toBe(75);
|
||||
});
|
||||
|
||||
it('rounds to nearest integer', () => {
|
||||
expect(computeScore({ total: 3, pass: 2, warning: 1, danger: 0, skipped: 0 })).toBe(67);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Testing React Components
|
||||
|
||||
**File:** `src/components/DashboardView.test.tsx`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DashboardView } from './DashboardView';
|
||||
import * as PolarisDataContext from '../api/PolarisDataContext';
|
||||
|
||||
describe('DashboardView', () => {
|
||||
it('renders loading state', () => {
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state', () => {
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: '403 Forbidden',
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText(/403 Forbidden/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays cluster score when data is loaded', () => {
|
||||
const mockData = {
|
||||
DisplayName: 'test-cluster',
|
||||
ClusterInfo: { Version: '1.27', Nodes: 3, Pods: 100, Namespaces: 10, Controllers: 50 },
|
||||
Results: [
|
||||
/* ... */
|
||||
],
|
||||
};
|
||||
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: mockData,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText(/Cluster Score/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### What to Unit Test
|
||||
|
||||
✅ **Do test:**
|
||||
|
||||
- Pure functions (score calculation, filtering, data transformation)
|
||||
- Data parsing and validation
|
||||
- Utility functions
|
||||
- Error handling logic
|
||||
- Edge cases (empty arrays, null values, invalid input)
|
||||
|
||||
❌ **Don't test:**
|
||||
|
||||
- Third-party libraries (Headlamp, React)
|
||||
- Simple prop passing
|
||||
- Trivial getters/setters
|
||||
- Implementation details (internal state)
|
||||
|
||||
---
|
||||
|
||||
## E2E Testing
|
||||
|
||||
### Framework: Playwright
|
||||
|
||||
Playwright provides cross-browser testing with auto-wait, network interception, and screenshot/video capture.
|
||||
|
||||
### Running E2E Tests
|
||||
|
||||
```bash
|
||||
# Run all E2E tests (headless)
|
||||
npm run e2e
|
||||
|
||||
# Run with browser visible (headed mode)
|
||||
npm run e2e:headed
|
||||
|
||||
# Run specific test file
|
||||
npx playwright test e2e/polaris.spec.ts
|
||||
|
||||
# Debug mode (step through tests)
|
||||
npx playwright test --debug
|
||||
|
||||
# Generate trace for debugging
|
||||
npx playwright test --trace on
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**1. Headlamp Instance**
|
||||
|
||||
E2E tests require a running Headlamp instance with:
|
||||
|
||||
- Polaris plugin installed (version being tested)
|
||||
- Polaris dashboard deployed and accessible
|
||||
- RBAC configured (service proxy permissions)
|
||||
|
||||
**2. Authentication**
|
||||
|
||||
Choose one of two authentication methods:
|
||||
|
||||
**Option A: OIDC via Authentik**
|
||||
|
||||
```bash
|
||||
export AUTHENTIK_USERNAME="user@example.com"
|
||||
export AUTHENTIK_PASSWORD="password"
|
||||
export HEADLAMP_URL="https://headlamp.example.com"
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
**Option B: Kubernetes Token**
|
||||
|
||||
```bash
|
||||
# Create token
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
|
||||
# Port-forward for local testing
|
||||
kubectl port-forward -n kube-system svc/headlamp 4466:80
|
||||
|
||||
# Run tests
|
||||
HEADLAMP_URL=http://localhost:4466 npm run e2e
|
||||
```
|
||||
|
||||
**3. Environment Variables**
|
||||
|
||||
Create `.env` file (optional, for persistent config):
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```bash
|
||||
HEADLAMP_URL=https://headlamp.example.com
|
||||
HEADLAMP_TOKEN=eyJhbGciOi...
|
||||
# OR
|
||||
AUTHENTIK_USERNAME=user@example.com
|
||||
AUTHENTIK_PASSWORD=secret
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
#### Current E2E Tests
|
||||
|
||||
**File:** `e2e/polaris.spec.ts`
|
||||
|
||||
| Test | Description | Validates |
|
||||
| --------------------------------------------- | ------------------------------- | ----------------------------- |
|
||||
| `sidebar contains Polaris entry` | Polaris appears in sidebar | Plugin registration |
|
||||
| `overview page renders cluster score` | Score displayed on overview | Data fetching, rendering |
|
||||
| `namespaces page renders table` | Namespace table loads | Data parsing, table rendering |
|
||||
| `namespace detail drawer opens` | Clicking namespace shows drawer | Navigation, drawer UI |
|
||||
| `namespace detail drawer closes with Escape` | Keyboard shortcut works | Keyboard navigation |
|
||||
| `namespace detail drawer opens from URL hash` | Direct URL navigation | URL routing, deep linking |
|
||||
|
||||
**File:** `e2e/settings.spec.ts`
|
||||
|
||||
| Test | Description | Validates |
|
||||
| ------------------------------------ | ------------------- | --------------------------- |
|
||||
| `plugin settings page is accessible` | Settings page loads | Settings registration |
|
||||
| `refresh interval can be changed` | Dropdown works | User preference persistence |
|
||||
| `dashboard URL can be customized` | Input field works | URL configuration |
|
||||
| `connection test button works` | Test functionality | API connectivity validation |
|
||||
|
||||
**File:** `e2e/appbar.spec.ts`
|
||||
|
||||
| Test | Description | Validates |
|
||||
| -------------------------------------- | ------------------------------- | ------------------- |
|
||||
| `app bar displays Polaris badge` | Badge visible in header | App bar integration |
|
||||
| `badge shows cluster score` | Score matches dashboard | Data consistency |
|
||||
| `clicking badge navigates to overview` | Navigation works | App bar action |
|
||||
| `badge color reflects score` | Red/yellow/green based on score | Visual feedback |
|
||||
|
||||
### Writing E2E Tests
|
||||
|
||||
**Example: Testing User Flow**
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('user can view namespace details and navigate back', async ({ page }) => {
|
||||
// Navigate to namespaces page
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
await expect(page.getByText('Polaris — Namespaces')).toBeVisible();
|
||||
|
||||
// Click first namespace
|
||||
const firstNamespace = page.locator('table tbody tr').first();
|
||||
const namespaceName = await firstNamespace.locator('td').first().textContent();
|
||||
await firstNamespace.getByRole('button').click();
|
||||
|
||||
// Drawer should open
|
||||
await expect(page.getByText(`Polaris — ${namespaceName}`)).toBeVisible();
|
||||
await expect(page).toHaveURL(new RegExp(`#${namespaceName}`));
|
||||
|
||||
// Close drawer with Escape
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.getByText(`Polaris — ${namespaceName}`)).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/namespaces$/);
|
||||
});
|
||||
```
|
||||
|
||||
**Example: Testing Dark Mode Adaptation**
|
||||
|
||||
```typescript
|
||||
test('plugin adapts to dark mode', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
// Toggle dark mode
|
||||
await page.getByLabel(/theme/i).click();
|
||||
|
||||
// Open namespace drawer
|
||||
const firstNamespace = page.locator('table tbody tr button').first();
|
||||
await firstNamespace.click();
|
||||
|
||||
// Drawer background should be dark
|
||||
const drawer = page.locator('[style*="position: fixed"][style*="right: 0"]');
|
||||
await expect(drawer).toHaveCSS('background-color', /rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
});
|
||||
```
|
||||
|
||||
### Debugging E2E Tests
|
||||
|
||||
**1. Headed Mode (See Browser)**
|
||||
|
||||
```bash
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
**2. Debug Mode (Step Through Tests)**
|
||||
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
This opens Playwright Inspector where you can:
|
||||
|
||||
- Step through each test action
|
||||
- Inspect page state
|
||||
- Edit test selectors live
|
||||
|
||||
**3. Screenshots on Failure**
|
||||
|
||||
Screenshots are automatically saved to `test-results/` on failure:
|
||||
|
||||
```bash
|
||||
test-results/
|
||||
└── polaris-overview-page-renders-cluster-score/
|
||||
├── test-failed-1.png
|
||||
└── trace.zip
|
||||
```
|
||||
|
||||
**4. Trace Viewer**
|
||||
|
||||
Record full trace (DOM snapshots, network, console):
|
||||
|
||||
```bash
|
||||
npx playwright test --trace on
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
**5. Verbose Logging**
|
||||
|
||||
```bash
|
||||
DEBUG=pw:api npx playwright test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Workflows
|
||||
|
||||
#### CI Workflow (`.github/workflows/ci.yaml`)
|
||||
|
||||
Runs on every push to `main` and all pull requests:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
lint-and-test:
|
||||
steps:
|
||||
- Build plugin
|
||||
- Lint (eslint)
|
||||
- Type-check (tsc)
|
||||
- Format check (prettier)
|
||||
- Run unit tests
|
||||
```
|
||||
|
||||
#### E2E Workflow (`.github/workflows/e2e.yaml`)
|
||||
|
||||
Runs E2E tests against live Headlamp instance:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
e2e:
|
||||
steps:
|
||||
- Install dependencies
|
||||
- Install Playwright browsers
|
||||
- Run auth setup
|
||||
- Run E2E tests
|
||||
- Upload artifacts on failure
|
||||
```
|
||||
|
||||
### Required GitHub Secrets
|
||||
|
||||
Configure in GitHub repository settings (Settings → Secrets and variables → Actions):
|
||||
|
||||
| Secret | Required | Description |
|
||||
| -------------------- | -------- | ------------------------------------------------------- |
|
||||
| `HEADLAMP_URL` | Optional | Headlamp instance URL (defaults to configured instance) |
|
||||
| `AUTHENTIK_USERNAME` | OIDC | Authentik username/email for CI user |
|
||||
| `AUTHENTIK_PASSWORD` | OIDC | Authentik password |
|
||||
| `HEADLAMP_TOKEN` | Token | Kubernetes service account token (alternative to OIDC) |
|
||||
|
||||
Set either `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` **or** `HEADLAMP_TOKEN`. OIDC takes priority if both are set.
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
Trigger workflows manually from GitHub Actions UI:
|
||||
|
||||
1. Go to Actions → [Workflow Name]
|
||||
2. Click "Run workflow"
|
||||
3. Select branch and run
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Current Coverage
|
||||
|
||||
| Category | Coverage | Notes |
|
||||
| -------------------- | -------- | ------------------------------ |
|
||||
| **API Functions** | 95% | Core utilities fully tested |
|
||||
| **React Components** | 60% | Focus on critical render paths |
|
||||
| **E2E User Flows** | 80% | Main features covered |
|
||||
|
||||
### Coverage Goals
|
||||
|
||||
- **Unit Tests**: 80%+ for `src/api/` (business logic)
|
||||
- **Component Tests**: 50%+ for `src/components/` (critical rendering)
|
||||
- **E2E Tests**: Cover all major user journeys
|
||||
|
||||
### Generating Coverage Reports
|
||||
|
||||
```bash
|
||||
# Unit test coverage
|
||||
npx vitest --coverage
|
||||
|
||||
# View HTML report
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Unit Testing
|
||||
|
||||
1. **Test behavior, not implementation**
|
||||
|
||||
- ✅ `expect(computeScore({ total: 100, pass: 75 })).toBe(75)`
|
||||
- ❌ `expect(mockInternalFunction).toHaveBeenCalled()`
|
||||
|
||||
2. **Use descriptive test names**
|
||||
|
||||
- ✅ `it('returns 0 when total checks is zero')`
|
||||
- ❌ `it('works')`
|
||||
|
||||
3. **One assertion per test (when possible)**
|
||||
|
||||
- Makes failures easier to debug
|
||||
- Exceptions: testing multiple properties of same object
|
||||
|
||||
4. **Mock external dependencies**
|
||||
|
||||
- Mock API calls, context providers, external libraries
|
||||
- Don't mock the code you're testing
|
||||
|
||||
5. **Test edge cases**
|
||||
- Empty arrays, null values, zero counts
|
||||
- Invalid input, malformed data
|
||||
|
||||
### E2E Testing
|
||||
|
||||
1. **Use semantic selectors**
|
||||
|
||||
- ✅ `page.getByRole('button', { name: 'Close' })`
|
||||
- ✅ `page.getByText('Polaris — Overview')`
|
||||
- ❌ `page.locator('.MuiButton-root')`
|
||||
|
||||
2. **Wait for visibility, not arbitrary timeouts**
|
||||
|
||||
- ✅ `await expect(element).toBeVisible()`
|
||||
- ❌ `await page.waitForTimeout(5000)`
|
||||
|
||||
3. **Keep tests independent**
|
||||
|
||||
- Each test should work in isolation
|
||||
- Don't rely on previous tests' state
|
||||
|
||||
4. **Test complete user flows**
|
||||
|
||||
- Navigate → Interact → Verify outcome
|
||||
- Don't just test page loads
|
||||
|
||||
5. **Clean up after tests**
|
||||
|
||||
- Close drawers/modals
|
||||
- Reset state if needed
|
||||
|
||||
6. **Use storage state for auth**
|
||||
|
||||
- Reuse authenticated session across tests
|
||||
- Faster than logging in for every test
|
||||
|
||||
7. **Parallelize carefully**
|
||||
- Tests must not interfere with each other
|
||||
- Currently disabled due to shared cluster state
|
||||
|
||||
### General
|
||||
|
||||
1. **Run tests before committing**
|
||||
|
||||
```bash
|
||||
npm run build && npm run lint && npm test
|
||||
```
|
||||
|
||||
2. **Fix failing tests immediately**
|
||||
|
||||
- Don't commit failing tests
|
||||
- Don't skip tests to "fix later"
|
||||
|
||||
3. **Update tests when changing code**
|
||||
|
||||
- Tests are documentation
|
||||
- Keep them in sync with implementation
|
||||
|
||||
4. **Review test failures in CI**
|
||||
- Check artifacts (screenshots, traces)
|
||||
- Reproduce locally before fixing
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
**Issue: Mock not working**
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Mock after import
|
||||
import { usePolarisData } from './polaris';
|
||||
vi.mock('./polaris');
|
||||
|
||||
// ✅ Correct: Mock before import
|
||||
vi.mock('./polaris', () => ({
|
||||
usePolarisData: vi.fn(),
|
||||
}));
|
||||
import { usePolarisData } from './polaris';
|
||||
```
|
||||
|
||||
**Issue: "Cannot read property of undefined"**
|
||||
|
||||
Check mocks are returning expected structure:
|
||||
|
||||
```typescript
|
||||
vi.spyOn(PolarisDataContext, 'usePolarisDataContext').mockReturnValue({
|
||||
data: mockData, // Ensure mockData has all required fields
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
```
|
||||
|
||||
#### E2E Tests
|
||||
|
||||
**Issue: "Element not found"**
|
||||
|
||||
```bash
|
||||
# Enable verbose logging
|
||||
DEBUG=pw:api npx playwright test
|
||||
|
||||
# Use headed mode to see what's happening
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
Common causes:
|
||||
|
||||
- Element hasn't rendered yet (use `toBeVisible()` instead of `toBeDefined()`)
|
||||
- Wrong selector (use Playwright Inspector to verify)
|
||||
- Authentication failed (check token/credentials)
|
||||
|
||||
**Issue: "Test timeout"**
|
||||
|
||||
Increase timeout for slow operations:
|
||||
|
||||
```typescript
|
||||
test('slow operation', async ({ page }) => {
|
||||
test.setTimeout(60000); // 60 seconds
|
||||
|
||||
await page.goto('/c/main/polaris');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Issue: Network errors in E2E tests**
|
||||
|
||||
```bash
|
||||
# Check Headlamp accessibility
|
||||
curl -I $HEADLAMP_URL
|
||||
|
||||
# Check Polaris service
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
```
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Run specific test file
|
||||
npx vitest src/api/polaris.test.ts
|
||||
|
||||
# Run specific test case
|
||||
npx vitest -t "computes score correctly"
|
||||
|
||||
# Run E2E test by name
|
||||
npx playwright test -g "sidebar contains Polaris"
|
||||
|
||||
# Update snapshots
|
||||
npx vitest -u
|
||||
|
||||
# Clear test cache
|
||||
npx vitest --clearCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Vitest Documentation](https://vitest.dev/)
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Testing Library (React)](https://testing-library.com/docs/react-testing-library/intro/)
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Project Architecture](./ARCHITECTURE.md)
|
||||
- [Contributing Guide](../CONTRIBUTING.md)
|
||||
@@ -0,0 +1,422 @@
|
||||
# Installation Guide
|
||||
|
||||
This guide covers all installation methods for the Headlamp Polaris Plugin.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation Methods](#installation-methods)
|
||||
- [Option 1: Plugin Manager (Recommended)](#option-1-plugin-manager-recommended)
|
||||
- [Option 2: Sidecar Container](#option-2-sidecar-container)
|
||||
- [Option 3: Manual Tarball](#option-3-manual-tarball)
|
||||
- [Option 4: Build from Source](#option-4-build-from-source)
|
||||
- [Post-Installation](#post-installation)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installation, ensure all [prerequisites](prerequisites.md) are met:
|
||||
|
||||
- Kubernetes v1.24+
|
||||
- Headlamp v0.26+ (v0.39+ recommended)
|
||||
- Polaris deployed with dashboard enabled
|
||||
- RBAC permissions configured
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Option 1: Plugin Manager (Recommended)
|
||||
|
||||
**Best for:** Production deployments, managed updates, ease of use
|
||||
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin) and can be installed via the Headlamp Plugin Manager.
|
||||
|
||||
#### Via Headlamp UI
|
||||
|
||||
1. **Navigate to Plugin Settings:**
|
||||
|
||||
- Open Headlamp in your browser
|
||||
- Go to **Settings → Plugins**
|
||||
- Click the **Catalog** tab
|
||||
|
||||
2. **Search and Install:**
|
||||
|
||||
- Search for "Polaris"
|
||||
- Find "Headlamp Polaris Plugin"
|
||||
- Click **Install**
|
||||
|
||||
3. **Hard Refresh Browser:**
|
||||
|
||||
- **Mac:** Cmd+Shift+R
|
||||
- **Windows/Linux:** Ctrl+Shift+R
|
||||
|
||||
4. **Verify Installation:**
|
||||
- Sidebar should show "Polaris" entry
|
||||
- Click **Polaris** → Overview page loads
|
||||
|
||||
#### Via Helm Configuration
|
||||
|
||||
Add to your Headlamp Helm values:
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
```
|
||||
|
||||
Deploy or update Headlamp:
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
Then install the plugin via Headlamp UI as described above.
|
||||
|
||||
### Option 2: Sidecar Container
|
||||
|
||||
**Best for:** Controlled plugin versions, air-gapped environments, specific version pinning
|
||||
|
||||
This method uses an init container to download and install the plugin from a tarball URL.
|
||||
|
||||
#### Helm Values Configuration
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
initContainers:
|
||||
- name: install-polaris-plugin
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml --plugins-dir /plugins
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
```
|
||||
|
||||
#### Plugin Configuration ConfigMap
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
version: 0.3.5
|
||||
url: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
#### Apply Configuration
|
||||
|
||||
```bash
|
||||
# Create ConfigMap
|
||||
kubectl apply -f headlamp-plugin-config.yaml
|
||||
|
||||
# Deploy/update Headlamp with sidecar
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
|
||||
# Wait for pod to be ready
|
||||
kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
|
||||
|
||||
# Verify plugin files
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x dist/
|
||||
# -rw-r--r-- package.json
|
||||
```
|
||||
|
||||
### Option 3: Manual Tarball
|
||||
|
||||
**Best for:** Testing specific versions, offline installations
|
||||
|
||||
Download the plugin tarball and extract it into Headlamp's plugin directory.
|
||||
|
||||
#### Download and Extract
|
||||
|
||||
```bash
|
||||
# Download latest release
|
||||
VERSION=0.3.5
|
||||
wget https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v${VERSION}/headlamp-polaris-plugin-${VERSION}.tar.gz
|
||||
|
||||
# Extract to plugin directory
|
||||
tar xzf headlamp-polaris-plugin-${VERSION}.tar.gz -C /headlamp/plugins/
|
||||
|
||||
# Verify extraction
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x dist/
|
||||
# -rw-r--r-- package.json
|
||||
```
|
||||
|
||||
#### Kubernetes Volume Mount
|
||||
|
||||
If Headlamp runs in Kubernetes, mount the plugin directory as a volume:
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /plugins
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
hostPath:
|
||||
path: /path/to/plugins # Where you extracted the tarball
|
||||
type: Directory
|
||||
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
readOnly: true
|
||||
```
|
||||
|
||||
**Note:** This method is not recommended for production (hostPath is node-specific).
|
||||
|
||||
### Option 4: Build from Source
|
||||
|
||||
**Best for:** Development, contributing, testing unreleased features
|
||||
|
||||
Clone the repository and build the plugin from source.
|
||||
|
||||
#### Clone and Build
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/privilegedescalation/headlamp-polaris-plugin.git
|
||||
cd headlamp-polaris-plugin
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build plugin
|
||||
npm run build
|
||||
|
||||
# Package tarball (optional)
|
||||
npm run package
|
||||
|
||||
# Extract to Headlamp plugin directory
|
||||
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||
```
|
||||
|
||||
#### Development Mode (Hot Reload)
|
||||
|
||||
For active development with hot reload:
|
||||
|
||||
```bash
|
||||
# Start Headlamp with plugin hot reload
|
||||
npm start
|
||||
|
||||
# Opens Headlamp at http://localhost:4466
|
||||
# Changes to src/ automatically rebuild and reload
|
||||
```
|
||||
|
||||
See [Development Workflow](../development/workflow.md) for detailed development setup.
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After installing the plugin via any method:
|
||||
|
||||
### 1. Configure RBAC
|
||||
|
||||
Apply RBAC permissions for the plugin to access Polaris:
|
||||
|
||||
```bash
|
||||
# Create polaris-plugin-rbac.yaml
|
||||
cat <<EOF | kubectl apply -f -
|
||||
---
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
See [RBAC Permissions](../user-guide/rbac-permissions.md) for detailed RBAC configuration.
|
||||
|
||||
### 2. Restart Headlamp (if needed)
|
||||
|
||||
```bash
|
||||
# If you updated Helm values or ConfigMaps
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
|
||||
# Wait for pod to be ready
|
||||
kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
|
||||
```
|
||||
|
||||
### 3. Clear Browser Cache
|
||||
|
||||
**Critical:** Hard refresh your browser to load the new plugin JavaScript:
|
||||
|
||||
- **Mac:** Cmd+Shift+R
|
||||
- **Windows/Linux:** Ctrl+Shift+R
|
||||
|
||||
### 4. Verify Installation
|
||||
|
||||
**UI Verification:**
|
||||
|
||||
1. Navigate to **Settings → Plugins**
|
||||
2. Verify "headlamp-polaris-plugin" is listed
|
||||
3. Check version matches installed version
|
||||
4. Verify **Polaris** appears in sidebar
|
||||
5. Click **Polaris** → Overview page loads
|
||||
6. Cluster score displays correctly
|
||||
|
||||
**CLI Verification:**
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x dist/
|
||||
# -rw-r--r-- package.json
|
||||
|
||||
# Check Headlamp logs for errors
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# Expected: No errors related to plugin loading
|
||||
|
||||
# Test Polaris API access
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Expected: "1.0" or similar
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not in Sidebar
|
||||
|
||||
**Symptom:** Plugin listed in Settings → Plugins but no "Polaris" entry in sidebar
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. Browser cache not cleared
|
||||
2. Plugin JavaScript failed to load
|
||||
3. Plugin files not properly installed
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# 1. Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected: dist/, package.json present
|
||||
|
||||
# 2. Check Headlamp logs for plugin errors
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# 3. Hard refresh browser (Cmd+Shift+R or Ctrl+Shift+R)
|
||||
|
||||
# 4. Check browser console for JavaScript errors
|
||||
# Open DevTools → Console tab
|
||||
```
|
||||
|
||||
### 403 Forbidden Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, 403 in browser console
|
||||
|
||||
**Cause:** RBAC permissions missing or incorrect
|
||||
|
||||
**Solution:**
|
||||
|
||||
See [RBAC Issues](../troubleshooting/rbac-issues.md) for detailed debugging.
|
||||
|
||||
### 404 Not Found Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, 404 in browser console
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. Polaris not deployed
|
||||
2. Polaris service name incorrect
|
||||
3. Polaris namespace incorrect
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Verify Polaris deployment
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# If service doesn't exist, install Polaris:
|
||||
helm install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
### Plugin Version Mismatch
|
||||
|
||||
**Symptom:** Settings shows old version, ArtifactHub shows new version
|
||||
|
||||
**Cause:** ArtifactHub sync delay (30 minutes) or plugin manager cache
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Wait 30 minutes for ArtifactHub sync
|
||||
# Or manually force Headlamp restart:
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Quick Start](quick-start.md)** - Get up and running in 5 minutes
|
||||
- **[Configuration](../user-guide/configuration.md)** - Customize refresh intervals, dashboard URLs
|
||||
- **[Features](../user-guide/features.md)** - Learn about all plugin features
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Comprehensive troubleshooting guide
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Plugin Documentation](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Headlamp Helm Chart](https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp)
|
||||
- [Polaris Installation](https://polaris.docs.fairwinds.com/infrastructure-as-code/)
|
||||
- [Artifact Hub Package](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin)
|
||||
@@ -0,0 +1,224 @@
|
||||
# Prerequisites
|
||||
|
||||
Before installing the Headlamp Polaris Plugin, ensure your environment meets the following requirements.
|
||||
|
||||
## Required Components
|
||||
|
||||
| Requirement | Minimum Version | Recommended Version |
|
||||
| ------------------------------- | ------------------ | --------------------------------- |
|
||||
| **Kubernetes** | v1.24+ | v1.28+ |
|
||||
| **Headlamp** | v0.26+ | v0.39+ |
|
||||
| **Polaris** (dashboard enabled) | Any recent release | Latest stable |
|
||||
| **Browser** | Modern (ES2020+) | Latest Chrome/Firefox/Safari/Edge |
|
||||
|
||||
## Polaris Requirements
|
||||
|
||||
The plugin requires Polaris to be deployed with the dashboard component enabled:
|
||||
|
||||
- **Namespace:** `polaris` (default expected namespace)
|
||||
- **Dashboard enabled:** `dashboard.enabled: true` in Helm chart (default)
|
||||
- **Service:** `polaris-dashboard` ClusterIP service on port 80
|
||||
|
||||
### Verify Polaris Installation
|
||||
|
||||
```bash
|
||||
# Check Polaris pods are running
|
||||
kubectl -n polaris get pods
|
||||
|
||||
# Expected output:
|
||||
# NAME READY STATUS RESTARTS AGE
|
||||
# polaris-dashboard-xxxxxxxxx-xxxxx 1/1 Running 0 1h
|
||||
# polaris-webhook-xxxxxxxxx-xxxxx 1/1 Running 0 1h
|
||||
|
||||
# Check Polaris dashboard service exists
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# Expected output:
|
||||
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
# polaris-dashboard ClusterIP 10.96.xxx.xxx <none> 80/TCP 1h
|
||||
|
||||
# Test Polaris dashboard API
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Expected output:
|
||||
# "1.0"
|
||||
```
|
||||
|
||||
### Install Polaris (if not present)
|
||||
|
||||
```bash
|
||||
# Add Fairwinds Helm repository
|
||||
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
|
||||
helm repo update
|
||||
|
||||
# Install Polaris with dashboard enabled
|
||||
helm install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
|
||||
# Wait for pods to be ready
|
||||
kubectl -n polaris wait --for=condition=ready pod -l app.kubernetes.io/name=polaris --timeout=300s
|
||||
```
|
||||
|
||||
## Headlamp Requirements
|
||||
|
||||
### Verify Headlamp Installation
|
||||
|
||||
```bash
|
||||
# Check Headlamp is deployed
|
||||
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
|
||||
# Expected output:
|
||||
# NAME READY STATUS RESTARTS AGE
|
||||
# headlamp-xxxxxxxxxx-xxxxx 1/1 Running 0 1h
|
||||
|
||||
# Check Headlamp version (must be v0.26+)
|
||||
kubectl -n kube-system get deployment headlamp -o jsonpath='{.spec.template.spec.containers[0].image}'
|
||||
|
||||
# Expected output:
|
||||
# ghcr.io/headlamp-k8s/headlamp:v0.39.0 (or similar)
|
||||
```
|
||||
|
||||
### Install Headlamp (if not present)
|
||||
|
||||
```bash
|
||||
# Add Headlamp Helm repository
|
||||
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
|
||||
helm repo update
|
||||
|
||||
# Install Headlamp
|
||||
helm install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--set config.pluginsDir="/headlamp/plugins" \
|
||||
--set pluginsManager.enabled=true
|
||||
|
||||
# Wait for pod to be ready
|
||||
kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
|
||||
```
|
||||
|
||||
## RBAC Requirements
|
||||
|
||||
The plugin requires permissions to access the Polaris dashboard via Kubernetes service proxy.
|
||||
|
||||
### Required Permission
|
||||
|
||||
| Verb | API Group | Resource | Resource Name | Namespace |
|
||||
| ----- | ----------- | ---------------- | ------------------- | --------- |
|
||||
| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` |
|
||||
|
||||
### Verify RBAC Permissions
|
||||
|
||||
```bash
|
||||
# Test if Headlamp service account has permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes
|
||||
|
||||
# If "no", you need to create RBAC (see installation guide)
|
||||
```
|
||||
|
||||
## Network Requirements
|
||||
|
||||
### Service Proxy Access
|
||||
|
||||
The plugin accesses Polaris through the Kubernetes API server's service proxy:
|
||||
|
||||
```
|
||||
Headlamp Pod → Kubernetes API Server → Polaris Dashboard Service
|
||||
```
|
||||
|
||||
**Required network paths:**
|
||||
|
||||
- Headlamp pod → Kubernetes API server (443)
|
||||
- Kubernetes API server → Polaris dashboard service (80)
|
||||
|
||||
### NetworkPolicy Considerations
|
||||
|
||||
If the `polaris` namespace has NetworkPolicies enabled, ensure the Kubernetes API server can reach the `polaris-dashboard` service on port 80.
|
||||
|
||||
### Test Network Connectivity
|
||||
|
||||
```bash
|
||||
# Test service proxy endpoint from API server
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq . > /dev/null
|
||||
|
||||
# If successful, no output
|
||||
# If failed, check NetworkPolicies and service status
|
||||
```
|
||||
|
||||
## Browser Requirements
|
||||
|
||||
The plugin uses modern JavaScript features and requires:
|
||||
|
||||
- **ES2020+ support**
|
||||
- **localStorage** enabled
|
||||
- **JavaScript** enabled
|
||||
- **Cookies** enabled (for Headlamp session)
|
||||
|
||||
### Tested Browsers
|
||||
|
||||
| Browser | Minimum Version |
|
||||
| --------------- | --------------- |
|
||||
| Chrome/Chromium | 80+ |
|
||||
| Firefox | 75+ |
|
||||
| Safari | 13.1+ |
|
||||
| Edge | 80+ |
|
||||
|
||||
## Optional Components
|
||||
|
||||
### OIDC Authentication (for multi-user deployments)
|
||||
|
||||
If using Headlamp with OIDC authentication, each user must have RBAC permissions for service proxy access (see [RBAC Permissions](../user-guide/rbac-permissions.md)).
|
||||
|
||||
### Ingress (for external access)
|
||||
|
||||
If exposing Headlamp externally, configure an Ingress with TLS:
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: headlamp-tls
|
||||
hosts:
|
||||
- headlamp.example.com
|
||||
```
|
||||
|
||||
## Pre-Installation Checklist
|
||||
|
||||
Before proceeding to installation, verify:
|
||||
|
||||
- [ ] Kubernetes cluster v1.24+ running
|
||||
- [ ] Polaris deployed in `polaris` namespace with dashboard enabled
|
||||
- [ ] Polaris dashboard service accessible via service proxy
|
||||
- [ ] Headlamp v0.26+ deployed
|
||||
- [ ] RBAC permissions configured (or ready to configure)
|
||||
- [ ] Network connectivity between API server and Polaris dashboard
|
||||
- [ ] Modern browser available
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once all prerequisites are met:
|
||||
|
||||
1. **[Installation Guide](installation.md)** - Choose installation method and deploy the plugin
|
||||
2. **[Quick Start](quick-start.md)** - Get up and running in 5 minutes
|
||||
3. **[RBAC Permissions](../user-guide/rbac-permissions.md)** - Detailed RBAC configuration
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If any prerequisite check fails, see:
|
||||
|
||||
- **[Troubleshooting Guide](../troubleshooting/README.md)** - Common issues and solutions
|
||||
- **[RBAC Issues](../troubleshooting/rbac-issues.md)** - Permission debugging
|
||||
- **[Network Problems](../troubleshooting/network-problems.md)** - Connectivity issues
|
||||
@@ -0,0 +1,278 @@
|
||||
# Quick Start
|
||||
|
||||
Get the Headlamp Polaris Plugin up and running in 5 minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure:
|
||||
|
||||
- Kubernetes cluster is running
|
||||
- Headlamp v0.26+ is deployed
|
||||
- Polaris is installed with dashboard enabled
|
||||
|
||||
Don't have these? See [Prerequisites](prerequisites.md) for installation instructions.
|
||||
|
||||
## Step 1: Install the Plugin (2 minutes)
|
||||
|
||||
### Via Headlamp UI
|
||||
|
||||
1. Open Headlamp in your browser
|
||||
2. Go to **Settings → Plugins → Catalog**
|
||||
3. Search for "Polaris"
|
||||
4. Click **Install** on "Headlamp Polaris Plugin"
|
||||
5. Hard refresh browser: **Cmd+Shift+R** (Mac) or **Ctrl+Shift+R** (Windows/Linux)
|
||||
|
||||
### Via Helm (if using Helm-managed Headlamp)
|
||||
|
||||
```bash
|
||||
# Add plugin manager config to Headlamp values
|
||||
cat <<EOF > headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
EOF
|
||||
|
||||
# Update Headlamp
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
Then install via Headlamp UI as described above.
|
||||
|
||||
## Step 2: Configure RBAC (1 minute)
|
||||
|
||||
Grant the plugin permission to access Polaris data:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
---
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
**Note:** Adjust the `namespace` in `subjects` if your Headlamp runs in a different namespace.
|
||||
|
||||
## Step 3: Verify Installation (1 minute)
|
||||
|
||||
### UI Verification
|
||||
|
||||
1. **Check Plugin is Loaded:**
|
||||
|
||||
- Go to **Settings → Plugins**
|
||||
- Verify "headlamp-polaris-plugin" is listed
|
||||
|
||||
2. **Check Sidebar:**
|
||||
|
||||
- Look for **Polaris** entry in the left sidebar
|
||||
- If not visible, hard refresh: **Cmd+Shift+R** / **Ctrl+Shift+R**
|
||||
|
||||
3. **View Overview Dashboard:**
|
||||
|
||||
- Click **Polaris** in sidebar
|
||||
- Overview page loads with:
|
||||
- Cluster score gauge
|
||||
- Check distribution charts
|
||||
- Top 10 failing checks
|
||||
- Cluster statistics
|
||||
|
||||
4. **Check App Bar Badge:**
|
||||
- Colored chip in top navigation bar shows cluster score
|
||||
- Click badge to navigate to overview
|
||||
|
||||
### CLI Verification
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
|
||||
ls /headlamp/plugins/headlamp-polaris-plugin/dist/
|
||||
|
||||
# Expected output:
|
||||
# main.js
|
||||
|
||||
# Verify RBAC is correct
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes
|
||||
|
||||
# Test Polaris API access
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json \
|
||||
| jq .PolarisOutputVersion
|
||||
|
||||
# Expected output: "1.0" or similar
|
||||
```
|
||||
|
||||
## Step 4: Explore Features (1 minute)
|
||||
|
||||
### Overview Dashboard
|
||||
|
||||
Navigate to **Polaris → Overview**:
|
||||
|
||||
- **Cluster Score Gauge:** Overall cluster health (0-100%)
|
||||
|
||||
- Green (≥80%): Excellent
|
||||
- Yellow (50-79%): Needs improvement
|
||||
- Red (<50%): Critical issues
|
||||
|
||||
- **Check Distribution:** Pass/Warning/Danger/Skipped counts with charts
|
||||
|
||||
- **Top 10 Failing Checks:** Most common issues across the cluster
|
||||
|
||||
- **Cluster Statistics:** Nodes, pods, namespaces, controllers count
|
||||
|
||||
- **Manual Refresh:** Click refresh button to fetch latest audit data
|
||||
|
||||
### Namespaces View
|
||||
|
||||
Navigate to **Polaris → Namespaces**:
|
||||
|
||||
- Table of all namespaces with per-namespace scores
|
||||
- Click a namespace to open detailed side panel
|
||||
- Side panel shows:
|
||||
- Namespace score and check counts
|
||||
- Resource-level audit results
|
||||
- Link to external Polaris dashboard
|
||||
|
||||
### Inline Resource Audits
|
||||
|
||||
View any workload detail page (Deployment, StatefulSet, DaemonSet, Job, CronJob):
|
||||
|
||||
- **Polaris Audit** section automatically appears
|
||||
- Shows compact score and failing checks
|
||||
- Link to full report
|
||||
|
||||
### App Bar Badge
|
||||
|
||||
Cluster score badge in top navigation:
|
||||
|
||||
- Color-coded by score (green/yellow/red)
|
||||
- Click to navigate to overview
|
||||
- Always visible for quick reference
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not in Sidebar
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
|
||||
ls /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# If missing, reinstall via Headlamp UI or sidecar method
|
||||
|
||||
# Hard refresh browser
|
||||
# Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux)
|
||||
```
|
||||
|
||||
### 403 Forbidden Error
|
||||
|
||||
```bash
|
||||
# Verify RBAC exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# If missing, apply RBAC from Step 2
|
||||
```
|
||||
|
||||
### 404 Not Found Error
|
||||
|
||||
```bash
|
||||
# Verify Polaris is running
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# If missing, install Polaris:
|
||||
helm install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
### Data Not Loading
|
||||
|
||||
```bash
|
||||
# Check Polaris dashboard is responding
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
|
||||
# If fails, check:
|
||||
# 1. Polaris pods are running
|
||||
# 2. NetworkPolicies allow API server → Polaris dashboard
|
||||
# 3. Polaris service exists and is ClusterIP type
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Configuration](../user-guide/configuration.md)** - Customize refresh intervals, dashboard URLs
|
||||
- **[Features](../user-guide/features.md)** - Learn about all plugin features
|
||||
- **[RBAC Permissions](../user-guide/rbac-permissions.md)** - Advanced RBAC configuration (token-auth, OIDC)
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Comprehensive troubleshooting guide
|
||||
|
||||
## Common Configuration Tasks
|
||||
|
||||
### Change Refresh Interval
|
||||
|
||||
1. Go to **Settings → Plugins → Polaris**
|
||||
2. Select refresh interval (1 / 5 / 10 / 30 minutes)
|
||||
3. Click **Save**
|
||||
|
||||
Default is 5 minutes.
|
||||
|
||||
### Use Custom Polaris URL
|
||||
|
||||
If Polaris is deployed externally or in a different namespace:
|
||||
|
||||
1. Go to **Settings → Plugins → Polaris**
|
||||
2. Update **Dashboard URL**:
|
||||
- Service proxy: `/api/v1/namespaces/custom-ns/services/polaris-dashboard:80/proxy/`
|
||||
- Full URL: `https://polaris.example.com/`
|
||||
3. Click **Test Connection** to verify
|
||||
4. Click **Save**
|
||||
|
||||
### Test Polaris Connectivity
|
||||
|
||||
1. Go to **Settings → Plugins → Polaris**
|
||||
2. Click **Test Connection**
|
||||
3. Verify green success message with Polaris version
|
||||
|
||||
If test fails, see [Troubleshooting](../troubleshooting/README.md).
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **[Full Installation Guide](installation.md)** - All installation methods (sidecar, manual, source)
|
||||
- **[Development Workflow](../development/workflow.md)** - Build from source, hot reload
|
||||
- **[RBAC Issues](../troubleshooting/rbac-issues.md)** - Permission debugging
|
||||
- **[Network Problems](../troubleshooting/network-problems.md)** - Connectivity troubleshooting
|
||||
|
||||
---
|
||||
|
||||
You're now running the Headlamp Polaris Plugin. Visit the **Polaris** section in Headlamp to explore your cluster's security, reliability, and efficiency audit results.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Troubleshooting
|
||||
|
||||
Quick diagnosis guide and common issues for the Headlamp Polaris Plugin.
|
||||
|
||||
## Quick Diagnosis
|
||||
|
||||
| Symptom | Likely Cause | Quick Fix | Details |
|
||||
| ------------------------------- | -------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| **Plugin not in sidebar** | Browser cache or plugin not installed | Hard refresh browser (Cmd+Shift+R) and verify plugin files exist | [Common Issues](common-issues.md#plugin-not-in-sidebar) |
|
||||
| **403 Access Denied** | Missing RBAC binding for `services/proxy` | Apply Role + RoleBinding from RBAC section | [RBAC Issues](rbac-issues.md) |
|
||||
| **404 or 503** | Polaris not installed, or dashboard disabled | Install Polaris with `dashboard.enabled: true` in `polaris` namespace | [Common Issues](common-issues.md#404-not-found) |
|
||||
| **Dark mode white backgrounds** | Old plugin version | Upgrade to v0.3.5+ and hard refresh browser | [Common Issues](common-issues.md#dark-mode-issues) |
|
||||
| **Settings page empty** | Old plugin version | Upgrade to v0.3.3+ | [Common Issues](common-issues.md#settings-page-empty) |
|
||||
| **No data / infinite spinner** | Network policy or Polaris pod down | Check network policies and `kubectl get pods -n polaris` | [Network Problems](network-problems.md) |
|
||||
| **Namespace drawer white** | CSS variable issue | Update to v0.3.5+ with `--mui-palette-background-paper` | [Common Issues](common-issues.md#dark-mode-issues) |
|
||||
| **Cluster score not updating** | Auto-refresh disabled or interval too long | Check Settings → Plugins → Polaris refresh interval | [Common Issues](common-issues.md#data-not-refreshing) |
|
||||
| **Custom URL not working** | CORS or incorrect URL format | Test with curl, check CORS headers | [Network Problems](network-problems.md#cors-issues) |
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
- **[Common Issues](common-issues.md)** - Comprehensive guide to frequent problems and solutions
|
||||
- **[RBAC Issues](rbac-issues.md)** - Permission debugging, 403 errors, token-auth mode
|
||||
- **[Network Problems](network-problems.md)** - NetworkPolicies, connectivity, proxy issues, CORS
|
||||
|
||||
## Diagnostic Commands
|
||||
|
||||
### Quick Health Check
|
||||
|
||||
```bash
|
||||
# 1. Verify Polaris is running
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# 2. Test Polaris API access
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Expected output: "1.0" or similar
|
||||
|
||||
# 3. Verify RBAC permissions
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes
|
||||
|
||||
# 4. Check Headlamp pod is running
|
||||
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
|
||||
# 5. Check Headlamp logs for plugin errors
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# Expected: No errors
|
||||
```
|
||||
|
||||
### Plugin Loading Verification
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x dist/
|
||||
# -rw-r--r-- package.json
|
||||
```
|
||||
|
||||
### RBAC Verification
|
||||
|
||||
```bash
|
||||
# Check Role exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
|
||||
# Check RoleBinding exists
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission (service account mode)
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Test permission (user token mode, replace with your user)
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=user@example.com \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
```
|
||||
|
||||
### Network Debugging
|
||||
|
||||
```bash
|
||||
# Test from kubectl (uses same service proxy)
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
|
||||
# Check NetworkPolicies
|
||||
kubectl -n polaris get networkpolicy
|
||||
|
||||
# Test direct service access (from within cluster)
|
||||
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
|
||||
curl http://polaris-dashboard.polaris/results.json
|
||||
```
|
||||
|
||||
## Browser Debugging
|
||||
|
||||
### Check Browser Console
|
||||
|
||||
1. Open browser DevTools (F12)
|
||||
2. Go to **Console** tab
|
||||
3. Look for errors containing "polaris" or "plugin"
|
||||
|
||||
**Common errors:**
|
||||
|
||||
- `createSvgIcon is not defined` → MUI import issue (plugin bug)
|
||||
- `403 Forbidden` → RBAC permission denied
|
||||
- `404 Not Found` → Polaris not installed or wrong URL
|
||||
- `Failed to fetch` → Network policy or CORS issue
|
||||
|
||||
### Clear Browser Cache
|
||||
|
||||
**Critical:** After plugin updates, hard refresh to clear cached JavaScript:
|
||||
|
||||
- **Mac:** Cmd+Shift+R
|
||||
- **Windows/Linux:** Ctrl+Shift+R
|
||||
- **Alternatively:** Clear all browser data for Headlamp URL
|
||||
|
||||
### Check localStorage
|
||||
|
||||
```javascript
|
||||
// Open browser console and run:
|
||||
localStorage.getItem('polaris-plugin-refresh-interval');
|
||||
localStorage.getItem('polaris-plugin-dashboard-url');
|
||||
|
||||
// Reset to defaults:
|
||||
localStorage.removeItem('polaris-plugin-refresh-interval');
|
||||
localStorage.removeItem('polaris-plugin-dashboard-url');
|
||||
```
|
||||
|
||||
## Still Having Issues?
|
||||
|
||||
If the quick diagnosis doesn't resolve your issue:
|
||||
|
||||
1. **Check detailed guides:**
|
||||
|
||||
- [Common Issues](common-issues.md)
|
||||
- [RBAC Issues](rbac-issues.md)
|
||||
- [Network Problems](network-problems.md)
|
||||
|
||||
2. **Review documentation:**
|
||||
|
||||
- [Installation Guide](../getting-started/installation.md)
|
||||
- [RBAC Permissions](../user-guide/rbac-permissions.md)
|
||||
- [Deployment Guide](../deployment/kubernetes.md)
|
||||
|
||||
3. **Open a GitHub issue:**
|
||||
- [GitHub Issues](https://github.com/privilegedescalation/headlamp-polaris-plugin/issues)
|
||||
- Include: Headlamp version, plugin version, error messages, logs
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Troubleshooting](https://headlamp.dev/docs/latest/troubleshooting/)
|
||||
- [Polaris Troubleshooting](https://polaris.docs.fairwinds.com/troubleshooting/)
|
||||
- [Kubernetes RBAC Troubleshooting](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#troubleshooting)
|
||||
@@ -0,0 +1,715 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide covers common issues encountered when using the Headlamp Polaris Plugin and their solutions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Plugin Not Showing in Sidebar](#plugin-not-showing-in-sidebar)
|
||||
- [403 Forbidden Error](#403-forbidden-error)
|
||||
- [404 Not Found Error](#404-not-found-error)
|
||||
- [Plugin Settings Page Empty](#plugin-settings-page-empty)
|
||||
- [Dark Mode Issues](#dark-mode-issues)
|
||||
- [Data Not Loading / Infinite Spinner](#data-not-loading--infinite-spinner)
|
||||
- [Browser Console Errors](#browser-console-errors)
|
||||
- [Network and RBAC Debugging](#network-and-rbac-debugging)
|
||||
- [Plugin Installation Issues](#plugin-installation-issues)
|
||||
- [ArtifactHub Sync Delays](#artifacthub-sync-delays)
|
||||
|
||||
---
|
||||
|
||||
## Plugin Not Showing in Sidebar
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Plugin appears in Settings → Plugins but sidebar entry is missing
|
||||
- No "Polaris" section in navigation
|
||||
- Routes like `/polaris` return 404 or blank page
|
||||
|
||||
### Common Causes
|
||||
|
||||
**1. Plugin Not Installed**
|
||||
|
||||
**Check plugin installation**:
|
||||
|
||||
```bash
|
||||
# View Headlamp pod logs (plugin sidecar)
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
|
||||
# Expected output:
|
||||
# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
|
||||
# Plugin installed successfully
|
||||
```
|
||||
|
||||
**Verify plugin files exist**:
|
||||
|
||||
```bash
|
||||
kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
|
||||
# Should show: headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
**2. JavaScript Cached by Browser**
|
||||
|
||||
After upgrading the plugin, old JavaScript may be cached.
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Hard refresh: Cmd+Shift+R (macOS) or Ctrl+Shift+R (Linux/Windows)
|
||||
- Clear browser cache for Headlamp domain
|
||||
- Open DevTools → Application → Clear Storage → Clear all
|
||||
|
||||
**3. Plugin Disabled in Settings**
|
||||
|
||||
**Check Settings → Plugins**:
|
||||
|
||||
- Navigate to Headlamp Settings → Plugins
|
||||
- Ensure "Polaris" plugin is enabled (toggle should be ON)
|
||||
- If disabled, enable it and refresh the page
|
||||
|
||||
---
|
||||
|
||||
## 403 Forbidden Error
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Error message: "Error loading Polaris audit data: 403 Forbidden"
|
||||
- Browser console shows 403 response from API proxy
|
||||
- Plugin sidebar shows but data fails to load
|
||||
|
||||
### Root Cause
|
||||
|
||||
User or service account lacks `services/proxy` permission on `polaris-dashboard` service in the `polaris` namespace.
|
||||
|
||||
### Solution
|
||||
|
||||
**1. Verify RBAC Configuration**
|
||||
|
||||
Check if Role exists:
|
||||
|
||||
```bash
|
||||
kubectl get role polaris-proxy-reader -n polaris -o yaml
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```yaml
|
||||
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']
|
||||
```
|
||||
|
||||
**2. Verify RoleBinding**
|
||||
|
||||
For service account mode:
|
||||
|
||||
```bash
|
||||
kubectl get rolebinding headlamp-polaris-proxy -n polaris -o yaml
|
||||
```
|
||||
|
||||
Expected subjects:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
```
|
||||
|
||||
For OIDC mode:
|
||||
|
||||
```bash
|
||||
kubectl get rolebinding -n polaris -o yaml | grep -A 5 polaris-proxy-reader
|
||||
```
|
||||
|
||||
Ensure your user or group is bound to the `polaris-proxy-reader` role.
|
||||
|
||||
**3. Create Missing RBAC**
|
||||
|
||||
If RBAC is missing, apply the minimal configuration:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
**4. Test RBAC Permissions**
|
||||
|
||||
Service account mode:
|
||||
|
||||
```bash
|
||||
# Impersonate Headlamp service account
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n polaris
|
||||
# Expected: yes
|
||||
```
|
||||
|
||||
OIDC mode (test as yourself):
|
||||
|
||||
```bash
|
||||
kubectl auth can-i get services/proxy \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n polaris
|
||||
# Expected: yes (if you have proper RoleBinding)
|
||||
```
|
||||
|
||||
**5. Restart Headlamp**
|
||||
|
||||
After applying RBAC changes:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment headlamp -n kube-system
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 404 Not Found Error
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Error message: "Error loading Polaris audit data: 404 Not Found"
|
||||
- Service proxy request returns 404
|
||||
- Polaris dashboard not reachable
|
||||
|
||||
### Root Cause
|
||||
|
||||
Polaris dashboard service doesn't exist or is in a different namespace.
|
||||
|
||||
### Solution
|
||||
|
||||
**1. Verify Polaris Installation**
|
||||
|
||||
Check if Polaris is installed:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n polaris
|
||||
# Expected: polaris-dashboard-* pod running
|
||||
```
|
||||
|
||||
Check if service exists:
|
||||
|
||||
```bash
|
||||
kubectl get service polaris-dashboard -n polaris
|
||||
# Expected: ClusterIP service on port 80
|
||||
```
|
||||
|
||||
**2. Verify Service Name and Port**
|
||||
|
||||
The plugin expects:
|
||||
|
||||
- **Namespace**: `polaris`
|
||||
- **Service Name**: `polaris-dashboard`
|
||||
- **Port**: `80` (or named port `dashboard`)
|
||||
|
||||
If your service has a different name or is in a different namespace, you'll need to modify the plugin source or redeploy Polaris with standard naming.
|
||||
|
||||
**3. Test Service Proxy Manually**
|
||||
|
||||
```bash
|
||||
kubectl proxy &
|
||||
curl http://localhost:8001/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
```
|
||||
|
||||
If this returns JSON, the service proxy works and the issue is elsewhere.
|
||||
If this returns 404, Polaris service is not configured correctly.
|
||||
|
||||
**4. Check Polaris Dashboard Configuration**
|
||||
|
||||
Verify Polaris is running with dashboard enabled:
|
||||
|
||||
```bash
|
||||
kubectl get deployment polaris-dashboard -n polaris -o yaml | grep -A 5 dashboard
|
||||
```
|
||||
|
||||
If `dashboard.enabled: false` in Helm values, enable it:
|
||||
|
||||
```yaml
|
||||
# values.yaml
|
||||
dashboard:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
**5. Reinstall Polaris**
|
||||
|
||||
If Polaris is missing or misconfigured:
|
||||
|
||||
```bash
|
||||
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
|
||||
helm upgrade --install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Settings Page Empty
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Settings → Polaris shows title but no content
|
||||
- Refresh interval and dashboard URL fields not visible
|
||||
|
||||
### Root Cause (Fixed in v0.3.3)
|
||||
|
||||
Plugin settings registration name didn't match `package.json` name.
|
||||
|
||||
### Solution
|
||||
|
||||
Upgrade to v0.3.3 or later:
|
||||
|
||||
```bash
|
||||
# Via Headlamp UI: Settings → Plugins → Update
|
||||
# Or redeploy with latest version
|
||||
```
|
||||
|
||||
If manually installing, ensure plugin name matches `package.json`:
|
||||
|
||||
```typescript
|
||||
registerPluginSettings('headlamp-polaris-plugin', PolarisSettings, true);
|
||||
// NOT 'polaris' — must match package.json name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode Issues
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Drawer background remains white in dark mode
|
||||
- Text is hard to read in dark mode
|
||||
- Theme colors don't match Headlamp UI
|
||||
|
||||
### Solution (Fixed in v0.3.5)
|
||||
|
||||
Upgrade to v0.3.5 or later for complete dark mode support.
|
||||
|
||||
**Verify CSS Variables**:
|
||||
The plugin uses MUI CSS variables for theming:
|
||||
|
||||
- `--mui-palette-background-default` (drawer background)
|
||||
- `--mui-palette-text-primary` (text color)
|
||||
- `--mui-palette-primary-main` (links, buttons)
|
||||
- `--mui-palette-error-main` (danger states)
|
||||
|
||||
These automatically adapt to Headlamp's theme (light/dark/system).
|
||||
|
||||
**Hard Refresh Required**:
|
||||
After upgrading from v0.3.4 or earlier, hard refresh your browser:
|
||||
|
||||
- macOS: Cmd+Shift+R
|
||||
- Linux/Windows: Ctrl+Shift+R
|
||||
|
||||
**Clear Browser Cache**:
|
||||
If hard refresh doesn't help, clear cache for Headlamp domain.
|
||||
|
||||
---
|
||||
|
||||
## Data Not Loading / Infinite Spinner
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Plugin shows "Loading Polaris audit data..." forever
|
||||
- No error message in UI
|
||||
- Data never appears
|
||||
|
||||
### Debugging Steps
|
||||
|
||||
**1. Check Browser Console**
|
||||
|
||||
Open DevTools (F12) → Console tab.
|
||||
|
||||
Look for:
|
||||
|
||||
- Network errors (CORS, timeouts, 5xx responses)
|
||||
- JavaScript errors
|
||||
- Failed API requests
|
||||
|
||||
**2. Check Network Tab**
|
||||
|
||||
Open DevTools → Network tab → Filter by "results.json"
|
||||
|
||||
Expected request:
|
||||
|
||||
```
|
||||
GET /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
Status: 200
|
||||
Response: JSON data
|
||||
```
|
||||
|
||||
Common issues:
|
||||
|
||||
- **Status 0 / Failed**: Network policy blocking request
|
||||
- **Status 403**: RBAC issue (see [403 Forbidden Error](#403-forbidden-error))
|
||||
- **Status 404**: Service not found (see [404 Not Found Error](#404-not-found-error))
|
||||
- **Status 500**: Polaris dashboard error
|
||||
- **Status 502/504**: Service unreachable (network policy or pod down)
|
||||
|
||||
**3. Check Polaris Dashboard Health**
|
||||
|
||||
```bash
|
||||
# Check if Polaris pod is running
|
||||
kubectl get pods -n polaris
|
||||
|
||||
# Check Polaris logs
|
||||
kubectl logs -n polaris deployment/polaris-dashboard
|
||||
|
||||
# Test direct access to Polaris
|
||||
kubectl port-forward -n polaris svc/polaris-dashboard 8080:80
|
||||
curl http://localhost:8080/results.json
|
||||
```
|
||||
|
||||
**4. Check Network Policies**
|
||||
|
||||
If your cluster uses NetworkPolicies:
|
||||
|
||||
```bash
|
||||
kubectl get networkpolicy -n polaris
|
||||
```
|
||||
|
||||
Ensure API server (or Headlamp pod) can reach Polaris dashboard.
|
||||
|
||||
**Example fix**:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-api-server-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector: {} # Allow from all namespaces (API server)
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
```
|
||||
|
||||
**5. Increase Timeout / Disable Auto-Refresh**
|
||||
|
||||
If Polaris responds slowly:
|
||||
|
||||
- Open Settings → Polaris
|
||||
- Increase refresh interval to 10+ minutes
|
||||
- Or set to "Manual only" to disable auto-refresh
|
||||
|
||||
---
|
||||
|
||||
## Browser Console Errors
|
||||
|
||||
### Common Errors and Solutions
|
||||
|
||||
**Error: "Failed to fetch"**
|
||||
|
||||
**Cause**: Network request failed (CORS, network policy, timeout)
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check Network tab for actual HTTP status
|
||||
2. Verify network policies allow API server → Polaris
|
||||
3. Check Polaris pod is running
|
||||
|
||||
---
|
||||
|
||||
**Error: "Unexpected token < in JSON"**
|
||||
|
||||
**Cause**: API returned HTML (error page) instead of JSON
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check Network tab response body (likely 404 or 500 error page)
|
||||
2. Verify Polaris service exists and is healthy
|
||||
3. Check service proxy URL is correct
|
||||
|
||||
---
|
||||
|
||||
**Error: "registerPluginSettings is not a function"**
|
||||
|
||||
**Cause**: Headlamp version too old (< v0.26)
|
||||
|
||||
**Solution**: Upgrade Headlamp to v0.26 or later.
|
||||
|
||||
---
|
||||
|
||||
**Error: "Cannot read property 'AuditData' of undefined"**
|
||||
|
||||
**Cause**: Polaris returned empty or malformed response
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. Check Polaris logs for errors
|
||||
2. Verify Polaris is scanning the cluster (check audit timestamp)
|
||||
3. Test `/results.json` endpoint directly
|
||||
|
||||
---
|
||||
|
||||
## Network and RBAC Debugging
|
||||
|
||||
### Comprehensive RBAC Test
|
||||
|
||||
Run this script to test all RBAC components:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
NS="polaris"
|
||||
SA="headlamp"
|
||||
SA_NS="kube-system"
|
||||
|
||||
echo "=== Testing RBAC for Polaris Plugin ==="
|
||||
|
||||
# 1. Check if service exists
|
||||
echo "1. Service check:"
|
||||
kubectl get svc polaris-dashboard -n $NS || echo "❌ Service not found"
|
||||
|
||||
# 2. Check if Role exists
|
||||
echo "2. Role check:"
|
||||
kubectl get role polaris-proxy-reader -n $NS || echo "❌ Role not found"
|
||||
|
||||
# 3. Check if RoleBinding exists
|
||||
echo "3. RoleBinding check:"
|
||||
kubectl get rolebinding headlamp-polaris-proxy -n $NS || echo "❌ RoleBinding not found"
|
||||
|
||||
# 4. Test service account permissions
|
||||
echo "4. Permission test (service account):"
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:$SA_NS:$SA \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n $NS
|
||||
|
||||
# 5. Test actual proxy request (requires kubectl proxy)
|
||||
echo "5. Proxy test:"
|
||||
kubectl proxy &
|
||||
PROXY_PID=$!
|
||||
sleep 2
|
||||
curl -s http://localhost:8001/api/v1/namespaces/$NS/services/polaris-dashboard:80/proxy/results.json | jq '.DisplayName' || echo "❌ Proxy request failed"
|
||||
kill $PROXY_PID
|
||||
|
||||
echo "=== Test complete ==="
|
||||
```
|
||||
|
||||
### Network Policy Debugging
|
||||
|
||||
Test connectivity from Headlamp to Polaris:
|
||||
|
||||
```bash
|
||||
# Create debug pod in kube-system namespace
|
||||
kubectl run netdebug -n kube-system --rm -it --image=nicolaka/netshoot -- bash
|
||||
|
||||
# Inside pod, test DNS and HTTP
|
||||
nslookup polaris-dashboard.polaris.svc.cluster.local
|
||||
curl -v http://polaris-dashboard.polaris.svc.cluster.local/results.json
|
||||
```
|
||||
|
||||
If this fails, network policies are blocking traffic.
|
||||
|
||||
### API Server Audit Logs
|
||||
|
||||
If you have audit logging enabled, check for denied requests:
|
||||
|
||||
```bash
|
||||
# View recent audit logs (location varies by cluster)
|
||||
kubectl logs -n kube-system kube-apiserver-* | grep polaris-dashboard
|
||||
|
||||
# Look for lines with:
|
||||
# "reason": "Forbidden"
|
||||
# "user": "system:serviceaccount:kube-system:headlamp"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Installation Issues
|
||||
|
||||
### Sidecar Fails to Install Plugin
|
||||
|
||||
**Symptoms**:
|
||||
|
||||
- Plugin sidecar logs show download errors
|
||||
- Plugin directory is empty
|
||||
- Settings → Plugins shows nothing
|
||||
|
||||
**Check sidecar logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
```
|
||||
|
||||
**Common errors**:
|
||||
|
||||
**1. Network timeout downloading tarball**
|
||||
|
||||
```
|
||||
Error: connect ETIMEDOUT
|
||||
```
|
||||
|
||||
**Solution**: Check cluster egress network policies allow HTTPS to GitHub.
|
||||
|
||||
---
|
||||
|
||||
**2. Invalid tarball URL**
|
||||
|
||||
```
|
||||
Error: 404 Not Found
|
||||
```
|
||||
|
||||
**Solution**: Verify `archive-url` in plugin config matches GitHub release:
|
||||
|
||||
```bash
|
||||
kubectl get configmap headlamp-plugin-config -n kube-system -o yaml
|
||||
```
|
||||
|
||||
Expected format:
|
||||
|
||||
```
|
||||
https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**3. Permission denied writing to /headlamp/plugins**
|
||||
|
||||
**Solution**: Ensure volume mount is writable:
|
||||
|
||||
```yaml
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /headlamp/plugins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Plugin Manager Not Working
|
||||
|
||||
**Symptoms**:
|
||||
|
||||
- Headlamp → Settings → Plugins shows "Catalog" tab but plugins don't install
|
||||
- "Install" button does nothing
|
||||
|
||||
**Root Cause**: Plugin manager requires `config.pluginsDir` to be set.
|
||||
|
||||
**Solution**: Configure Headlamp for plugin manager:
|
||||
|
||||
```yaml
|
||||
# HelmRelease values
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ArtifactHub Sync Delays
|
||||
|
||||
### Symptoms
|
||||
|
||||
- New version released on GitHub but not showing in ArtifactHub
|
||||
- Headlamp plugin catalog shows old version
|
||||
|
||||
### Root Cause
|
||||
|
||||
ArtifactHub pulls metadata every 30 minutes. There is no webhook or push mechanism.
|
||||
|
||||
### Solution
|
||||
|
||||
**Wait 30 minutes** after pushing a GitHub release, then check:
|
||||
|
||||
```
|
||||
https://artifacthub.io/packages/headlamp/headlamp-polaris-plugin/headlamp-polaris-plugin
|
||||
```
|
||||
|
||||
**Verify metadata**:
|
||||
|
||||
1. Check `artifacthub-pkg.yml` is in repository root
|
||||
2. Check `headlamp/plugin/archive-url` points to GitHub release
|
||||
3. Check `headlamp/plugin/archive-checksum` matches tarball SHA256
|
||||
|
||||
**Force sync** (ArtifactHub UI):
|
||||
|
||||
- Log in to ArtifactHub as package maintainer
|
||||
- Go to package settings
|
||||
- Click "Reindex now"
|
||||
|
||||
**Note**: First sync after repository registration may take up to 1 hour.
|
||||
|
||||
---
|
||||
|
||||
## Still Having Issues?
|
||||
|
||||
If none of these solutions work, gather debugging information and open an issue:
|
||||
|
||||
### Required Information
|
||||
|
||||
1. **Version Information**:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system -l app.kubernetes.io/name=headlamp -o yaml | grep image:
|
||||
```
|
||||
|
||||
2. **Plugin Version**:
|
||||
|
||||
- Check Settings → Plugins in Headlamp UI
|
||||
- Or: `kubectl exec -n kube-system deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
|
||||
|
||||
3. **Browser Console Output**:
|
||||
|
||||
- Open DevTools (F12) → Console
|
||||
- Screenshot or copy errors
|
||||
|
||||
4. **Network Tab**:
|
||||
|
||||
- Open DevTools → Network
|
||||
- Screenshot failed requests to `results.json`
|
||||
|
||||
5. **Pod Logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp --tail=100
|
||||
kubectl logs -n polaris deployment/polaris-dashboard --tail=100
|
||||
```
|
||||
|
||||
6. **RBAC Configuration**:
|
||||
```bash
|
||||
kubectl get role,rolebinding -n polaris
|
||||
```
|
||||
|
||||
### Where to Get Help
|
||||
|
||||
- **GitHub Issues**: [https://github.com/privilegedescalation/headlamp-polaris-plugin/issues](https://github.com/privilegedescalation/headlamp-polaris-plugin/issues)
|
||||
- **GitHub Discussions**: [https://github.com/privilegedescalation/headlamp-polaris-plugin/discussions](https://github.com/privilegedescalation/headlamp-polaris-plugin/discussions)
|
||||
|
||||
Include the debugging information above when opening an issue.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Network Problems
|
||||
|
||||
Troubleshooting network connectivity issues for the Headlamp Polaris Plugin.
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin accesses Polaris through the Kubernetes service proxy. Network issues can occur at multiple points in this chain:
|
||||
|
||||
```
|
||||
Headlamp Pod → K8s API Server → Polaris Dashboard Service
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### NetworkPolicy Blocking Access
|
||||
|
||||
**Symptom:** Timeout or connection errors despite correct RBAC
|
||||
|
||||
**Cause:** NetworkPolicy in `polaris` namespace blocking API server ingress
|
||||
|
||||
**Solution:**
|
||||
|
||||
Allow ingress from the Kubernetes API server to Polaris dashboard:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-apiserver-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
app.kubernetes.io/component: dashboard
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from API server
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
component: kube-apiserver
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
```
|
||||
|
||||
**Note:** The API server performs the proxy hop, not the Headlamp pod directly.
|
||||
|
||||
### Test Network Connectivity
|
||||
|
||||
```bash
|
||||
# 1. Test service proxy endpoint
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json
|
||||
|
||||
# If successful: JSON output
|
||||
# If failed: Check NetworkPolicies and service status
|
||||
|
||||
# 2. Check NetworkPolicies
|
||||
kubectl -n polaris get networkpolicy
|
||||
|
||||
# 3. Test direct service access (from within cluster)
|
||||
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
|
||||
curl http://polaris-dashboard.polaris/results.json
|
||||
|
||||
# If this works but service proxy doesn't, check API server network access
|
||||
```
|
||||
|
||||
### CORS Issues (Custom URL)
|
||||
|
||||
**Symptom:** Error when using custom Polaris URL in settings
|
||||
|
||||
**Cause:** CORS not configured on external Polaris deployment
|
||||
|
||||
**Solution:**
|
||||
|
||||
Configure Polaris dashboard to allow Headlamp origin:
|
||||
|
||||
```yaml
|
||||
# Polaris Helm values
|
||||
dashboard:
|
||||
enabled: true
|
||||
env:
|
||||
- name: CORS_ALLOWED_ORIGINS
|
||||
value: 'https://headlamp.example.com'
|
||||
```
|
||||
|
||||
Test CORS headers:
|
||||
|
||||
```bash
|
||||
curl -v -H "Origin: https://headlamp.example.com" \
|
||||
https://my-polaris.example.com/results.json
|
||||
|
||||
# Check for:
|
||||
# Access-Control-Allow-Origin: https://headlamp.example.com
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Kubernetes NetworkPolicies](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
|
||||
- [Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
@@ -0,0 +1,104 @@
|
||||
# RBAC Issues
|
||||
|
||||
Troubleshooting RBAC permissions and 403 errors for the Headlamp Polaris Plugin.
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin requires `get` permission on `services/proxy` resource for the `polaris-dashboard` service in the `polaris` namespace. Without this permission, you'll see 403 Forbidden errors.
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### 403 Forbidden Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, "Access denied (403)" in UI
|
||||
|
||||
**Cause:** Missing or incorrect RBAC binding
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# 1. Verify RBAC resources exist
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# If missing, apply RBAC:
|
||||
kubectl apply -f - <<EOF
|
||||
---
|
||||
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
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
### Token-Auth Mode
|
||||
|
||||
**Symptom:** 403 error when using Headlamp with user-supplied tokens
|
||||
|
||||
**Cause:** User's own identity lacks the RoleBinding
|
||||
|
||||
**Solution:**
|
||||
|
||||
Bind the Role to authenticated users or specific users/groups:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: users-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: system:authenticated # All authenticated users
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Testing Permissions
|
||||
|
||||
```bash
|
||||
# Test service account (in-cluster mode)
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Test user (token-auth mode)
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=user@example.com \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes
|
||||
```
|
||||
|
||||
For detailed RBAC configuration, see [RBAC Permissions](../user-guide/rbac-permissions.md).
|
||||
|
||||
## References
|
||||
|
||||
- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
- [Service Proxy RBAC](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
@@ -0,0 +1,412 @@
|
||||
# Configuration Guide
|
||||
|
||||
Customize the Headlamp Polaris Plugin to fit your environment.
|
||||
|
||||
## Plugin Settings
|
||||
|
||||
Access plugin settings via **Settings → Plugins → Polaris** in the Headlamp UI.
|
||||
|
||||
## Refresh Interval
|
||||
|
||||
**What it does:** Controls how often the plugin fetches the latest audit data from Polaris.
|
||||
|
||||
### Available Options
|
||||
|
||||
- **1 minute** - Most frequent updates, highest API load
|
||||
- **5 minutes** - **Default**, balanced load and freshness
|
||||
- **10 minutes** - Moderate refresh rate
|
||||
- **30 minutes** - Light load, best for large clusters
|
||||
|
||||
### How to Change
|
||||
|
||||
1. Navigate to **Settings → Plugins → Polaris**
|
||||
2. Click the **Refresh Interval** dropdown
|
||||
3. Select your desired interval
|
||||
4. Click **Save**
|
||||
5. Changes take effect immediately (no browser refresh needed)
|
||||
|
||||
### Impact
|
||||
|
||||
**Affects:**
|
||||
|
||||
- Dashboard overview page
|
||||
- Namespace list and detail views
|
||||
- Inline audit sections on resource pages
|
||||
- App bar score badge
|
||||
|
||||
**API Load:**
|
||||
|
||||
- Each refresh triggers one HTTP GET to Polaris dashboard
|
||||
- Each request is logged in Kubernetes audit logs
|
||||
- Longer intervals reduce API server and audit log pressure
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
**For small clusters (<100 pods):**
|
||||
|
||||
- Recommended: 5 minutes (default)
|
||||
- Acceptable: 1 minute (if real-time data is critical)
|
||||
|
||||
**For large clusters (>1000 pods):**
|
||||
|
||||
- Recommended: 10-30 minutes
|
||||
- Reason: Reduces audit log volume and API server load
|
||||
- Example: 10 users × 1-minute refresh = ~14,400 audit logs/day
|
||||
- Example: 10 users × 30-minute refresh = ~480 audit logs/day
|
||||
|
||||
**For production environments:**
|
||||
|
||||
- Start with 5 minutes
|
||||
- Monitor API server metrics and audit log volume
|
||||
- Increase interval if needed
|
||||
|
||||
## Dashboard URL
|
||||
|
||||
**What it does:** Specifies which Polaris instance the plugin connects to.
|
||||
|
||||
### Default Configuration
|
||||
|
||||
**Service proxy path (default):**
|
||||
|
||||
```
|
||||
/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/
|
||||
```
|
||||
|
||||
This uses the Kubernetes API server to proxy requests to the Polaris dashboard service in the `polaris` namespace.
|
||||
|
||||
**Advantages:**
|
||||
|
||||
- Uses existing Headlamp authentication (service account or user token)
|
||||
- Works with Headlamp's OIDC and token-auth modes
|
||||
- No additional RBAC or network configuration needed
|
||||
- Respects Kubernetes NetworkPolicies
|
||||
|
||||
### Custom URL Scenarios
|
||||
|
||||
#### External Polaris (HTTPS)
|
||||
|
||||
If Polaris is deployed outside the cluster with an external URL:
|
||||
|
||||
```
|
||||
https://polaris.example.com/
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Polaris dashboard must be accessible from browser
|
||||
- CORS must be configured on Polaris to allow Headlamp origin
|
||||
- HTTPS recommended for production
|
||||
|
||||
#### Custom Namespace
|
||||
|
||||
If Polaris is deployed in a different namespace:
|
||||
|
||||
```
|
||||
/api/v1/namespaces/custom-namespace/services/polaris-dashboard:80/proxy/
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Update RBAC Role namespace to match
|
||||
- Service name must still be `polaris-dashboard` (or adjust in URL)
|
||||
|
||||
#### Non-Standard Port
|
||||
|
||||
If Polaris dashboard uses a different port:
|
||||
|
||||
```
|
||||
/api/v1/namespaces/polaris/services/polaris-dashboard:8080/proxy/
|
||||
```
|
||||
|
||||
#### Local Development
|
||||
|
||||
For local Polaris development instance:
|
||||
|
||||
```
|
||||
http://localhost:8080/
|
||||
```
|
||||
|
||||
**Note:** Browser may block mixed content (HTTPS Headlamp → HTTP Polaris).
|
||||
|
||||
### How to Change Dashboard URL
|
||||
|
||||
1. Navigate to **Settings → Plugins → Polaris**
|
||||
2. Update the **Dashboard URL** field
|
||||
3. Click **Test Connection** to verify (recommended)
|
||||
4. Click **Save** if connection test succeeds
|
||||
|
||||
### Connection Testing
|
||||
|
||||
**What it does:** Verifies the plugin can reach the Polaris dashboard and fetch audit data.
|
||||
|
||||
**To test:**
|
||||
|
||||
1. Enter Dashboard URL in settings
|
||||
2. Click **Test Connection**
|
||||
3. Wait for response (2-5 seconds)
|
||||
|
||||
**Success Response:**
|
||||
|
||||
```
|
||||
✓ Connected to Polaris v4.2.0
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Error | Meaning | Solution |
|
||||
| --------------------------- | ------------------------- | ----------------------------------------------------------- |
|
||||
| **403 Forbidden** | RBAC permission denied | Check RBAC bindings (see [RBAC Guide](rbac-permissions.md)) |
|
||||
| **404 Not Found** | Polaris service not found | Verify Polaris is running: `kubectl get svc -n polaris` |
|
||||
| **503 Service Unavailable** | Polaris pod not ready | Check pod status: `kubectl get pods -n polaris` |
|
||||
| **Network Error** | Cannot reach URL | Check URL format, CORS (for external), NetworkPolicies |
|
||||
| **CORS Error** | Cross-origin blocked | Configure Polaris dashboard CORS headers |
|
||||
|
||||
### CORS Configuration (External Polaris)
|
||||
|
||||
If using an external Polaris URL, configure CORS to allow Headlamp origin.
|
||||
|
||||
**Polaris Helm values:**
|
||||
|
||||
```yaml
|
||||
dashboard:
|
||||
enabled: true
|
||||
env:
|
||||
- name: CORS_ALLOWED_ORIGINS
|
||||
value: 'https://headlamp.example.com'
|
||||
```
|
||||
|
||||
**Test CORS:**
|
||||
|
||||
```bash
|
||||
curl -v -H "Origin: https://headlamp.example.com" \
|
||||
https://polaris.example.com/results.json \
|
||||
| grep -i "access-control"
|
||||
|
||||
# Expected:
|
||||
# Access-Control-Allow-Origin: https://headlamp.example.com
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Persistent Settings Storage
|
||||
|
||||
Plugin settings are stored in browser **localStorage**:
|
||||
|
||||
**Keys:**
|
||||
|
||||
- `polaris-plugin-refresh-interval` - Refresh interval in seconds (number)
|
||||
- `polaris-plugin-dashboard-url` - Dashboard URL (string)
|
||||
|
||||
**View settings:**
|
||||
|
||||
```javascript
|
||||
// Open browser DevTools Console (F12)
|
||||
console.log('Refresh Interval:', localStorage.getItem('polaris-plugin-refresh-interval'));
|
||||
console.log('Dashboard URL:', localStorage.getItem('polaris-plugin-dashboard-url'));
|
||||
```
|
||||
|
||||
**Reset to defaults:**
|
||||
|
||||
```javascript
|
||||
// Open browser DevTools Console (F12)
|
||||
localStorage.removeItem('polaris-plugin-refresh-interval');
|
||||
localStorage.removeItem('polaris-plugin-dashboard-url');
|
||||
// Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux)
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Settings are per-browser, per-user
|
||||
- Private/incognito mode may clear settings on browser close
|
||||
- Settings are NOT synced across devices
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
### For Development Clusters
|
||||
|
||||
**Recommended Settings:**
|
||||
|
||||
- **Refresh Interval:** 1-5 minutes (faster feedback loop)
|
||||
- **Dashboard URL:** Service proxy (default)
|
||||
|
||||
**Why:** Development clusters are typically small, so API load is minimal. Faster refresh helps catch issues quickly during development.
|
||||
|
||||
### For Staging Clusters
|
||||
|
||||
**Recommended Settings:**
|
||||
|
||||
- **Refresh Interval:** 5-10 minutes (balanced)
|
||||
- **Dashboard URL:** Service proxy (default)
|
||||
|
||||
**Why:** Staging should mirror production configuration. 5-10 minutes provides reasonable freshness without excessive load.
|
||||
|
||||
### For Production Clusters
|
||||
|
||||
**Recommended Settings:**
|
||||
|
||||
- **Refresh Interval:** 10-30 minutes (reduce load)
|
||||
- **Dashboard URL:** Service proxy (default)
|
||||
|
||||
**Why:** Production clusters are larger and more critical. Longer intervals reduce audit log volume and API pressure. Polaris audits typically run every 10-30 minutes anyway, so more frequent plugin refreshes don't provide much value.
|
||||
|
||||
### For Multi-Tenant Environments
|
||||
|
||||
**Recommended Settings:**
|
||||
|
||||
- **Refresh Interval:** 10-30 minutes (minimize per-user load)
|
||||
- **Dashboard URL:** Service proxy with per-namespace RBAC
|
||||
|
||||
**Why:** Many concurrent Headlamp users can create significant API load. Longer intervals prevent thundering herd issues.
|
||||
|
||||
### For External Polaris
|
||||
|
||||
**Recommended Settings:**
|
||||
|
||||
- **Refresh Interval:** 5-10 minutes (depends on network latency)
|
||||
- **Dashboard URL:** `https://polaris.example.com/`
|
||||
- **CORS:** Must be configured on Polaris side
|
||||
|
||||
**Why:** External Polaris avoids Kubernetes service proxy overhead but requires CORS configuration and network accessibility.
|
||||
|
||||
## Troubleshooting Configuration
|
||||
|
||||
### Settings Not Saving
|
||||
|
||||
**Symptom:** Changes to settings revert after clicking Save
|
||||
|
||||
**Possible Causes:**
|
||||
|
||||
1. Browser blocks localStorage (privacy mode)
|
||||
2. Browser extension interfering
|
||||
3. JavaScript error in console
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Open browser DevTools Console (F12)
|
||||
2. Check for JavaScript errors
|
||||
3. Disable privacy mode or try different browser
|
||||
4. Check if localStorage is enabled:
|
||||
```javascript
|
||||
console.log('localStorage available:', typeof localStorage !== 'undefined');
|
||||
```
|
||||
|
||||
### Settings Lost After Browser Restart
|
||||
|
||||
**Symptom:** Settings reset to defaults when you reopen browser
|
||||
|
||||
**Cause:** Browser privacy settings clear localStorage on exit
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Use normal browsing mode (not private/incognito)
|
||||
- Check browser settings for "Clear data on exit"
|
||||
- Consider requesting ConfigMap-based settings (future feature)
|
||||
|
||||
### Connection Test Fails
|
||||
|
||||
**Symptom:** Test Connection button shows error
|
||||
|
||||
**Solutions by error type:**
|
||||
|
||||
**403 Forbidden:**
|
||||
|
||||
```bash
|
||||
# Verify RBAC exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
```
|
||||
|
||||
**404 Not Found:**
|
||||
|
||||
```bash
|
||||
# Verify Polaris is running
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# If missing, install Polaris
|
||||
helm install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
**503 Service Unavailable:**
|
||||
|
||||
```bash
|
||||
# Check pod status
|
||||
kubectl -n polaris get pods
|
||||
|
||||
# Check pod logs
|
||||
kubectl -n polaris logs deployment/polaris-dashboard
|
||||
```
|
||||
|
||||
**Network Error / CORS:**
|
||||
|
||||
```bash
|
||||
# For external Polaris, test CORS
|
||||
curl -v -H "Origin: https://headlamp.example.com" \
|
||||
https://polaris.example.com/results.json
|
||||
|
||||
# Check for Access-Control-Allow-Origin header
|
||||
```
|
||||
|
||||
### Refresh Interval Not Working
|
||||
|
||||
**Symptom:** Data doesn't refresh automatically
|
||||
|
||||
**Check:**
|
||||
|
||||
1. Verify setting is saved (localStorage key exists)
|
||||
2. Check browser console for errors
|
||||
3. Verify Polaris is returning data (manual refresh works)
|
||||
4. Ensure you're on a Polaris plugin page (not other Headlamp pages)
|
||||
|
||||
**Debug:**
|
||||
|
||||
```javascript
|
||||
// Check refresh interval
|
||||
console.log(localStorage.getItem('polaris-plugin-refresh-interval'));
|
||||
|
||||
// Should return: "300" (5 minutes), "600" (10 minutes), etc.
|
||||
```
|
||||
|
||||
## Configuration Checklist
|
||||
|
||||
Before going to production, verify:
|
||||
|
||||
- [ ] Refresh interval set appropriately (10-30 min for large clusters)
|
||||
- [ ] Dashboard URL tested and working
|
||||
- [ ] Connection test passes
|
||||
- [ ] RBAC permissions granted (see [RBAC Guide](rbac-permissions.md))
|
||||
- [ ] NetworkPolicies allow API server → Polaris (if using network policies)
|
||||
- [ ] CORS configured (if using external Polaris)
|
||||
- [ ] Browser localStorage enabled
|
||||
- [ ] Settings persist across browser restarts
|
||||
|
||||
## Future Configuration Options
|
||||
|
||||
**Planned features:**
|
||||
|
||||
- ConfigMap-based settings (server-side, not localStorage)
|
||||
- Per-cluster settings (multi-cluster Headlamp support)
|
||||
- Webhook notifications for score changes
|
||||
- Custom check severity overrides
|
||||
- Exemption management UI (requires RBAC PATCH permission)
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Features Guide](features.md)** - Learn about all plugin features
|
||||
- **[RBAC Permissions](rbac-permissions.md)** - Configure advanced RBAC for token-auth, OIDC
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Diagnose common configuration issues
|
||||
|
||||
## References
|
||||
|
||||
- [Polaris Configuration](https://polaris.docs.fairwinds.com/customization/checks/)
|
||||
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
- [CORS Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||
@@ -0,0 +1,288 @@
|
||||
# Features Guide
|
||||
|
||||
Learn about all features in the Headlamp Polaris Plugin.
|
||||
|
||||
## Overview Dashboard
|
||||
|
||||
The main dashboard provides cluster-wide visibility. Navigate to **Polaris → Overview**.
|
||||
|
||||
### Cluster Score Gauge
|
||||
|
||||
Overall cluster health score (0-100%) with color-coded status:
|
||||
|
||||
- **Green (≥80%):** Excellent - cluster follows best practices
|
||||
- **Yellow (50-79%):** Needs attention - some issues present
|
||||
- **Red (<50%):** Critical - significant security/reliability concerns
|
||||
|
||||
The score is calculated as: `(passing checks / total checks) × 100`
|
||||
|
||||
### Check Distribution
|
||||
|
||||
Visual breakdown of all Polaris checks across the cluster:
|
||||
|
||||
- **Pass** - Checks that passed (green)
|
||||
- **Warning** - Failed checks with warning severity (yellow)
|
||||
- **Danger** - Failed checks with danger severity (red)
|
||||
- **Skipped** - Checks with severity "ignore" (gray)
|
||||
|
||||
**Note:** Skipped count only reflects checks with `Severity: "ignore"` from Polaris config. Annotation-based exemptions (e.g., `polaris.fairwinds.com/cpuLimitsMissing-exempt: "true"`) are not included. See "View in Polaris Dashboard" link for full exemption count.
|
||||
|
||||
### Top 10 Failing Checks
|
||||
|
||||
Most common issues across the entire cluster:
|
||||
|
||||
- Grouped by check type (e.g., "CPU Limits Missing", "Host IPC Set")
|
||||
- Shows count and severity
|
||||
- Helps identify cluster-wide patterns
|
||||
- Click check name for details
|
||||
|
||||
### Cluster Statistics
|
||||
|
||||
Quick cluster metadata:
|
||||
|
||||
- **Polaris Version** - e.g., "4.2.0"
|
||||
- **Last Audit** - ISO 8601 timestamp of most recent audit
|
||||
- **Nodes** - Total node count
|
||||
- **Pods** - Total pod count
|
||||
- **Namespaces** - Total namespace count
|
||||
- **Controllers** - Total workload controller count
|
||||
|
||||
### Manual Refresh
|
||||
|
||||
Click the refresh button to fetch the latest audit data immediately (bypasses auto-refresh interval).
|
||||
|
||||
## Namespace Views
|
||||
|
||||
### Namespaces List
|
||||
|
||||
Navigate to **Polaris → Namespaces** to see all namespaces with audit results.
|
||||
|
||||
**Table Columns:**
|
||||
|
||||
- **Namespace** - Clickable namespace name (opens detail panel)
|
||||
- **Score** - Per-namespace score with color coding
|
||||
- **Pass** - Passing checks count
|
||||
- **Warning** - Warning severity failures
|
||||
- **Danger** - Danger severity failures
|
||||
- **Skipped** - Skipped checks count
|
||||
|
||||
**Sorting:** Table is sortable by any column. Default sort is by score (lowest first) to surface problematic namespaces.
|
||||
|
||||
### Namespace Detail Panel
|
||||
|
||||
Click any namespace to open a 1000px-wide side panel with detailed information.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Namespace Score** - Color-coded score gauge
|
||||
- **Check Counts** - Pass/Warning/Danger/Skipped breakdown
|
||||
- **Resource Table** - Per-resource audit results:
|
||||
- Resource name
|
||||
- Resource kind (Deployment, StatefulSet, DaemonSet, Job, CronJob)
|
||||
- Pass/Warning/Danger counts per resource
|
||||
- **External Link** - "View in Polaris Dashboard" button for full Polaris UI
|
||||
- **URL Hash Navigation** - Browser back/forward works with drawer state
|
||||
- **Keyboard Shortcut** - Press **Escape** to close panel
|
||||
- **Click-to-Close** - Click backdrop to close panel
|
||||
|
||||
The drawer respects Headlamp's theme (light/dark mode).
|
||||
|
||||
## Inline Resource Audits
|
||||
|
||||
Polaris audit results automatically appear on resource detail pages.
|
||||
|
||||
### Supported Resources
|
||||
|
||||
Inline audit sections appear on:
|
||||
|
||||
- Deployments
|
||||
- StatefulSets
|
||||
- DaemonSets
|
||||
- Jobs
|
||||
- CronJobs
|
||||
|
||||
### What's Shown
|
||||
|
||||
**Compact Audit Section:**
|
||||
|
||||
- **Score Badge** - Color-coded score
|
||||
- **Check Counts** - Pass/Warning/Danger summary
|
||||
- **Failing Checks Table** - Only failed checks listed:
|
||||
- Check name (human-readable)
|
||||
- Severity badge (Warning/Danger)
|
||||
- Message describing the issue
|
||||
- **Link to Full Report** - Navigate to namespace detail for complete audit
|
||||
|
||||
**If no audit data:** Shows "No audit data available for this resource" message.
|
||||
|
||||
## App Bar Score Badge
|
||||
|
||||
Top-right corner of Headlamp shows a persistent cluster score badge.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Color-Coded Chip** - Green/Yellow/Red based on score
|
||||
- **Shield Icon** - Visual indicator
|
||||
- **Score Percentage** - e.g., "85%"
|
||||
- **Clickable** - Click to navigate to Polaris overview
|
||||
- **Real-Time Updates** - Updates on auto-refresh interval
|
||||
- **Always Visible** - Appears on all Headlamp pages
|
||||
|
||||
**Example:** Shield icon with "85%" (green chip)
|
||||
|
||||
## Settings & Configuration
|
||||
|
||||
Access plugin settings via **Settings → Plugins → Polaris**.
|
||||
|
||||
### Refresh Interval
|
||||
|
||||
Controls how often the plugin fetches new audit data.
|
||||
|
||||
**Options:**
|
||||
|
||||
- 1 minute - Most frequent (highest API load)
|
||||
- 5 minutes - **Default** (recommended)
|
||||
- 10 minutes - Moderate refresh rate
|
||||
- 30 minutes - Light load (large clusters)
|
||||
|
||||
**Impact:**
|
||||
|
||||
- Affects all views (dashboard, namespaces, inline audits, app bar badge)
|
||||
- Longer intervals reduce Kubernetes API audit logging
|
||||
- Changes take effect immediately (no restart required)
|
||||
|
||||
See [Configuration Guide](configuration.md) for details.
|
||||
|
||||
### Dashboard URL
|
||||
|
||||
Specifies which Polaris instance to connect to.
|
||||
|
||||
**Default:** Kubernetes service proxy path
|
||||
|
||||
```
|
||||
/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/
|
||||
```
|
||||
|
||||
**Custom Options:**
|
||||
|
||||
- External Polaris: `https://polaris.example.com/`
|
||||
- Different namespace: `/api/v1/namespaces/custom-ns/services/polaris-dashboard:80/proxy/`
|
||||
|
||||
**Test Connection Button:** Verifies connectivity before saving.
|
||||
|
||||
See [Configuration Guide](configuration.md) for advanced setup.
|
||||
|
||||
## Dark Mode Support
|
||||
|
||||
Full theme adaptation for Headlamp's light and dark modes.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Auto Dark Mode** - Respects system preference when Headlamp uses it
|
||||
- **Theme Toggle** - Adapts when you change Headlamp theme
|
||||
- **All UI Elements** - Drawer backgrounds, tables, buttons, badges, score gauge
|
||||
- **CSS Variables** - Uses MUI theme variables (`--mui-palette-*`)
|
||||
|
||||
**No configuration required** - works automatically with Headlamp's theme.
|
||||
|
||||
## Exemption Management
|
||||
|
||||
**Status:** Planned feature (UI components exist but not fully integrated)
|
||||
|
||||
**Future Capability:**
|
||||
|
||||
- View current exemptions on resources
|
||||
- Add exemptions for specific failing checks
|
||||
- Remove exemptions
|
||||
- Apply via annotation patches (`polaris.fairwinds.com/*-exempt`)
|
||||
|
||||
This feature requires additional RBAC permissions (PATCH on workload resources) and is not yet enabled by default.
|
||||
|
||||
## Data Refresh Behavior
|
||||
|
||||
**Initial Load:**
|
||||
|
||||
- Data fetched when you first navigate to any Polaris view
|
||||
- Shared across all views via React Context (no duplicate fetches)
|
||||
- Loading spinner displayed during initial fetch
|
||||
|
||||
**Auto-Refresh:**
|
||||
|
||||
- Configured via Settings → Plugins → Polaris
|
||||
- Default: 5 minutes
|
||||
- Triggers background fetch without disrupting UI
|
||||
|
||||
**Manual Refresh:**
|
||||
|
||||
- Click refresh button on overview dashboard
|
||||
- Forces immediate data fetch
|
||||
- Updates all views simultaneously
|
||||
|
||||
**Error Handling:**
|
||||
|
||||
- 403 errors show RBAC permission guidance
|
||||
- 404/503 errors indicate Polaris not installed
|
||||
- Network errors show generic failure with retry suggestion
|
||||
|
||||
## Browser Requirements
|
||||
|
||||
**Supported Browsers:**
|
||||
|
||||
- Chrome/Chromium 80+
|
||||
- Firefox 75+
|
||||
- Safari 13.1+
|
||||
- Edge 80+
|
||||
|
||||
**Required:**
|
||||
|
||||
- JavaScript enabled
|
||||
- localStorage enabled (for settings persistence)
|
||||
- Cookies enabled (for Headlamp session)
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
**Bundle Size:** ~27 KB minified (gzip: ~7.6 KB)
|
||||
|
||||
**Data Volume:** Depends on cluster size. Example:
|
||||
|
||||
- Small cluster (50 resources): ~100 KB JSON
|
||||
- Medium cluster (500 resources): ~1 MB JSON
|
||||
- Large cluster (5000 resources): ~10 MB JSON
|
||||
|
||||
**Rendering Performance:**
|
||||
|
||||
- Handles up to 100 namespaces without virtual scrolling
|
||||
- Namespace detail drawer renders instantly for up to 500 resources
|
||||
- React Context prevents unnecessary re-fetches
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### Skipped Count Incomplete
|
||||
|
||||
The "Skipped" count only reflects checks with `Severity: "ignore"` in Polaris configuration. Annotation-based exemptions are not counted because:
|
||||
|
||||
- Polaris API omits exempted checks from `results.json`
|
||||
- Native Polaris dashboard computes skipped count by querying raw Kubernetes resources
|
||||
- Plugin only has access to processed audit results (not raw resources)
|
||||
|
||||
**Workaround:** Use "View in Polaris Dashboard" link for accurate exemption count.
|
||||
|
||||
### Single Cluster Support
|
||||
|
||||
Plugin shows data for the current Headlamp cluster only. Multi-cluster aggregation is not supported.
|
||||
|
||||
### No Real-Time Updates
|
||||
|
||||
Data refreshes on interval (1-30 minutes), not real-time. Polaris dashboard doesn't support WebSocket/SSE.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Configuration Guide](configuration.md)** - Customize refresh intervals, dashboard URLs, test connections
|
||||
- **[RBAC Permissions](rbac-permissions.md)** - Advanced RBAC setup for token-auth, OIDC, multi-user
|
||||
- **[Troubleshooting](../troubleshooting/README.md)** - Quick diagnosis for common issues
|
||||
|
||||
## References
|
||||
|
||||
- [Fairwinds Polaris Documentation](https://polaris.docs.fairwinds.com/)
|
||||
- [Headlamp Documentation](https://headlamp.dev/docs/)
|
||||
- [Kubernetes Best Practices](https://kubernetes.io/docs/concepts/configuration/overview/)
|
||||
@@ -0,0 +1,641 @@
|
||||
# RBAC Permissions Guide
|
||||
|
||||
Understanding and configuring RBAC for the Headlamp Polaris Plugin.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
The plugin requires **one permission** to function:
|
||||
|
||||
| Verb | API Group | Resource | Resource Name | Namespace |
|
||||
| ----- | ----------- | ---------------- | ------------------- | --------- |
|
||||
| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` |
|
||||
|
||||
This allows the plugin to fetch audit results via the Kubernetes service proxy.
|
||||
|
||||
**Why this permission?**
|
||||
|
||||
- Plugin accesses Polaris through Kubernetes API server's service proxy
|
||||
- Service proxy requires `get` verb on `services/proxy` resource
|
||||
- Scoped to specific service (`polaris-dashboard`) for security
|
||||
- Read-only (no write operations)
|
||||
|
||||
## Standard Setup (Service Account Mode)
|
||||
|
||||
**Best for:** Headlamp running with a fixed service account in the cluster (in-cluster mode)
|
||||
|
||||
This is the most common deployment pattern for production Headlamp instances.
|
||||
|
||||
### Step 1: Create Role
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
rules:
|
||||
- apiGroups: ['']
|
||||
resources: ['services/proxy']
|
||||
resourceNames: ['polaris-dashboard']
|
||||
verbs: ['get']
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- **Role** (not ClusterRole) - Scoped to `polaris` namespace only
|
||||
- **resourceNames** - Restricts access to `polaris-dashboard` service only
|
||||
- **verbs: ["get"]** - Read-only permission
|
||||
|
||||
### Step 2: Create RoleBinding
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp # Adjust to your Headlamp SA name
|
||||
namespace: kube-system # Adjust to Headlamp's namespace
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Adjust for your environment:**
|
||||
|
||||
- `subjects[0].name` - Your Headlamp service account name (often `headlamp`)
|
||||
- `subjects[0].namespace` - Namespace where Headlamp runs (often `kube-system`)
|
||||
|
||||
### Step 3: Apply and Verify
|
||||
|
||||
```bash
|
||||
# Apply RBAC manifests
|
||||
kubectl apply -f polaris-rbac.yaml
|
||||
|
||||
# Verify Role exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
|
||||
# Verify RoleBinding exists
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes
|
||||
```
|
||||
|
||||
## Token-Auth Mode (Per-User Permissions)
|
||||
|
||||
**Best for:** Headlamp configured for user-supplied tokens, OIDC, or external authentication
|
||||
|
||||
In token-auth mode, **each user's own identity** is used for Kubernetes API requests (not a shared service account).
|
||||
|
||||
### Why Per-User RBAC?
|
||||
|
||||
With service account mode:
|
||||
|
||||
- Single RoleBinding grants access to all Headlamp users
|
||||
- Kubernetes sees all requests as `system:serviceaccount:kube-system:headlamp`
|
||||
|
||||
With token-auth mode:
|
||||
|
||||
- Each user's own token (OIDC, kubeconfig) is used
|
||||
- Kubernetes sees requests as `user@example.com` or `system:serviceaccount:team-ns:user-sa`
|
||||
- **Each user needs individual RBAC permissions**
|
||||
|
||||
### Option 1: Grant to All Authenticated Users
|
||||
|
||||
**Use case:** Everyone with cluster access should see Polaris data
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: authenticated-users-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: system:authenticated # All authenticated users
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Security consideration:** This grants Polaris access to **everyone** with cluster access. Ensure Polaris data is not sensitive in your environment.
|
||||
|
||||
### Option 2: Grant to Specific Users
|
||||
|
||||
**Use case:** Fine-grained control, only SRE/DevOps teams
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: sre-team-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: User
|
||||
name: alice@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: User
|
||||
name: bob@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Maintenance:** Add/remove users as team membership changes.
|
||||
|
||||
### Option 3: Grant to OIDC Groups
|
||||
|
||||
**Use case:** OIDC provider with group claims (most flexible)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: oidc-group-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: sre-team # OIDC group claim
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: Group
|
||||
name: devops-team
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- OIDC provider must include group claims in token
|
||||
- Headlamp must be configured to extract groups from OIDC token
|
||||
- Group names must match exactly (case-sensitive)
|
||||
|
||||
**Example OIDC group claim:**
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "user@example.com",
|
||||
"groups": ["sre-team", "developers"]
|
||||
}
|
||||
```
|
||||
|
||||
### Verify User Permission
|
||||
|
||||
```bash
|
||||
# Test specific user
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=user@example.com \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Test OIDC group
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=user@example.com \
|
||||
--as-group=sre-team \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected output: yes (if bound correctly)
|
||||
```
|
||||
|
||||
## Multi-Namespace Polaris Deployments
|
||||
|
||||
**Scenario:** Polaris deployed in multiple namespaces (e.g., per-team Polaris instances)
|
||||
|
||||
### Create Role per Namespace
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: team-a-polaris # First Polaris instance
|
||||
rules:
|
||||
- apiGroups: ['']
|
||||
resources: ['services/proxy']
|
||||
resourceNames: ['polaris-dashboard']
|
||||
verbs: ['get']
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: team-b-polaris # Second Polaris instance
|
||||
rules:
|
||||
- apiGroups: ['']
|
||||
resources: ['services/proxy']
|
||||
resourceNames: ['polaris-dashboard']
|
||||
verbs: ['get']
|
||||
```
|
||||
|
||||
### Create RoleBindings per Namespace
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: team-a-polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: team-b-polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Plugin configuration:**
|
||||
Users can switch between instances via **Settings → Plugins → Polaris → Dashboard URL**.
|
||||
|
||||
## Network Security
|
||||
|
||||
### NetworkPolicy Requirements
|
||||
|
||||
If the `polaris` namespace enforces NetworkPolicies, ensure the Kubernetes API server can reach Polaris dashboard.
|
||||
|
||||
**Why?** The Kubernetes API server proxies plugin requests, so it needs network access to Polaris.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-apiserver-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
app.kubernetes.io/component: dashboard
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from Kubernetes API server
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
component: kube-apiserver
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
```
|
||||
|
||||
**Note:** Headlamp pod itself does NOT need direct network access to Polaris (API server does the proxying).
|
||||
|
||||
### Service Mesh Considerations
|
||||
|
||||
If using Istio, Linkerd, or other service meshes:
|
||||
|
||||
**No special configuration needed** - Service proxy requests bypass the mesh (go through API server).
|
||||
|
||||
## OAuth2 / OIDC Integration
|
||||
|
||||
When using OAuth2/OIDC authentication with Headlamp:
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **User authenticates** with OIDC provider (e.g., Google, Okta, Keycloak)
|
||||
2. **OIDC provider issues token** with user identity and group claims
|
||||
3. **Headlamp receives token** and passes it to Kubernetes API
|
||||
4. **Plugin makes request** using user's token (not service account)
|
||||
5. **Kubernetes RBAC evaluates** user's permissions against RoleBinding
|
||||
|
||||
### Required Configuration
|
||||
|
||||
**Headlamp Helm values:**
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_ID
|
||||
value: 'headlamp'
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: headlamp-oidc
|
||||
key: client-secret
|
||||
- name: HEADLAMP_CONFIG_OIDC_ISSUER_URL
|
||||
value: 'https://auth.example.com/realms/kubernetes'
|
||||
- name: HEADLAMP_CONFIG_OIDC_SCOPES
|
||||
value: 'openid,profile,email,groups'
|
||||
```
|
||||
|
||||
**RBAC for OIDC users:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: oidc-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: kubernetes-admins # OIDC group claim
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Testing OIDC Permissions
|
||||
|
||||
```bash
|
||||
# Simulate OIDC user with group
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=user@example.com \
|
||||
--as-group=kubernetes-admins \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Expected: yes
|
||||
```
|
||||
|
||||
## Audit Logging Considerations
|
||||
|
||||
Every plugin data fetch creates a Kubernetes API audit log entry.
|
||||
|
||||
### Example Audit Log
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "Event",
|
||||
"apiVersion": "audit.k8s.io/v1",
|
||||
"level": "Metadata",
|
||||
"verb": "get",
|
||||
"user": {
|
||||
"username": "system:serviceaccount:kube-system:headlamp"
|
||||
},
|
||||
"sourceIPs": ["10.96.0.1"],
|
||||
"objectRef": {
|
||||
"resource": "services",
|
||||
"subresource": "proxy",
|
||||
"namespace": "polaris",
|
||||
"name": "polaris-dashboard",
|
||||
"apiVersion": "v1"
|
||||
},
|
||||
"responseStatus": {
|
||||
"code": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Volume Estimates
|
||||
|
||||
**Per user:**
|
||||
|
||||
- 1 refresh per 5 minutes = 288 requests/day
|
||||
- 1 refresh per 30 minutes = 48 requests/day
|
||||
|
||||
**Cluster-wide:**
|
||||
|
||||
- 10 concurrent users × 5-minute refresh = 2,880 audit logs/day
|
||||
- 100 concurrent users × 30-minute refresh = 4,800 audit logs/day
|
||||
|
||||
### Reducing Audit Volume
|
||||
|
||||
**Option 1: Increase refresh interval**
|
||||
|
||||
```
|
||||
Settings → Plugins → Polaris → Refresh Interval → 30 minutes
|
||||
```
|
||||
|
||||
**Option 2: Adjust audit policy level**
|
||||
|
||||
```yaml
|
||||
# kube-apiserver audit policy
|
||||
apiVersion: audit.k8s.io/v1
|
||||
kind: Policy
|
||||
rules:
|
||||
- level: Metadata # Log metadata only, not full request/response
|
||||
verbs: ['get']
|
||||
resources:
|
||||
- group: ''
|
||||
resources: ['services/proxy']
|
||||
namespaces: ['polaris']
|
||||
```
|
||||
|
||||
**Option 3: Filter audit logs**
|
||||
If using a log aggregator (e.g., Elasticsearch), create filters to exclude or downsample Polaris proxy requests.
|
||||
|
||||
## Troubleshooting RBAC
|
||||
|
||||
### "403 Forbidden" Error in Plugin
|
||||
|
||||
**Symptom:** Plugin shows "Access denied (403)" error when loading data
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
1. **Check Role exists:**
|
||||
|
||||
```bash
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
```
|
||||
|
||||
If missing: Apply Role manifest
|
||||
|
||||
2. **Check RoleBinding exists:**
|
||||
|
||||
```bash
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
```
|
||||
|
||||
If missing: Apply RoleBinding manifest
|
||||
|
||||
3. **Test permission:**
|
||||
|
||||
```bash
|
||||
# Service account mode
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
# Token-auth mode (replace with your username)
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=user@example.com \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
```
|
||||
|
||||
Expected: `yes`
|
||||
|
||||
4. **Verify RoleBinding subjects match:**
|
||||
```bash
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy -o yaml
|
||||
```
|
||||
Check `subjects[].name` and `subjects[].namespace` match your Headlamp SA or user
|
||||
|
||||
### "404 Not Found" Error
|
||||
|
||||
**This is NOT an RBAC issue.** 404 means Polaris service doesn't exist.
|
||||
|
||||
**Check:**
|
||||
|
||||
```bash
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
```
|
||||
|
||||
If missing, install Polaris with dashboard enabled.
|
||||
|
||||
### Permission Test Passes but Plugin Still Shows 403
|
||||
|
||||
**Possible causes:**
|
||||
|
||||
1. **Wrong namespace in RoleBinding:**
|
||||
|
||||
- RoleBinding must be in `polaris` namespace (where the service is)
|
||||
- Common mistake: Creating RoleBinding in `kube-system`
|
||||
|
||||
2. **Wrong resourceName:**
|
||||
|
||||
- Must match service name exactly: `polaris-dashboard`
|
||||
- Check: `kubectl -n polaris get svc`
|
||||
|
||||
3. **Browser caching old 403:**
|
||||
|
||||
- Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux)
|
||||
|
||||
4. **Token expired (OIDC mode):**
|
||||
- Re-authenticate with OIDC provider
|
||||
- Check token expiration in browser DevTools (Application → Session Storage)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Use Namespaced Roles (Not ClusterRoles)
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```yaml
|
||||
kind: Role
|
||||
metadata:
|
||||
namespace: polaris
|
||||
```
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```yaml
|
||||
kind: ClusterRole
|
||||
# Grants access to all namespaces
|
||||
```
|
||||
|
||||
**Why:** Namespaced Roles limit scope to `polaris` namespace only. ClusterRoles would allow access to service proxies in all namespaces.
|
||||
|
||||
### 2. Always Specify resourceNames
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```yaml
|
||||
resourceNames: ['polaris-dashboard']
|
||||
```
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```yaml
|
||||
resourceNames: [] # Allows access to ALL services
|
||||
```
|
||||
|
||||
**Why:** `resourceNames` restricts permission to a specific service. Without it, the binding grants access to proxy all services in the namespace.
|
||||
|
||||
### 3. Use Read-Only Verb
|
||||
|
||||
✅ **Good:**
|
||||
|
||||
```yaml
|
||||
verbs: ['get']
|
||||
```
|
||||
|
||||
❌ **Bad:**
|
||||
|
||||
```yaml
|
||||
verbs: ['get', 'create', 'update', 'delete']
|
||||
```
|
||||
|
||||
**Why:** Plugin only needs `get` to fetch audit results. Additional verbs violate principle of least privilege.
|
||||
|
||||
### 4. Review Bindings Quarterly
|
||||
|
||||
- Remove users who no longer need access
|
||||
- Update OIDC group bindings when org structure changes
|
||||
- Audit who has access: `kubectl -n polaris get rolebindings -o yaml`
|
||||
|
||||
### 5. Monitor Audit Logs
|
||||
|
||||
Set alerts for:
|
||||
|
||||
- Unusual access patterns (e.g., 403 spikes = permission issues)
|
||||
- High request volume (e.g., misconfigured refresh interval)
|
||||
- Access from unexpected users (security monitoring)
|
||||
|
||||
### 6. Avoid Wildcard Permissions
|
||||
|
||||
❌ **Never do this:**
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- apiGroups: ['*']
|
||||
resources: ['*']
|
||||
verbs: ['*']
|
||||
```
|
||||
|
||||
This grants cluster-admin equivalent permissions. Always use specific resources and verbs.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Features Guide](features.md)** - Learn about plugin capabilities
|
||||
- **[Configuration Guide](configuration.md)** - Configure refresh intervals and dashboard URL
|
||||
- **[Troubleshooting RBAC](../troubleshooting/rbac-issues.md)** - Detailed RBAC debugging
|
||||
|
||||
## References
|
||||
|
||||
- [Kubernetes RBAC Documentation](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
- [Service Proxy Authorization](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
- [Headlamp OIDC Configuration](https://headlamp.dev/docs/latest/installation/in-cluster/configuration/#oidc-configuration)
|
||||
- [Kubernetes Audit Logging](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/)
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
# E2E Smoke Tests
|
||||
|
||||
Playwright-based smoke tests that validate the Polaris plugin against a live Headlamp deployment.
|
||||
|
||||
## CI
|
||||
|
||||
E2E tests run automatically in GitHub Actions on pushes to `main` and pull requests. The workflow (`.github/workflows/e2e.yaml`):
|
||||
|
||||
1. Builds the plugin (`npm run build`)
|
||||
2. Creates a ConfigMap from the built `dist/` output
|
||||
3. Deploys a stock Headlamp instance via Helm with the plugin mounted as a ConfigMap volume
|
||||
4. Generates a ServiceAccount token for test auth
|
||||
5. Runs Playwright tests against the E2E instance
|
||||
6. Tears down the E2E instance
|
||||
|
||||
This approach uses the stock `ghcr.io/headlamp-k8s/headlamp` image with no custom Docker builds. The plugin is loaded via `HEADLAMP_PLUGINS_DIR` volume mount.
|
||||
|
||||
### Required GitHub Secrets
|
||||
|
||||
Configure these in GitHub repository settings (Settings → Secrets and variables → Actions):
|
||||
|
||||
| Secret | Required | Description |
|
||||
| -------------------- | -------- | -------------------------------------------------------------- |
|
||||
| `AUTHENTIK_USERNAME` | OIDC | Authentik email or username for a CI user with Headlamp access |
|
||||
| `AUTHENTIK_PASSWORD` | OIDC | Password for that user |
|
||||
|
||||
Token-based auth is auto-generated by the deploy script. OIDC secrets are only needed if testing against the shared Headlamp instance.
|
||||
|
||||
No `GHCR_TOKEN` or Docker registry secrets are needed — the stock Headlamp image is public.
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Option 1: OIDC via Authentik (same as CI)
|
||||
|
||||
```bash
|
||||
AUTHENTIK_USERNAME=you@example.com AUTHENTIK_PASSWORD=... npm run e2e
|
||||
```
|
||||
|
||||
The default base URL is `https://headlamp.animaniacs.farh.net`. Override with `HEADLAMP_URL` if needed.
|
||||
|
||||
### Option 2: K8s bearer token (port-forward)
|
||||
|
||||
```bash
|
||||
kubectl port-forward -n kube-system svc/headlamp 4466:80
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system)
|
||||
HEADLAMP_URL=http://localhost:4466 npm run e2e
|
||||
```
|
||||
|
||||
Or in headed mode (opens a browser window):
|
||||
|
||||
```bash
|
||||
HEADLAMP_URL=http://localhost:4466 npm run e2e:headed
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
| -------------------- | -------- | -------------------------------------- | --------------------------------------- |
|
||||
| `HEADLAMP_URL` | No | `https://headlamp.animaniacs.farh.net` | Base URL of the Headlamp instance |
|
||||
| `AUTHENTIK_USERNAME` | OIDC | — | Authentik email/username |
|
||||
| `AUTHENTIK_PASSWORD` | OIDC | — | Authentik password |
|
||||
| `HEADLAMP_TOKEN` | Token | — | Kubernetes bearer token (auto-generated in CI) |
|
||||
|
||||
In CI, `HEADLAMP_URL` and `HEADLAMP_TOKEN` are set automatically by the deploy script. For local runs, set either OIDC credentials or a token manually.
|
||||
|
||||
## What the Tests Validate
|
||||
|
||||
- **Sidebar entry** — The Polaris sidebar item appears after login
|
||||
- **Overview page** — Cluster score and check distribution render correctly
|
||||
- **Namespaces page** — Table of namespaces loads with clickable links
|
||||
- **Namespace detail** — Clicking a namespace shows its score and resource table
|
||||
|
||||
These are smoke tests against real cluster data. They verify the plugin loads and renders without errors, not specific data values.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Current Tests (`polaris.spec.ts`)
|
||||
|
||||
1. **`sidebar contains Polaris entry`**
|
||||
- Verifies Polaris appears in the navigation sidebar
|
||||
- Ensures plugin successfully registered sidebar entry
|
||||
|
||||
2. **`overview page renders cluster score`**
|
||||
- Navigates to `/c/main/polaris`
|
||||
- Checks for "Polaris — Overview" heading
|
||||
- Verifies cluster score percentage is displayed
|
||||
- Validates data fetching and rendering
|
||||
|
||||
3. **`namespaces page renders table with namespace buttons`**
|
||||
- Navigates to `/c/main/polaris/namespaces`
|
||||
- Checks for "Polaris — Namespaces" heading
|
||||
- Verifies table is visible with at least one row
|
||||
- Ensures namespace buttons are clickable
|
||||
|
||||
4. **`namespace detail drawer opens from table button`**
|
||||
- Clicks first namespace button in table
|
||||
- Verifies drawer opens with namespace name in heading
|
||||
- Checks "Namespace Score" section is visible
|
||||
- Confirms "Resources" table is displayed
|
||||
- Validates URL hash is updated with namespace name
|
||||
|
||||
5. **`namespace detail drawer closes with Escape key`**
|
||||
- Opens namespace drawer
|
||||
- Presses Escape key
|
||||
- Verifies drawer closes
|
||||
- Checks URL hash is cleared
|
||||
|
||||
6. **`namespace detail drawer opens from URL hash`**
|
||||
- Navigates directly to `/c/main/polaris/namespaces#<namespace>`
|
||||
- Verifies drawer automatically opens
|
||||
- Checks namespace details are displayed
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Cluster Requirements
|
||||
|
||||
1. **Polaris Deployment**
|
||||
```bash
|
||||
# Verify Polaris is running
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
```
|
||||
|
||||
2. **Polaris Audit Data**
|
||||
```bash
|
||||
# Check if Polaris has generated audit results
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq '.AuditTime'
|
||||
```
|
||||
|
||||
3. **RBAC Permissions**
|
||||
- Headlamp service account (or test user) needs `get` on `services/proxy` for `polaris-dashboard`
|
||||
- See main README for RBAC setup
|
||||
|
||||
### Local Setup
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
|
||||
# 2. Create .env file (optional, for persistent config)
|
||||
cp .env.example .env
|
||||
|
||||
# 3. Set environment variables
|
||||
export HEADLAMP_URL=https://your-headlamp-instance.com
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system)
|
||||
|
||||
# 4. Run tests
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Run in Headed Mode
|
||||
|
||||
See the browser UI while tests run:
|
||||
|
||||
```bash
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
Step through tests with Playwright Inspector:
|
||||
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### Generate Trace
|
||||
|
||||
Record full trace for failed tests:
|
||||
|
||||
```bash
|
||||
npx playwright test --trace on
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
### Screenshot on Failure
|
||||
|
||||
Tests automatically capture screenshots on failure in `test-results/`
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Auth fails with "Sign In button not found":**
|
||||
- Check HEADLAMP_URL is correct
|
||||
- Verify Headlamp is accessible
|
||||
- Ensure OIDC is configured if using Authentik
|
||||
|
||||
**Polaris sidebar entry not found:**
|
||||
- Plugin may not be installed: Check Settings → Plugins in Headlamp
|
||||
- Plugin may have failed to load: Check browser console
|
||||
- Clear browser cache and hard refresh
|
||||
|
||||
**Cluster score not displayed:**
|
||||
- Polaris may not have audit data yet
|
||||
- Check Polaris is running: `kubectl -n polaris get pods`
|
||||
- Verify service proxy: `kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
|
||||
|
||||
**Namespace table empty:**
|
||||
- Polaris hasn't run audit yet (wait a few minutes)
|
||||
- Check Polaris logs: `kubectl -n polaris logs -l app.kubernetes.io/name=polaris`
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Example: Testing Plugin Settings
|
||||
|
||||
```typescript
|
||||
test('plugin settings page shows Polaris configuration', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Find and click Polaris plugin
|
||||
await page.getByText('headlamp-polaris-plugin').click();
|
||||
|
||||
// Check settings are visible
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible();
|
||||
await expect(page.getByText('Refresh Interval')).toBeVisible();
|
||||
await expect(page.getByText('Dashboard URL')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Testing App Bar Badge
|
||||
|
||||
```typescript
|
||||
test('app bar displays Polaris score badge', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Badge should be visible in app bar
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible();
|
||||
|
||||
// Clicking should navigate to overview
|
||||
await badge.click();
|
||||
await expect(page).toHaveURL(/\/c\/main\/polaris$/);
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Testing Dark Mode
|
||||
|
||||
```typescript
|
||||
test('plugin UI adapts to dark mode', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris');
|
||||
|
||||
// Toggle dark mode
|
||||
await page.getByRole('button', { name: /theme/i }).click();
|
||||
|
||||
// Check background color changes
|
||||
const body = page.locator('body');
|
||||
await expect(body).toHaveCSS('background-color', 'rgb(18, 18, 18)');
|
||||
|
||||
// Plugin components should adapt
|
||||
const sectionBox = page.locator('[class*="MuiPaper"]').first();
|
||||
await expect(sectionBox).not.toHaveCSS('background-color', 'rgb(255, 255, 255)');
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically in GitHub Actions on pushes to `main` and pull requests. See `.github/workflows/e2e.yaml` for workflow configuration.
|
||||
|
||||
### Architecture
|
||||
|
||||
The E2E workflow deploys a **dedicated Headlamp instance** for each test run:
|
||||
|
||||
1. Build plugin (`npm run build`)
|
||||
2. Create ConfigMap from `dist/` output (`scripts/deploy-e2e-headlamp.sh`)
|
||||
3. Deploy stock Headlamp via Helm with ConfigMap volume mount
|
||||
4. Run Playwright tests against the E2E instance
|
||||
5. Tear down (`scripts/teardown-e2e-headlamp.sh`)
|
||||
|
||||
No custom Docker images, no PVCs, no kubectl exec/cp, no patching of existing deployments. The plugin is mounted from a ConfigMap into the stock Headlamp image.
|
||||
|
||||
### Cluster Prerequisites
|
||||
|
||||
One-time setup by a cluster admin:
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml
|
||||
```
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
You can manually trigger E2E tests from GitHub Actions:
|
||||
1. Go to Actions → E2E Tests
|
||||
2. Click "Run workflow"
|
||||
3. Select branch and run
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use semantic selectors**: `getByRole`, `getByText` over CSS selectors
|
||||
2. **Wait for visibility**: Use `await expect(...).toBeVisible()` instead of `waitForTimeout`
|
||||
3. **Keep tests independent**: Each test should work in isolation
|
||||
4. **Test user flows**: Complete journeys, not just page loads
|
||||
5. **Clean up state**: Close drawers/modals after tests
|
||||
6. **Use storage state**: Reuse auth across tests (already configured)
|
||||
7. **Parallelize carefully**: Currently disabled due to shared state
|
||||
|
||||
## Resources
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Project Main README](../README.md)
|
||||
@@ -0,0 +1,90 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Polaris app bar badge', () => {
|
||||
test('badge displays cluster score in app bar', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Wait for page to load
|
||||
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible();
|
||||
|
||||
// Badge should be visible in app bar with score percentage
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Badge should show shield emoji
|
||||
await expect(badge).toContainText('🛡️');
|
||||
});
|
||||
|
||||
test('clicking badge navigates to overview page', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Find and click the badge
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
await badge.click();
|
||||
|
||||
// Should navigate to Polaris overview
|
||||
await expect(page).toHaveURL(/\/c\/main\/polaris$/);
|
||||
await expect(page.getByRole('heading', { name: 'Polaris — Overview' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('badge color reflects score level', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Get the badge
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Extract score from button text
|
||||
const badgeText = await badge.textContent();
|
||||
const scoreMatch = badgeText?.match(/(\d+)%/);
|
||||
expect(scoreMatch).toBeTruthy();
|
||||
|
||||
const score = parseInt(scoreMatch![1]);
|
||||
|
||||
// Check background color matches score level
|
||||
const bgColor = await badge.evaluate(el =>
|
||||
window.getComputedStyle(el).backgroundColor
|
||||
);
|
||||
|
||||
// Verify that the badge has a non-default background color applied
|
||||
// (theme-dependent RGB values vary across Headlamp versions, so we
|
||||
// only assert that a real color is set rather than transparent/default)
|
||||
expect(bgColor).not.toBe('rgba(0, 0, 0, 0)');
|
||||
expect(bgColor).not.toBe('transparent');
|
||||
expect(bgColor).toMatch(/^rgb/);
|
||||
});
|
||||
|
||||
test('badge updates when navigating between clusters', async ({ page }) => {
|
||||
// This test assumes multi-cluster setup; skip if only one cluster
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Get initial badge score
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
const initialScore = await badge.textContent();
|
||||
|
||||
// Try to switch clusters (if available)
|
||||
const clusterSelector = page.getByRole('button', { name: /cluster/i });
|
||||
if (await clusterSelector.isVisible()) {
|
||||
// Note: This part will only work in multi-cluster setups
|
||||
// For single-cluster, this test will just verify badge persists
|
||||
await clusterSelector.click();
|
||||
|
||||
// Select different cluster if available
|
||||
const clusterOptions = page.getByRole('menuitem');
|
||||
const count = await clusterOptions.count();
|
||||
|
||||
if (count > 1) {
|
||||
await clusterOptions.nth(1).click();
|
||||
|
||||
// Badge should update or disappear (if new cluster doesn't have Polaris)
|
||||
// This is just verifying no crash occurs
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Badge should still be functional
|
||||
await expect(badge).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { test as setup, expect, Page } from '@playwright/test';
|
||||
|
||||
const AUTH_STATE_PATH = 'e2e/.auth/state.json';
|
||||
|
||||
async function authenticateWithOIDC(page: Page, username: string, password: string): Promise<void> {
|
||||
// Navigate to login — Headlamp redirects / to /c/main/login
|
||||
await page.goto('/');
|
||||
await page.waitForURL('**/login');
|
||||
|
||||
// Click "Sign In" and capture the Authentik popup
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
const popup = await popupPromise;
|
||||
|
||||
// Wait for the Authentik popup to fully load before interacting
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
await popup.waitForLoadState('networkidle');
|
||||
|
||||
// Authentik step 1: fill username — wait for the form to render
|
||||
const usernameField = popup.getByRole('textbox', { name: /email or username/i });
|
||||
await usernameField.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await usernameField.fill(username);
|
||||
await popup.getByRole('button', { name: /log in/i }).click();
|
||||
|
||||
// Authentik step 2: fill password — wait for the next step to load
|
||||
await popup.waitForLoadState('networkidle');
|
||||
const passwordField = popup.getByRole('textbox', { name: /password/i });
|
||||
await passwordField.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await passwordField.fill(password);
|
||||
await popup.getByRole('button', { name: /continue|log in/i }).click();
|
||||
|
||||
// Wait for the popup to close (Authentik redirects back, Headlamp processes callback)
|
||||
await popup.waitForEvent('close', { timeout: 15_000 });
|
||||
|
||||
// Original page should now be authenticated — wait for sidebar
|
||||
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function authenticateWithToken(page: Page, token: string): Promise<void> {
|
||||
await page.goto('/');
|
||||
// Headlamp goes to /token directly when no OIDC is configured,
|
||||
// or through /login when OIDC is configured
|
||||
await page.waitForURL(/\/(login|token)$/);
|
||||
|
||||
if (page.url().includes('/login')) {
|
||||
// OIDC login page — click "use a token" to reach token auth
|
||||
await page.getByRole('button', { name: /use a token/i }).click();
|
||||
await page.waitForURL('**/token');
|
||||
}
|
||||
|
||||
// Fill the "ID token" field and submit
|
||||
await page.getByRole('textbox', { name: /id token/i }).fill(token);
|
||||
await page.getByRole('button', { name: /authenticate/i }).click();
|
||||
|
||||
// Wait for the main UI to load
|
||||
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
setup('authenticate with Headlamp', async ({ page }) => {
|
||||
const username = process.env.AUTHENTIK_USERNAME;
|
||||
const password = process.env.AUTHENTIK_PASSWORD;
|
||||
const token = process.env.HEADLAMP_TOKEN;
|
||||
|
||||
if (username && password) {
|
||||
await authenticateWithOIDC(page, username, password);
|
||||
} else if (token) {
|
||||
await authenticateWithToken(page, token);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Set AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD for OIDC auth, or HEADLAMP_TOKEN for token auth'
|
||||
);
|
||||
}
|
||||
|
||||
await page.context().storageState({ path: AUTH_STATE_PATH });
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Polaris plugin smoke tests', () => {
|
||||
test('sidebar contains Polaris entry', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// The sidebar is the "Navigation" nav element (not "Appbar Tools")
|
||||
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
|
||||
await expect(sidebar).toBeVisible({ timeout: 15_000 });
|
||||
await expect(sidebar.getByRole('button', { name: 'Polaris' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('overview page renders cluster score', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris');
|
||||
|
||||
// SectionHeader renders a heading
|
||||
await expect(page.getByRole('heading', { name: 'Polaris \u2014 Overview' })).toBeVisible();
|
||||
|
||||
// "Cluster Score" section exists with a percentage
|
||||
await expect(page.getByText('Cluster Score')).toBeVisible();
|
||||
await expect(page.locator('main').getByText(/%/).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('namespaces page renders table with namespace buttons', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Polaris \u2014 Namespaces' })).toBeVisible();
|
||||
|
||||
// Table should have at least one row with a namespace button
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const rows = table.locator('tbody tr');
|
||||
await expect(rows.first()).toBeVisible();
|
||||
|
||||
// Each namespace row should contain a button (now buttons instead of links for drawer)
|
||||
const firstButton = rows.first().locator('button');
|
||||
await expect(firstButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('namespace detail drawer opens from table button', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
// Click the first namespace button in the table
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||
const namespaceName = await firstButton.textContent();
|
||||
await firstButton.click();
|
||||
|
||||
// Drawer should open and show the namespace name in the heading
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).toBeVisible();
|
||||
|
||||
// "Namespace Score" section should be present in drawer
|
||||
await expect(page.getByText('Namespace Score')).toBeVisible();
|
||||
|
||||
// Resources table should exist in drawer
|
||||
await expect(page.getByRole('heading', { name: 'Resources' })).toBeVisible();
|
||||
|
||||
// URL hash should be updated with namespace name
|
||||
await expect(page).toHaveURL(/\/polaris\/namespaces#/);
|
||||
});
|
||||
|
||||
test('namespace detail drawer closes with Escape key', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
// Open the drawer by clicking a namespace button
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||
const namespaceName = await firstButton.textContent();
|
||||
await firstButton.click();
|
||||
|
||||
// Verify drawer is open
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).toBeVisible();
|
||||
|
||||
// Press Escape key
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Drawer should close (heading should not be visible anymore)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).not.toBeVisible();
|
||||
|
||||
// URL hash should be cleared
|
||||
await expect(page).toHaveURL(/\/polaris\/namespaces$/);
|
||||
});
|
||||
|
||||
test('namespace detail drawer opens from URL hash', async ({ page }) => {
|
||||
// Get a namespace name first
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||
const namespaceName = await firstButton.textContent();
|
||||
|
||||
// Navigate directly to URL with hash
|
||||
await page.goto(`/c/main/polaris/namespaces#${namespaceName}`);
|
||||
|
||||
// Drawer should automatically open with the namespace details
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).toBeVisible();
|
||||
|
||||
// "Namespace Score" section should be present
|
||||
await expect(page.getByText('Namespace Score')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/** Navigate to the Polaris plugin settings page and wait for settings to render. */
|
||||
async function goToPolarisSettings(page: Page) {
|
||||
// Headlamp's plugin settings page is a HOME-context route at /settings/plugins,
|
||||
// not an in-cluster route (/c/main/settings/plugins would 404). Headlamp loads
|
||||
// plugin scripts asynchronously on SPA init. When registerPluginSettings() fires,
|
||||
// it dispatches a Redux action — PluginSettings uses useTypedSelector so it
|
||||
// re-renders automatically once the plugin registers. No preloading needed.
|
||||
await page.goto('/settings/plugins');
|
||||
|
||||
// Wait for the plugin to appear in the settings list. The timeout covers
|
||||
// async plugin script loading + registration.
|
||||
const pluginEntry = page.locator('text=headlamp-polaris').first();
|
||||
await expect(pluginEntry).toBeVisible({ timeout: 30_000 });
|
||||
await pluginEntry.click();
|
||||
|
||||
// Wait for the PolarisSettings component to render
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
test.describe('Polaris plugin settings', () => {
|
||||
test('settings page shows configuration options', async ({ page }) => {
|
||||
await goToPolarisSettings(page);
|
||||
|
||||
// SectionBox title should be visible
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible();
|
||||
});
|
||||
|
||||
test('refresh interval setting is configurable', async ({ page }) => {
|
||||
await goToPolarisSettings(page);
|
||||
|
||||
// Find the refresh interval dropdown
|
||||
const intervalSelect = page.locator('select').filter({ hasText: /minute|second/ });
|
||||
await expect(intervalSelect).toBeVisible();
|
||||
|
||||
// Get current value
|
||||
const currentValue = await intervalSelect.inputValue();
|
||||
|
||||
// Change to a different value
|
||||
const newValue = currentValue === '300' ? '600' : '300';
|
||||
await intervalSelect.selectOption(newValue);
|
||||
|
||||
// Value should be updated
|
||||
await expect(intervalSelect).toHaveValue(newValue);
|
||||
});
|
||||
|
||||
test('dashboard URL setting is configurable', async ({ page }) => {
|
||||
await goToPolarisSettings(page);
|
||||
|
||||
// Find the dashboard URL input
|
||||
const urlInput = page.getByPlaceholder(/polaris-dashboard/);
|
||||
await expect(urlInput).toBeVisible();
|
||||
|
||||
// Input should have the default proxy URL or custom URL
|
||||
const currentUrl = await urlInput.inputValue();
|
||||
expect(currentUrl).toBeTruthy();
|
||||
|
||||
// Examples text should be visible
|
||||
await expect(page.getByText('Examples:')).toBeVisible();
|
||||
await expect(page.getByText(/K8s proxy:/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('connection test button is available', async ({ page }) => {
|
||||
await goToPolarisSettings(page);
|
||||
|
||||
// Find and verify test connection button
|
||||
const testButton = page.getByRole('button', { name: /test connection/i });
|
||||
await expect(testButton).toBeVisible();
|
||||
await expect(testButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('connection test works with valid URL', async ({ page }) => {
|
||||
await goToPolarisSettings(page);
|
||||
|
||||
// Click test connection
|
||||
const testButton = page.getByRole('button', { name: /test connection/i });
|
||||
await testButton.click();
|
||||
|
||||
// Wait for either success or error message
|
||||
// Note: This will succeed if Polaris is accessible, fail otherwise
|
||||
await page.waitForSelector('text=/Connected successfully|Connection failed/', {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
// Either success or failure is acceptable (depends on environment)
|
||||
const result = await page.textContent('body');
|
||||
expect(result).toMatch(/(Connected successfully|Connection failed)/);
|
||||
});
|
||||
});
|
||||
Generated
+1047
-557
File diff suppressed because it is too large
Load Diff
+53
-4
@@ -1,14 +1,63 @@
|
||||
{
|
||||
"name": "polaris-headlamp-plugin",
|
||||
"version": "0.0.1",
|
||||
"name": "headlamp-polaris",
|
||||
"version": "1.0.0",
|
||||
"description": "Headlamp plugin for Fairwinds Polaris audit results",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/privilegedescalation/headlamp-polaris-plugin.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/privilegedescalation/headlamp-polaris-plugin/issues"
|
||||
},
|
||||
"homepage": "https://github.com/privilegedescalation/headlamp-polaris-plugin#readme",
|
||||
"author": "privilegedescalation",
|
||||
"license": "Apache-2.0",
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"scripts": {
|
||||
"start": "headlamp-plugin start",
|
||||
"build": "headlamp-plugin build",
|
||||
"package": "headlamp-plugin package",
|
||||
"tsc": "tsc --noEmit"
|
||||
"tsc": "tsc --noEmit",
|
||||
"lint": "eslint --ext .ts,.tsx src/",
|
||||
"lint:fix": "eslint --ext .ts,.tsx --fix src/",
|
||||
"format": "prettier --write src/",
|
||||
"format:check": "prettier --check src/",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"e2e": "playwright test",
|
||||
"e2e:headed": "playwright test --headed"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"tar": "^7.5.11",
|
||||
"undici": "^7.24.3",
|
||||
"flatted": "^3.4.2"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@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",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@headlamp-k8s/eslint-config": "^0.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"tar": "^7.5.11",
|
||||
"typescript": "~5.6.2",
|
||||
"undici": "^7.24.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 10_000 },
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: process.env.HEADLAMP_URL || 'https://headlamp.animaniacs.farh.net',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: /auth\.setup\.ts/, timeout: 60_000 },
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: 'e2e/.auth/state.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
Generated
+11751
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"baseBranches": ["main"],
|
||||
"schedule": ["every weekend"],
|
||||
"prConcurrentLimit": 10,
|
||||
"pinDigests": true,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "npm minor and patch"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "github-actions minor and patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Executable
+200
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-e2e-headlamp.sh
|
||||
#
|
||||
# Deploys a stock Headlamp instance with the polaris plugin loaded via
|
||||
# a ConfigMap volume mount. No custom Docker images — the plugin is built
|
||||
# in CI and injected as a ConfigMap.
|
||||
#
|
||||
# E2E resources are deployed to the `privilegedescalation-dev` namespace. Nothing
|
||||
# persists beyond the test run — teardown cleans up all created resources.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Plugin built (dist/ exists with plugin-main.js + package.json)
|
||||
# - kubectl configured with cluster access
|
||||
# - RBAC applied: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml
|
||||
#
|
||||
# Environment:
|
||||
# E2E_NAMESPACE — namespace for E2E Headlamp (default: privilegedescalation-dev)
|
||||
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)
|
||||
# HEADLAMP_VERSION — Headlamp image tag (default: latest)
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DIST_DIR="$REPO_ROOT/dist"
|
||||
|
||||
E2E_NAMESPACE="${E2E_NAMESPACE:-privilegedescalation-dev}"
|
||||
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}"
|
||||
HEADLAMP_VERSION="${HEADLAMP_VERSION:-latest}"
|
||||
|
||||
if [ ! -d "$DIST_DIR" ]; then
|
||||
echo "ERROR: dist/ not found. Run 'npm run build' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Preflight: verify RBAC before touching the cluster ---
|
||||
echo "Checking RBAC permissions in namespace '${E2E_NAMESPACE}'..."
|
||||
if ! kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" --quiet 2>/dev/null; then
|
||||
echo "ERROR: Missing RBAC — cannot delete configmaps in namespace '${E2E_NAMESPACE}'." >&2
|
||||
echo " Apply RBAC first: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== E2E Headlamp Deployment ==="
|
||||
echo " Image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}"
|
||||
echo " Namespace: $E2E_NAMESPACE"
|
||||
echo " Release: $E2E_RELEASE"
|
||||
|
||||
# --- Create ConfigMap from built plugin ---
|
||||
echo ""
|
||||
echo "Creating ConfigMap with plugin files..."
|
||||
|
||||
# Delete existing ConfigMap if present (idempotent redeploy)
|
||||
kubectl delete configmap headlamp-polaris-plugin \
|
||||
-n "$E2E_NAMESPACE" --ignore-not-found
|
||||
|
||||
# Create ConfigMap from dist/ contents and package.json
|
||||
kubectl create configmap headlamp-polaris-plugin \
|
||||
-n "$E2E_NAMESPACE" \
|
||||
--from-file="$DIST_DIR" \
|
||||
--from-file=package.json="$REPO_ROOT/package.json"
|
||||
|
||||
# --- Deploy Headlamp via kubectl apply ---
|
||||
echo ""
|
||||
echo "Deploying Headlamp E2E instance..."
|
||||
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: ${E2E_RELEASE}
|
||||
namespace: ${E2E_NAMESPACE}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ${E2E_RELEASE}
|
||||
namespace: ${E2E_NAMESPACE}
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/instance: ${E2E_RELEASE}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/instance: ${E2E_RELEASE}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/instance: ${E2E_RELEASE}
|
||||
spec:
|
||||
serviceAccountName: ${E2E_RELEASE}
|
||||
automountServiceAccountToken: true
|
||||
securityContext: {}
|
||||
containers:
|
||||
- name: headlamp
|
||||
image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
privileged: false
|
||||
runAsUser: 100
|
||||
runAsGroup: 101
|
||||
args:
|
||||
- "-in-cluster"
|
||||
- "-in-cluster-context-name=main"
|
||||
- "-plugins-dir=/headlamp/plugins"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 4466
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumeMounts:
|
||||
- name: polaris-plugin
|
||||
mountPath: /headlamp/plugins/headlamp-polaris
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: polaris-plugin
|
||||
configMap:
|
||||
name: headlamp-polaris-plugin
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ${E2E_RELEASE}
|
||||
namespace: ${E2E_NAMESPACE}
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/instance: ${E2E_RELEASE}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/instance: ${E2E_RELEASE}
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
EOF
|
||||
|
||||
echo "Waiting for rollout..."
|
||||
kubectl rollout status "deployment/${E2E_RELEASE}" \
|
||||
-n "$E2E_NAMESPACE" --timeout=120s
|
||||
|
||||
# --- Generate a service URL for tests ---
|
||||
SVC_URL="http://${E2E_RELEASE}.${E2E_NAMESPACE}.svc.cluster.local"
|
||||
|
||||
# --- Wait for DNS and HTTP reachability ---
|
||||
# rollout status only confirms the pod is ready per readinessProbe.
|
||||
# Kubernetes Service DNS may still be propagating to the runner pod.
|
||||
# Poll until the service is reachable over HTTP before handing off.
|
||||
echo ""
|
||||
echo "Waiting for ${SVC_URL} to be reachable..."
|
||||
ATTEMPTS=0
|
||||
MAX_ATTEMPTS=24 # 24 × 5s = 120s max
|
||||
until curl -sf --max-time 5 "${SVC_URL}" -o /dev/null 2>/dev/null; do
|
||||
ATTEMPTS=$((ATTEMPTS + 1))
|
||||
if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
|
||||
echo "ERROR: ${SVC_URL} not reachable after $((MAX_ATTEMPTS * 5))s" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " [${ATTEMPTS}/${MAX_ATTEMPTS}] not yet reachable, retrying in 5s..."
|
||||
sleep 5
|
||||
done
|
||||
echo ""
|
||||
echo "E2E Headlamp is ready at: ${SVC_URL}"
|
||||
echo " export HEADLAMP_URL=${SVC_URL}"
|
||||
|
||||
# --- Generate a token for test auth ---
|
||||
echo ""
|
||||
echo "Creating service account token for E2E auth..."
|
||||
kubectl create serviceaccount headlamp-e2e-test \
|
||||
-n "$E2E_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
TOKEN=$(kubectl create token headlamp-e2e-test -n "$E2E_NAMESPACE" --duration=1h 2>/dev/null || echo "")
|
||||
if [ -n "$TOKEN" ]; then
|
||||
echo " export HEADLAMP_TOKEN=<generated>"
|
||||
echo ""
|
||||
echo "HEADLAMP_URL=${SVC_URL}" > "$REPO_ROOT/.env.e2e"
|
||||
echo "HEADLAMP_TOKEN=${TOKEN}" >> "$REPO_ROOT/.env.e2e"
|
||||
echo "Wrote .env.e2e with HEADLAMP_URL and HEADLAMP_TOKEN"
|
||||
else
|
||||
echo " WARNING: Could not generate token. Set HEADLAMP_TOKEN manually or use OIDC."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "E2E deployment complete."
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# teardown-e2e-headlamp.sh
|
||||
#
|
||||
# Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh.
|
||||
#
|
||||
# Environment:
|
||||
# E2E_NAMESPACE — namespace to clean up (default: privilegedescalation-dev)
|
||||
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
E2E_NAMESPACE="${E2E_NAMESPACE:-privilegedescalation-dev}"
|
||||
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}"
|
||||
|
||||
echo "=== E2E Headlamp Teardown ==="
|
||||
echo " Namespace: $E2E_NAMESPACE"
|
||||
echo " Release: $E2E_RELEASE"
|
||||
|
||||
echo "Removing Headlamp Deployment, Service, and ServiceAccount..."
|
||||
kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found
|
||||
kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found
|
||||
kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found
|
||||
|
||||
echo "Cleaning up ConfigMap..."
|
||||
kubectl delete configmap headlamp-polaris-plugin -n "$E2E_NAMESPACE" --ignore-not-found
|
||||
|
||||
echo "Cleaning up test service account..."
|
||||
kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found
|
||||
|
||||
# Clean up local env file
|
||||
rm -f "$REPO_ROOT/.env.e2e"
|
||||
|
||||
echo "Teardown complete."
|
||||
@@ -0,0 +1,50 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
}));
|
||||
|
||||
// Mock usePolarisData so PolarisDataProvider doesn't make real API calls
|
||||
vi.mock('./polaris', async importOriginal => {
|
||||
const actual = await importOriginal<typeof import('./polaris')>();
|
||||
return {
|
||||
...actual,
|
||||
usePolarisData: vi.fn(() => ({
|
||||
data: makeAuditData([makeResult()]),
|
||||
loading: false,
|
||||
error: null,
|
||||
triggerRefresh: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
import { PolarisDataProvider, usePolarisDataContext } from './PolarisDataContext';
|
||||
|
||||
describe('usePolarisDataContext', () => {
|
||||
it('throws when used outside PolarisDataProvider', () => {
|
||||
// Suppress console.error from React during expected error
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => usePolarisDataContext());
|
||||
}).toThrow('usePolarisDataContext must be used within a PolarisDataProvider');
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('returns context value when inside PolarisDataProvider', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PolarisDataProvider>{children}</PolarisDataProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => usePolarisDataContext(), { wrapper });
|
||||
|
||||
expect(result.current.data).not.toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(result.current.refresh).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { AuditData, getRefreshInterval, usePolarisData } from './polaris';
|
||||
|
||||
/**
|
||||
* Shared Polaris data context value provided to all plugin components.
|
||||
*/
|
||||
interface PolarisDataContextValue {
|
||||
/** Polaris audit data (null if not loaded or error) */
|
||||
data: AuditData | null;
|
||||
/** Whether data is currently being loaded */
|
||||
loading: boolean;
|
||||
/** Error message (null if no error) */
|
||||
error: string | null;
|
||||
/** Function to manually trigger a data refresh */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const PolarisDataContext = React.createContext<PolarisDataContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* React Context provider for shared Polaris data across all plugin components.
|
||||
*
|
||||
* Fetches data once and shares it with all consuming components to avoid
|
||||
* duplicate API requests. Auto-refreshes based on user's configured interval.
|
||||
*
|
||||
* @param props - Component props
|
||||
* @param props.children - Child components that will consume the context
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* <PolarisDataProvider>
|
||||
* <DashboardView />
|
||||
* <NamespacesListView />
|
||||
* </PolarisDataProvider>
|
||||
* ```
|
||||
*/
|
||||
export function PolarisDataProvider(props: { children: React.ReactNode }) {
|
||||
// Re-read interval on every render to pick up changes from settings
|
||||
const [refreshInterval, setRefreshInterval] = React.useState(getRefreshInterval());
|
||||
|
||||
// Poll for interval changes (localStorage changes from settings)
|
||||
React.useEffect(() => {
|
||||
const intervalId = window.setInterval(() => {
|
||||
const newInterval = getRefreshInterval();
|
||||
setRefreshInterval(prev => (prev !== newInterval ? newInterval : prev));
|
||||
}, 1000); // Check every second
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const state = usePolarisData(refreshInterval);
|
||||
|
||||
// Rename triggerRefresh to refresh for consistency
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
data: state.data,
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
refresh: state.triggerRefresh,
|
||||
}),
|
||||
[state]
|
||||
);
|
||||
|
||||
return <PolarisDataContext.Provider value={value}>{props.children}</PolarisDataContext.Provider>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to access the shared Polaris data context.
|
||||
*
|
||||
* Must be used within a PolarisDataProvider. Throws an error if used outside.
|
||||
*
|
||||
* @returns Polaris data context value (data, loading, error, refresh)
|
||||
* @throws Error if used outside PolarisDataProvider
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function MyComponent() {
|
||||
* const { data, loading, error, refresh } = usePolarisDataContext();
|
||||
* if (loading) return <Loader />;
|
||||
* if (error) return <Error message={error} />;
|
||||
* return <div>{data.DisplayName}</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function usePolarisDataContext(): PolarisDataContextValue {
|
||||
const ctx = React.useContext(PolarisDataContext);
|
||||
if (ctx === null) {
|
||||
throw new Error('usePolarisDataContext must be used within a PolarisDataProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
CHECK_MAPPING,
|
||||
getCheckCategory,
|
||||
getCheckDescription,
|
||||
getCheckName,
|
||||
getSeverityStatus,
|
||||
} from './checkMapping';
|
||||
|
||||
describe('checkMapping', () => {
|
||||
describe('getCheckName', () => {
|
||||
it('returns human-readable name for known check IDs', () => {
|
||||
expect(getCheckName('hostIPCSet')).toBe('Host IPC');
|
||||
expect(getCheckName('cpuRequestsMissing')).toBe('CPU Requests');
|
||||
expect(getCheckName('readinessProbeMissing')).toBe('Readiness Probe');
|
||||
});
|
||||
|
||||
it('returns the raw check ID for unknown checks', () => {
|
||||
expect(getCheckName('unknownCheck')).toBe('unknownCheck');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCheckDescription', () => {
|
||||
it('returns description for known checks', () => {
|
||||
expect(getCheckDescription('hostIPCSet')).toBe('Host IPC should not be configured');
|
||||
});
|
||||
|
||||
it('returns "Unknown check" for unknown checks', () => {
|
||||
expect(getCheckDescription('unknownCheck')).toBe('Unknown check');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCheckCategory', () => {
|
||||
it('returns correct category for each type', () => {
|
||||
expect(getCheckCategory('hostIPCSet')).toBe('Security');
|
||||
expect(getCheckCategory('cpuRequestsMissing')).toBe('Efficiency');
|
||||
expect(getCheckCategory('readinessProbeMissing')).toBe('Reliability');
|
||||
});
|
||||
|
||||
it('defaults to Security for unknown checks', () => {
|
||||
expect(getCheckCategory('unknownCheck')).toBe('Security');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSeverityStatus', () => {
|
||||
it('maps danger to error', () => {
|
||||
expect(getSeverityStatus('danger')).toBe('error');
|
||||
});
|
||||
|
||||
it('maps warning to warning', () => {
|
||||
expect(getSeverityStatus('warning')).toBe('warning');
|
||||
});
|
||||
|
||||
it('defaults to success for other values', () => {
|
||||
expect(getSeverityStatus('ignore')).toBe('success');
|
||||
expect(getSeverityStatus('unknown')).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CHECK_MAPPING', () => {
|
||||
it('has entries for all expected categories', () => {
|
||||
const categories = new Set(Object.values(CHECK_MAPPING).map(c => c.category));
|
||||
expect(categories).toContain('Security');
|
||||
expect(categories).toContain('Efficiency');
|
||||
expect(categories).toContain('Reliability');
|
||||
});
|
||||
|
||||
it('all entries have required fields', () => {
|
||||
for (const [id, info] of Object.entries(CHECK_MAPPING)) {
|
||||
expect(info.name, `${id} missing name`).toBeTruthy();
|
||||
expect(info.description, `${id} missing description`).toBeTruthy();
|
||||
expect(['Security', 'Efficiency', 'Reliability']).toContain(info.category);
|
||||
expect(['danger', 'warning', 'ignore']).toContain(info.defaultSeverity);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Mapping of Polaris check IDs to human-readable names and descriptions
|
||||
* Sourced from Polaris documentation
|
||||
*/
|
||||
|
||||
export interface CheckInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'Security' | 'Efficiency' | 'Reliability';
|
||||
defaultSeverity: 'danger' | 'warning' | 'ignore';
|
||||
}
|
||||
|
||||
export const CHECK_MAPPING: Record<string, CheckInfo> = {
|
||||
// Security checks
|
||||
hostIPCSet: {
|
||||
name: 'Host IPC',
|
||||
description: 'Host IPC should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
hostPIDSet: {
|
||||
name: 'Host PID',
|
||||
description: 'Host PID should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
hostNetworkSet: {
|
||||
name: 'Host Network',
|
||||
description: 'Host network should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
hostPortSet: {
|
||||
name: 'Host Port',
|
||||
description: 'Host port should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
runAsRootAllowed: {
|
||||
name: 'Run as Root',
|
||||
description: 'Should not be allowed to run as root',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
runAsPrivileged: {
|
||||
name: 'Privileged Container',
|
||||
description: 'Should not run as privileged',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
notReadOnlyRootFilesystem: {
|
||||
name: 'Read-Only Root Filesystem',
|
||||
description: 'Filesystem should be read-only',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
privilegeEscalationAllowed: {
|
||||
name: 'Privilege Escalation',
|
||||
description: 'Privilege escalation should not be allowed',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
dangerousCapabilities: {
|
||||
name: 'Dangerous Capabilities',
|
||||
description: 'Dangerous capabilities should not be allowed',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
insecureCapabilities: {
|
||||
name: 'Insecure Capabilities',
|
||||
description: 'Insecure capabilities should not be allowed',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
sensitiveContainerEnvVar: {
|
||||
name: 'Sensitive Environment Variables',
|
||||
description: 'Sensitive env vars detected',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
sensitiveConfigmapContent: {
|
||||
name: 'Sensitive ConfigMap',
|
||||
description: 'Sensitive ConfigMap content detected',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
automountServiceAccountToken: {
|
||||
name: 'Service Account Token Auto-mount',
|
||||
description: 'Service account token auto-mount',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
tlsSettingsMissing: {
|
||||
name: 'TLS Settings',
|
||||
description: 'TLS settings missing',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
missingNetworkPolicy: {
|
||||
name: 'Network Policy',
|
||||
description: 'Missing NetworkPolicy',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
|
||||
// Reliability checks
|
||||
tagNotSpecified: {
|
||||
name: 'Image Tag',
|
||||
description: 'Image tag should be specified',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
pullPolicyNotAlways: {
|
||||
name: 'Pull Policy',
|
||||
description: 'Pull policy should be Always',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
readinessProbeMissing: {
|
||||
name: 'Readiness Probe',
|
||||
description: 'Readiness probe should be configured',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
livenessProbeMissing: {
|
||||
name: 'Liveness Probe',
|
||||
description: 'Liveness probe should be configured',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
deploymentMissingReplicas: {
|
||||
name: 'Deployment Replicas',
|
||||
description: 'Deployment should have multiple replicas',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
priorityClassNotSet: {
|
||||
name: 'Priority Class',
|
||||
description: 'Priority class should be set',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
metadataAndNameMismatched: {
|
||||
name: 'Metadata Mismatch',
|
||||
description: 'Metadata and name should match',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
missingPodDisruptionBudget: {
|
||||
name: 'Pod Disruption Budget',
|
||||
description: 'PodDisruptionBudget should exist',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
pdbDisruptionsIsZero: {
|
||||
name: 'PDB Disruptions',
|
||||
description: 'PDB maxUnavailable should not be zero',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
|
||||
// Efficiency checks
|
||||
cpuRequestsMissing: {
|
||||
name: 'CPU Requests',
|
||||
description: 'CPU requests should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
cpuLimitsMissing: {
|
||||
name: 'CPU Limits',
|
||||
description: 'CPU limits should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
memoryRequestsMissing: {
|
||||
name: 'Memory Requests',
|
||||
description: 'Memory requests should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
memoryLimitsMissing: {
|
||||
name: 'Memory Limits',
|
||||
description: 'Memory limits should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get human-readable name for a check ID
|
||||
*/
|
||||
export function getCheckName(checkId: string): string {
|
||||
return CHECK_MAPPING[checkId]?.name || checkId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get check description
|
||||
*/
|
||||
export function getCheckDescription(checkId: string): string {
|
||||
return CHECK_MAPPING[checkId]?.description || 'Unknown check';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get check category
|
||||
*/
|
||||
export function getCheckCategory(checkId: string): 'Security' | 'Efficiency' | 'Reliability' {
|
||||
return CHECK_MAPPING[checkId]?.category || 'Security';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status for StatusLabel component
|
||||
*/
|
||||
export function getSeverityStatus(severity: string): 'error' | 'warning' | 'success' {
|
||||
switch (severity) {
|
||||
case 'danger':
|
||||
return 'error';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
}));
|
||||
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
computeScore,
|
||||
countResults,
|
||||
countResultsForItems,
|
||||
filterResultsByNamespace,
|
||||
getNamespaces,
|
||||
getRefreshInterval,
|
||||
Result,
|
||||
ResultCounts,
|
||||
setRefreshInterval,
|
||||
usePolarisData,
|
||||
} from './polaris';
|
||||
|
||||
// --- computeScore ---
|
||||
|
||||
describe('computeScore', () => {
|
||||
it('returns 0 when total is 0', () => {
|
||||
const counts: ResultCounts = { total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 };
|
||||
expect(computeScore(counts)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 100 when all checks pass', () => {
|
||||
const counts: ResultCounts = { total: 10, pass: 10, warning: 0, danger: 0, skipped: 0 };
|
||||
expect(computeScore(counts)).toBe(100);
|
||||
});
|
||||
|
||||
it('rounds to nearest integer', () => {
|
||||
const counts: ResultCounts = { total: 3, pass: 1, warning: 1, danger: 1, skipped: 0 };
|
||||
expect(computeScore(counts)).toBe(33);
|
||||
});
|
||||
|
||||
it('includes skipped in total denominator', () => {
|
||||
const counts: ResultCounts = { total: 10, pass: 5, warning: 2, danger: 1, skipped: 2 };
|
||||
expect(computeScore(counts)).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
// --- countResults / countResultsForItems ---
|
||||
|
||||
describe('countResults', () => {
|
||||
it('returns zero counts for empty results', () => {
|
||||
const data = makeAuditData([]);
|
||||
const counts = countResults(data);
|
||||
expect(counts).toEqual({ total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 });
|
||||
});
|
||||
|
||||
it('counts top-level result set entries', () => {
|
||||
const result = makeResult({
|
||||
Results: {
|
||||
check1: {
|
||||
ID: 'check1',
|
||||
Message: 'ok',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'Security',
|
||||
},
|
||||
check2: {
|
||||
ID: 'check2',
|
||||
Message: 'bad',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
});
|
||||
const counts = countResults(makeAuditData([result]));
|
||||
expect(counts.total).toBe(2);
|
||||
expect(counts.pass).toBe(1);
|
||||
expect(counts.danger).toBe(1);
|
||||
expect(counts.warning).toBe(0);
|
||||
expect(counts.skipped).toBe(0);
|
||||
});
|
||||
|
||||
it('counts skipped (severity=ignore, success=false) entries', () => {
|
||||
const result = makeResult({
|
||||
Results: {
|
||||
skipped1: {
|
||||
ID: 'skipped1',
|
||||
Message: 'skipped',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'ignore',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
});
|
||||
const counts = countResults(makeAuditData([result]));
|
||||
expect(counts.total).toBe(1);
|
||||
expect(counts.skipped).toBe(1);
|
||||
expect(counts.pass).toBe(0);
|
||||
});
|
||||
|
||||
it('counts PodResult and ContainerResults', () => {
|
||||
const result = makeResult({
|
||||
Results: {
|
||||
top: {
|
||||
ID: 'top',
|
||||
Message: 'ok',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'Reliability',
|
||||
},
|
||||
},
|
||||
PodResult: {
|
||||
Name: 'pod-1',
|
||||
Results: {
|
||||
podCheck: {
|
||||
ID: 'podCheck',
|
||||
Message: 'warn',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Reliability',
|
||||
},
|
||||
},
|
||||
ContainerResults: [
|
||||
{
|
||||
Name: 'container-1',
|
||||
Results: {
|
||||
containerCheck: {
|
||||
ID: 'containerCheck',
|
||||
Message: 'danger',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const counts = countResults(makeAuditData([result]));
|
||||
expect(counts.total).toBe(3);
|
||||
expect(counts.pass).toBe(1);
|
||||
expect(counts.warning).toBe(1);
|
||||
expect(counts.danger).toBe(1);
|
||||
});
|
||||
|
||||
it('aggregates across multiple results', () => {
|
||||
const r1 = makeResult({
|
||||
Name: 'deploy-a',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
});
|
||||
const r2 = makeResult({
|
||||
Name: 'deploy-b',
|
||||
Results: {
|
||||
c2: {
|
||||
ID: 'c2',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
});
|
||||
const counts = countResults(makeAuditData([r1, r2]));
|
||||
expect(counts.total).toBe(2);
|
||||
expect(counts.pass).toBe(1);
|
||||
expect(counts.warning).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('countResultsForItems', () => {
|
||||
it('works on a subset of results', () => {
|
||||
const results: Result[] = [
|
||||
makeResult({
|
||||
Results: {
|
||||
a: {
|
||||
ID: 'a',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
const counts = countResultsForItems(results);
|
||||
expect(counts.danger).toBe(1);
|
||||
expect(counts.total).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// --- getNamespaces ---
|
||||
|
||||
describe('getNamespaces', () => {
|
||||
it('returns empty array for no results', () => {
|
||||
expect(getNamespaces(makeAuditData([]))).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns sorted unique namespaces', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({ Namespace: 'beta' }),
|
||||
makeResult({ Namespace: 'alpha' }),
|
||||
makeResult({ Namespace: 'beta' }),
|
||||
makeResult({ Namespace: 'gamma' }),
|
||||
]);
|
||||
expect(getNamespaces(data)).toEqual(['alpha', 'beta', 'gamma']);
|
||||
});
|
||||
|
||||
it('excludes results with empty namespace (cluster-scoped resources)', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({ Namespace: '' }),
|
||||
makeResult({ Namespace: 'alpha' }),
|
||||
makeResult({ Namespace: '' }),
|
||||
]);
|
||||
expect(getNamespaces(data)).toEqual(['alpha']);
|
||||
});
|
||||
});
|
||||
|
||||
// --- filterResultsByNamespace ---
|
||||
|
||||
describe('filterResultsByNamespace', () => {
|
||||
it('returns only results matching the namespace', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({ Name: 'a', Namespace: 'ns1' }),
|
||||
makeResult({ Name: 'b', Namespace: 'ns2' }),
|
||||
makeResult({ Name: 'c', Namespace: 'ns1' }),
|
||||
]);
|
||||
const filtered = filterResultsByNamespace(data, 'ns1');
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered.map(r => r.Name)).toEqual(['a', 'c']);
|
||||
});
|
||||
|
||||
it('returns empty array for non-existent namespace', () => {
|
||||
const data = makeAuditData([makeResult({ Namespace: 'ns1' })]);
|
||||
expect(filterResultsByNamespace(data, 'ns-missing')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// --- getRefreshInterval / setRefreshInterval ---
|
||||
|
||||
describe('getRefreshInterval', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.removeItem('polaris-plugin-refresh-interval');
|
||||
});
|
||||
|
||||
it('returns default (300) when nothing stored', () => {
|
||||
expect(getRefreshInterval()).toBe(300);
|
||||
});
|
||||
|
||||
it('returns stored value when valid', () => {
|
||||
localStorage.setItem('polaris-plugin-refresh-interval', '60');
|
||||
expect(getRefreshInterval()).toBe(60);
|
||||
});
|
||||
|
||||
it('returns default for non-numeric stored value', () => {
|
||||
localStorage.setItem('polaris-plugin-refresh-interval', 'abc');
|
||||
expect(getRefreshInterval()).toBe(300);
|
||||
});
|
||||
|
||||
it('returns default for zero stored value', () => {
|
||||
localStorage.setItem('polaris-plugin-refresh-interval', '0');
|
||||
expect(getRefreshInterval()).toBe(300);
|
||||
});
|
||||
|
||||
it('returns default for negative stored value', () => {
|
||||
localStorage.setItem('polaris-plugin-refresh-interval', '-10');
|
||||
expect(getRefreshInterval()).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRefreshInterval', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.removeItem('polaris-plugin-refresh-interval');
|
||||
});
|
||||
|
||||
it('stores value that getRefreshInterval reads back', () => {
|
||||
setRefreshInterval(1800);
|
||||
expect(getRefreshInterval()).toBe(1800);
|
||||
});
|
||||
});
|
||||
|
||||
// --- usePolarisData ---
|
||||
|
||||
describe('usePolarisData', () => {
|
||||
const mockRequest = ApiProxy.request as ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest.mockReset();
|
||||
});
|
||||
|
||||
it('returns data on successful fetch', async () => {
|
||||
const auditData = makeAuditData([makeResult()]);
|
||||
mockRequest.mockResolvedValue(auditData);
|
||||
|
||||
const { result } = renderHook(() => usePolarisData(300));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(auditData);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('returns RBAC error on 403', async () => {
|
||||
mockRequest.mockRejectedValue({ status: 403 });
|
||||
|
||||
const { result } = renderHook(() => usePolarisData(300));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.error).toContain('403');
|
||||
expect(result.current.error).toContain('RBAC');
|
||||
});
|
||||
|
||||
it('returns not-installed error on 404', async () => {
|
||||
mockRequest.mockRejectedValue({ status: 404 });
|
||||
|
||||
const { result } = renderHook(() => usePolarisData(300));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain('not reachable');
|
||||
});
|
||||
|
||||
it('returns not-installed error on 503', async () => {
|
||||
mockRequest.mockRejectedValue({ status: 503 });
|
||||
|
||||
const { result } = renderHook(() => usePolarisData(300));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain('not reachable');
|
||||
});
|
||||
|
||||
it('returns generic error for other failures', async () => {
|
||||
mockRequest.mockRejectedValue(new Error('network down'));
|
||||
|
||||
const { result } = renderHook(() => usePolarisData(300));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain('Failed to fetch');
|
||||
expect(result.current.error).toContain('network down');
|
||||
});
|
||||
|
||||
it('does not update state after unmount', async () => {
|
||||
let resolveFetch: (value: unknown) => void = () => {};
|
||||
mockRequest.mockReturnValue(
|
||||
new Promise(resolve => {
|
||||
resolveFetch = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
const { result, unmount } = renderHook(() => usePolarisData(300));
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
unmount();
|
||||
|
||||
// Resolve after unmount — should not throw or update state
|
||||
await act(async () => {
|
||||
resolveFetch(makeAuditData([]));
|
||||
});
|
||||
});
|
||||
});
|
||||
+314
-86
@@ -1,67 +1,128 @@
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
|
||||
// --- Polaris AuditData schema (matches pkg/validator/output.go) ---
|
||||
|
||||
/**
|
||||
* Severity level for a Polaris check result.
|
||||
*/
|
||||
type Severity = 'ignore' | 'warning' | 'danger';
|
||||
|
||||
/**
|
||||
* A single Polaris check result message.
|
||||
*/
|
||||
interface ResultMessage {
|
||||
/** Unique identifier for the check */
|
||||
ID: string;
|
||||
/** Human-readable message describing the check */
|
||||
Message: string;
|
||||
/** Additional details or context for the check */
|
||||
Details: string[];
|
||||
/** Whether the check passed (true) or failed (false) */
|
||||
Success: boolean;
|
||||
/** Severity level of the check */
|
||||
Severity: Severity;
|
||||
/** Category/group this check belongs to (e.g., "Security", "Efficiency") */
|
||||
Category: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of check results keyed by check ID.
|
||||
*/
|
||||
type ResultSet = Record<string, ResultMessage>;
|
||||
|
||||
/**
|
||||
* Polaris audit results for a single container within a pod.
|
||||
*/
|
||||
interface ContainerResult {
|
||||
/** Container name */
|
||||
Name: string;
|
||||
/** Check results for this container */
|
||||
Results: ResultSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polaris audit results for a pod and its containers.
|
||||
*/
|
||||
interface PodResult {
|
||||
/** Pod name */
|
||||
Name: string;
|
||||
/** Pod-level check results */
|
||||
Results: ResultSet;
|
||||
/** Per-container check results */
|
||||
ContainerResults: ContainerResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Polaris audit result for a single Kubernetes resource.
|
||||
*/
|
||||
export interface Result {
|
||||
/** Resource name */
|
||||
Name: string;
|
||||
/** Kubernetes namespace */
|
||||
Namespace: string;
|
||||
/** Kubernetes resource kind (e.g., "Deployment", "StatefulSet") */
|
||||
Kind: string;
|
||||
/** Resource-level check results */
|
||||
Results: ResultSet;
|
||||
/** Pod-level results (for workload controllers) */
|
||||
PodResult?: PodResult;
|
||||
/** ISO 8601 timestamp when resource was created */
|
||||
CreatedTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cluster metadata from Polaris audit.
|
||||
*/
|
||||
interface ClusterInfo {
|
||||
/** Kubernetes version */
|
||||
Version: string;
|
||||
/** Number of nodes in cluster */
|
||||
Nodes: number;
|
||||
/** Number of pods in cluster */
|
||||
Pods: number;
|
||||
/** Number of namespaces in cluster */
|
||||
Namespaces: number;
|
||||
/** Number of controllers (workloads) in cluster */
|
||||
Controllers: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete Polaris audit data structure returned by the dashboard API.
|
||||
*/
|
||||
export interface AuditData {
|
||||
/** Polaris output schema version */
|
||||
PolarisOutputVersion: string;
|
||||
/** ISO 8601 timestamp of when audit was performed */
|
||||
AuditTime: string;
|
||||
/** Source type (e.g., "Cluster") */
|
||||
SourceType: string;
|
||||
/** Source identifier */
|
||||
SourceName: string;
|
||||
/** Human-readable cluster name */
|
||||
DisplayName: string;
|
||||
/** Cluster statistics */
|
||||
ClusterInfo: ClusterInfo;
|
||||
/** All audit results across the cluster */
|
||||
Results: Result[];
|
||||
Score: number;
|
||||
}
|
||||
|
||||
// --- Result counting ---
|
||||
|
||||
/**
|
||||
* Aggregated counts of check results by status.
|
||||
*/
|
||||
export interface ResultCounts {
|
||||
/** Total number of checks performed */
|
||||
total: number;
|
||||
/** Number of checks that passed */
|
||||
pass: number;
|
||||
/** Number of checks with warning severity that failed */
|
||||
warning: number;
|
||||
/** Number of checks with danger severity that failed */
|
||||
danger: number;
|
||||
/** Number of checks with severity "ignore" that failed (skipped by Polaris config) */
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
function countResultSet(rs: ResultSet, counts: ResultCounts): void {
|
||||
@@ -70,6 +131,8 @@ function countResultSet(rs: ResultSet, counts: ResultCounts): void {
|
||||
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') {
|
||||
@@ -78,9 +141,9 @@ function countResultSet(rs: ResultSet, counts: ResultCounts): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function countResults(data: AuditData): ResultCounts {
|
||||
const counts: ResultCounts = { total: 0, pass: 0, warning: 0, danger: 0 };
|
||||
for (const result of data.Results) {
|
||||
function countResultItems(results: Result[]): ResultCounts {
|
||||
const counts: ResultCounts = { total: 0, pass: 0, warning: 0, danger: 0, skipped: 0 };
|
||||
for (const result of results) {
|
||||
countResultSet(result.Results, counts);
|
||||
if (result.PodResult) {
|
||||
countResultSet(result.PodResult.Results, counts);
|
||||
@@ -92,13 +155,79 @@ export function countResults(data: AuditData): ResultCounts {
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts check results by status across all resources in the audit data.
|
||||
*
|
||||
* @param data - Complete Polaris audit data
|
||||
* @returns Aggregated counts (total, pass, warning, danger, skipped)
|
||||
*/
|
||||
export function countResults(data: AuditData): ResultCounts {
|
||||
return countResultItems(data.Results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts check results for a specific set of resources.
|
||||
*
|
||||
* @param results - Array of Polaris audit results
|
||||
* @returns Aggregated counts (total, pass, warning, danger, skipped)
|
||||
*/
|
||||
export function countResultsForItems(results: Result[]): ResultCounts {
|
||||
return countResultItems(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts unique namespaces from audit data, sorted alphabetically.
|
||||
*
|
||||
* @param data - Complete Polaris audit data
|
||||
* @returns Sorted array of namespace names
|
||||
*/
|
||||
export 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters audit results to only those in a specific namespace.
|
||||
*
|
||||
* @param data - Complete Polaris audit data
|
||||
* @param namespace - Target namespace name
|
||||
* @returns Array of results matching the namespace
|
||||
*/
|
||||
export function filterResultsByNamespace(data: AuditData, namespace: string): Result[] {
|
||||
return data.Results.filter(r => r.Namespace === namespace);
|
||||
}
|
||||
|
||||
// --- Settings ---
|
||||
|
||||
const STORAGE_KEY = 'polaris-plugin-refresh-interval';
|
||||
/**
|
||||
* Predefined refresh interval options for the plugin settings UI.
|
||||
*/
|
||||
export const INTERVAL_OPTIONS = [
|
||||
{ label: '1 minute', value: 60 },
|
||||
{ label: '5 minutes', value: 300 },
|
||||
{ label: '10 minutes', value: 600 },
|
||||
{ label: '30 minutes', value: 1800 },
|
||||
];
|
||||
|
||||
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/http:polaris-dashboard:80/proxy/';
|
||||
|
||||
/**
|
||||
* Retrieves the configured refresh interval from localStorage.
|
||||
*
|
||||
* @returns Refresh interval in seconds (default: 300)
|
||||
*/
|
||||
export function getRefreshInterval(): number {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const stored = localStorage.getItem(REFRESH_STORAGE_KEY);
|
||||
if (stored !== null) {
|
||||
const parsed = parseInt(stored, 10);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
@@ -108,100 +237,199 @@ export function getRefreshInterval(): number {
|
||||
return DEFAULT_INTERVAL_SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the refresh interval to localStorage.
|
||||
*
|
||||
* @param seconds - Refresh interval in seconds
|
||||
*/
|
||||
export function setRefreshInterval(seconds: number): void {
|
||||
localStorage.setItem(STORAGE_KEY, String(seconds));
|
||||
localStorage.setItem(REFRESH_STORAGE_KEY, String(seconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configured Polaris dashboard URL from localStorage.
|
||||
*
|
||||
* @returns Dashboard URL (default: Kubernetes service proxy path)
|
||||
*/
|
||||
export function getDashboardUrl(): string {
|
||||
const stored = localStorage.getItem(URL_STORAGE_KEY);
|
||||
if (stored !== null && stored.trim() !== '') {
|
||||
return stored.trim();
|
||||
}
|
||||
return DEFAULT_DASHBOARD_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the Polaris dashboard URL to localStorage.
|
||||
*
|
||||
* @param url - Dashboard URL (service proxy path or full URL)
|
||||
*/
|
||||
export function setDashboardUrl(url: string): void {
|
||||
localStorage.setItem(URL_STORAGE_KEY, url.trim());
|
||||
}
|
||||
|
||||
// --- Polaris dashboard proxy URL ---
|
||||
|
||||
/**
|
||||
* Returns the base URL for the Polaris dashboard proxy.
|
||||
*
|
||||
* @returns Dashboard base URL (without /results.json)
|
||||
*/
|
||||
export function getPolarisProxyUrl(): string {
|
||||
return getDashboardUrl();
|
||||
}
|
||||
|
||||
// --- Score computation ---
|
||||
|
||||
/**
|
||||
* Computes the Polaris score as a percentage (0-100).
|
||||
*
|
||||
* Formula: (pass / total) * 100, rounded to nearest integer.
|
||||
*
|
||||
* @param counts - Result counts to compute score from
|
||||
* @returns Score percentage (0 if total is 0)
|
||||
*/
|
||||
export function computeScore(counts: ResultCounts): number {
|
||||
if (counts.total === 0) return 0;
|
||||
return Math.round((counts.pass / counts.total) * 100);
|
||||
}
|
||||
|
||||
// --- Data fetching hook ---
|
||||
|
||||
interface PolarisDataState {
|
||||
data: AuditData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
/**
|
||||
* Constructs the full API path for fetching Polaris results.
|
||||
*
|
||||
* @returns Full path to results.json endpoint
|
||||
*/
|
||||
export function getPolarisApiPath(): string {
|
||||
const baseUrl = getDashboardUrl();
|
||||
return baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is a full URL (http:// or https://) vs. a relative path.
|
||||
*
|
||||
* @param url - URL to check
|
||||
* @returns true if full URL, false if relative path
|
||||
*/
|
||||
export function isFullUrl(url: string): boolean {
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
}
|
||||
|
||||
/**
|
||||
* State returned by the usePolarisData hook.
|
||||
*/
|
||||
interface PolarisDataState {
|
||||
/** Polaris audit data (null if not yet loaded or error occurred) */
|
||||
data: AuditData | null;
|
||||
/** Whether data is currently being loaded */
|
||||
loading: boolean;
|
||||
/** Error message (null if no error) */
|
||||
error: string | null;
|
||||
/** Function to manually trigger a data refresh */
|
||||
triggerRefresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for fetching and auto-refreshing Polaris audit data.
|
||||
*
|
||||
* Handles both Kubernetes service proxy paths and full URLs.
|
||||
* Automatically refreshes data at the specified interval.
|
||||
*
|
||||
* @param refreshIntervalSeconds - How often to refresh data (0 to disable auto-refresh)
|
||||
* @returns Polaris data state (data, loading, error, triggerRefresh)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { data, loading, error, triggerRefresh } = usePolarisData(300);
|
||||
* if (loading) return <Loader />;
|
||||
* if (error) return <Error message={error} />;
|
||||
* return <Dashboard data={data} onRefresh={triggerRefresh} />;
|
||||
* ```
|
||||
*/
|
||||
export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState {
|
||||
const [configMap, fetchError] = K8s.ResourceClasses.ConfigMap.useGet(
|
||||
'polaris-dashboard',
|
||||
'polaris'
|
||||
);
|
||||
const [cachedData, setCachedData] = React.useState<AuditData | null>(null);
|
||||
const [parseError, setParseError] = React.useState<string | null>(null);
|
||||
const [lastFetchTime, setLastFetchTime] = React.useState<number>(0);
|
||||
const [, setTick] = React.useState(0);
|
||||
const [data, setData] = React.useState<AuditData | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [tick, setTick] = React.useState(0);
|
||||
|
||||
// Parse ConfigMap data when it arrives
|
||||
React.useEffect(() => {
|
||||
if (!configMap) {
|
||||
return;
|
||||
}
|
||||
const dataMap = configMap.data as Record<string, string> | undefined;
|
||||
const raw = dataMap?.['dashboard.json'];
|
||||
if (!raw) {
|
||||
setParseError('ConfigMap exists but dashboard.json key is missing.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed: AuditData = JSON.parse(raw);
|
||||
setCachedData(parsed);
|
||||
setParseError(null);
|
||||
setLastFetchTime(Date.now());
|
||||
} catch {
|
||||
setParseError('Failed to parse dashboard.json: malformed JSON.');
|
||||
}
|
||||
}, [configMap]);
|
||||
const triggerRefresh = React.useCallback(() => {
|
||||
setTick(t => t + 1);
|
||||
}, []);
|
||||
|
||||
// Periodic refresh via re-render trigger
|
||||
React.useEffect(() => {
|
||||
if (refreshIntervalSeconds <= 0) {
|
||||
return;
|
||||
let cancelled = false;
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const apiPath = getPolarisApiPath();
|
||||
let result: AuditData;
|
||||
|
||||
if (isFullUrl(apiPath)) {
|
||||
// Direct fetch for full URLs
|
||||
const response = await fetch(apiPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
} else {
|
||||
// Kubernetes proxy for relative URLs
|
||||
result = await ApiProxy.request(apiPath, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setData(result);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (cancelled) return;
|
||||
const apiPath = getPolarisApiPath();
|
||||
const status = (err as { status?: number }).status;
|
||||
|
||||
if (isFullUrl(apiPath)) {
|
||||
// Full URL errors
|
||||
if (status === 403) {
|
||||
setError('Access denied (403). Check authentication and CORS configuration.');
|
||||
} else if (status === 404) {
|
||||
setError('Polaris dashboard not found (404). Verify the URL is correct.');
|
||||
} else {
|
||||
setError(`Failed to fetch from ${apiPath}: ${String(err)}`);
|
||||
}
|
||||
} else {
|
||||
// Kubernetes proxy errors
|
||||
if (status === 403) {
|
||||
setError(
|
||||
'Access denied (403). Check that your RBAC permissions allow proxying to the Polaris service.'
|
||||
);
|
||||
} else if (status === 404 || status === 503) {
|
||||
setError(
|
||||
'Polaris dashboard not reachable. Ensure Polaris is installed in the configured namespace.'
|
||||
);
|
||||
} else {
|
||||
setError(`Failed to fetch Polaris data: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [tick]);
|
||||
|
||||
// Periodic refresh
|
||||
React.useEffect(() => {
|
||||
if (refreshIntervalSeconds <= 0) return;
|
||||
const intervalId = window.setInterval(() => {
|
||||
setTick((t) => t + 1);
|
||||
setTick(t => t + 1);
|
||||
}, refreshIntervalSeconds * 1000);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [refreshIntervalSeconds]);
|
||||
|
||||
// Determine error state
|
||||
if (fetchError) {
|
||||
const status = (fetchError as { status?: number }).status;
|
||||
if (status === 403) {
|
||||
return {
|
||||
data: cachedData,
|
||||
loading: false,
|
||||
error:
|
||||
'Access denied (403). Check that your RBAC permissions allow reading ConfigMaps in the polaris namespace.',
|
||||
};
|
||||
}
|
||||
if (status === 404) {
|
||||
return {
|
||||
data: cachedData,
|
||||
loading: false,
|
||||
error:
|
||||
'Polaris dashboard ConfigMap not found (404). Ensure Polaris is installed in the polaris namespace.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: cachedData,
|
||||
loading: false,
|
||||
error: `Failed to fetch Polaris data: ${String(fetchError)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (parseError) {
|
||||
return { data: cachedData, loading: false, error: parseError };
|
||||
}
|
||||
|
||||
const isLoading = !configMap && !fetchError;
|
||||
|
||||
// Return cached data while loading if we have it
|
||||
if (isLoading && cachedData && lastFetchTime > 0) {
|
||||
return { data: cachedData, loading: false, error: null };
|
||||
}
|
||||
|
||||
return {
|
||||
data: cachedData,
|
||||
loading: isLoading,
|
||||
error: null,
|
||||
};
|
||||
return { data, loading, error, triggerRefresh };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
import { getTopIssues } from './topIssues';
|
||||
|
||||
describe('getTopIssues', () => {
|
||||
it('returns empty array when no results', () => {
|
||||
const data = makeAuditData([]);
|
||||
expect(getTopIssues(data)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when all checks pass', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(getTopIssues(data)).toEqual([]);
|
||||
});
|
||||
|
||||
it('aggregates failing checks from controller-level results', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: 'missing',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const issues = getTopIssues(data);
|
||||
expect(issues).toHaveLength(1);
|
||||
expect(issues[0].checkId).toBe('cpuRequestsMissing');
|
||||
expect(issues[0].checkName).toBe('CPU Requests');
|
||||
expect(issues[0].severity).toBe('warning');
|
||||
expect(issues[0].count).toBe(1);
|
||||
});
|
||||
|
||||
it('aggregates failing checks from pod and container results', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {},
|
||||
PodResult: {
|
||||
Name: 'pod-1',
|
||||
Results: {
|
||||
hostIPCSet: {
|
||||
ID: 'hostIPCSet',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
ContainerResults: [
|
||||
{
|
||||
Name: 'container-1',
|
||||
Results: {
|
||||
cpuLimitsMissing: {
|
||||
ID: 'cpuLimitsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const issues = getTopIssues(data);
|
||||
expect(issues).toHaveLength(2);
|
||||
// Danger first
|
||||
expect(issues[0].checkId).toBe('hostIPCSet');
|
||||
expect(issues[0].severity).toBe('danger');
|
||||
expect(issues[1].checkId).toBe('cpuLimitsMissing');
|
||||
});
|
||||
|
||||
it('counts same check across multiple workloads', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-1',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
}),
|
||||
makeResult({
|
||||
Name: 'deploy-2',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const issues = getTopIssues(data);
|
||||
expect(issues).toHaveLength(1);
|
||||
expect(issues[0].count).toBe(2);
|
||||
});
|
||||
|
||||
it('ignores checks with severity "ignore"', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'ignore',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(getTopIssues(data)).toEqual([]);
|
||||
});
|
||||
|
||||
it('sorts danger before warning, then by count descending', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-1',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
}),
|
||||
makeResult({
|
||||
Name: 'deploy-2',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
hostIPCSet: {
|
||||
ID: 'hostIPCSet',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const issues = getTopIssues(data);
|
||||
// Danger first regardless of count
|
||||
expect(issues[0].severity).toBe('danger');
|
||||
expect(issues[1].severity).toBe('warning');
|
||||
expect(issues[1].count).toBe(2);
|
||||
});
|
||||
|
||||
it('returns at most 10 issues', () => {
|
||||
const results: Record<
|
||||
string,
|
||||
{
|
||||
ID: string;
|
||||
Message: string;
|
||||
Details: string[];
|
||||
Success: boolean;
|
||||
Severity: 'warning';
|
||||
Category: string;
|
||||
}
|
||||
> = {};
|
||||
for (let i = 0; i < 15; i++) {
|
||||
results[`check${i}`] = {
|
||||
ID: `check${i}`,
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
};
|
||||
}
|
||||
const data = makeAuditData([makeResult({ Results: results })]);
|
||||
expect(getTopIssues(data)).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { getCheckCategory, getCheckName } from './checkMapping';
|
||||
import { AuditData } from './polaris';
|
||||
|
||||
export interface TopIssue {
|
||||
checkId: string;
|
||||
checkName: string;
|
||||
category: 'Security' | 'Efficiency' | 'Reliability';
|
||||
severity: 'danger' | 'warning';
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the most common failing checks across the cluster
|
||||
* Returns top 10 issues sorted by severity then count
|
||||
*/
|
||||
export function getTopIssues(data: AuditData): TopIssue[] {
|
||||
const issueCounts = new Map<string, { severity: 'danger' | 'warning'; count: number }>();
|
||||
|
||||
// Aggregate all failing checks
|
||||
for (const result of data.Results) {
|
||||
// Pod-level checks
|
||||
if (result.PodResult?.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(result.PodResult.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
const existing = issueCounts.get(checkId);
|
||||
issueCounts.set(checkId, {
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
count: (existing?.count || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container-level checks
|
||||
if (result.PodResult?.ContainerResults) {
|
||||
for (const container of result.PodResult.ContainerResults) {
|
||||
for (const [checkId, checkResult] of Object.entries(container.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
const existing = issueCounts.get(checkId);
|
||||
issueCounts.set(checkId, {
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
count: (existing?.count || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Controller-level checks (if any)
|
||||
if (result.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(result.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
const existing = issueCounts.get(checkId);
|
||||
issueCounts.set(checkId, {
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
count: (existing?.count || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array and format
|
||||
const issues: TopIssue[] = Array.from(issueCounts.entries()).map(([checkId, data]) => ({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
category: getCheckCategory(checkId),
|
||||
severity: data.severity,
|
||||
count: data.count,
|
||||
}));
|
||||
|
||||
// Sort by severity (danger first) then by count (descending)
|
||||
issues.sort((a, b) => {
|
||||
if (a.severity === 'danger' && b.severity !== 'danger') return -1;
|
||||
if (a.severity !== 'danger' && b.severity === 'danger') return 1;
|
||||
return b.count - a.count;
|
||||
});
|
||||
|
||||
// Return top 10
|
||||
return issues.slice(0, 10);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
|
||||
// Mock Headlamp lib
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
K8s: {},
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
useTheme: () => ({
|
||||
palette: {
|
||||
success: { main: '#4caf50', contrastText: '#fff' },
|
||||
warning: { main: '#ff9800', contrastText: '#000' },
|
||||
error: { main: '#f44336', contrastText: '#fff' },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockPush = vi.fn();
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({ push: mockPush }),
|
||||
}));
|
||||
|
||||
// Set window.location.pathname for cluster extraction
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { pathname: '/c/test-cluster/some-page' },
|
||||
writable: true,
|
||||
});
|
||||
mockPush.mockClear();
|
||||
});
|
||||
|
||||
const mockUsePolarisDataContext = vi.fn();
|
||||
vi.mock('../api/PolarisDataContext', () => ({
|
||||
usePolarisDataContext: () => mockUsePolarisDataContext(),
|
||||
}));
|
||||
|
||||
import AppBarScoreBadge from './AppBarScoreBadge';
|
||||
|
||||
describe('AppBarScoreBadge', () => {
|
||||
it('returns null when loading', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({ data: null, loading: true });
|
||||
const { container } = render(<AppBarScoreBadge />);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when no data', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({ data: null, loading: false });
|
||||
const { container } = render(<AppBarScoreBadge />);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('renders score with success color for high score', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false });
|
||||
|
||||
render(<AppBarScoreBadge />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveTextContent('Polaris: 100%');
|
||||
expect(button.style.backgroundColor).toBe('rgb(76, 175, 80)');
|
||||
});
|
||||
|
||||
it('renders score with error color for low score', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false });
|
||||
|
||||
render(<AppBarScoreBadge />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveTextContent('Polaris: 0%');
|
||||
expect(button.style.backgroundColor).toBe('rgb(244, 67, 54)');
|
||||
});
|
||||
|
||||
it('navigates to /c/<cluster>/polaris on click', async () => {
|
||||
const user = userEvent.setup();
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false });
|
||||
|
||||
render(<AppBarScoreBadge />);
|
||||
await user.click(screen.getByRole('button'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/c/test-cluster/polaris');
|
||||
});
|
||||
|
||||
it('navigates to /polaris when no cluster in URL', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { pathname: '/settings' },
|
||||
writable: true,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false });
|
||||
|
||||
render(<AppBarScoreBadge />);
|
||||
await user.click(screen.getByRole('button'));
|
||||
expect(mockPush).toHaveBeenCalledWith('/polaris');
|
||||
});
|
||||
|
||||
it('has correct aria-label', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false });
|
||||
|
||||
render(<AppBarScoreBadge />);
|
||||
expect(screen.getByLabelText('Polaris: 100%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { computeScore, countResults } from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
|
||||
/**
|
||||
* Extract the cluster name from the current browser URL.
|
||||
* Headlamp cluster routes follow the pattern /c/<cluster>/...
|
||||
* We read window.location.pathname directly because the AppBar renders
|
||||
* outside the cluster route context, so useCluster() returns null and
|
||||
* React Router's useLocation() may not reflect the cluster prefix.
|
||||
*/
|
||||
function getClusterFromUrl(): string | null {
|
||||
const match = window.location.pathname.match(/\/c\/([^/]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* App bar badge showing cluster Polaris score
|
||||
* Clicking navigates to the overview dashboard
|
||||
*/
|
||||
export default function AppBarScoreBadge() {
|
||||
const theme = useTheme();
|
||||
const { data, loading } = usePolarisDataContext();
|
||||
const history = useHistory();
|
||||
|
||||
if (loading || !data) {
|
||||
return null; // Graceful degradation when Polaris unavailable
|
||||
}
|
||||
|
||||
const counts = countResults(data);
|
||||
const score = computeScore(counts);
|
||||
|
||||
// Color based on score using theme palette
|
||||
const getColor = (s: number): string => {
|
||||
if (s >= 80) return theme.palette.success.main;
|
||||
if (s >= 50) return theme.palette.warning.main;
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
const getContrastColor = (s: number): string => {
|
||||
if (s >= 80) return theme.palette.success.contrastText;
|
||||
if (s >= 50) return theme.palette.warning.contrastText;
|
||||
return theme.palette.error.contrastText;
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
const cluster = getClusterFromUrl();
|
||||
const prefix = cluster ? `/c/${cluster}` : '';
|
||||
history.push(`${prefix}/polaris`);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginRight: '8px',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '16px',
|
||||
border: 'none',
|
||||
backgroundColor: getColor(score),
|
||||
color: getContrastColor(score),
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
aria-label={`Polaris: ${score}%`}
|
||||
>
|
||||
<span>{'\u{1F6E1}\uFE0F'}</span>
|
||||
<span>Polaris: {score}%</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
|
||||
// Mock Headlamp lib
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
useTheme: () => ({
|
||||
palette: {
|
||||
primary: { main: '#1976d2' },
|
||||
text: { primary: '#000', secondary: '#666' },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock Headlamp CommonComponents as thin pass-throughs
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => (
|
||||
<div data-testid="section-box" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <div data-testid="section-header">{title}</div>,
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-testid="status-label" data-status={status}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
NameValueTable: ({ rows }: { rows: Array<{ name: string; value: React.ReactNode }> }) => (
|
||||
<table data-testid="name-value-table">
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={row.name}>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (row: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table data-testid="simple-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={col.label}>{col.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(col => (
|
||||
<td key={col.label}>{col.getter(row)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
PercentageCircle: ({ label }: { label: string }) => (
|
||||
<div data-testid="percentage-circle">{label}</div>
|
||||
),
|
||||
PercentageBar: () => <div data-testid="percentage-bar" />,
|
||||
}));
|
||||
|
||||
// Mock the context hook — we'll override per test via mockReturnValue
|
||||
const mockUsePolarisDataContext = vi.fn();
|
||||
vi.mock('../api/PolarisDataContext', () => ({
|
||||
usePolarisDataContext: () => mockUsePolarisDataContext(),
|
||||
}));
|
||||
|
||||
import DashboardView from './DashboardView';
|
||||
|
||||
describe('DashboardView', () => {
|
||||
it('renders loader when loading', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading Polaris audit data');
|
||||
});
|
||||
|
||||
it('renders error message when error is set', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: 'Access denied (403)',
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText('Access denied (403)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders score, check distribution, and cluster info with data', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
c2: {
|
||||
ID: 'c2',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
|
||||
// Score circle shows 50%
|
||||
expect(screen.getByTestId('percentage-circle')).toHaveTextContent('50%');
|
||||
|
||||
// Check distribution values
|
||||
expect(screen.getByText('Total Checks')).toBeInTheDocument();
|
||||
|
||||
// Cluster info section (title is in data-title attr of SectionBox)
|
||||
const sectionBoxes = screen.getAllByTestId('section-box');
|
||||
const clusterInfoBox = sectionBoxes.find(
|
||||
el => el.getAttribute('data-title') === 'Cluster Info'
|
||||
);
|
||||
expect(clusterInfoBox).toBeDefined();
|
||||
|
||||
// Cluster info values
|
||||
expect(screen.getByText('Nodes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pods')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "No Data" when no data and no error', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<DashboardView />);
|
||||
expect(screen.getByText('No Polaris audit results found.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
Loader,
|
||||
NameValueTable,
|
||||
PercentageBar,
|
||||
PercentageCircle,
|
||||
SectionBox,
|
||||
SectionHeader,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import { getSeverityStatus } from '../api/checkMapping';
|
||||
import { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
import { getTopIssues, TopIssue } from '../api/topIssues';
|
||||
|
||||
const COLORS = {
|
||||
pass: '#4caf50',
|
||||
warning: '#ff9800',
|
||||
danger: '#f44336',
|
||||
skipped: '#9e9e9e',
|
||||
};
|
||||
|
||||
function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
||||
const { counts } = props;
|
||||
const score = computeScore(counts);
|
||||
|
||||
const chartData = [
|
||||
{ name: 'Pass', value: counts.pass, fill: COLORS.pass },
|
||||
{ name: 'Warning', value: counts.warning, fill: COLORS.warning },
|
||||
{ name: 'Danger', value: counts.danger, fill: COLORS.danger },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionBox title="Cluster Score">
|
||||
<PercentageCircle data={chartData} total={counts.total} label={`${score}%`} />
|
||||
</SectionBox>
|
||||
<SectionBox title="Check Distribution">
|
||||
<PercentageBar data={chartData} total={counts.total} />
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{ name: 'Total Checks', value: String(counts.total) },
|
||||
{
|
||||
name: 'Pass',
|
||||
value: <StatusLabel status="success">{counts.pass}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Warning',
|
||||
value: <StatusLabel status="warning">{counts.warning}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Danger',
|
||||
value: <StatusLabel status="error">{counts.danger}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
<SectionBox title="Cluster Info">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{ name: 'Nodes', value: String(props.data.ClusterInfo.Nodes) },
|
||||
{ name: 'Pods', value: String(props.data.ClusterInfo.Pods) },
|
||||
{ name: 'Namespaces', value: String(props.data.ClusterInfo.Namespaces) },
|
||||
{ name: 'Controllers', value: String(props.data.ClusterInfo.Controllers) },
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function formatAuditTime(auditTime: string): string {
|
||||
const date = new Date(auditTime);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
export default function DashboardView() {
|
||||
const theme = useTheme();
|
||||
const { data, loading, error, refresh } = usePolarisDataContext();
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading Polaris audit data..." />;
|
||||
}
|
||||
|
||||
const counts = data ? countResults(data) : null;
|
||||
const topIssues = data ? getTopIssues(data) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<SectionHeader title="Polaris — Overview" />
|
||||
{data && (
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '14px', color: theme.palette.text.secondary }}>
|
||||
Last updated: {formatAuditTime(data.AuditTime)}
|
||||
</span>
|
||||
<button
|
||||
onClick={refresh}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<span>🔄</span>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<SectionBox title="Error">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="error">{error}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{data && counts && (
|
||||
<>
|
||||
<OverviewSection data={data} counts={counts} />
|
||||
|
||||
{topIssues.length > 0 && (
|
||||
<SectionBox title="Top Issues">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Check', getter: (issue: TopIssue) => issue.checkName },
|
||||
{ label: 'Category', getter: (issue: TopIssue) => issue.category },
|
||||
{
|
||||
label: 'Severity',
|
||||
getter: (issue: TopIssue) => (
|
||||
<StatusLabel status={getSeverityStatus(issue.severity)}>
|
||||
{issue.severity}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Affected Workloads',
|
||||
getter: (issue: TopIssue) => String(issue.count),
|
||||
},
|
||||
]}
|
||||
data={topIssues}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!data && !error && (
|
||||
<SectionBox title="No Data">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: 'No Polaris audit results found.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { makeResult } from '../test-utils';
|
||||
|
||||
const { mockApiRequest } = vi.hoisted(() => ({ mockApiRequest: vi.fn() }));
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: mockApiRequest },
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
useTheme: () => ({
|
||||
palette: {
|
||||
primary: { main: '#1976d2', contrastText: '#fff' },
|
||||
action: { disabledBackground: '#e0e0e0', disabled: '#9e9e9e' },
|
||||
divider: '#e0e0e0',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => (
|
||||
<div data-testid="section-box" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-testid="status-label" data-status={status}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
Dialog: ({
|
||||
open,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
}) =>
|
||||
open ? (
|
||||
<div data-testid="dialog" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
import ExemptionManager from './ExemptionManager';
|
||||
|
||||
const defaultProps = {
|
||||
workloadResult: makeResult(),
|
||||
namespace: 'default',
|
||||
kind: 'Deployment',
|
||||
name: 'my-deploy',
|
||||
};
|
||||
|
||||
const resultWithPodFailures = makeResult({
|
||||
PodResult: {
|
||||
Name: 'pod',
|
||||
Results: {
|
||||
hostIPCSet: {
|
||||
ID: 'hostIPCSet',
|
||||
Message: 'Host IPC is set',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
hostPIDSet: {
|
||||
ID: 'hostPIDSet',
|
||||
Message: 'Host PID is set',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
ContainerResults: [],
|
||||
},
|
||||
});
|
||||
|
||||
const resultWithContainerFailures = makeResult({
|
||||
PodResult: {
|
||||
Name: 'pod',
|
||||
Results: {},
|
||||
ContainerResults: [
|
||||
{
|
||||
Name: 'container-1',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: 'CPU requests missing',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const resultWithIgnoredFailures = makeResult({
|
||||
PodResult: {
|
||||
Name: 'pod',
|
||||
Results: {
|
||||
hostIPCSet: {
|
||||
ID: 'hostIPCSet',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'ignore',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
ContainerResults: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('ExemptionManager', () => {
|
||||
describe('rendering failing checks', () => {
|
||||
it('shows disabled Add Exemption button when no failing checks', () => {
|
||||
render(<ExemptionManager {...defaultProps} />);
|
||||
const btn = screen.getByRole('button', { name: /add exemption/i });
|
||||
expect(btn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows enabled Add Exemption button when there are failing checks', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
const btn = screen.getByRole('button', { name: /add exemption/i });
|
||||
expect(btn).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('does not include ignored-severity checks as failing', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithIgnoredFailures} />);
|
||||
const btn = screen.getByRole('button', { name: /add exemption/i });
|
||||
expect(btn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('collects failing checks from pod-level results', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
expect(screen.getByText('Host IPC')).toBeInTheDocument();
|
||||
expect(screen.getByText('Host PID')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('collects failing checks from container-level results', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithContainerFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
expect(screen.getByText('CPU Requests')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('deduplicates checks that appear in multiple containers', () => {
|
||||
const resultWithDuplicate = makeResult({
|
||||
PodResult: {
|
||||
Name: 'pod',
|
||||
Results: {},
|
||||
ContainerResults: [
|
||||
{
|
||||
Name: 'container-1',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: 'container-2',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithDuplicate} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
const items = screen.getAllByText('CPU Requests');
|
||||
expect(items).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dialog interactions', () => {
|
||||
it('opens dialog when Add Exemption button is clicked', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
expect(screen.getByTestId('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes dialog when Cancel button is clicked', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
expect(screen.getByTestId('dialog')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles individual check selection', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
|
||||
// Find the checkbox next to "Host IPC"
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
// First checkbox is "Exempt from all checks", rest are individual checks
|
||||
const hostIPCCheckbox = checkboxes[1];
|
||||
expect(hostIPCCheckbox).not.toBeChecked();
|
||||
fireEvent.click(hostIPCCheckbox);
|
||||
expect(hostIPCCheckbox).toBeChecked();
|
||||
fireEvent.click(hostIPCCheckbox);
|
||||
expect(hostIPCCheckbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('hides individual checks list when exempt-all is toggled', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
expect(screen.getByText('Host IPC')).toBeInTheDocument();
|
||||
|
||||
const exemptAllCheckbox = screen.getByRole('checkbox', { name: /exempt from all checks/i });
|
||||
fireEvent.click(exemptAllCheckbox);
|
||||
expect(screen.queryByText('Host IPC')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Apply button is disabled when no checks selected and exemptAll is false', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
expect(screen.getByRole('button', { name: /apply/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Apply button is enabled when exemptAll is checked', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
const exemptAllCheckbox = screen.getByRole('checkbox', { name: /exempt from all checks/i });
|
||||
fireEvent.click(exemptAllCheckbox);
|
||||
expect(screen.getByRole('button', { name: /apply/i })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('Apply button is enabled when at least one individual check is selected', () => {
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(checkboxes[1]); // select first individual check
|
||||
expect(screen.getByRole('button', { name: /apply/i })).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApiProxy.request calls', () => {
|
||||
it('patches with exempt-all annotation when exemptAll is selected', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiRequest).toHaveBeenCalledWith(
|
||||
'/apis/apps/v1/namespaces/default/deployments/my-deploy',
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/strategic-merge-patch+json' },
|
||||
body: JSON.stringify({
|
||||
metadata: {
|
||||
annotations: { 'polaris.fairwinds.com/exempt': 'true' },
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('patches with per-check annotations when individual checks selected', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
// Select first check (hostIPCSet)
|
||||
fireEvent.click(screen.getAllByRole('checkbox')[1]);
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiRequest).toHaveBeenCalledWith(
|
||||
'/apis/apps/v1/namespaces/default/deployments/my-deploy',
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
metadata: {
|
||||
annotations: { 'polaris.fairwinds.com/hostIPCSet-exempt': 'true' },
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses core API path for Pod kind (no api group)', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(
|
||||
<ExemptionManager {...defaultProps} kind="Pod" workloadResult={resultWithPodFailures} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiRequest).toHaveBeenCalledWith(
|
||||
'/api/v1/namespaces/default/pods/my-deploy',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses batch API group for Job kind', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(
|
||||
<ExemptionManager {...defaultProps} kind="Job" workloadResult={resultWithPodFailures} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiRequest).toHaveBeenCalledWith(
|
||||
'/apis/batch/v1/namespaces/default/jobs/my-deploy',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses batch API group for CronJob kind', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(
|
||||
<ExemptionManager {...defaultProps} kind="CronJob" workloadResult={resultWithPodFailures} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiRequest).toHaveBeenCalledWith(
|
||||
'/apis/batch/v1/namespaces/default/cronjobs/my-deploy',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses apps API group for StatefulSet kind', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(
|
||||
<ExemptionManager
|
||||
{...defaultProps}
|
||||
kind="StatefulSet"
|
||||
workloadResult={resultWithPodFailures}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiRequest).toHaveBeenCalledWith(
|
||||
'/apis/apps/v1/namespaces/default/statefulsets/my-deploy',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedback states', () => {
|
||||
it('shows success feedback and closes dialog after successful apply', async () => {
|
||||
mockApiRequest.mockResolvedValue({});
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
|
||||
const label = screen.getByTestId('status-label');
|
||||
expect(label).toHaveAttribute('data-status', 'success');
|
||||
expect(label).toHaveTextContent('Exemptions applied successfully');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error feedback and keeps dialog closed after failed apply', async () => {
|
||||
mockApiRequest.mockRejectedValue(new Error('403 Forbidden'));
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
const label = screen.getByTestId('status-label');
|
||||
expect(label).toHaveAttribute('data-status', 'error');
|
||||
expect(label).toHaveTextContent(/failed to apply exemptions/i);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "Applying..." text on Apply button while in-flight', async () => {
|
||||
let resolveRequest!: () => void;
|
||||
mockApiRequest.mockReturnValue(
|
||||
new Promise<void>(res => {
|
||||
resolveRequest = res;
|
||||
})
|
||||
);
|
||||
|
||||
render(<ExemptionManager {...defaultProps} workloadResult={resultWithPodFailures} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /add exemption/i }));
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /exempt from all checks/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /apply/i }));
|
||||
|
||||
expect(screen.getByRole('button', { name: /applying/i })).toBeInTheDocument();
|
||||
resolveRequest();
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { Dialog, SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import { getCheckName } from '../api/checkMapping';
|
||||
import { Result } from '../api/polaris';
|
||||
|
||||
interface ExemptionManagerProps {
|
||||
workloadResult: Result;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CheckFailure {
|
||||
checkId: string;
|
||||
checkName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exemption management UI for adding/removing Polaris exemptions
|
||||
* Uses annotation patches on the workload resource
|
||||
*/
|
||||
export default function ExemptionManager({
|
||||
workloadResult,
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
}: ExemptionManagerProps) {
|
||||
const theme = useTheme();
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [selectedChecks, setSelectedChecks] = React.useState<Set<string>>(new Set());
|
||||
const [exemptAll, setExemptAll] = React.useState(false);
|
||||
const [applying, setApplying] = React.useState(false);
|
||||
const [feedback, setFeedback] = React.useState<{ success: boolean; message: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// Extract failing checks for this workload
|
||||
const getFailingChecks = (): CheckFailure[] => {
|
||||
const failures: CheckFailure[] = [];
|
||||
|
||||
// Pod-level checks
|
||||
if (workloadResult.PodResult?.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(workloadResult.PodResult.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container checks
|
||||
if (workloadResult.PodResult?.ContainerResults) {
|
||||
for (const container of workloadResult.PodResult.ContainerResults) {
|
||||
for (const [checkId, checkResult] of Object.entries(container.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
// Avoid duplicates
|
||||
if (!failures.some(f => f.checkId === checkId)) {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
};
|
||||
|
||||
const failingChecks = getFailingChecks();
|
||||
|
||||
const handleCheckToggle = (checkId: string) => {
|
||||
const newSelected = new Set(selectedChecks);
|
||||
if (newSelected.has(checkId)) {
|
||||
newSelected.delete(checkId);
|
||||
} else {
|
||||
newSelected.add(checkId);
|
||||
}
|
||||
setSelectedChecks(newSelected);
|
||||
};
|
||||
|
||||
const applyExemptions = async () => {
|
||||
setApplying(true);
|
||||
setFeedback(null);
|
||||
|
||||
try {
|
||||
// Construct the API path based on kind
|
||||
const apiGroup = getApiGroup(kind);
|
||||
const plural = getPlural(kind);
|
||||
|
||||
const patchPath = apiGroup
|
||||
? `/apis/${apiGroup}/v1/namespaces/${namespace}/${plural}/${name}`
|
||||
: `/api/v1/namespaces/${namespace}/${plural}/${name}`;
|
||||
|
||||
// Build annotations patch
|
||||
const annotations: Record<string, string> = {};
|
||||
|
||||
if (exemptAll) {
|
||||
annotations['polaris.fairwinds.com/exempt'] = 'true';
|
||||
} else {
|
||||
for (const checkId of selectedChecks) {
|
||||
annotations[`polaris.fairwinds.com/${checkId}-exempt`] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
const patch = {
|
||||
metadata: {
|
||||
annotations,
|
||||
},
|
||||
};
|
||||
|
||||
await ApiProxy.request(patchPath, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/strategic-merge-patch+json',
|
||||
},
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
|
||||
setDialogOpen(false);
|
||||
setSelectedChecks(new Set());
|
||||
setExemptAll(false);
|
||||
setFeedback({ success: true, message: 'Exemptions applied successfully' });
|
||||
} catch (err) {
|
||||
setFeedback({ success: false, message: `Failed to apply exemptions: ${String(err)}` });
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = applying || (!exemptAll && selectedChecks.size === 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionBox title="Exemptions">
|
||||
<p>No exemptions configured</p>
|
||||
|
||||
{feedback && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<StatusLabel status={feedback.success ? 'success' : 'error'}>
|
||||
{feedback.message}
|
||||
</StatusLabel>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setDialogOpen(true)}
|
||||
disabled={failingChecks.length === 0}
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
padding: '6px 16px',
|
||||
backgroundColor:
|
||||
failingChecks.length === 0 ? theme.palette.action.disabledBackground : 'transparent',
|
||||
color:
|
||||
failingChecks.length === 0
|
||||
? theme.palette.action.disabled
|
||||
: theme.palette.primary.main,
|
||||
border: '1px solid',
|
||||
borderColor:
|
||||
failingChecks.length === 0 ? theme.palette.divider : theme.palette.primary.main,
|
||||
borderRadius: '4px',
|
||||
cursor: failingChecks.length === 0 ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
Add Exemption
|
||||
</button>
|
||||
</SectionBox>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} title="Add Exemptions">
|
||||
<div style={{ padding: '16px', minWidth: '400px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exemptAll}
|
||||
onChange={e => setExemptAll(e.target.checked)}
|
||||
/>
|
||||
<span>Exempt from all checks</span>
|
||||
</label>
|
||||
|
||||
{!exemptAll && (
|
||||
<>
|
||||
<div style={{ marginTop: '16px', marginBottom: '8px', fontWeight: 600 }}>
|
||||
Select checks to exempt:
|
||||
</div>
|
||||
<div>
|
||||
{failingChecks.map(check => (
|
||||
<label
|
||||
key={check.checkId}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '8px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedChecks.has(check.checkId)}
|
||||
onChange={() => handleCheckToggle(check.checkId)}
|
||||
/>
|
||||
<span>{check.checkName}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ marginTop: '16px', display: 'flex', gap: '8px', justifyContent: 'flex-end' }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setDialogOpen(false)}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={applyExemptions}
|
||||
disabled={isDisabled}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: isDisabled
|
||||
? theme.palette.action.disabledBackground
|
||||
: theme.palette.primary.main,
|
||||
color: isDisabled
|
||||
? theme.palette.action.disabled
|
||||
: theme.palette.primary.contrastText,
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{applying ? 'Applying...' : 'Apply'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper functions to get API info based on kind
|
||||
function getApiGroup(kind: string): string | null {
|
||||
switch (kind) {
|
||||
case 'Deployment':
|
||||
case 'StatefulSet':
|
||||
case 'DaemonSet':
|
||||
return 'apps';
|
||||
case 'Job':
|
||||
case 'CronJob':
|
||||
return 'batch';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlural(kind: string): string {
|
||||
switch (kind) {
|
||||
case 'Deployment':
|
||||
return 'deployments';
|
||||
case 'StatefulSet':
|
||||
return 'statefulsets';
|
||||
case 'DaemonSet':
|
||||
return 'daemonsets';
|
||||
case 'Job':
|
||||
return 'jobs';
|
||||
case 'CronJob':
|
||||
return 'cronjobs';
|
||||
default:
|
||||
return kind.toLowerCase() + 's';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
|
||||
// Mock Headlamp lib
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
useTheme: () => ({
|
||||
palette: {
|
||||
primary: { main: '#1976d2' },
|
||||
text: { primary: '#000', secondary: '#666' },
|
||||
action: { disabledBackground: '#e0e0e0', disabled: '#9e9e9e' },
|
||||
divider: '#e0e0e0',
|
||||
error: { main: '#f44336', contrastText: '#fff' },
|
||||
success: { main: '#4caf50' },
|
||||
warning: { main: '#ff9800' },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock react-router-dom
|
||||
vi.mock('react-router-dom', () => ({
|
||||
Link: ({ to, children, style }: { to: string; children: React.ReactNode; style?: object }) => (
|
||||
<a href={to} style={style}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock Headlamp CommonComponents
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => (
|
||||
<div data-testid="section-box" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-testid="status-label" data-status={status}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
NameValueTable: ({ rows }: { rows: Array<{ name: string; value: React.ReactNode }> }) => (
|
||||
<table data-testid="name-value-table">
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={row.name}>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (row: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table data-testid="simple-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={col.label}>{col.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(col => (
|
||||
<td key={col.label}>{col.getter(row)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
Dialog: () => null,
|
||||
}));
|
||||
|
||||
// Mock ExemptionManager
|
||||
vi.mock('./ExemptionManager', () => ({
|
||||
default: () => <div data-testid="exemption-manager" />,
|
||||
}));
|
||||
|
||||
const mockUsePolarisDataContext = vi.fn();
|
||||
vi.mock('../api/PolarisDataContext', () => ({
|
||||
usePolarisDataContext: () => mockUsePolarisDataContext(),
|
||||
}));
|
||||
|
||||
import InlineAuditSection from './InlineAuditSection';
|
||||
|
||||
describe('InlineAuditSection', () => {
|
||||
it('returns null when loading', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({ data: null, loading: true, error: null });
|
||||
const { container } = render(
|
||||
<InlineAuditSection
|
||||
resource={{ kind: 'Deployment', metadata: { name: 'x', namespace: 'y' } }}
|
||||
/>
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null for unsupported kind', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: makeAuditData([]),
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
const { container } = render(
|
||||
<InlineAuditSection resource={{ kind: 'Service', metadata: { name: 'x', namespace: 'y' } }} />
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('shows "not detected" when workload not found in audit data', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: makeAuditData([]),
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
render(
|
||||
<InlineAuditSection
|
||||
resource={{ kind: 'Deployment', metadata: { name: 'my-app', namespace: 'default' } }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/Polaris dashboard not detected/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders score and summary for a matching workload', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'my-app',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
c2: {
|
||||
ID: 'c2',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false, error: null });
|
||||
|
||||
render(
|
||||
<InlineAuditSection
|
||||
resource={{ kind: 'Deployment', metadata: { name: 'my-app', namespace: 'default' } }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('50%')).toBeInTheDocument();
|
||||
expect(screen.getByText(/1 passing, 1 warnings, 0 dangers/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders failing checks table with pod and container results', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'my-app',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {},
|
||||
PodResult: {
|
||||
Name: 'pod',
|
||||
Results: {
|
||||
hostIPCSet: {
|
||||
ID: 'hostIPCSet',
|
||||
Message: 'Host IPC is set',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'Security',
|
||||
},
|
||||
},
|
||||
ContainerResults: [
|
||||
{
|
||||
Name: 'container-1',
|
||||
Results: {
|
||||
cpuRequestsMissing: {
|
||||
ID: 'cpuRequestsMissing',
|
||||
Message: 'CPU requests missing',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'warning',
|
||||
Category: 'Efficiency',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false, error: null });
|
||||
|
||||
render(
|
||||
<InlineAuditSection
|
||||
resource={{ kind: 'Deployment', metadata: { name: 'my-app', namespace: 'default' } }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Host IPC')).toBeInTheDocument();
|
||||
expect(screen.getByText('CPU Requests')).toBeInTheDocument();
|
||||
expect(screen.getByText('Host IPC is set')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders link to full report', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'my-app',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false, error: null });
|
||||
|
||||
render(
|
||||
<InlineAuditSection
|
||||
resource={{ kind: 'Deployment', metadata: { name: 'my-app', namespace: 'default' } }}
|
||||
/>
|
||||
);
|
||||
const link = screen.getByText('View Full Report →');
|
||||
expect(link).toHaveAttribute('href', '/polaris/namespaces#default');
|
||||
});
|
||||
|
||||
it('renders ExemptionManager', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'my-app',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockUsePolarisDataContext.mockReturnValue({ data, loading: false, error: null });
|
||||
|
||||
render(
|
||||
<InlineAuditSection
|
||||
resource={{ kind: 'Deployment', metadata: { name: 'my-app', namespace: 'default' } }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('exemption-manager')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getCheckName, getSeverityStatus } from '../api/checkMapping';
|
||||
import { computeScore, countResultsForItems } from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
import ExemptionManager from './ExemptionManager';
|
||||
|
||||
interface CheckFailure {
|
||||
checkId: string;
|
||||
checkName: string;
|
||||
severity: 'danger' | 'warning';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface KubeResource {
|
||||
kind: string;
|
||||
metadata?: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
annotations?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
interface InlineAuditSectionProps {
|
||||
resource: KubeResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Polaris audit section for resource detail views
|
||||
* Shows a compact summary of Polaris findings for Deployments, StatefulSets, etc.
|
||||
*/
|
||||
export default function InlineAuditSection({ resource }: InlineAuditSectionProps) {
|
||||
const theme = useTheme();
|
||||
const { data, loading } = usePolarisDataContext();
|
||||
|
||||
if (loading || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is a supported controller kind
|
||||
const supportedKinds = ['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob'];
|
||||
const kind = resource.kind;
|
||||
|
||||
if (!supportedKinds.includes(kind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = resource.metadata?.name;
|
||||
const namespace = resource.metadata?.namespace;
|
||||
|
||||
if (!name || !namespace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find this workload in Polaris audit data
|
||||
const workloadResult = data.Results.find(
|
||||
r => r.Kind === kind && r.Name === name && r.Namespace === namespace
|
||||
);
|
||||
|
||||
if (!workloadResult) {
|
||||
return (
|
||||
<SectionBox title="Polaris Audit">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: 'Polaris dashboard not detected — install Polaris to see audit results',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate score and counts
|
||||
const counts = countResultsForItems([workloadResult]);
|
||||
const score = computeScore(counts);
|
||||
|
||||
// Extract failing checks
|
||||
const failures: CheckFailure[] = [];
|
||||
|
||||
// Pod-level checks
|
||||
if (workloadResult.PodResult?.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(workloadResult.PodResult.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
message: checkResult.Message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container checks
|
||||
if (workloadResult.PodResult?.ContainerResults) {
|
||||
for (const container of workloadResult.PodResult.ContainerResults) {
|
||||
for (const [checkId, checkResult] of Object.entries(container.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
// Avoid duplicates
|
||||
if (!failures.some(f => f.checkId === checkId)) {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
message: checkResult.Message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by severity
|
||||
failures.sort((a, b) => {
|
||||
if (a.severity === 'danger' && b.severity !== 'danger') return -1;
|
||||
if (a.severity !== 'danger' && b.severity === 'danger') return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionBox title="Polaris Audit">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Score',
|
||||
value: (
|
||||
<StatusLabel status={score >= 80 ? 'success' : score >= 50 ? 'warning' : 'error'}>
|
||||
{score}%
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Summary',
|
||||
value: `${counts.pass} passing, ${counts.warning} warnings, ${counts.danger} dangers`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{failures.length > 0 && (
|
||||
<>
|
||||
<div style={{ marginTop: '16px', marginBottom: '8px', fontWeight: 600 }}>
|
||||
Failing Checks:
|
||||
</div>
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Check', getter: (f: CheckFailure) => f.checkName },
|
||||
{
|
||||
label: 'Severity',
|
||||
getter: (f: CheckFailure) => (
|
||||
<StatusLabel status={getSeverityStatus(f.severity)}>{f.severity}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{ label: 'Message', getter: (f: CheckFailure) => f.message },
|
||||
]}
|
||||
data={failures}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Link to={`/polaris/namespaces#${namespace}`} style={{ color: theme.palette.primary.main }}>
|
||||
View Full Report →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<ExemptionManager
|
||||
workloadResult={workloadResult}
|
||||
namespace={namespace}
|
||||
kind={kind}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { makeAuditData, makeResult } from '../test-utils';
|
||||
|
||||
// Mock Headlamp lib
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
Router: {
|
||||
createRouteURL: (name: string, params: Record<string, string>) =>
|
||||
`/polaris/ns/${params.namespace}`,
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock MUI components
|
||||
vi.mock('@mui/material/Drawer', () => ({
|
||||
default: ({
|
||||
open,
|
||||
children,
|
||||
}: {
|
||||
open: boolean;
|
||||
children: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
anchor?: string;
|
||||
}) => (open ? <div data-testid="mui-drawer">{children}</div> : null),
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/IconButton', () => ({
|
||||
default: ({
|
||||
children,
|
||||
onClick,
|
||||
'aria-label': ariaLabel,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick: () => void;
|
||||
'aria-label': string;
|
||||
title?: string;
|
||||
size?: string;
|
||||
}) => (
|
||||
<button onClick={onClick} aria-label={ariaLabel}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
useTheme: () => ({
|
||||
palette: {
|
||||
primary: { main: '#1976d2' },
|
||||
text: { primary: '#000', secondary: '#666' },
|
||||
action: { hover: 'rgba(0,0,0,0.04)' },
|
||||
background: { default: '#fafafa' },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock Headlamp CommonComponents
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => (
|
||||
<div data-testid="section-box" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <div data-testid="section-header">{title}</div>,
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-testid="status-label" data-status={status}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
NameValueTable: ({ rows }: { rows: Array<{ name: string; value: React.ReactNode }> }) => (
|
||||
<table data-testid="name-value-table">
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={row.name}>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
emptyMessage,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (row: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
emptyMessage?: string;
|
||||
}) =>
|
||||
data.length === 0 ? (
|
||||
<div data-testid="simple-table-empty">{emptyMessage}</div>
|
||||
) : (
|
||||
<table data-testid="simple-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={col.label}>{col.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(col => (
|
||||
<td key={col.label}>{col.getter(row)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockUsePolarisDataContext = vi.fn();
|
||||
vi.mock('../api/PolarisDataContext', () => ({
|
||||
usePolarisDataContext: () => mockUsePolarisDataContext(),
|
||||
}));
|
||||
|
||||
import NamespacesListView from './NamespacesListView';
|
||||
|
||||
function renderWithRouter(ui: React.ReactElement) {
|
||||
return render(<MemoryRouter>{ui}</MemoryRouter>);
|
||||
}
|
||||
|
||||
describe('NamespacesListView', () => {
|
||||
it('renders loader when loading', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading Polaris audit data');
|
||||
});
|
||||
|
||||
it('renders error message when error is set', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: 'Polaris dashboard not reachable',
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
expect(screen.getByText('Polaris dashboard not reachable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "No Data" when no data and no error', () => {
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
expect(screen.getByText('No Polaris audit results found.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders namespace rows with correct scores and buttons', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-a',
|
||||
Namespace: 'alpha',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
makeResult({
|
||||
Name: 'deploy-b',
|
||||
Namespace: 'beta',
|
||||
Results: {
|
||||
c2: {
|
||||
ID: 'c2',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
|
||||
// Namespace buttons (now buttons instead of links for drawer)
|
||||
const alphaButton = screen.getByText('alpha');
|
||||
expect(alphaButton).toBeInTheDocument();
|
||||
expect(alphaButton.tagName).toBe('BUTTON');
|
||||
|
||||
const betaButton = screen.getByText('beta');
|
||||
expect(betaButton).toBeInTheDocument();
|
||||
expect(betaButton.tagName).toBe('BUTTON');
|
||||
});
|
||||
|
||||
it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => {
|
||||
// Create a namespace with 100% score (1 pass) and one with 0% (1 danger)
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'perfect',
|
||||
Namespace: 'good-ns',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
makeResult({
|
||||
Name: 'bad',
|
||||
Namespace: 'bad-ns',
|
||||
Results: {
|
||||
c2: {
|
||||
ID: 'c2',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: false,
|
||||
Severity: 'danger',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
|
||||
// Find score StatusLabels - good-ns has 100% (success), bad-ns has 0% (error)
|
||||
const statusLabels = screen.getAllByTestId('status-label');
|
||||
const scoreLabels = statusLabels.filter(el => el.textContent?.includes('%'));
|
||||
|
||||
const successScore = scoreLabels.find(el => el.textContent === '100%');
|
||||
expect(successScore).toHaveAttribute('data-status', 'success');
|
||||
|
||||
const errorScore = scoreLabels.find(el => el.textContent === '0%');
|
||||
expect(errorScore).toHaveAttribute('data-status', 'error');
|
||||
});
|
||||
|
||||
it('opens drawer when namespace button is clicked and URL hash is updated', async () => {
|
||||
const user = userEvent.setup();
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-a',
|
||||
Namespace: 'alpha',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
|
||||
// Click the namespace button
|
||||
const alphaButton = screen.getByText('alpha');
|
||||
await user.click(alphaButton);
|
||||
|
||||
// Drawer should open (check for the panel title)
|
||||
expect(screen.getByText(/Polaris — alpha/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('initializes drawer from URL hash', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-a',
|
||||
Namespace: 'test-ns',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Render with initial hash in URL
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/polaris/namespaces#test-ns']}>
|
||||
<NamespacesListView />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Drawer should be open with the namespace from hash
|
||||
expect(screen.getByText(/Polaris — test-ns/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,351 @@
|
||||
import {
|
||||
Loader,
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
SectionHeader,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
computeScore,
|
||||
countResultsForItems,
|
||||
filterResultsByNamespace,
|
||||
getNamespaces,
|
||||
getPolarisProxyUrl,
|
||||
Result,
|
||||
ResultCounts,
|
||||
} from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
|
||||
function scoreStatus(score: number): 'success' | 'warning' | 'error' {
|
||||
if (score >= 80) return 'success';
|
||||
if (score >= 50) return 'warning';
|
||||
return 'error';
|
||||
}
|
||||
|
||||
interface NamespaceRow {
|
||||
namespace: string;
|
||||
score: number;
|
||||
pass: number;
|
||||
warning: number;
|
||||
danger: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
function resourceCounts(result: Result): ResultCounts {
|
||||
return countResultsForItems([result]);
|
||||
}
|
||||
|
||||
interface NamespaceDetailPanelProps {
|
||||
namespace: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function NamespaceDetailPanel({ namespace, onClose }: NamespaceDetailPanelProps) {
|
||||
const theme = useTheme();
|
||||
const [isMaximized, setIsMaximized] = React.useState(false);
|
||||
const { data, loading, error } = usePolarisDataContext();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Loader title={`Loading Polaris data for ${namespace}...`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<SectionBox title="Error">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="error">{error}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<SectionBox title="No Data">
|
||||
<NameValueTable rows={[{ name: 'Status', value: 'No Polaris audit results found.' }]} />
|
||||
</SectionBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const results = filterResultsByNamespace(data, namespace);
|
||||
const counts = countResultsForItems(results);
|
||||
const score = computeScore(counts);
|
||||
const status = scoreStatus(score);
|
||||
|
||||
const countsPerResource = new Map<string, ResultCounts>();
|
||||
for (const r of results) {
|
||||
countsPerResource.set(`${r.Namespace}/${r.Kind}/${r.Name}`, resourceCounts(r));
|
||||
}
|
||||
|
||||
function getResourceCounts(row: Result): ResultCounts {
|
||||
return countsPerResource.get(`${row.Namespace}/${row.Kind}/${row.Name}`) ?? resourceCounts(row);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: isMaximized ? 'calc(100vw - 240px)' : '1000px',
|
||||
padding: '20px',
|
||||
transition: 'width 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, color: theme.palette.text.primary }}>Polaris — {namespace}</h2>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => setIsMaximized(!isMaximized)}
|
||||
aria-label={isMaximized ? 'Minimize panel' : 'Maximize panel'}
|
||||
title={isMaximized ? 'Minimize' : 'Maximize'}
|
||||
size="small"
|
||||
>
|
||||
{isMaximized ? '\u229F' : '\u22A1'}
|
||||
</IconButton>
|
||||
<IconButton onClick={onClose} aria-label="Close panel" title="Close" size="small">
|
||||
\u00D7
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionBox title="External">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Polaris Dashboard',
|
||||
value: (
|
||||
<a href={getPolarisProxyUrl()} target="_blank" rel="noopener noreferrer">
|
||||
View in Polaris Dashboard
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Namespace Score">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Score',
|
||||
value: <StatusLabel status={status}>{score}%</StatusLabel>,
|
||||
},
|
||||
{ name: 'Total Checks', value: String(counts.total) },
|
||||
{
|
||||
name: 'Pass',
|
||||
value: <StatusLabel status="success">{counts.pass}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Warning',
|
||||
value: <StatusLabel status="warning">{counts.warning}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Danger',
|
||||
value: <StatusLabel status="error">{counts.danger}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Skipped',
|
||||
value: (
|
||||
<span title="Only counts checks with Severity=ignore. Annotation-based exemptions are not included.">
|
||||
{counts.skipped}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Resources">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Name', getter: (row: Result) => row.Name },
|
||||
{ label: 'Kind', getter: (row: Result) => row.Kind },
|
||||
{
|
||||
label: 'Pass',
|
||||
getter: (row: Result) => (
|
||||
<StatusLabel status="success">{getResourceCounts(row).pass}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Warning',
|
||||
getter: (row: Result) => (
|
||||
<StatusLabel status="warning">{getResourceCounts(row).warning}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
getter: (row: Result) => (
|
||||
<StatusLabel status="error">{getResourceCounts(row).danger}</StatusLabel>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={results}
|
||||
emptyMessage={`No resources found in namespace "${namespace}".`}
|
||||
/>
|
||||
</SectionBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NamespacesListView() {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { data, loading, error } = usePolarisDataContext();
|
||||
|
||||
// Initialize from URL hash
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(
|
||||
location.hash.slice(1) || null
|
||||
);
|
||||
|
||||
// Sync drawer state when URL hash changes (browser back/forward)
|
||||
useEffect(() => {
|
||||
const hashNs = location.hash.slice(1);
|
||||
setSelectedNamespace(hashNs || null);
|
||||
}, [location.hash]);
|
||||
|
||||
const openNamespace = (ns: string) => {
|
||||
setSelectedNamespace(ns);
|
||||
history.push(`${location.pathname}#${ns}`);
|
||||
};
|
||||
|
||||
const closeNamespace = () => {
|
||||
setSelectedNamespace(null);
|
||||
history.push(location.pathname);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading Polaris audit data..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Polaris — Namespaces" />
|
||||
<SectionBox title="Error">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="error">{error}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Polaris — Namespaces" />
|
||||
<SectionBox title="No Data">
|
||||
<NameValueTable rows={[{ name: 'Status', value: 'No Polaris audit results found.' }]} />
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const namespaces = getNamespaces(data);
|
||||
const rows: NamespaceRow[] = namespaces.map(ns => {
|
||||
const results = filterResultsByNamespace(data, ns);
|
||||
const counts = countResultsForItems(results);
|
||||
const score = computeScore(counts);
|
||||
return {
|
||||
namespace: ns,
|
||||
score,
|
||||
pass: counts.pass,
|
||||
warning: counts.warning,
|
||||
danger: counts.danger,
|
||||
skipped: counts.skipped,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Polaris — Namespaces" />
|
||||
<SectionBox>
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{
|
||||
label: 'Namespace',
|
||||
getter: (row: NamespaceRow) => (
|
||||
<button
|
||||
onClick={() => openNamespace(row.namespace)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: theme.palette.primary.main,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline',
|
||||
padding: 0,
|
||||
font: 'inherit',
|
||||
}}
|
||||
>
|
||||
{row.namespace}
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Score',
|
||||
getter: (row: NamespaceRow) => (
|
||||
<StatusLabel status={scoreStatus(row.score)}>{row.score}%</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Pass',
|
||||
getter: (row: NamespaceRow) => <StatusLabel status="success">{row.pass}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
label: 'Warning',
|
||||
getter: (row: NamespaceRow) => (
|
||||
<StatusLabel status="warning">{row.warning}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
getter: (row: NamespaceRow) => <StatusLabel status="error">{row.danger}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
label: 'Skipped',
|
||||
getter: (row: NamespaceRow) => String(row.skipped),
|
||||
},
|
||||
]}
|
||||
data={rows}
|
||||
emptyMessage="No namespaces found in Polaris audit data."
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<Drawer anchor="right" open={Boolean(selectedNamespace)} onClose={closeNamespace}>
|
||||
{selectedNamespace && (
|
||||
<NamespaceDetailPanel namespace={selectedNamespace} onClose={closeNamespace} />
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock Headlamp lib
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
useTheme: () => ({
|
||||
palette: {
|
||||
primary: { main: '#1976d2', contrastText: '#fff' },
|
||||
text: { primary: '#000', secondary: '#666' },
|
||||
action: { disabledBackground: '#e0e0e0', disabled: '#9e9e9e' },
|
||||
divider: '#e0e0e0',
|
||||
background: { paper: '#fff' },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock Headlamp CommonComponents
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => (
|
||||
<div data-testid="section-box" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
NameValueTable: ({ rows }: { rows: Array<{ name: string; value: React.ReactNode }> }) => (
|
||||
<div data-testid="name-value-table">
|
||||
{rows.map(row => (
|
||||
<div key={row.name}>
|
||||
<span>{row.name}</span>
|
||||
<span>{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
import PolarisSettings from './PolarisSettings';
|
||||
|
||||
describe('PolarisSettings', () => {
|
||||
it('renders with interval from props.data', () => {
|
||||
render(<PolarisSettings data={{ refreshInterval: 60 }} />);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
expect(select).toHaveValue('60');
|
||||
});
|
||||
|
||||
it('falls back to getRefreshInterval when no prop data', () => {
|
||||
// Default is 300 (5 minutes)
|
||||
render(<PolarisSettings />);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
expect(select).toHaveValue('300');
|
||||
});
|
||||
|
||||
it('renders all interval options', () => {
|
||||
render(<PolarisSettings />);
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]).toHaveTextContent('1 minute');
|
||||
expect(options[1]).toHaveTextContent('5 minutes');
|
||||
expect(options[2]).toHaveTextContent('10 minutes');
|
||||
expect(options[3]).toHaveTextContent('30 minutes');
|
||||
});
|
||||
|
||||
it('calls setRefreshInterval and onDataChange when selection changes', async () => {
|
||||
const onDataChange = vi.fn();
|
||||
render(<PolarisSettings data={{ refreshInterval: 300 }} onDataChange={onDataChange} />);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
await userEvent.selectOptions(select, '1800');
|
||||
|
||||
// Check localStorage was updated
|
||||
expect(localStorage.getItem('polaris-plugin-refresh-interval')).toBe('1800');
|
||||
|
||||
// Check callback was called with merged data
|
||||
expect(onDataChange).toHaveBeenCalledWith({ refreshInterval: 1800 });
|
||||
});
|
||||
|
||||
it('works without onDataChange callback', async () => {
|
||||
render(<PolarisSettings data={{ refreshInterval: 300 }} />);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
// Should not throw even without onDataChange
|
||||
await userEvent.selectOptions(select, '60');
|
||||
|
||||
expect(localStorage.getItem('polaris-plugin-refresh-interval')).toBe('60');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import React from 'react';
|
||||
import {
|
||||
AuditData,
|
||||
getDashboardUrl,
|
||||
getPolarisApiPath,
|
||||
getRefreshInterval,
|
||||
INTERVAL_OPTIONS,
|
||||
isFullUrl,
|
||||
setDashboardUrl,
|
||||
setRefreshInterval,
|
||||
} from '../api/polaris';
|
||||
|
||||
interface PluginSettingsProps {
|
||||
data?: { [key: string]: string | number | boolean };
|
||||
onDataChange?: (data: { [key: string]: string | number | boolean }) => void;
|
||||
}
|
||||
|
||||
export default function PolarisSettings(props: PluginSettingsProps) {
|
||||
const theme = useTheme();
|
||||
const { data, onDataChange } = props;
|
||||
const currentInterval = (data?.refreshInterval as number) ?? getRefreshInterval();
|
||||
const currentUrl = (data?.dashboardUrl as string) ?? getDashboardUrl();
|
||||
const [testing, setTesting] = React.useState(false);
|
||||
const [testResult, setTestResult] = React.useState<{ success: boolean; message: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
function handleIntervalChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const seconds = Number(e.target.value);
|
||||
setRefreshInterval(seconds);
|
||||
onDataChange?.({ ...data, refreshInterval: seconds });
|
||||
}
|
||||
|
||||
function handleUrlChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const url = e.target.value;
|
||||
setDashboardUrl(url);
|
||||
onDataChange?.({ ...data, dashboardUrl: url });
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
const apiPath = getPolarisApiPath();
|
||||
|
||||
let result: AuditData;
|
||||
|
||||
if (isFullUrl(apiPath)) {
|
||||
const response = await fetch(apiPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
} else {
|
||||
result = await ApiProxy.request(apiPath, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `Connected successfully! Version: ${
|
||||
result.PolarisOutputVersion
|
||||
}, Last audit: ${new Date(result.AuditTime).toLocaleString()}`,
|
||||
});
|
||||
} catch (err) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: `Connection failed: ${String(err)}`,
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionBox title="Polaris Settings">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Refresh Interval',
|
||||
value: (
|
||||
<select value={currentInterval} onChange={handleIntervalChange}>
|
||||
{INTERVAL_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Dashboard URL',
|
||||
value: (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUrl}
|
||||
onChange={handleUrlChange}
|
||||
placeholder="/api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px 8px',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: theme.palette.text.secondary,
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
Examples:
|
||||
<br />• K8s proxy:{' '}
|
||||
<code>/api/v1/namespaces/polaris/services/http:polaris-dashboard:80/proxy/</code>
|
||||
<br />• Full URL: <code>https://my-polaris.example.com</code>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Connection Test',
|
||||
value: (
|
||||
<div>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={testing}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: testing
|
||||
? theme.palette.action.disabledBackground
|
||||
: theme.palette.primary.main,
|
||||
color: testing
|
||||
? theme.palette.action.disabled
|
||||
: theme.palette.primary.contrastText,
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: testing ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{testing ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
{testResult && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<StatusLabel status={testResult.success ? 'success' : 'error'}>
|
||||
{testResult.message}
|
||||
</StatusLabel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import {
|
||||
Loader,
|
||||
SectionBox,
|
||||
SectionHeader,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import {
|
||||
AuditData,
|
||||
countResults,
|
||||
getRefreshInterval,
|
||||
ResultCounts,
|
||||
setRefreshInterval,
|
||||
usePolarisData,
|
||||
} from '../api/polaris';
|
||||
|
||||
const INTERVAL_OPTIONS = [
|
||||
{ label: '1 minute', value: 60 },
|
||||
{ label: '5 minutes', value: 300 },
|
||||
{ label: '10 minutes', value: 600 },
|
||||
{ label: '30 minutes', value: 1800 },
|
||||
];
|
||||
|
||||
function RefreshSettings(props: {
|
||||
interval: number;
|
||||
onChange: (seconds: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<label htmlFor="polaris-refresh-interval">Refresh interval:</label>
|
||||
<select
|
||||
id="polaris-refresh-interval"
|
||||
value={props.interval}
|
||||
onChange={(e) => props.onChange(Number(e.target.value))}
|
||||
>
|
||||
{INTERVAL_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard(props: { label: string; value: number; color?: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 24px',
|
||||
textAlign: 'center',
|
||||
minWidth: '120px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
color: props.color,
|
||||
}}
|
||||
>
|
||||
{props.value}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', opacity: 0.8 }}>{props.label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScoreBadge(props: { score: number }) {
|
||||
const color = props.score >= 80 ? '#4caf50' : props.score >= 50 ? '#ff9800' : '#f44336';
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '3rem', fontWeight: 'bold', color }}>
|
||||
{props.score}%
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', opacity: 0.8 }}>Cluster Score</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
||||
return (
|
||||
<>
|
||||
<SectionBox title="Score">
|
||||
<ScoreBadge score={props.data.Score} />
|
||||
</SectionBox>
|
||||
<SectionBox title="Check Summary">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<StatCard label="Total" value={props.counts.total} />
|
||||
<StatCard label="Pass" value={props.counts.pass} color="#4caf50" />
|
||||
<StatCard label="Warning" value={props.counts.warning} color="#ff9800" />
|
||||
<StatCard label="Danger" value={props.counts.danger} color="#f44336" />
|
||||
</div>
|
||||
</SectionBox>
|
||||
<SectionBox title="Cluster Info">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<StatCard label="Nodes" value={props.data.ClusterInfo.Nodes} />
|
||||
<StatCard label="Pods" value={props.data.ClusterInfo.Pods} />
|
||||
<StatCard label="Namespaces" value={props.data.ClusterInfo.Namespaces} />
|
||||
<StatCard label="Controllers" value={props.data.ClusterInfo.Controllers} />
|
||||
</div>
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PolarisView() {
|
||||
const [interval, setInterval] = React.useState(getRefreshInterval);
|
||||
|
||||
function handleIntervalChange(seconds: number) {
|
||||
setInterval(seconds);
|
||||
setRefreshInterval(seconds);
|
||||
}
|
||||
|
||||
const { data, loading, error } = usePolarisData(interval);
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading Polaris audit data..." />;
|
||||
}
|
||||
|
||||
const counts = data ? countResults(data) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Polaris" actions={[
|
||||
<RefreshSettings
|
||||
key="refresh"
|
||||
interval={interval}
|
||||
onChange={handleIntervalChange}
|
||||
/>,
|
||||
]} />
|
||||
|
||||
{error && (
|
||||
<SectionBox title="Error">
|
||||
<div style={{ padding: '16px', color: '#f44336' }}>{error}</div>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{data && counts && <OverviewSection data={data} counts={counts} />}
|
||||
|
||||
{!data && !error && (
|
||||
<SectionBox title="No Data">
|
||||
<div style={{ padding: '16px' }}>
|
||||
No Polaris audit results found.
|
||||
</div>
|
||||
</SectionBox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+109
-3
@@ -1,9 +1,48 @@
|
||||
import {
|
||||
registerAppBarAction,
|
||||
registerDetailsViewSection,
|
||||
registerPluginSettings,
|
||||
registerRoute,
|
||||
registerSidebarEntry,
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import PolarisView from './components/PolarisView';
|
||||
import { PolarisDataProvider } from './api/PolarisDataContext';
|
||||
import AppBarScoreBadge from './components/AppBarScoreBadge';
|
||||
import DashboardView from './components/DashboardView';
|
||||
import InlineAuditSection from './components/InlineAuditSection';
|
||||
import NamespacesListView from './components/NamespacesListView';
|
||||
import PolarisSettings from './components/PolarisSettings';
|
||||
|
||||
// --- Error boundary for plugin components ---
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
class PolarisErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode },
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { error: error.message };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<SectionBox title="Polaris Plugin Error">
|
||||
<StatusLabel status="error">{this.state.error}</StatusLabel>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sidebar entries ---
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: null,
|
||||
@@ -13,10 +52,77 @@ registerSidebarEntry({
|
||||
icon: 'mdi:shield-check',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'polaris',
|
||||
name: 'polaris-overview',
|
||||
label: 'Overview',
|
||||
url: '/polaris',
|
||||
icon: 'mdi:view-dashboard',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'polaris',
|
||||
name: 'polaris-namespaces',
|
||||
label: 'Namespaces',
|
||||
url: '/polaris/namespaces',
|
||||
icon: 'mdi:dns',
|
||||
});
|
||||
|
||||
// --- Routes ---
|
||||
|
||||
registerRoute({
|
||||
path: '/polaris',
|
||||
sidebar: 'polaris',
|
||||
sidebar: 'polaris-overview',
|
||||
name: 'polaris',
|
||||
exact: true,
|
||||
component: () => <PolarisView />,
|
||||
component: () => (
|
||||
<PolarisErrorBoundary>
|
||||
<PolarisDataProvider>
|
||||
<DashboardView />
|
||||
</PolarisDataProvider>
|
||||
</PolarisErrorBoundary>
|
||||
),
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/polaris/namespaces',
|
||||
sidebar: 'polaris-namespaces',
|
||||
name: 'polaris-namespaces',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<PolarisErrorBoundary>
|
||||
<PolarisDataProvider>
|
||||
<NamespacesListView />
|
||||
</PolarisDataProvider>
|
||||
</PolarisErrorBoundary>
|
||||
),
|
||||
});
|
||||
|
||||
// Register plugin settings
|
||||
registerPluginSettings('headlamp-polaris', PolarisSettings, true);
|
||||
|
||||
// Register details view section for supported controller types
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
const supportedKinds = ['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob'];
|
||||
|
||||
if (!supportedKinds.includes(resource?.kind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PolarisErrorBoundary>
|
||||
<PolarisDataProvider>
|
||||
<InlineAuditSection resource={resource} />
|
||||
</PolarisDataProvider>
|
||||
</PolarisErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
// Register app bar score badge
|
||||
registerAppBarAction(() => (
|
||||
<PolarisErrorBoundary>
|
||||
<PolarisDataProvider>
|
||||
<AppBarScoreBadge />
|
||||
</PolarisDataProvider>
|
||||
</PolarisErrorBoundary>
|
||||
));
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AuditData, Result } from './api/polaris';
|
||||
|
||||
// --- Fixtures ---
|
||||
|
||||
export function makeResult(overrides: Partial<Result> = {}): Result {
|
||||
return {
|
||||
Name: 'my-deploy',
|
||||
Namespace: 'default',
|
||||
Kind: 'Deployment',
|
||||
Results: {},
|
||||
CreatedTime: '2025-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeAuditData(results: Result[]): AuditData {
|
||||
return {
|
||||
PolarisOutputVersion: '1.0',
|
||||
AuditTime: '2025-01-01T00:00:00Z',
|
||||
SourceType: 'Cluster',
|
||||
SourceName: 'test',
|
||||
DisplayName: 'test',
|
||||
ClusterInfo: { Version: '1.28', Nodes: 3, Pods: 10, Namespaces: 2, Controllers: 5 },
|
||||
Results: results,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"test"',
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
exclude: ['e2e/**', 'node_modules/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.{ts,tsx}'],
|
||||
exclude: ['src/**/*.test.{ts,tsx}', 'src/test-utils.tsx', 'src/index.tsx'],
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Node 22+ ships a minimal built-in `localStorage` global (property-bag only,
|
||||
// no getItem/setItem/removeItem/clear) that shadows jsdom's Web Storage
|
||||
// implementation. Provide a spec-compliant shim so code under test works.
|
||||
if (typeof localStorage !== 'undefined' && typeof localStorage.getItem !== 'function') {
|
||||
const store = new Map<string, string>();
|
||||
|
||||
const storage = {
|
||||
getItem(key: string): string | null {
|
||||
return store.get(key) ?? null;
|
||||
},
|
||||
setItem(key: string, value: string): void {
|
||||
store.set(key, String(value));
|
||||
},
|
||||
removeItem(key: string): void {
|
||||
store.delete(key);
|
||||
},
|
||||
clear(): void {
|
||||
store.clear();
|
||||
},
|
||||
get length(): number {
|
||||
return store.size;
|
||||
},
|
||||
key(index: number): string | null {
|
||||
return [...store.keys()][index] ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: storage,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: storage,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user