Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63f2b1ae56 | |||
| 74110f1704 | |||
| 2666042318 | |||
| 7a0c068a93 | |||
| 2d629809a2 | |||
| 3fe787a550 | |||
| aa1db9215a | |||
| 202ce66c61 | |||
| 58c9597388 | |||
| dff1265435 | |||
| 7c58826668 | |||
| 4edc829b3f | |||
| 8f10be39bd | |||
| 27212a91e1 | |||
| 7b72306133 | |||
| e16e6255d0 | |||
| 4beb0c4d0e | |||
| 175d3ec6a2 | |||
| e63cd03267 | |||
| 4d878c8737 | |||
| 5f817ec4f6 | |||
| 490807cef6 | |||
| 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 |
@@ -0,0 +1,20 @@
|
||||
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
|
||||
with:
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
+153
-45
@@ -7,63 +7,159 @@ on:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Only one E2E run at a time: the shared E2E_RELEASE (headlamp-e2e) in
|
||||
# headlamp-dev cannot be shared across concurrent runs.
|
||||
# cancel-in-progress: false (queue, don't cancel) — cancelling in-flight
|
||||
# runs may skip the if:always() teardown, leaving dangling cluster resources.
|
||||
concurrency:
|
||||
group: e2e-${{ github.repository }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
E2E_NAMESPACE: headlamp-dev
|
||||
E2E_RELEASE: headlamp-e2e
|
||||
# Pin to a known-good Headlamp version. Using :latest is risky because
|
||||
# the tag can change between CI runs, causing flaky failures when a newer
|
||||
# image is pulled on some nodes but not others (IfNotPresent pull policy).
|
||||
# Update this when Headlamp is upgraded in production (kube-system).
|
||||
HEADLAMP_VERSION: v0.40.1
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: local-ubuntu-latest
|
||||
runs-on: runners-privilegedescalation
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v4
|
||||
|
||||
- name: Get kubeconfig
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=== Runner environment diagnostic ==="
|
||||
echo "HOME=${HOME:-}"
|
||||
echo "KUBECONFIG=${KUBECONFIG:-}"
|
||||
echo "ACTIONS_KUBECONFIG=${ACTIONS_KUBECONFIG:-}"
|
||||
echo "RUNNER_CONFIG=${RUNNER_CONFIG:-}"
|
||||
echo "RUNNER_CONFIG_DIR=${RUNNER_CONFIG_DIR:-}"
|
||||
echo ""
|
||||
echo "=== Checking known kubeconfig locations ==="
|
||||
for path in /runner/config /home/runner/.kube/config "${HOME:-}/.kube/config" "${HOME:-}/.kube"; do
|
||||
if [ -f "$path" ]; then
|
||||
echo "FOUND kubeconfig at: $path"
|
||||
elif [ -d "$path" ]; then
|
||||
echo "DIR exists at: $path, contents:"
|
||||
ls -la "$path" 2>&1 || echo " (cannot list)"
|
||||
else
|
||||
echo "NOT FOUND: $path"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "=== In-cluster service account check ==="
|
||||
in_cluster=false
|
||||
if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then
|
||||
echo "Service account token present — in-cluster mode available"
|
||||
echo "KUBERNETES_SERVICE_HOST=${KUBERNETES_SERVICE_HOST:-}"
|
||||
echo "KUBERNETES_SERVICE_PORT=${KUBERNETES_SERVICE_PORT:-}"
|
||||
in_cluster=true
|
||||
else
|
||||
echo "No service account token at /var/run/secrets/kubernetes.io/serviceaccount/"
|
||||
fi
|
||||
echo ""
|
||||
if [ -f /runner/config ]; then
|
||||
echo "KUBECONFIG=/runner/config" >> "$GITHUB_ENV"
|
||||
echo "Using kubeconfig from /runner/config"
|
||||
elif [ -f /home/runner/.kube/config ]; then
|
||||
echo "KUBECONFIG=/home/runner/.kube/config" >> "$GITHUB_ENV"
|
||||
echo "Using kubeconfig from /home/runner/.kube/config"
|
||||
elif [ -f "${HOME:-}/.kube/config" ]; then
|
||||
echo "KUBECONFIG=${HOME:-}/.kube/config" >> "$GITHUB_ENV"
|
||||
echo "Using kubeconfig from HOME"
|
||||
elif [ "$in_cluster" = true ]; then
|
||||
echo "No static kubeconfig found — generating in-cluster kubeconfig"
|
||||
KUBECFG_DIR="${HOME:-}/.kube"
|
||||
mkdir -p "$KUBECFG_DIR"
|
||||
kubectl config set-cluster in-cluster \
|
||||
--server="https://${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc}:${KUBERNETES_SERVICE_PORT:-443}" \
|
||||
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
|
||||
--embed-certs=true \
|
||||
--kubeconfig="$KUBECFG_DIR/config" 2>&1
|
||||
kubectl config set-credentials in-cluster \
|
||||
--token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
|
||||
--kubeconfig="$KUBECFG_DIR/config" 2>&1
|
||||
kubectl config set-context in-cluster \
|
||||
--cluster=in-cluster \
|
||||
--user=in-cluster \
|
||||
--kubeconfig="$KUBECFG_DIR/config" 2>&1
|
||||
kubectl config use-context in-cluster \
|
||||
--kubeconfig="$KUBECFG_DIR/config" 2>&1
|
||||
echo "KUBECONFIG=$KUBECFG_DIR/config" >> "$GITHUB_ENV"
|
||||
echo "Generated in-cluster kubeconfig at $KUBECFG_DIR/config"
|
||||
else
|
||||
echo "::error::No kubeconfig found in /runner/config, /home/runner/.kube/config, HOME, or in-cluster service account"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Apply RBAC for E2E pipeline
|
||||
run: |
|
||||
set -x
|
||||
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml --dry-run=server 2>&1 || true
|
||||
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml 2>&1
|
||||
echo "exit code: $?"
|
||||
echo "Waiting for RBAC propagation..."
|
||||
sleep 5
|
||||
echo "Verifying RBAC resources were created..."
|
||||
kubectl get role e2e-ci-runner -n headlamp-dev 2>&1 | tail -3
|
||||
kubectl get role e2e-ci-runner-polaris -n headlamp-dev 2>&1 | tail -3
|
||||
kubectl get rolebinding e2e-ci-runner-binding -n headlamp-dev 2>&1 | tail -3
|
||||
set +x
|
||||
|
||||
- name: Apply Polaris dashboard RBAC
|
||||
run: kubectl apply -f deployment/polaris-rbac.yaml
|
||||
|
||||
- name: RBAC pre-flight check
|
||||
run: |
|
||||
echo "Checking RBAC resources..."
|
||||
MISSING=0
|
||||
kubectl get role polaris-dashboard-proxy-reader -n polaris -o name >/dev/null 2>&1 || MISSING=1
|
||||
kubectl get rolebinding polaris-dashboard-proxy-reader -n polaris -o name >/dev/null 2>&1 || MISSING=1
|
||||
kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" 2>/dev/null || MISSING=1
|
||||
if [ "$MISSING" -eq 0 ]; then
|
||||
echo "RBAC pre-flight check passed."
|
||||
else
|
||||
echo "::error::RBAC pre-flight check failed. Missing required permissions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Preflight — verify Headlamp and plugin version
|
||||
env:
|
||||
HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }}
|
||||
- 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: |
|
||||
EXPECTED=$(node -p "require('./package.json').version")
|
||||
PLUGIN_NAME=$(node -p "require('./package.json').artifacthub?.name || require('./package.json').name")
|
||||
echo "Expected: $PLUGIN_NAME@$EXPECTED"
|
||||
|
||||
# Check Headlamp connectivity
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 10 "$HEADLAMP_URL" || true)
|
||||
if [ "$HTTP_CODE" = "000" ]; then
|
||||
echo "::error::Cannot reach Headlamp at $HEADLAMP_URL"
|
||||
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
|
||||
echo "Headlamp responded HTTP $HTTP_CODE"
|
||||
|
||||
# Check installed plugins and version match
|
||||
PLUGIN_JSON=$(curl -sf --connect-timeout 10 "$HEADLAMP_URL/plugins" 2>/dev/null || echo "[]")
|
||||
node -e "
|
||||
const expected = '$EXPECTED';
|
||||
const pluginName = '$PLUGIN_NAME';
|
||||
const plugins = JSON.parse(process.argv[1]);
|
||||
console.log('Installed plugins:');
|
||||
for (const p of plugins) console.log(' ' + p.name + '@' + (p.version||'unknown'));
|
||||
const ours = plugins.find(p => p.name === pluginName || p.name === 'polaris' || p.name.includes('polaris'));
|
||||
if (!ours) {
|
||||
console.log('::warning::Plugin ' + pluginName + ' not found in Headlamp — data-dependent tests will fail');
|
||||
} else {
|
||||
console.log('Found plugin: ' + ours.name + ' at path ' + ours.path);
|
||||
}
|
||||
" "$PLUGIN_JSON"
|
||||
|
||||
# Fetch deployed plugin version from package.json
|
||||
DEPLOYED_VERSION=$(curl -sf --connect-timeout 10 "$HEADLAMP_URL/plugins/$PLUGIN_NAME/package.json" 2>/dev/null \
|
||||
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "unknown")
|
||||
echo "Deployed version: $DEPLOYED_VERSION"
|
||||
if [ "$DEPLOYED_VERSION" != "$EXPECTED" ] && [ "$DEPLOYED_VERSION" != "unknown" ]; then
|
||||
echo "::warning::Version mismatch — repo has $EXPECTED but Headlamp runs $DEPLOYED_VERSION. Tests may fail due to stale plugin."
|
||||
fi
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
@@ -71,13 +167,25 @@ jobs:
|
||||
- name: Run E2E tests
|
||||
run: npm run e2e
|
||||
env:
|
||||
HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'http://headlamp.kube-system.svc.cluster.local' }}
|
||||
HEADLAMP_TOKEN: ${{ secrets.HEADLAMP_TOKEN }}
|
||||
AUTHENTIK_USERNAME: ${{ secrets.AUTHENTIK_USERNAME }}
|
||||
AUTHENTIK_PASSWORD: ${{ secrets.AUTHENTIK_PASSWORD }}
|
||||
HEADLAMP_URL: ${{ env.HEADLAMP_URL }}
|
||||
HEADLAMP_TOKEN: ${{ env.HEADLAMP_TOKEN }}
|
||||
|
||||
- name: Collect deployment diagnostics on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Pod state ==="
|
||||
kubectl get pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true
|
||||
echo "=== Pod describe ==="
|
||||
kubectl describe pods -n "$E2E_NAMESPACE" -l "app.kubernetes.io/instance=$E2E_RELEASE" 2>&1 || true
|
||||
echo "=== Recent namespace events ==="
|
||||
kubectl get events -n "$E2E_NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -20 || true
|
||||
|
||||
- name: Teardown E2E instance
|
||||
if: always()
|
||||
run: scripts/teardown-e2e-headlamp.sh
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: playwright-report
|
||||
@@ -85,7 +193,7 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results
|
||||
|
||||
@@ -15,6 +15,9 @@ permissions:
|
||||
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'
|
||||
|
||||
@@ -6,5 +6,6 @@ e2e/.auth/
|
||||
test-results/
|
||||
.playwright-mcp/
|
||||
.env
|
||||
.env.e2e
|
||||
.env.local
|
||||
.eslintcache
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"config": {
|
||||
// Line length — not enforced for docs with code examples
|
||||
"MD013": false,
|
||||
// First line heading — files use YAML frontmatter, not headings
|
||||
"MD041": false,
|
||||
// Emphasis as heading — common pattern for Option 1/2/3 sections
|
||||
"MD036": false,
|
||||
// No duplicate heading — changelog files repeat section names intentionally
|
||||
"MD024": false,
|
||||
// Fenced code language — not always applicable for diagram blocks
|
||||
"MD040": false,
|
||||
// Table column style — table alignment is visual, not semantic
|
||||
"MD060": false,
|
||||
// Ordered list item prefix — number resets are intentional in documents
|
||||
"MD029": false,
|
||||
// No inline HTML — each elements are valid in valid Markdown
|
||||
"MD033": false,
|
||||
// List marker space — spacing after list markers varies by editor
|
||||
"MD030": false,
|
||||
// Blanks around headings — not always needed in compact docs
|
||||
"MD022": false,
|
||||
// Blanks around lists — not always needed in compact docs
|
||||
"MD032": false,
|
||||
// Blanks around fences — not always needed between adjacent blocks
|
||||
"MD031": false,
|
||||
// Multiple blanks — editor artifacts, not semantic
|
||||
"MD012": false,
|
||||
// Single title — files may have multiple H1 sections
|
||||
"MD025": false,
|
||||
// Trailing spaces — editor artifacts
|
||||
"MD009": false,
|
||||
// Bare URLs — URL shortening not always needed
|
||||
"MD034": false,
|
||||
// Single trailing newline — editor artifacts
|
||||
"MD047": false,
|
||||
// Trailing punctuation — heading punctuation is intentional
|
||||
"MD026": false,
|
||||
// Space in emphasis — double-asterisk bold spacing varies by renderer
|
||||
"MD037": false,
|
||||
// No hard tabs — some generated docs use tabs for indentation
|
||||
"MD010": false,
|
||||
// Code block style — generated docs may use inconsistent styles
|
||||
"MD046": false,
|
||||
// Comment style — generated docs have no comments
|
||||
"MD048": false,
|
||||
// Commands show output — shell examples intentionally show only commands
|
||||
"MD014": false
|
||||
},
|
||||
"ignores": [
|
||||
"docs/api-reference/generated/**"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
docs/api-reference/generated/**
|
||||
+27
-1
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
@@ -270,7 +295,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Automated release workflow
|
||||
- Basic CI/CD pipeline
|
||||
|
||||
[Unreleased]: https://github.com/privilegedescalation/headlamp-polaris-plugin/compare/v0.6.0...HEAD
|
||||
[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
|
||||
|
||||
@@ -229,7 +229,7 @@ Headlamp v0.39.0 with default `watchPlugins: true` treats catalog-managed plugin
|
||||
**Action Items:**
|
||||
- [ ] Parallelize test execution
|
||||
- [ ] Add npm cache to GitHub Actions
|
||||
- [ ] Integrate Dependabot
|
||||
- [x] Renovate is configured org-wide via `github>privilegedescalation/.github:renovate-config`
|
||||
- [ ] Add semantic-release
|
||||
|
||||
---
|
||||
|
||||
@@ -48,9 +48,14 @@ Polaris must be deployed in the `polaris` namespace with the dashboard component
|
||||
|
||||
## Installing
|
||||
|
||||
### Option 1: Headlamp Plugin Manager (Recommended)
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin). Install via the Headlamp UI:
|
||||
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin). Configure Headlamp via Helm:
|
||||
1. Go to **Settings → Plugins**
|
||||
2. Click **Catalog** tab
|
||||
3. Search for "Polaris"
|
||||
4. Click **Install**
|
||||
|
||||
Or configure Headlamp via Helm:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
@@ -62,56 +67,6 @@ pluginsManager:
|
||||
url: https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
```
|
||||
|
||||
Or install via the Headlamp UI:
|
||||
|
||||
1. Go to **Settings → Plugins**
|
||||
2. Click **Catalog** tab
|
||||
3. Search for "Polaris"
|
||||
4. Click **Install**
|
||||
|
||||
### Option 2: Sidecar Container (Alternative)
|
||||
|
||||
For detailed sidecar installation instructions, see [docs/DEPLOYMENT.md#installation-method-2-sidecar-container](docs/DEPLOYMENT.md#installation-method-2-sidecar-container).
|
||||
|
||||
```yaml
|
||||
sidecars:
|
||||
- name: headlamp-plugin
|
||||
image: node:lts-alpine
|
||||
command: ['/bin/sh']
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml
|
||||
tail -f /dev/null
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /headlamp/plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
```
|
||||
|
||||
### Option 3: Manual Tarball Install
|
||||
|
||||
Download the `.tar.gz` from the [GitHub releases page](https://github.com/privilegedescalation/headlamp-polaris-plugin/releases), then extract into Headlamp's plugin directory:
|
||||
|
||||
```bash
|
||||
wget https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.3.10/polaris-0.3.10.tar.gz
|
||||
tar xzf polaris-0.3.10.tar.gz -C /headlamp/plugins/
|
||||
```
|
||||
|
||||
### Option 4: Build from Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/privilegedescalation/headlamp-polaris-plugin.git
|
||||
cd headlamp-polaris-plugin
|
||||
npm install
|
||||
npm run build
|
||||
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||
```
|
||||
|
||||
For complete installation instructions including Helm integration, FluxCD examples, and production deployment checklist, see **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)**.
|
||||
|
||||
## 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:
|
||||
@@ -142,7 +97,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp # adjust to match your Headlamp service account
|
||||
namespace: kube-system # adjust to match the namespace Headlamp runs in
|
||||
namespace: headlamp # adjust to match the namespace Headlamp runs in
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -242,7 +197,7 @@ npm test
|
||||
npm run test:watch
|
||||
|
||||
# E2E tests (Playwright)
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n headlamp --duration=24h)
|
||||
npm run e2e
|
||||
npm run e2e:headed # see browser
|
||||
```
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -212,7 +212,7 @@ If you discover a security vulnerability in this plugin, please report it via:
|
||||
|
||||
The project uses:
|
||||
- **npm audit**: Runs automatically during `npm install`
|
||||
- **Dependabot**: GitHub Dependabot monitors dependencies and creates PRs for updates
|
||||
- **Renovate**: Automated dependency updates via Mend Renovate (org-wide configured)
|
||||
- **GitHub Actions**: CI workflow runs `npm audit` on every commit
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# PRI-324 Spec: Make E2E Workflow Self-Sufficient with RBAC
|
||||
|
||||
## Context
|
||||
|
||||
PR #123 introduced an RBAC pre-flight check to the E2E workflow. QA (Nancy, acting as QA) verified the "fails fast without RBAC" path works, but found that the "with RBAC passes" path had no green CI evidence — the workflow did not apply RBAC before the pre-flight check.
|
||||
|
||||
PR #131 attempted to fix this by adding `kubectl apply` steps and extending the CI runner RBAC, but its merge commit (739db6fe) was reverted by the next commit on main (aa1db921) due to a vulnerability fix PR (#128).
|
||||
|
||||
The current E2E workflow on `main` lacks the RBAC apply steps and CI runner permissions needed to make the pre-flight check meaningful.
|
||||
|
||||
## Required Changes
|
||||
|
||||
### 1. `.github/workflows/e2e.yaml`
|
||||
|
||||
Add between the "Setup kubectl" and "Install dependencies" steps:
|
||||
|
||||
```yaml
|
||||
- name: Apply RBAC for E2E pipeline
|
||||
run: |
|
||||
set -x
|
||||
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml --dry-run=server 2>&1 || true
|
||||
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml 2>&1
|
||||
echo "exit code: $?"
|
||||
echo "Waiting for RBAC propagation..."
|
||||
sleep 5
|
||||
echo "Verifying CI runner permissions..."
|
||||
kubectl auth can-i create roles -n headlamp-dev --as="system:serviceaccount:arc-runners:runners-privilegedescalation-gha-rs-no-permission" 2>&1 || { echo "::error::CI runner still lacks roles permission after propagation wait"; exit 1; }
|
||||
set +x
|
||||
|
||||
- name: Apply Polaris dashboard RBAC
|
||||
run: kubectl apply -f deployment/polaris-rbac.yaml
|
||||
|
||||
- name: RBAC pre-flight check
|
||||
run: |
|
||||
echo "Checking RBAC resources..."
|
||||
MISSING=0
|
||||
kubectl get role polaris-dashboard-proxy-reader -n polaris -o name >/dev/null 2>&1 || MISSING=1
|
||||
kubectl get rolebinding polaris-dashboard-proxy-reader -n polaris -o name >/dev/null 2>&1 || MISSING=1
|
||||
kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" --quiet 2>/dev/null || MISSING=1
|
||||
if [ "$MISSING" -eq 0 ]; then
|
||||
echo "RBAC pre-flight check passed."
|
||||
else
|
||||
echo "::error::RBAC pre-flight check failed. Missing required permissions."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. `deployment/e2e-ci-runner-rbac.yaml`
|
||||
|
||||
Add a new Role + RoleBinding for the `polaris` namespace (from PR #131):
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: e2e-ci-runner-polaris
|
||||
namespace: polaris
|
||||
rules:
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["roles", "rolebindings"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: e2e-ci-runner-polaris
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: runners-privilegedescalation-gha-rs-no-permission
|
||||
namespace: arc-runners
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: e2e-ci-runner-polaris
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
And add to the existing `e2e-ci-runner` Role in the `headlamp-dev` namespace:
|
||||
```yaml
|
||||
# Apply Polaris dashboard RBAC in the polaris namespace
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["roles", "rolebindings"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Workflow applies `deployment/e2e-ci-runner-rbac.yaml` before the pre-flight check
|
||||
- [ ] Workflow applies `deployment/polaris-rbac.yaml` before the pre-flight check
|
||||
- [ ] CI runner has RBAC to apply the manifests (added via new Role+RoleBinding in polaris namespace)
|
||||
- [ ] E2E pipeline passes on the PR branch (proof of green path)
|
||||
- [ ] `kubectl get … --quiet` flag removed (QA nit)
|
||||
- [ ] `MISSING_ROLE`/`MISSING_ROLEBINDING` collapsed to single `MISSING` flag (QA nit)
|
||||
|
||||
## Definition of Done
|
||||
|
||||
PR #123 QA changes-requested are addressed: the workflow is self-sufficient (applies its own RBAC), the green path is demonstrated, and QA review is re-requested.
|
||||
+46
-4
@@ -1,4 +1,4 @@
|
||||
version: "0.7.0"
|
||||
version: "1.0.0"
|
||||
name: headlamp-polaris
|
||||
displayName: Polaris
|
||||
createdAt: "2026-02-05T19:00:00Z"
|
||||
@@ -11,6 +11,7 @@ description: >-
|
||||
`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
|
||||
@@ -24,11 +25,52 @@ links:
|
||||
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/v0.7.0/headlamp-polaris-0.7.0.tar.gz"
|
||||
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:69b7adf74416d72580981387b1861e2b5fcf7f96ae9f0620f85f623f8b6dab56
|
||||
headlamp/plugin/distro-compat: in-cluster
|
||||
headlamp/plugin/archive-checksum: sha256:a165e871b40f11a44950aa9f10eb7f7883276f749026ae7a4f886278ecd9bd7d
|
||||
headlamp/plugin/distro-compat: "in-cluster,web,desktop"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
# 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 headlamp-dev
|
||||
# namespace to deploy and tear down a dedicated Headlamp instance via Helm.
|
||||
# E2E resources run in `headlamp-dev` — nothing persists beyond a test run.
|
||||
#
|
||||
# Plugin is loaded via ConfigMap volume mount — no custom Docker images.
|
||||
#
|
||||
# Note: This RBAC is mirrored in privilegedescalation/infra (base/rbac/)
|
||||
# and managed by Flux GitOps. The infra repo is the source of truth.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: e2e-ci-runner
|
||||
namespace: headlamp-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", "events"]
|
||||
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"]
|
||||
# Apply Polaris dashboard RBAC in the polaris namespace
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["roles", "rolebindings"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: e2e-ci-runner-polaris
|
||||
namespace: polaris
|
||||
rules:
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["roles", "rolebindings"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: e2e-ci-runner-polaris
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: runners-privilegedescalation-gha-rs-no-permission
|
||||
namespace: arc-runners
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: e2e-ci-runner-polaris
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: e2e-ci-runner-binding
|
||||
namespace: headlamp-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
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
# Custom Headlamp values for static plugin installation
|
||||
# This disables the plugin manager and uses an init container instead
|
||||
|
||||
# Disable the plugin manager sidecar
|
||||
pluginsManager:
|
||||
enabled: false
|
||||
|
||||
# Use an init container to install plugins to /headlamp/static-plugins
|
||||
initContainers:
|
||||
- name: install-plugins
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
echo "Installing plugins to /headlamp/static-plugins..."
|
||||
|
||||
# Create plugins directory
|
||||
mkdir -p /headlamp/static-plugins
|
||||
|
||||
# Set up npm cache
|
||||
export NPM_CONFIG_CACHE=/tmp/npm-cache
|
||||
export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig
|
||||
mkdir -p /tmp/npm-cache /tmp/npm-userconfig
|
||||
|
||||
# Install polaris plugin
|
||||
echo "Installing polaris plugin..."
|
||||
cd /headlamp/static-plugins
|
||||
npm pack headlamp-polaris-plugin@0.3.0
|
||||
tar -xzf headlamp-polaris-plugin-0.3.0.tgz
|
||||
mv package headlamp-polaris-plugin
|
||||
rm headlamp-polaris-plugin-0.3.0.tgz
|
||||
|
||||
# Install other plugins
|
||||
npx --yes @headlamp-k8s/plugin@latest install \
|
||||
--source https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_flux \
|
||||
--folderName /headlamp/static-plugins
|
||||
|
||||
npx --yes @headlamp-k8s/plugin@latest install \
|
||||
--source https://artifacthub.io/packages/headlamp/headlamp-trivy/headlamp_trivy \
|
||||
--folderName /headlamp/static-plugins
|
||||
|
||||
npx --yes @headlamp-k8s/plugin@latest install \
|
||||
--source https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_cert-manager \
|
||||
--folderName /headlamp/static-plugins
|
||||
|
||||
npx --yes @headlamp-k8s/plugin@latest install \
|
||||
--source https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_ai_assistant \
|
||||
--folderName /headlamp/static-plugins
|
||||
|
||||
echo "All plugins installed successfully"
|
||||
ls -la /headlamp/static-plugins
|
||||
securityContext:
|
||||
runAsUser: 100
|
||||
runAsGroup: 101
|
||||
runAsNonRoot: true
|
||||
privileged: false
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
volumeMounts:
|
||||
- name: static-plugins
|
||||
mountPath: /headlamp/static-plugins
|
||||
|
||||
# Configure headlamp to use static plugins
|
||||
config:
|
||||
pluginsDir: /headlamp/static-plugins
|
||||
|
||||
# Add volume for static plugins
|
||||
volumes:
|
||||
- name: static-plugins
|
||||
emptyDir: {}
|
||||
|
||||
# Add volume mount to main container
|
||||
volumeMounts:
|
||||
- name: static-plugins
|
||||
mountPath: /headlamp/static-plugins
|
||||
readOnly: true
|
||||
+2
-2
@@ -33,7 +33,7 @@ kubectl -n polaris get svc polaris-dashboard
|
||||
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
|
||||
kubectl -n headlamp get pods -l app.kubernetes.io/name=headlamp
|
||||
```
|
||||
|
||||
## Installation Methods
|
||||
@@ -59,7 +59,7 @@ kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
|
||||
+2
-3
@@ -268,10 +268,9 @@ npm run e2e
|
||||
|
||||
```bash
|
||||
# Create token
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n headlamp --duration=24h)
|
||||
|
||||
# Port-forward for local testing
|
||||
kubectl port-forward -n kube-system svc/headlamp 4466:80
|
||||
kubectl port-forward -n headlamp svc/headlamp 4466:80
|
||||
|
||||
# Run tests
|
||||
HEADLAMP_URL=http://localhost:4466 npm run e2e
|
||||
|
||||
+16
-16
@@ -33,7 +33,7 @@ This guide covers common issues encountered when using the Headlamp Polaris Plug
|
||||
|
||||
```bash
|
||||
# View Headlamp pod logs (plugin sidecar)
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
kubectl logs -n headlamp deployment/headlamp -c headlamp-plugin
|
||||
|
||||
# Expected output:
|
||||
# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
|
||||
@@ -43,7 +43,7 @@ kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
**Verify plugin files exist**:
|
||||
|
||||
```bash
|
||||
kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
|
||||
kubectl exec -n headlamp deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
|
||||
# Should show: headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
@@ -118,7 +118,7 @@ Expected subjects:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
```
|
||||
|
||||
For OIDC mode:
|
||||
@@ -154,7 +154,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -169,7 +169,7 @@ Service account mode:
|
||||
```bash
|
||||
# Impersonate Headlamp service account
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n polaris
|
||||
# Expected: yes
|
||||
@@ -189,7 +189,7 @@ kubectl auth can-i get services/proxy \
|
||||
After applying RBAC changes:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment headlamp -n kube-system
|
||||
kubectl rollout restart deployment headlamp -n headlamp
|
||||
```
|
||||
|
||||
---
|
||||
@@ -490,7 +490,7 @@ Run this script to test all RBAC components:
|
||||
#!/bin/bash
|
||||
NS="polaris"
|
||||
SA="headlamp"
|
||||
SA_NS="kube-system"
|
||||
SA_NS="headlamp"
|
||||
|
||||
echo "=== Testing RBAC for Polaris Plugin ==="
|
||||
|
||||
@@ -529,8 +529,8 @@ echo "=== Test complete ==="
|
||||
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
|
||||
# Create debug pod in headlamp namespace
|
||||
kubectl run netdebug -n headlamp --rm -it --image=nicolaka/netshoot -- bash
|
||||
|
||||
# Inside pod, test DNS and HTTP
|
||||
nslookup polaris-dashboard.polaris.svc.cluster.local
|
||||
@@ -545,11 +545,11 @@ 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
|
||||
kubectl logs -n headlamp kube-apiserver-* | grep polaris-dashboard
|
||||
|
||||
# Look for lines with:
|
||||
# "reason": "Forbidden"
|
||||
# "user": "system:serviceaccount:kube-system:headlamp"
|
||||
# "user": "system:serviceaccount:headlamp:headlamp"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -567,7 +567,7 @@ kubectl logs -n kube-system kube-apiserver-* | grep polaris-dashboard
|
||||
**Check sidecar logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
kubectl logs -n headlamp deployment/headlamp -c headlamp-plugin
|
||||
```
|
||||
|
||||
**Common errors**:
|
||||
@@ -591,7 +591,7 @@ 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
|
||||
kubectl get configmap headlamp-plugin-config -n headlamp -o yaml
|
||||
```
|
||||
|
||||
Expected format:
|
||||
@@ -677,13 +677,13 @@ If none of these solutions work, gather debugging information and open an issue:
|
||||
1. **Version Information**:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system -l app.kubernetes.io/name=headlamp -o yaml | grep image:
|
||||
kubectl get pods -n headlamp -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`
|
||||
- Or: `kubectl exec -n headlamp deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
|
||||
|
||||
3. **Browser Console Output**:
|
||||
|
||||
@@ -698,7 +698,7 @@ If none of these solutions work, gather debugging information and open an issue:
|
||||
5. **Pod Logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp --tail=100
|
||||
kubectl logs -n headlamp deployment/headlamp -c headlamp --tail=100
|
||||
kubectl logs -n polaris deployment/polaris-dashboard --tail=100
|
||||
```
|
||||
|
||||
|
||||
+22
-22
@@ -19,7 +19,7 @@ Helm provides the easiest way to deploy and manage the plugin in production. Thi
|
||||
|
||||
```bash
|
||||
# Add Headlamp Helm repository
|
||||
helm repo add headlamp https://headlamp-k8s.github.io/headlamp/
|
||||
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
|
||||
helm repo update
|
||||
```
|
||||
|
||||
@@ -41,11 +41,11 @@ pluginsManager:
|
||||
```bash
|
||||
# Install Headlamp
|
||||
helm install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml
|
||||
|
||||
# Wait for deployment
|
||||
kubectl -n kube-system wait --for=condition=available deployment/headlamp --timeout=300s
|
||||
kubectl -n headlamp wait --for=condition=available deployment/headlamp --timeout=300s
|
||||
```
|
||||
|
||||
After installation, install the plugin via Headlamp UI (**Settings → Plugins → Catalog**).
|
||||
@@ -131,7 +131,7 @@ Deploy:
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
@@ -177,7 +177,7 @@ apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
@@ -191,7 +191,7 @@ Apply ConfigMap then deploy Headlamp:
|
||||
kubectl apply -f headlamp-plugin-config.yaml
|
||||
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
@@ -210,7 +210,7 @@ metadata:
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://headlamp-k8s.github.io/headlamp/
|
||||
url: https://kubernetes-sigs.github.io/headlamp/
|
||||
```
|
||||
|
||||
### HelmRelease
|
||||
@@ -221,7 +221,7 @@ apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
spec:
|
||||
interval: 30m
|
||||
chart:
|
||||
@@ -300,7 +300,7 @@ kubectl apply -f helmrepository.yaml
|
||||
kubectl apply -f helmrelease.yaml
|
||||
|
||||
# Watch deployment
|
||||
flux get helmreleases -n kube-system --watch
|
||||
flux get helmreleases -n headlamp --watch
|
||||
```
|
||||
|
||||
## RBAC Configuration
|
||||
@@ -329,7 +329,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -349,7 +349,7 @@ helm repo update
|
||||
|
||||
# Upgrade Headlamp (preserves plugin configuration)
|
||||
helm upgrade headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml \
|
||||
--wait
|
||||
```
|
||||
@@ -365,15 +365,15 @@ helm upgrade headlamp headlamp/headlamp \
|
||||
|
||||
```bash
|
||||
# Update ConfigMap with new version
|
||||
kubectl -n kube-system edit configmap headlamp-plugin-config
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp rollout restart deployment/headlamp
|
||||
kubectl -n headlamp rollout status deployment/headlamp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -382,25 +382,25 @@ kubectl -n kube-system rollout status deployment/headlamp
|
||||
|
||||
```bash
|
||||
# Check Headlamp values
|
||||
helm get values headlamp -n kube-system
|
||||
helm get values headlamp -n headlamp
|
||||
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp logs deployment/headlamp -c install-polaris-plugin
|
||||
```
|
||||
|
||||
### Helm Release Stuck
|
||||
|
||||
```bash
|
||||
# Check Helm release status
|
||||
helm list -n kube-system
|
||||
helm list -n headlamp
|
||||
|
||||
# If stuck, force upgrade
|
||||
helm upgrade headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml \
|
||||
--force \
|
||||
--wait
|
||||
@@ -410,13 +410,13 @@ helm upgrade headlamp headlamp/headlamp \
|
||||
|
||||
```bash
|
||||
# Check HelmRelease status
|
||||
flux get helmreleases -n kube-system
|
||||
flux get helmreleases -n headlamp
|
||||
|
||||
# Check events
|
||||
kubectl -n kube-system describe helmrelease headlamp
|
||||
kubectl -n headlamp describe helmrelease headlamp
|
||||
|
||||
# Force reconciliation
|
||||
flux reconcile helmrelease headlamp -n kube-system
|
||||
flux reconcile helmrelease headlamp -n headlamp
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -47,7 +47,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -71,7 +71,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
@@ -90,7 +90,7 @@ apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
app.kubernetes.io/component: plugin-config
|
||||
@@ -109,7 +109,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
spec:
|
||||
@@ -194,7 +194,7 @@ apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
|
||||
@@ -204,7 +204,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
spec:
|
||||
@@ -235,27 +235,27 @@ 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
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp logs deployment/headlamp -c install-plugins
|
||||
|
||||
# Expected output:
|
||||
# Plugin installation complete
|
||||
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
kubectl -n headlamp exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
@@ -273,7 +273,7 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
|
||||
|
||||
```bash
|
||||
# Port-forward to access locally
|
||||
kubectl -n kube-system port-forward service/headlamp 8080:80
|
||||
kubectl -n headlamp port-forward service/headlamp 8080:80
|
||||
|
||||
# Open browser to http://localhost:8080
|
||||
```
|
||||
@@ -309,7 +309,7 @@ k8s/
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: headlamp
|
||||
@@ -401,7 +401,7 @@ spec:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
```
|
||||
|
||||
## Upgrading the Plugin
|
||||
@@ -410,24 +410,24 @@ spec:
|
||||
|
||||
```bash
|
||||
# Edit ConfigMap with new version
|
||||
kubectl -n kube-system edit configmap headlamp-plugin-config
|
||||
kubectl -n headlamp 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 headlamp rollout restart deployment/headlamp
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl -n kube-system rollout status deployment/headlamp
|
||||
kubectl -n headlamp rollout status deployment/headlamp
|
||||
```
|
||||
|
||||
### Verify Upgrade
|
||||
|
||||
```bash
|
||||
# Check init container logs
|
||||
kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
kubectl -n headlamp logs deployment/headlamp -c install-plugins
|
||||
|
||||
# Verify new version in UI
|
||||
# Navigate to Settings → Plugins in Headlamp
|
||||
@@ -439,7 +439,7 @@ kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
|
||||
```bash
|
||||
# Check init container logs
|
||||
kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
kubectl -n headlamp logs deployment/headlamp -c install-plugins
|
||||
|
||||
# Common issues:
|
||||
# 1. Network connectivity to GitHub
|
||||
@@ -451,14 +451,14 @@ kubectl -n kube-system logs deployment/headlamp -c install-plugins
|
||||
|
||||
```bash
|
||||
# Verify HEADLAMP_CONFIG_WATCH_PLUGINS is false
|
||||
kubectl -n kube-system get deployment headlamp -o yaml | grep WATCH_PLUGINS
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp edit deployment headlamp
|
||||
```
|
||||
|
||||
### RBAC Permissions Denied
|
||||
@@ -466,7 +466,7 @@ kubectl -n kube-system edit deployment headlamp
|
||||
```bash
|
||||
# Test RBAC
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ kubectl -n polaris get svc polaris-dashboard
|
||||
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
|
||||
kubectl -n headlamp get deployment headlamp
|
||||
kubectl -n headlamp get svc headlamp
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
@@ -60,17 +60,17 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
|
||||
|
||||
# 2. Verify RBAC permissions
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp: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
|
||||
kubectl -n headlamp 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/
|
||||
kubectl -n headlamp exec deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
# Expected: dist/, package.json present
|
||||
```
|
||||
|
||||
@@ -241,7 +241,7 @@ apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: headlamp-pdb
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
@@ -295,7 +295,7 @@ apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -312,10 +312,10 @@ spec:
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
kubectl -n kube-system logs deployment/headlamp -f
|
||||
kubectl -n headlamp logs deployment/headlamp -f
|
||||
|
||||
# Filter for plugin-related logs
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
kubectl -n headlamp logs deployment/headlamp | grep -i polaris
|
||||
```
|
||||
|
||||
**Polaris Dashboard Logs:**
|
||||
@@ -341,14 +341,14 @@ apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: headlamp-alerts
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
spec:
|
||||
groups:
|
||||
- name: headlamp
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: HeadlampPodNotReady
|
||||
expr: kube_pod_status_ready{namespace="kube-system", pod=~"headlamp-.*"} == 0
|
||||
expr: kube_pod_status_ready{namespace="headlamp", pod=~"headlamp-.*"} == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
@@ -422,9 +422,9 @@ If Headlamp or plugin becomes unavailable:
|
||||
2. **Redeploy Headlamp:**
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
3. **Reapply RBAC:**
|
||||
@@ -436,7 +436,7 @@ If Headlamp or plugin becomes unavailable:
|
||||
4. **Verify plugin files:**
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system exec deployment/headlamp -- \
|
||||
kubectl -n headlamp exec deployment/headlamp -- \
|
||||
ls /headlamp/plugins/headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
|
||||
@@ -268,10 +268,9 @@ npm run e2e
|
||||
|
||||
```bash
|
||||
# Create token
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n headlamp --duration=24h)
|
||||
|
||||
# Port-forward for local testing
|
||||
kubectl port-forward -n kube-system svc/headlamp 4466:80
|
||||
kubectl port-forward -n headlamp svc/headlamp 4466:80
|
||||
|
||||
# Run tests
|
||||
HEADLAMP_URL=http://localhost:4466 npm run e2e
|
||||
|
||||
@@ -72,7 +72,7 @@ Deploy or update Headlamp:
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
@@ -122,7 +122,7 @@ apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
@@ -138,14 +138,14 @@ kubectl apply -f headlamp-plugin-config.yaml
|
||||
|
||||
# Deploy/update Headlamp with sidecar
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--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
|
||||
kubectl -n headlamp 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/
|
||||
kubectl -n headlamp exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x dist/
|
||||
@@ -270,7 +270,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -284,10 +284,10 @@ See [RBAC Permissions](../user-guide/rbac-permissions.md) for detailed RBAC conf
|
||||
|
||||
```bash
|
||||
# If you updated Helm values or ConfigMaps
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
|
||||
```
|
||||
|
||||
### 3. Clear Browser Cache
|
||||
@@ -312,14 +312,14 @@ kubectl -n kube-system wait --for=condition=ready pod -l app.kubernetes.io/name=
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# Expected: No errors related to plugin loading
|
||||
|
||||
@@ -345,13 +345,13 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
|
||||
|
||||
```bash
|
||||
# 1. Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# 3. Hard refresh browser (Cmd+Shift+R or Ctrl+Shift+R)
|
||||
|
||||
@@ -404,7 +404,7 @@ helm install polaris fairwinds-stable/polaris \
|
||||
```bash
|
||||
# Wait 30 minutes for ArtifactHub sync
|
||||
# Or manually force Headlamp restart:
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
kubectl -n headlamp rollout restart deployment/headlamp
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -67,14 +67,14 @@ kubectl -n polaris wait --for=condition=ready pod -l app.kubernetes.io/name=pola
|
||||
|
||||
```bash
|
||||
# Check Headlamp is deployed
|
||||
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
kubectl -n headlamp 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}'
|
||||
kubectl -n headlamp get deployment headlamp -o jsonpath='{.spec.template.spec.containers[0].image}'
|
||||
|
||||
# Expected output:
|
||||
# ghcr.io/headlamp-k8s/headlamp:v0.39.0 (or similar)
|
||||
@@ -84,17 +84,17 @@ kubectl -n kube-system get deployment headlamp -o jsonpath='{.spec.template.spec
|
||||
|
||||
```bash
|
||||
# Add Headlamp Helm repository
|
||||
helm repo add headlamp https://headlamp-k8s.github.io/headlamp/
|
||||
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
|
||||
helm repo update
|
||||
|
||||
# Install Headlamp
|
||||
helm install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--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
|
||||
kubectl -n headlamp wait --for=condition=ready pod -l app.kubernetes.io/name=headlamp --timeout=300s
|
||||
```
|
||||
|
||||
## RBAC Requirements
|
||||
@@ -112,7 +112,7 @@ The plugin requires permissions to access the Polaris dashboard via Kubernetes s
|
||||
```bash
|
||||
# Test if Headlamp service account has permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ EOF
|
||||
|
||||
# Update Headlamp
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--namespace headlamp \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
@@ -70,7 +70,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -111,7 +111,7 @@ EOF
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
|
||||
kubectl -n headlamp exec -it deployment/headlamp -c headlamp -- \
|
||||
ls /headlamp/plugins/headlamp-polaris-plugin/dist/
|
||||
|
||||
# Expected output:
|
||||
@@ -119,7 +119,7 @@ kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
|
||||
|
||||
# Verify RBAC is correct
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
@@ -185,7 +185,7 @@ Cluster score badge in top navigation:
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -c headlamp -- \
|
||||
kubectl -n headlamp exec -it deployment/headlamp -c headlamp -- \
|
||||
ls /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# If missing, reinstall via Headlamp UI or sidecar method
|
||||
|
||||
@@ -38,17 +38,17 @@ kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy
|
||||
|
||||
# 3. Verify RBAC permissions
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp: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
|
||||
kubectl -n headlamp 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
|
||||
kubectl -n headlamp logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# Expected: No errors
|
||||
```
|
||||
@@ -57,7 +57,7 @@ kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
|
||||
```bash
|
||||
# Verify plugin files exist
|
||||
kubectl -n kube-system exec deployment/headlamp -c headlamp -- \
|
||||
kubectl -n headlamp exec deployment/headlamp -c headlamp -- \
|
||||
ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected output:
|
||||
@@ -76,7 +76,7 @@ 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 \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ This guide covers common issues encountered when using the Headlamp Polaris Plug
|
||||
|
||||
```bash
|
||||
# View Headlamp pod logs (plugin sidecar)
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
kubectl logs -n headlamp deployment/headlamp -c headlamp-plugin
|
||||
|
||||
# Expected output:
|
||||
# Installing plugin from https://github.com/.../headlamp-polaris-plugin-X.Y.Z.tar.gz
|
||||
@@ -43,7 +43,7 @@ kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
**Verify plugin files exist**:
|
||||
|
||||
```bash
|
||||
kubectl exec -n kube-system deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
|
||||
kubectl exec -n headlamp deployment/headlamp -c headlamp -- ls -la /headlamp/plugins/
|
||||
# Should show: headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
@@ -118,7 +118,7 @@ Expected subjects:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
```
|
||||
|
||||
For OIDC mode:
|
||||
@@ -154,7 +154,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -169,7 +169,7 @@ Service account mode:
|
||||
```bash
|
||||
# Impersonate Headlamp service account
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
--resource-name=polaris-dashboard \
|
||||
-n polaris
|
||||
# Expected: yes
|
||||
@@ -189,7 +189,7 @@ kubectl auth can-i get services/proxy \
|
||||
After applying RBAC changes:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment headlamp -n kube-system
|
||||
kubectl rollout restart deployment headlamp -n headlamp
|
||||
```
|
||||
|
||||
---
|
||||
@@ -490,7 +490,7 @@ Run this script to test all RBAC components:
|
||||
#!/bin/bash
|
||||
NS="polaris"
|
||||
SA="headlamp"
|
||||
SA_NS="kube-system"
|
||||
SA_NS="headlamp"
|
||||
|
||||
echo "=== Testing RBAC for Polaris Plugin ==="
|
||||
|
||||
@@ -529,8 +529,8 @@ echo "=== Test complete ==="
|
||||
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
|
||||
# Create debug pod in headlamp namespace
|
||||
kubectl run netdebug -n headlamp --rm -it --image=nicolaka/netshoot -- bash
|
||||
|
||||
# Inside pod, test DNS and HTTP
|
||||
nslookup polaris-dashboard.polaris.svc.cluster.local
|
||||
@@ -545,11 +545,11 @@ 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
|
||||
kubectl logs -n headlamp kube-apiserver-* | grep polaris-dashboard
|
||||
|
||||
# Look for lines with:
|
||||
# "reason": "Forbidden"
|
||||
# "user": "system:serviceaccount:kube-system:headlamp"
|
||||
# "user": "system:serviceaccount:headlamp:headlamp"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -567,7 +567,7 @@ kubectl logs -n kube-system kube-apiserver-* | grep polaris-dashboard
|
||||
**Check sidecar logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp-plugin
|
||||
kubectl logs -n headlamp deployment/headlamp -c headlamp-plugin
|
||||
```
|
||||
|
||||
**Common errors**:
|
||||
@@ -591,7 +591,7 @@ 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
|
||||
kubectl get configmap headlamp-plugin-config -n headlamp -o yaml
|
||||
```
|
||||
|
||||
Expected format:
|
||||
@@ -677,13 +677,13 @@ If none of these solutions work, gather debugging information and open an issue:
|
||||
1. **Version Information**:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system -l app.kubernetes.io/name=headlamp -o yaml | grep image:
|
||||
kubectl get pods -n headlamp -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`
|
||||
- Or: `kubectl exec -n headlamp deployment/headlamp -c headlamp -- cat /headlamp/plugins/headlamp-polaris-plugin/package.json`
|
||||
|
||||
3. **Browser Console Output**:
|
||||
|
||||
@@ -698,7 +698,7 @@ If none of these solutions work, gather debugging information and open an issue:
|
||||
5. **Pod Logs**:
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system deployment/headlamp -c headlamp --tail=100
|
||||
kubectl logs -n headlamp deployment/headlamp -c headlamp --tail=100
|
||||
kubectl logs -n polaris deployment/polaris-dashboard --tail=100
|
||||
```
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -83,7 +83,7 @@ roleRef:
|
||||
```bash
|
||||
# Test service account (in-cluster mode)
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
```
|
||||
|
||||
@@ -65,7 +65,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp # Adjust to your Headlamp SA name
|
||||
namespace: kube-system # Adjust to Headlamp's namespace
|
||||
namespace: headlamp # Adjust to Headlamp's namespace
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -75,7 +75,7 @@ roleRef:
|
||||
**Adjust for your environment:**
|
||||
|
||||
- `subjects[0].name` - Your Headlamp service account name (often `headlamp`)
|
||||
- `subjects[0].namespace` - Namespace where Headlamp runs (often `kube-system`)
|
||||
- `subjects[0].namespace` - Namespace where Headlamp runs (often `headlamp`)
|
||||
|
||||
### Step 3: Apply and Verify
|
||||
|
||||
@@ -91,7 +91,7 @@ kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
@@ -109,7 +109,7 @@ In token-auth mode, **each user's own identity** is used for Kubernetes API requ
|
||||
With service account mode:
|
||||
|
||||
- Single RoleBinding grants access to all Headlamp users
|
||||
- Kubernetes sees all requests as `system:serviceaccount:kube-system:headlamp`
|
||||
- Kubernetes sees all requests as `system:serviceaccount:headlamp:headlamp`
|
||||
|
||||
With token-auth mode:
|
||||
|
||||
@@ -267,7 +267,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -281,7 +281,7 @@ metadata:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
namespace: headlamp
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
@@ -411,7 +411,7 @@ Every plugin data fetch creates a Kubernetes API audit log entry.
|
||||
"level": "Metadata",
|
||||
"verb": "get",
|
||||
"user": {
|
||||
"username": "system:serviceaccount:kube-system:headlamp"
|
||||
"username": "system:serviceaccount:headlamp:headlamp"
|
||||
},
|
||||
"sourceIPs": ["10.96.0.1"],
|
||||
"objectRef": {
|
||||
@@ -494,7 +494,7 @@ If using a log aggregator (e.g., Elasticsearch), create filters to exclude or do
|
||||
```bash
|
||||
# Service account mode
|
||||
kubectl auth can-i get services/proxy \
|
||||
--as=system:serviceaccount:kube-system:headlamp \
|
||||
--as=system:serviceaccount:headlamp:headlamp \
|
||||
-n polaris \
|
||||
--resource-name=polaris-dashboard
|
||||
|
||||
|
||||
+33
-24
@@ -4,7 +4,16 @@ Playwright-based smoke tests that validate the Polaris plugin against a live Hea
|
||||
|
||||
## CI
|
||||
|
||||
E2E tests run automatically in GitHub Actions on pushes to `main` and pull requests. The workflow (`.github/workflows/e2e.yaml`) uses either Authentik OIDC or token-based authentication via repository secrets.
|
||||
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
|
||||
|
||||
@@ -12,12 +21,12 @@ Configure these in GitHub repository settings (Settings → Secrets and variable
|
||||
|
||||
| Secret | Required | Description |
|
||||
| -------------------- | -------- | -------------------------------------------------------------- |
|
||||
| `HEADLAMP_URL` | Optional | Headlamp instance URL (defaults to `https://headlamp.animaniacs.farh.net`) |
|
||||
| `AUTHENTIK_USERNAME` | OIDC | Authentik email or username for a CI user with Headlamp access |
|
||||
| `AUTHENTIK_PASSWORD` | OIDC | Password for that user |
|
||||
| `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.
|
||||
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
|
||||
|
||||
@@ -47,12 +56,12 @@ HEADLAMP_URL=http://localhost:4466 npm run e2e:headed
|
||||
|
||||
| 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 (fallback auth) |
|
||||
| `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) |
|
||||
|
||||
Set either `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` or `HEADLAMP_TOKEN`. OIDC takes priority if both are set.
|
||||
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
|
||||
|
||||
@@ -249,25 +258,25 @@ test('plugin UI adapts to dark mode', async ({ page }) => {
|
||||
|
||||
Tests run automatically in GitHub Actions on pushes to `main` and pull requests. See `.github/workflows/e2e.yaml` for workflow configuration.
|
||||
|
||||
### Required Secrets
|
||||
### Architecture
|
||||
|
||||
Configure these in GitHub repository settings (Settings → Secrets and variables → Actions):
|
||||
The E2E workflow deploys a **dedicated Headlamp instance** for each test run:
|
||||
|
||||
- `HEADLAMP_URL` (optional): Headlamp instance URL
|
||||
- `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` (for OIDC auth)
|
||||
- OR `HEADLAMP_TOKEN` (for token-based auth)
|
||||
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`)
|
||||
|
||||
### Workflow Overview
|
||||
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.
|
||||
|
||||
1. Checkout code
|
||||
2. Setup Node.js 20 with npm cache
|
||||
3. Install dependencies (`npm ci`)
|
||||
4. Install Playwright browsers (`chromium` only)
|
||||
5. Run auth setup (creates session in `e2e/.auth/state.json`)
|
||||
6. Run all E2E tests
|
||||
7. Upload artifacts on failure:
|
||||
- `playwright-report/` - HTML test report
|
||||
- `test-results/` - Screenshots, traces, videos
|
||||
### Cluster Prerequisites
|
||||
|
||||
One-time setup by a cluster admin:
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment/e2e-ci-runner-rbac.yaml
|
||||
```
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
|
||||
+12
-5
@@ -39,13 +39,20 @@ async function authenticateWithOIDC(page: Page, username: string, password: stri
|
||||
}
|
||||
|
||||
async function authenticateWithToken(page: Page, token: string): Promise<void> {
|
||||
// Navigate to login — Headlamp redirects / to /c/main/login
|
||||
await page.goto('/');
|
||||
await page.waitForURL('**/login');
|
||||
// Headlamp goes to /token directly when no OIDC is configured,
|
||||
// or through /login when OIDC is configured
|
||||
await page.waitForURL(/\/(login|token)$/);
|
||||
|
||||
// Click the token auth option
|
||||
await page.getByRole('button', { name: /use a token/i }).click();
|
||||
await page.waitForURL('**/token');
|
||||
if (page.url().includes('/login')) {
|
||||
// OIDC login page — click "use a token" to reach token auth.
|
||||
// Wait explicitly before clicking so failures surface at 15 s
|
||||
// with a clear message rather than silently timing out at 60 s.
|
||||
const useTokenBtn = page.getByRole('button', { name: /use a token/i });
|
||||
await useTokenBtn.waitFor({ state: 'visible', timeout: 15_000 });
|
||||
await useTokenBtn.click();
|
||||
await page.waitForURL('**/token');
|
||||
}
|
||||
|
||||
// Fill the "ID token" field and submit
|
||||
await page.getByRole('textbox', { name: /id token/i }).fill(token);
|
||||
|
||||
+10
-4
@@ -2,11 +2,17 @@ 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) {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
// 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');
|
||||
|
||||
// Find and click the Polaris plugin entry to open its settings
|
||||
const pluginEntry = page.locator('text=polaris').first();
|
||||
await expect(pluginEntry).toBeVisible({ timeout: 15_000 });
|
||||
// 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
|
||||
|
||||
Generated
+93
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "headlamp-polaris",
|
||||
"version": "0.7.1",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "headlamp-polaris",
|
||||
"version": "0.7.1",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
@@ -17,10 +17,14 @@
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"jsdom": "^24.0.0",
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -35,6 +39,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
|
||||
@@ -429,6 +447,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@bundled-es-modules/cookie": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz",
|
||||
@@ -4844,6 +4872,40 @@
|
||||
"vitest": "3.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
|
||||
"integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"ast-v8-to-istanbul": "^0.3.3",
|
||||
"debug": "^4.4.1",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
"istanbul-reports": "^3.1.7",
|
||||
"magic-string": "^0.30.17",
|
||||
"magicast": "^0.3.5",
|
||||
"std-env": "^3.9.0",
|
||||
"test-exclude": "^7.0.1",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.2.4",
|
||||
"vitest": "3.2.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
@@ -5649,6 +5711,35 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
|
||||
"integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||
|
||||
+20
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "headlamp-polaris",
|
||||
"version": "0.7.1",
|
||||
"version": "1.0.0",
|
||||
"description": "Headlamp plugin for Fairwinds Polaris audit results",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -12,6 +12,7 @@
|
||||
"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",
|
||||
@@ -30,6 +31,17 @@
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"tar": "^7.5.11",
|
||||
"undici": "^7.24.3",
|
||||
"flatted": "^3.4.2",
|
||||
"lodash": ">=4.18.0",
|
||||
"picomatch": ">=4.0.4",
|
||||
"vite": ">=6.4.2",
|
||||
"elliptic": ">=6.6.1"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@mui/material": "^5.15.14",
|
||||
@@ -39,10 +51,17 @@
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+623
-162
File diff suppressed because it is too large
Load Diff
+2
-16
@@ -1,19 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"baseBranches": ["main"],
|
||||
"schedule": ["every weekend"],
|
||||
"prConcurrentLimit": 10,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "npm minor and patch"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "github-actions minor and patch"
|
||||
}
|
||||
]
|
||||
"extends": ["github>privilegedescalation/.github:renovate-config"]
|
||||
}
|
||||
|
||||
|
||||
Executable
+210
@@ -0,0 +1,210 @@
|
||||
#!/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 `headlamp-dev` namespace. Nothing
|
||||
# persists beyond a 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 (managed by Flux GitOps in privilegedescalation/infra)
|
||||
#
|
||||
# Environment:
|
||||
# E2E_NAMESPACE — namespace for E2E Headlamp (default: headlamp-dev)
|
||||
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)
|
||||
# HEADLAMP_VERSION — Headlamp image tag (default: v0.40.1, pinned to match production)
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DIST_DIR="$REPO_ROOT/dist"
|
||||
|
||||
E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}"
|
||||
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}"
|
||||
HEADLAMP_VERSION="${HEADLAMP_VERSION:-v0.40.1}"
|
||||
|
||||
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"
|
||||
|
||||
# --- Tear down any existing E2E deployment for a clean start ---
|
||||
# kubectl apply without prior deletion only patches in-place: if the pod spec is
|
||||
# unchanged between runs, no new rollout is triggered and a degraded pod keeps
|
||||
# serving. Delete first to guarantee a fresh pod regardless of prior state.
|
||||
echo ""
|
||||
echo "Removing any existing E2E deployment (clean-start)..."
|
||||
kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait
|
||||
kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait
|
||||
kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait
|
||||
|
||||
# --- 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: headlamp-dev)
|
||||
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-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."
|
||||
@@ -7,13 +7,7 @@ import { makeAuditData, makeResult } from '../test-utils';
|
||||
// Mock Headlamp lib
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn() },
|
||||
K8s: {
|
||||
useCluster: () => 'test-cluster',
|
||||
},
|
||||
Router: {
|
||||
createRouteURL: (name: string, params?: { cluster?: string }) =>
|
||||
`/c/${params?.cluster ?? 'default'}/${name}`,
|
||||
},
|
||||
K8s: {},
|
||||
}));
|
||||
|
||||
vi.mock('@mui/material/styles', () => ({
|
||||
@@ -31,6 +25,15 @@ 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(),
|
||||
@@ -97,7 +100,7 @@ describe('AppBarScoreBadge', () => {
|
||||
expect(button.style.backgroundColor).toBe('rgb(244, 67, 54)');
|
||||
});
|
||||
|
||||
it('navigates to /polaris on click', async () => {
|
||||
it('navigates to /c/<cluster>/polaris on click', async () => {
|
||||
const user = userEvent.setup();
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
@@ -120,6 +123,33 @@ describe('AppBarScoreBadge', () => {
|
||||
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({
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { K8s, Router } from '@kinvolk/headlamp-plugin/lib';
|
||||
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
|
||||
@@ -13,7 +24,6 @@ export default function AppBarScoreBadge() {
|
||||
const theme = useTheme();
|
||||
const { data, loading } = usePolarisDataContext();
|
||||
const history = useHistory();
|
||||
const cluster = K8s.useCluster();
|
||||
|
||||
if (loading || !data) {
|
||||
return null; // Graceful degradation when Polaris unavailable
|
||||
@@ -36,7 +46,9 @@ export default function AppBarScoreBadge() {
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
history.push(Router.createRouteURL('polaris', { cluster: cluster ?? '' }));
|
||||
const cluster = getClusterFromUrl();
|
||||
const prefix = cluster ? `/c/${cluster}` : '';
|
||||
history.push(`${prefix}/polaris`);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,5 +9,16 @@ export default defineConfig({
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user