Compare commits
81 Commits
v0.2.0-dev.5
...
v0.3.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f7a7ed73 | |||
| 96a0cce17e | |||
| 8672ae8956 | |||
| dfbff8d539 | |||
| 7ad5b7ecc3 | |||
| 0e711f5dfd | |||
| 67ced98bcd | |||
| f02b4cf051 | |||
| 51b174e68d | |||
| 5188bc289e | |||
| 33ef816826 | |||
| ea28ab3a81 | |||
| 57f0bf6b4b | |||
| 8dd71772f5 | |||
| c3b2877c51 | |||
| 62aa181433 | |||
| 2c077907e9 | |||
| 17495d4883 | |||
| 01eed82efc | |||
| 5dab426fe8 | |||
| 5eaa6603f1 | |||
| b67f770660 | |||
| 20e8063cbb | |||
| c1156e5cf5 | |||
| cab2118a88 | |||
| a18710ccb1 | |||
| 811059cf75 | |||
| a404c075d6 | |||
| db17a08d26 | |||
| e52670dee4 | |||
| 8d219a9c6e | |||
| b2cbce16c1 | |||
| c95aab3ca3 | |||
| 604106c688 | |||
| 44a0016a4d | |||
| 03d7379e13 | |||
| 861dff6901 | |||
| 03b75a836b | |||
| 83a5342011 | |||
| 3daa1cbc14 | |||
| 9c03d912df | |||
| 00d4b224eb | |||
| c1248ec3c4 | |||
| 7ac5d0a494 | |||
| 59c1d4e844 | |||
| a507ba1d4a | |||
| d03fb81cd5 | |||
| d4d593cf74 | |||
| 2facb1b22b | |||
| 104a7fb2ba | |||
| b9e9484bf0 | |||
| 22d88cfca4 | |||
| 48dcb214b9 | |||
| c0681162e7 | |||
| 762056e46c | |||
| ab1f028fe0 | |||
| fe2e5d53e7 | |||
| 4378ad39f3 | |||
| 93bfb9e1bb | |||
| a95f132413 | |||
| d3203b1890 | |||
| cd69cef2af | |||
| 0461ee8f23 | |||
| 14e323200c | |||
| a8e7dfca6d | |||
| 66903ca5e5 | |||
| f274203092 | |||
| 1273f94ae5 | |||
| 9d4b2e17aa | |||
| 82261a1c19 | |||
| 863889eca4 | |||
| 99bac773cc | |||
| 9fdb7c04cd | |||
| 0bd90ca317 | |||
| 975a31d1f3 | |||
| e54630410e | |||
| 088c74323b | |||
| d837987916 | |||
| 1b082a24db | |||
| 4544284df0 | |||
| 4838b22a02 |
@@ -0,0 +1,13 @@
|
||||
# Headlamp E2E Test Configuration
|
||||
|
||||
# Headlamp instance URL
|
||||
HEADLAMP_URL=https://headlamp.animaniacs.farh.net
|
||||
|
||||
# Authentication: Choose ONE of the following methods
|
||||
|
||||
# Option 1: OIDC Authentication (Authentik)
|
||||
# AUTHENTIK_USERNAME=your-username
|
||||
# AUTHENTIK_PASSWORD=your-password
|
||||
|
||||
# Option 2: Token Authentication
|
||||
# HEADLAMP_TOKEN=your-headlamp-token
|
||||
@@ -1,36 +0,0 @@
|
||||
name: AI Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ai-review:
|
||||
name: AI Code Review
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: AI Review
|
||||
uses: Nikita-Filonov/ai-review@v0.56.0
|
||||
with:
|
||||
review-command: run
|
||||
env:
|
||||
LLM__PROVIDER: "OPENAI"
|
||||
LLM__META__MODEL: ${{ vars.AI_REVIEW_MODEL }}
|
||||
LLM__META__MAX_TOKENS: "15000"
|
||||
LLM__META__TEMPERATURE: "0.3"
|
||||
LLM__HTTP_CLIENT__API_URL: "https://api.openai.com/v1"
|
||||
LLM__HTTP_CLIENT__API_TOKEN: ${{ secrets.OPENAI_API_KEY }}
|
||||
VCS__PROVIDER: "GITEA"
|
||||
VCS__PIPELINE__OWNER: ${{ github.repository_owner }}
|
||||
VCS__PIPELINE__REPO: ${{ github.event.repository.name }}
|
||||
VCS__PIPELINE__PULL_NUMBER: ${{ github.event.pull_request.number }}
|
||||
VCS__HTTP_CLIENT__API_URL: ${{ github.server_url }}/api/v1
|
||||
VCS__HTTP_CLIENT__API_TOKEN: ${{ secrets.AI_REVIEW_GITEA_TOKEN }}
|
||||
@@ -1,28 +0,0 @@
|
||||
name: E2E
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Chromium
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E smoke tests
|
||||
env:
|
||||
HEADLAMP_URL: https://headlamp.animaniacs.farh.net
|
||||
AUTHENTIK_USERNAME: ${{ secrets.AUTHENTIK_USERNAME }}
|
||||
AUTHENTIK_PASSWORD: ${{ secrets.AUTHENTIK_PASSWORD }}
|
||||
run: npx playwright test
|
||||
@@ -1,201 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if release is already finalized
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
TARBALL_URL="https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz"
|
||||
HTTP_CODE=$(curl -sL -o /tmp/release.tar.gz -w "%{http_code}" "$TARBALL_URL" 2>/dev/null)
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
ACTUAL="sha256:$(sha256sum /tmp/release.tar.gz | awk '{print $1}')"
|
||||
EXPECTED=$(grep 'archive-checksum' artifacthub-pkg.yml | awk '{print $2}')
|
||||
echo "Release tarball checksum: $ACTUAL"
|
||||
echo "Metadata checksum: $EXPECTED"
|
||||
if [ "$ACTUAL" = "$EXPECTED" ]; then
|
||||
echo "SKIP_BUILD=true" >> $GITHUB_ENV
|
||||
echo "Checksums match - release is finalized, nothing to do"
|
||||
fi
|
||||
else
|
||||
echo "No existing release (HTTP $HTTP_CODE) - will build"
|
||||
fi
|
||||
rm -f /tmp/release.tar.gz
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Package tarball
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
npx @kinvolk/headlamp-plugin package
|
||||
|
||||
- name: Compute tarball checksum
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
TARBALL=$(ls *.tar.gz)
|
||||
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
|
||||
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
|
||||
echo "Tarball: $TARBALL"
|
||||
echo "Checksum: sha256:$CHECKSUM"
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
apt-get update && apt-get install -y docker.io
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
docker build -t git.farh.net/${{ github.repository }}:${{ github.ref_name }} -t git.farh.net/${{ github.repository }}:latest .
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.farh.net -u ${{ github.actor }} --password-stdin
|
||||
docker push git.farh.net/${{ github.repository }}:${{ github.ref_name }}
|
||||
docker push git.farh.net/${{ github.repository }}:latest
|
||||
|
||||
- name: Create Gitea release
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
# Create release (or get existing)
|
||||
RELEASE=$(curl -s -X POST \
|
||||
-H "Authorization: token ${{ github.token }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_URL}/releases" \
|
||||
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\"}")
|
||||
RELEASE_ID=$(echo "$RELEASE" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
|
||||
if [ "$RELEASE_ID" = "undefined" ]; then
|
||||
RELEASE=$(curl -sf \
|
||||
-H "Authorization: token ${{ github.token }}" \
|
||||
"${API_URL}/releases/tags/${GITHUB_REF_NAME}")
|
||||
RELEASE_ID=$(echo "$RELEASE" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
|
||||
fi
|
||||
echo "Gitea Release ID: $RELEASE_ID"
|
||||
# Delete existing assets
|
||||
ASSETS=$(curl -sf \
|
||||
-H "Authorization: token ${{ github.token }}" \
|
||||
"${API_URL}/releases/${RELEASE_ID}/assets")
|
||||
echo "$ASSETS" | node -e "
|
||||
process.stdin.resume();let d='';
|
||||
process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{
|
||||
JSON.parse(d).forEach(a=>console.log(a.id));
|
||||
})" | while read -r ASSET_ID; do
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${{ github.token }}" \
|
||||
"${API_URL}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
|
||||
done
|
||||
# Upload tarball
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${{ github.token }}" \
|
||||
-F "attachment=@${TARBALL}" \
|
||||
"${API_URL}/releases/${RELEASE_ID}/assets?name=${TARBALL}"
|
||||
echo "Gitea release updated"
|
||||
|
||||
- name: Create GitHub release
|
||||
continue-on-error: true
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
# Push tag to GitHub first so it exists before creating the release
|
||||
git remote add github-release https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true
|
||||
git push -f github-release ${GITHUB_REF_NAME} 2>/dev/null || true
|
||||
GH_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
|
||||
# Create release or fetch existing one
|
||||
BODY=$(curl -s -X POST \
|
||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"${GH_API}/releases" \
|
||||
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"generate_release_notes\":true}")
|
||||
RELEASE_ID=$(echo "$BODY" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
|
||||
if [ "$RELEASE_ID" = "undefined" ]; then
|
||||
echo "Release already exists, fetching it..."
|
||||
BODY=$(curl -sf \
|
||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"${GH_API}/releases/tags/${GITHUB_REF_NAME}")
|
||||
RELEASE_ID=$(echo "$BODY" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
|
||||
fi
|
||||
echo "GitHub Release ID: $RELEASE_ID"
|
||||
# Delete existing assets with the same name
|
||||
ASSETS=$(curl -sf \
|
||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"${GH_API}/releases/${RELEASE_ID}/assets")
|
||||
echo "$ASSETS" | node -e "
|
||||
process.stdin.resume();let d='';
|
||||
process.stdin.on('data',c=>d+=c);
|
||||
process.stdin.on('end',()=>{
|
||||
const assets=JSON.parse(d);
|
||||
assets.filter(a=>a.name==='${TARBALL}').forEach(a=>console.log(a.id));
|
||||
})" | while read -r ASSET_ID; do
|
||||
echo "Deleting existing asset $ASSET_ID..."
|
||||
curl -sf -X DELETE \
|
||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
||||
"${GH_API}/releases/assets/${ASSET_ID}"
|
||||
done
|
||||
# Upload tarball
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
||||
-H "Content-Type: application/gzip" \
|
||||
"https://uploads.github.com/repos/cpfarhood/headlamp-polaris-plugin/releases/${RELEASE_ID}/assets?name=${TARBALL}" \
|
||||
--data-binary "@${TARBALL}"
|
||||
echo "GitHub release updated with same tarball"
|
||||
|
||||
- name: Update metadata and align tag
|
||||
run: |
|
||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git config user.email "gitea-actions[bot]@git.farh.net"
|
||||
# Determine which Gitea branch to update based on version suffix
|
||||
if [[ "$VERSION" == *"-dev."* ]]; then
|
||||
GITEA_BRANCH="dev"
|
||||
else
|
||||
GITEA_BRANCH="main"
|
||||
fi
|
||||
git fetch origin ${GITEA_BRANCH}
|
||||
git checkout origin/${GITEA_BRANCH} -B temp-update
|
||||
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
|
||||
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml
|
||||
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
||||
git add artifacthub-pkg.yml
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "ci: update artifact hub metadata for ${GITHUB_REF_NAME}"
|
||||
git push origin temp-update:${GITEA_BRANCH}
|
||||
}
|
||||
# Force-move tag to the commit with correct checksum.
|
||||
# This triggers a new CI run, but the guard step will detect
|
||||
# that the release checksum already matches and skip the build.
|
||||
git tag -f ${GITHUB_REF_NAME}
|
||||
git push -f origin ${GITHUB_REF_NAME}
|
||||
# Only push to GitHub main branch for STABLE releases
|
||||
# Dev releases only create GitHub releases, don't update main branch
|
||||
# This keeps GitHub main branch at latest stable for ArtifactHub
|
||||
git remote add github https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true
|
||||
if [[ "$VERSION" != *"-dev."* ]]; then
|
||||
echo "Stable release detected - pushing to GitHub main branch"
|
||||
git push github temp-update:main 2>/dev/null || true
|
||||
else
|
||||
echo "Dev release detected - skipping GitHub main branch update"
|
||||
fi
|
||||
git push -f github ${GITHUB_REF_NAME} 2>/dev/null || true
|
||||
echo "Tag ${GITHUB_REF_NAME} aligned with updated metadata"
|
||||
@@ -2,22 +2,29 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Lint
|
||||
@@ -28,3 +35,6 @@ jobs:
|
||||
|
||||
- name: Format check
|
||||
run: npx prettier --check src/
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
@@ -0,0 +1,53 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run e2e
|
||||
env:
|
||||
HEADLAMP_URL: ${{ secrets.HEADLAMP_URL || 'https://headlamp.animaniacs.farh.net' }}
|
||||
HEADLAMP_TOKEN: ${{ secrets.HEADLAMP_TOKEN }}
|
||||
AUTHENTIK_USERNAME: ${{ secrets.AUTHENTIK_USERNAME }}
|
||||
AUTHENTIK_PASSWORD: ${{ secrets.AUTHENTIK_PASSWORD }}
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,102 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if release is already finalized
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
TARBALL_URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz"
|
||||
HTTP_CODE=$(curl -sL -o /tmp/release.tar.gz -w "%{http_code}" "$TARBALL_URL" 2>/dev/null)
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
ACTUAL="sha256:$(sha256sum /tmp/release.tar.gz | awk '{print $1}')"
|
||||
EXPECTED=$(grep 'archive-checksum' artifacthub-pkg.yml | awk '{print $2}')
|
||||
echo "Release tarball checksum: $ACTUAL"
|
||||
echo "Metadata checksum: $EXPECTED"
|
||||
if [ "$ACTUAL" = "$EXPECTED" ]; then
|
||||
echo "SKIP_BUILD=true" >> $GITHUB_ENV
|
||||
echo "Checksums match - release is finalized, nothing to do"
|
||||
fi
|
||||
else
|
||||
echo "No existing release (HTTP $HTTP_CODE) - will build"
|
||||
fi
|
||||
rm -f /tmp/release.tar.gz
|
||||
|
||||
- name: Setup Node.js
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Package tarball
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
run: npx @kinvolk/headlamp-plugin package
|
||||
|
||||
- name: Compute tarball checksum
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
run: |
|
||||
TARBALL=$(ls *.tar.gz)
|
||||
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
|
||||
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
|
||||
echo "Tarball: $TARBALL"
|
||||
echo "Checksum: sha256:$CHECKSUM"
|
||||
|
||||
- name: Create GitHub release and upload tarball
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ${{ env.TARBALL }}
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update metadata and align tag
|
||||
if: env.SKIP_BUILD != 'true'
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME#v}
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Update metadata
|
||||
git fetch origin main
|
||||
git checkout origin/main -B temp-update
|
||||
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
|
||||
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml
|
||||
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
||||
git add artifacthub-pkg.yml
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "ci: update artifact hub metadata for ${GITHUB_REF_NAME}"
|
||||
git push origin temp-update:main
|
||||
fi
|
||||
|
||||
# Force-move tag to the commit with correct checksum.
|
||||
# This triggers a new CI run, but the guard step will detect
|
||||
# that the release checksum already matches and skip the build.
|
||||
git tag -f ${GITHUB_REF_NAME}
|
||||
git push -f origin ${GITHUB_REF_NAME}
|
||||
echo "Tag ${GITHUB_REF_NAME} aligned with updated metadata"
|
||||
@@ -6,3 +6,5 @@ dist/
|
||||
e2e/.auth/
|
||||
test-results/
|
||||
.playwright-mcp/
|
||||
.env
|
||||
.env.local
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Headlamp Polaris Plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.5] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Fixed drawer background remaining white in dark mode by using correct CSS variable (`--mui-palette-background-default`)
|
||||
|
||||
### Documentation
|
||||
- Added comprehensive Priority 2 documentation (ARCHITECTURE.md, DEPLOYMENT.md, SECURITY.md)
|
||||
- Added CONTRIBUTING.md with development workflow, branching strategy, and code style guidelines
|
||||
- Added complete CHANGELOG.md documenting all releases from v0.0.1 to current
|
||||
|
||||
## [0.3.4] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Removed all `@mui/material` and `@mui/icons-material` imports causing plugin load failure
|
||||
- Fixed plugin settings page registration (changed name from 'polaris' to 'headlamp-polaris-plugin')
|
||||
- Added dark mode support using MUI CSS variables for proper theme adaptation
|
||||
- Resolved TypeScript compilation errors in plugin registration calls
|
||||
|
||||
### Changed
|
||||
- Replaced all MUI components with standard HTML elements and inline styles
|
||||
- Updated `registerDetailsViewSection` and `registerAppBarAction` to match Headlamp plugin API v0.13.0
|
||||
- App bar badge, settings buttons, and UI elements now use theme-aware CSS variables
|
||||
|
||||
### Infrastructure
|
||||
- Migrated from Gitea to GitHub Actions exclusively
|
||||
- Added CI workflow for lint, type-check, build, and test
|
||||
- Enhanced E2E testing documentation with comprehensive guides
|
||||
- Added documentation-engineer subagent
|
||||
|
||||
## [0.3.3] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Corrected plugin settings registration name to match package.json
|
||||
- Added displaySaveButton parameter to settings registration
|
||||
|
||||
## [0.3.2] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- Removed all MUI dependencies to fix plugin loading in Headlamp v0.39.0+
|
||||
- Plugin now loads correctly in sidebar and routes
|
||||
|
||||
## [0.3.1] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
- TypeScript compilation errors in `registerDetailsViewSection` and `registerAppBarAction` calls
|
||||
- Test failures in DashboardView (added missing SimpleTable mock)
|
||||
|
||||
## [0.3.0] - 2026-02-11
|
||||
|
||||
### Added
|
||||
- App bar badge displaying cluster Polaris score
|
||||
- Inline audit sections in resource detail views (Deployment, StatefulSet, DaemonSet, Job, CronJob)
|
||||
- Exemption management UI (view/add exemptions via annotations)
|
||||
- Connection testing button in plugin settings
|
||||
- Top issues dashboard with severity-based filtering
|
||||
- Namespace drawer navigation with URL hash support
|
||||
|
||||
### Changed
|
||||
- Migrated namespace detail to right-side drawer panel
|
||||
- Improved drawer keyboard navigation (Escape to close)
|
||||
- Enhanced settings page with connection testing
|
||||
|
||||
### Fixed
|
||||
- Empty namespace crash handling
|
||||
- Drawer navigation pattern for better UX
|
||||
|
||||
## [0.2.5] - 2025-12-XX
|
||||
|
||||
### Fixed
|
||||
- Improved theming and settings visibility
|
||||
|
||||
## [0.2.4] - 2025-12-XX
|
||||
|
||||
### Changed
|
||||
- Increased namespace detail panel width to 1000px for better readability
|
||||
|
||||
## [0.2.3] - 2025-12-XX
|
||||
|
||||
### Added
|
||||
- Full URL support for custom Polaris dashboards
|
||||
- Support for external Polaris instances (not just service proxy)
|
||||
|
||||
## [0.2.2] - 2025-12-XX
|
||||
|
||||
### Added
|
||||
- Configurable Polaris dashboard URL setting
|
||||
- Settings page for plugin configuration
|
||||
- Refresh interval configuration
|
||||
|
||||
## [0.2.1] - 2025-12-XX
|
||||
|
||||
### Infrastructure
|
||||
- Migrated to GitHub as primary repository
|
||||
- Fixed v0.2.0 checksum in ArtifactHub metadata
|
||||
|
||||
## [0.2.0] - 2025-12-XX
|
||||
|
||||
### Added
|
||||
- Namespace drawer navigation
|
||||
- URL hash-based routing for namespaces
|
||||
- Keyboard shortcuts (Escape to close drawer)
|
||||
|
||||
### Infrastructure
|
||||
- GitHub release automation
|
||||
- Improved CI/CD workflow
|
||||
|
||||
## [0.1.7] - 2025-11-XX
|
||||
|
||||
### Documentation
|
||||
- Removed incorrect development installation instructions
|
||||
|
||||
## [0.1.6] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Plugin settings display name changed to "Polaris"
|
||||
|
||||
### Documentation
|
||||
- Added tooltip to skipped count explaining limitation
|
||||
- Documented skipped count limitation in README
|
||||
|
||||
## [0.1.5] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Restored `:80` port in service proxy URL for correct dashboard access
|
||||
|
||||
## [0.1.4] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Playwright E2E smoke tests
|
||||
- Test coverage for sidebar, overview, namespaces, and detail views
|
||||
|
||||
### Fixed
|
||||
- Empty namespace crash (graceful handling)
|
||||
- Removed `:80` port suffix from service proxy URL for RBAC compatibility
|
||||
|
||||
## [0.1.3] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Service proxy URL format for consistent RBAC requirements
|
||||
|
||||
## [0.1.2] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Namespace filtering and sorting
|
||||
- Enhanced resource table in namespace detail view
|
||||
|
||||
## [0.1.1] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Score calculation for resources with mixed results
|
||||
- Percentage display formatting
|
||||
|
||||
## [0.1.0] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Namespace detail view with resource-level audit results
|
||||
- Drill-down navigation from namespace list
|
||||
|
||||
### Changed
|
||||
- Improved data fetching with error handling
|
||||
- Better loading states
|
||||
|
||||
## [0.0.10] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- **RBAC Documentation:** Corrected to use `services/proxy` permission instead of ConfigMap access
|
||||
|
||||
### Documentation
|
||||
- Updated README with accurate RBAC requirements
|
||||
- Added minimal Role example
|
||||
|
||||
## [0.0.9] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Refresh button for manual data reload
|
||||
- Last updated timestamp display
|
||||
|
||||
## [0.0.8] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Skipped checks display in check summary
|
||||
- Improved check categorization (pass/warning/danger/skipped)
|
||||
|
||||
## [0.0.7] - 2025-11-XX
|
||||
|
||||
### Changed
|
||||
- Enhanced overview dashboard layout
|
||||
- Better visual hierarchy for cluster score
|
||||
|
||||
## [0.0.6] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Namespace list view with per-namespace scores
|
||||
- Navigation between overview and namespace views
|
||||
|
||||
## [0.0.5] - 2025-11-XX
|
||||
|
||||
### Fixed
|
||||
- Data fetching error handling
|
||||
- API proxy path configuration
|
||||
|
||||
## [0.0.4] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Check distribution visualization
|
||||
- Pass/Warning/Danger count display
|
||||
|
||||
## [0.0.3] - 2025-11-XX
|
||||
|
||||
### Changed
|
||||
- Improved cluster score calculation
|
||||
- Better result aggregation logic
|
||||
|
||||
## [0.0.2] - 2025-11-XX
|
||||
|
||||
### Added
|
||||
- Cluster score display
|
||||
- Basic check summary table
|
||||
|
||||
## [0.0.1] - 2025-10-XX
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
- Basic Polaris plugin structure
|
||||
- Sidebar entry "Polaris"
|
||||
- Overview page with cluster info
|
||||
- Data fetching from Polaris dashboard via Kubernetes service proxy
|
||||
- TypeScript support with strict mode
|
||||
- React components using Headlamp CommonComponents
|
||||
|
||||
### Infrastructure
|
||||
- GitHub repository setup
|
||||
- ArtifactHub package registration
|
||||
- Automated release workflow
|
||||
- Basic CI/CD pipeline
|
||||
|
||||
[Unreleased]: https://github.com/cpfarhood/headlamp-polaris-plugin/compare/v0.3.5...HEAD
|
||||
[0.3.5]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.3.5
|
||||
[0.3.4]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.3.4
|
||||
[0.3.3]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.3.3
|
||||
[0.3.2]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.3.2
|
||||
[0.3.1]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.3.1
|
||||
[0.3.0]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.3.0
|
||||
[0.2.5]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.2.5
|
||||
[0.2.4]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.2.4
|
||||
[0.2.3]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.2.3
|
||||
[0.2.2]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.2.2
|
||||
[0.2.1]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.2.1
|
||||
[0.2.0]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.2.0
|
||||
[0.1.7]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.7
|
||||
[0.1.6]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.6
|
||||
[0.1.5]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.5
|
||||
[0.1.4]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.4
|
||||
[0.1.3]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.3
|
||||
[0.1.2]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.2
|
||||
[0.1.1]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.1
|
||||
[0.1.0]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.1.0
|
||||
[0.0.10]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.10
|
||||
[0.0.9]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.9
|
||||
[0.0.8]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.8
|
||||
[0.0.7]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.7
|
||||
[0.0.6]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.6
|
||||
[0.0.5]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.5
|
||||
[0.0.4]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.4
|
||||
[0.0.3]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.3
|
||||
[0.0.2]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.2
|
||||
[0.0.1]: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/tag/v0.0.1
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
# Contributing to Headlamp Polaris Plugin
|
||||
|
||||
Thank you for your interest in contributing to the Headlamp Polaris Plugin! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Branching Strategy](#branching-strategy)
|
||||
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Code Style](#code-style)
|
||||
- [Testing Requirements](#testing-requirements)
|
||||
- [Documentation](#documentation)
|
||||
- [Release Process](#release-process)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows a standard code of conduct:
|
||||
- Be respectful and inclusive
|
||||
- Welcome newcomers and help them get started
|
||||
- Focus on constructive feedback
|
||||
- Assume good intentions
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20 or later
|
||||
- npm or yarn
|
||||
- Access to a Kubernetes cluster with Headlamp installed (for testing)
|
||||
- Git
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Fork and clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/headlamp-polaris-plugin.git
|
||||
cd headlamp-polaris-plugin
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start development mode:**
|
||||
```bash
|
||||
npm start
|
||||
# Plugin will be available at http://localhost:4466
|
||||
```
|
||||
|
||||
4. **Run tests:**
|
||||
```bash
|
||||
# Unit tests
|
||||
npm test
|
||||
|
||||
# E2E tests (requires Headlamp instance)
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
5. **Build the plugin:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Feature Development
|
||||
|
||||
1. Create a feature branch from `main`
|
||||
2. Make your changes
|
||||
3. Write/update tests
|
||||
4. Update documentation
|
||||
5. Run lint and tests locally
|
||||
6. Submit a pull request
|
||||
|
||||
### Local Testing
|
||||
|
||||
**Option 1: Development Mode**
|
||||
```bash
|
||||
npm start
|
||||
# Opens Headlamp at http://localhost:4466 with hot reload
|
||||
```
|
||||
|
||||
**Option 2: Production Build**
|
||||
```bash
|
||||
npm run build
|
||||
# Plugin bundle created in dist/
|
||||
```
|
||||
|
||||
**Option 3: E2E Testing**
|
||||
```bash
|
||||
# Set up environment (see e2e/README.md)
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system --duration=24h)
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
## Branching Strategy
|
||||
|
||||
### Main Branch
|
||||
|
||||
- **Purpose:** Stable, production-ready code
|
||||
- **Protection:** Only merge via pull requests
|
||||
- **Naming:** `main`
|
||||
|
||||
### Feature Branches
|
||||
|
||||
- **Purpose:** Development of new features or fixes
|
||||
- **Naming Convention:**
|
||||
- Features: `feat/description` or `feature/description`
|
||||
- Bug fixes: `fix/description`
|
||||
- Documentation: `docs/description`
|
||||
- Refactoring: `refactor/description`
|
||||
- Chores: `chore/description`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
feat/add-exemption-support
|
||||
fix/dark-mode-theme-colors
|
||||
docs/update-rbac-guide
|
||||
refactor/polaris-api-client
|
||||
chore/upgrade-dependencies
|
||||
```
|
||||
|
||||
### Branching Rules
|
||||
|
||||
**✅ ALWAYS use feature branches for:**
|
||||
- Code changes (new features, bug fixes, refactors)
|
||||
- Test updates
|
||||
- CI/CD workflow changes
|
||||
- Package updates
|
||||
|
||||
**✅ MAY push directly to main for:**
|
||||
- Documentation-only changes (README.md, CLAUDE.md, comments)
|
||||
- Version bump commits (`package.json` + `artifacthub-pkg.yml`)
|
||||
|
||||
**❌ NEVER push directly to main for:**
|
||||
- Any code changes to `src/`
|
||||
- Test file changes
|
||||
- Workflow changes
|
||||
- Dependency updates
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/) format:
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- **feat:** New feature
|
||||
- **fix:** Bug fix
|
||||
- **docs:** Documentation only
|
||||
- **style:** Code style (formatting, no logic change)
|
||||
- **refactor:** Code change that neither fixes a bug nor adds a feature
|
||||
- **perf:** Performance improvement
|
||||
- **test:** Adding or updating tests
|
||||
- **chore:** Maintenance tasks (deps, build, CI)
|
||||
- **ci:** CI/CD changes
|
||||
|
||||
### Scope (Optional)
|
||||
|
||||
- `api` - API-related changes
|
||||
- `ui` - UI component changes
|
||||
- `settings` - Plugin settings
|
||||
- `tests` - Test-related changes
|
||||
- `docs` - Documentation changes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
feat(api): add support for custom Polaris dashboard URLs
|
||||
|
||||
fix(ui): resolve dark mode theme color inconsistencies
|
||||
|
||||
docs: update RBAC examples with NetworkPolicy
|
||||
|
||||
chore: bump version to 0.3.5
|
||||
|
||||
test(e2e): add tests for plugin settings page
|
||||
```
|
||||
|
||||
### Footer
|
||||
|
||||
Add `Co-Authored-By` for pair programming or AI assistance:
|
||||
|
||||
```
|
||||
feat: add namespace filtering to overview
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||||
Co-Authored-By: Happy <yesreply@happy.engineering>
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Creating a PR
|
||||
|
||||
1. **Run all checks locally:**
|
||||
```bash
|
||||
npm run build # Verify build succeeds
|
||||
npm run lint # Check for linting errors
|
||||
npm run tsc # Type-check TypeScript
|
||||
npm test # Run unit tests
|
||||
npm run format # Format code with Prettier
|
||||
```
|
||||
|
||||
2. **Update documentation:**
|
||||
- Update README.md if you added features or changed behavior
|
||||
- Update CLAUDE.md if you changed architecture or constraints
|
||||
- Add/update JSDoc comments for new APIs
|
||||
|
||||
3. **Write/update tests:**
|
||||
- Add unit tests for new functions/components
|
||||
- Update E2E tests if UI behavior changed
|
||||
- Ensure all tests pass
|
||||
|
||||
### Creating a PR
|
||||
|
||||
1. **Push your branch:**
|
||||
```bash
|
||||
git push origin feat/your-feature
|
||||
```
|
||||
|
||||
2. **Create PR on GitHub:**
|
||||
- Use a descriptive title following commit conventions
|
||||
- Fill out the PR template (if available)
|
||||
- Link related issues with `Fixes #123` or `Closes #456`
|
||||
|
||||
3. **PR Title Format:**
|
||||
```
|
||||
feat: add exemption management UI
|
||||
fix: correct score calculation for skipped checks
|
||||
docs: improve deployment guide with Helm examples
|
||||
```
|
||||
|
||||
4. **PR Description Should Include:**
|
||||
- Summary of changes
|
||||
- Motivation and context
|
||||
- Testing performed
|
||||
- Screenshots (for UI changes)
|
||||
- Breaking changes (if any)
|
||||
|
||||
### PR Review Process
|
||||
|
||||
1. **Automated Checks:**
|
||||
- ✅ CI workflow (lint, type-check, build, test)
|
||||
- ✅ E2E tests (may fail if plugin not deployed)
|
||||
|
||||
2. **Maintainer Review:**
|
||||
- Code quality and style
|
||||
- Test coverage
|
||||
- Documentation completeness
|
||||
- Breaking changes assessment
|
||||
|
||||
3. **Merging:**
|
||||
- Use **merge commits** (not squash, not rebase)
|
||||
- Delete feature branch after merge
|
||||
- Maintainers will handle version bumps and releases
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript
|
||||
|
||||
- **Strictness:** Full TypeScript strict mode enabled
|
||||
- **No `any`:** Use specific types or `unknown`
|
||||
- **Interfaces over types:** Prefer `interface` for object shapes
|
||||
- **Named exports:** Use named exports, not default exports
|
||||
|
||||
### React
|
||||
|
||||
- **Functional components:** Use function components with hooks
|
||||
- **Props interfaces:** Always define props as interfaces
|
||||
- **Headlamp components:** Use CommonComponents from Headlamp, never raw MUI
|
||||
- **No inline styles:** Use theme-aware CSS variables
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Auto-fix linting issues
|
||||
npm run lint:fix
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Check formatting
|
||||
npm run format:check
|
||||
```
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports are automatically sorted by eslint. Order:
|
||||
1. React imports
|
||||
2. Third-party libraries
|
||||
3. Headlamp plugin imports
|
||||
4. Local imports (components, API, types)
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
import { computeScore } from '../api/polaris';
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Components:** PascalCase (`DashboardView`, `PolarisSettings`)
|
||||
- **Files:** Match component name (`DashboardView.tsx`)
|
||||
- **Hooks:** Prefix with `use` (`usePolarisData`)
|
||||
- **Utilities:** camelCase (`countResults`, `computeScore`)
|
||||
- **Constants:** UPPER_SNAKE_CASE (`DASHBOARD_URL_DEFAULT`)
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests (Required)
|
||||
|
||||
- All new functions must have unit tests
|
||||
- All bug fixes should include regression tests
|
||||
- Aim for meaningful coverage, not just numbers
|
||||
- Use descriptive test names
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
describe('countResults', () => {
|
||||
it('counts passing, warning, and danger results correctly', () => {
|
||||
// Test implementation
|
||||
});
|
||||
|
||||
it('includes skipped checks in total count', () => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Tests (Recommended)
|
||||
|
||||
- Add E2E tests for new UI features
|
||||
- Update existing tests if behavior changes
|
||||
- See `e2e/README.md` for detailed instructions
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run E2E tests
|
||||
npm run e2e
|
||||
|
||||
# Run E2E tests in headed mode (see browser)
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Documentation Updates Required
|
||||
|
||||
When making changes, update relevant documentation:
|
||||
|
||||
#### Code Changes
|
||||
- **README.md:** User-facing features, installation, configuration
|
||||
- **CLAUDE.md:** Architecture, constraints, MCP integrations
|
||||
- **JSDoc:** All public APIs, components, hooks
|
||||
|
||||
#### Test Changes
|
||||
- **e2e/README.md:** New test scenarios or setup changes
|
||||
|
||||
#### Build/CI Changes
|
||||
- **README.md:** Build commands, release process
|
||||
- **.github/workflows/*.yaml:** Workflow comments
|
||||
|
||||
### JSDoc Style
|
||||
|
||||
Use JSDoc for all exported functions, components, and types:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Counts passing, warning, danger, and skipped Polaris check results.
|
||||
*
|
||||
* Skipped checks are identified by severity "ignore" with success false.
|
||||
*
|
||||
* @param data - AuditData from Polaris dashboard API
|
||||
* @returns ResultCounts with totals by status (pass/warning/danger/skipped)
|
||||
*/
|
||||
export function countResults(data: AuditData): ResultCounts {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
### Version Numbering
|
||||
|
||||
We follow [Semantic Versioning](https://semver.org/):
|
||||
- **Major (1.0.0):** Breaking changes
|
||||
- **Minor (0.1.0):** New features, backward compatible
|
||||
- **Patch (0.0.1):** Bug fixes, backward compatible
|
||||
|
||||
### Creating a Release
|
||||
|
||||
**Maintainers only:**
|
||||
|
||||
1. **Merge feature PRs to main**
|
||||
|
||||
2. **Bump version:**
|
||||
```bash
|
||||
# Edit package.json and artifacthub-pkg.yml
|
||||
# Update version and archive-url
|
||||
git add package.json artifacthub-pkg.yml
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
3. **Create and push tag:**
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
4. **GitHub Actions automatically:**
|
||||
- Builds plugin tarball
|
||||
- Creates GitHub release
|
||||
- Uploads tarball to release
|
||||
- Updates `artifacthub-pkg.yml` with checksum
|
||||
|
||||
5. **ArtifactHub syncs within 30 minutes**
|
||||
|
||||
### Pre-release Versions
|
||||
|
||||
For testing before stable release:
|
||||
- Use `-dev.N` suffix: `v0.3.5-dev.1`
|
||||
- Follow same process as stable releases
|
||||
- Mark as "pre-release" on GitHub
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Questions:** Open a [GitHub Discussion](https://github.com/cpfarhood/headlamp-polaris-plugin/discussions)
|
||||
- **Bugs:** Open a [GitHub Issue](https://github.com/cpfarhood/headlamp-polaris-plugin/issues)
|
||||
- **E2E Testing:** See [e2e/README.md](e2e/README.md)
|
||||
- **Architecture:** See [CLAUDE.md](CLAUDE.md)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
@@ -0,0 +1,290 @@
|
||||
# Headlamp Polaris Plugin - Project Assessment
|
||||
|
||||
**Date:** 2026-02-11
|
||||
**Version:** v0.3.0
|
||||
**Status:** Active Development
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This assessment identifies critical issues and improvement opportunities for the headlamp-polaris-plugin project. The plugin is currently non-functional in production due to Headlamp v0.39.0 compatibility issues, and has several TypeScript compilation errors that need immediate attention.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Critical Issues (Must Fix Immediately)
|
||||
|
||||
### 1. TypeScript Compilation Errors
|
||||
**Severity:** CRITICAL
|
||||
**Impact:** Build failures, type safety compromised
|
||||
|
||||
**Issues:**
|
||||
- `src/index.tsx:72` - `registerDetailsViewSection` expects 1 argument, got 2
|
||||
- `src/index.tsx:87` - `registerAppBarAction` expects 1 argument, got 2
|
||||
|
||||
**Recommendation:**
|
||||
Update Headlamp plugin API calls to match the current version. Check @kinvolk/headlamp-plugin version compatibility.
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Review Headlamp plugin API documentation
|
||||
- [ ] Update `registerDetailsViewSection` and `registerAppBarAction` calls
|
||||
- [ ] Run `npm run tsc` to verify fixes
|
||||
- [ ] Update CI to fail on TypeScript errors
|
||||
|
||||
---
|
||||
|
||||
### 2. Production Plugin Loading Failure
|
||||
**Severity:** CRITICAL
|
||||
**Impact:** Plugin is completely non-functional in production
|
||||
|
||||
**Root Cause:**
|
||||
Headlamp v0.39.0 with default `watchPlugins: true` treats catalog-managed plugins as "development directory" plugins, preventing frontend JavaScript execution.
|
||||
|
||||
**Current Status:**
|
||||
- Deployment patched to install plugins to `/headlamp/static-plugins`
|
||||
- `watchPlugins: false` configured
|
||||
- Waiting for user to test if plugins now load
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Confirm plugins load after recent deployment changes
|
||||
- [ ] Document the fix in deployment guide
|
||||
- [ ] Update MEMORY.md with final resolution
|
||||
- [ ] Consider downgrading Headlamp if issue persists
|
||||
|
||||
---
|
||||
|
||||
### 3. Test Failures
|
||||
**Severity:** HIGH
|
||||
**Impact:** CI failures, reduced confidence in changes
|
||||
|
||||
**Current Status:**
|
||||
- 1 test file failing (DashboardView)
|
||||
- 49 tests passing
|
||||
- Error related to `SimpleTable` component mock
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Fix DashboardView test mocking
|
||||
- [ ] Ensure all tests pass before merging PRs
|
||||
- [ ] Add test for top issues feature
|
||||
- [ ] Increase test coverage to >80%
|
||||
|
||||
---
|
||||
|
||||
## 🟡 High Priority Improvements
|
||||
|
||||
### 4. Type Safety Enhancements
|
||||
**Severity:** HIGH
|
||||
**Impact:** Better developer experience, catch errors earlier
|
||||
|
||||
**Recommendations:**
|
||||
- Enable stricter TypeScript checks in `tsconfig.json`
|
||||
- Add type definitions for all Headlamp plugin APIs
|
||||
- Ensure no `any` types in production code
|
||||
- Add JSDoc comments for complex types
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Audit codebase for `any` types
|
||||
- [ ] Enable `noImplicitAny` and `strictNullChecks`
|
||||
- [ ] Add type guards for API responses
|
||||
- [ ] Document complex type structures
|
||||
|
||||
---
|
||||
|
||||
### 5. Security Hardening
|
||||
**Severity:** HIGH
|
||||
**Impact:** Prevent vulnerabilities, protect user data
|
||||
|
||||
**Current Risks:**
|
||||
- Direct Kubernetes API access via service proxy
|
||||
- User input in exemption annotations (potential injection)
|
||||
- External URL configuration for Polaris dashboard
|
||||
|
||||
**Recommendations:**
|
||||
- Validate and sanitize all user inputs
|
||||
- Implement input validation for dashboard URL
|
||||
- Add CSRF protection for exemption management
|
||||
- Audit dependencies for known vulnerabilities
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Add input validation utilities
|
||||
- [ ] Sanitize exemption annotation values
|
||||
- [ ] Validate URL format for dashboard configuration
|
||||
- [ ] Run `npm audit` and fix vulnerabilities
|
||||
- [ ] Add security testing to CI/CD
|
||||
|
||||
---
|
||||
|
||||
### 6. Error Handling & User Experience
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Better error messages, improved debugging
|
||||
|
||||
**Current Gaps:**
|
||||
- Generic error messages don't help users troubleshoot
|
||||
- No retry logic for transient API failures
|
||||
- Missing loading states in some components
|
||||
|
||||
**Recommendations:**
|
||||
- Provide specific, actionable error messages
|
||||
- Implement retry logic with exponential backoff
|
||||
- Add loading skeletons for all async operations
|
||||
- Show connection test results with specific failure reasons
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Create error message constants with solutions
|
||||
- [ ] Add retry logic to API calls
|
||||
- [ ] Implement loading skeletons
|
||||
- [ ] Improve connection test error messages
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Medium Priority Enhancements
|
||||
|
||||
### 7. Testing Coverage
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Confidence in changes, regression prevention
|
||||
|
||||
**Current Coverage:**
|
||||
- Unit tests: Good coverage for API utilities
|
||||
- Component tests: Some coverage, gaps exist
|
||||
- E2E tests: Minimal (Playwright configured but underutilized)
|
||||
|
||||
**Recommendations:**
|
||||
- Add E2E tests for critical user flows
|
||||
- Test error scenarios and edge cases
|
||||
- Add visual regression tests
|
||||
- Test RBAC permission denied scenarios
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Write E2E test for complete audit workflow
|
||||
- [ ] Add tests for error states
|
||||
- [ ] Test exemption management flow
|
||||
- [ ] Add Playwright tests to CI
|
||||
|
||||
---
|
||||
|
||||
### 8. Performance Optimization
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Faster load times, better UX
|
||||
|
||||
**Opportunities:**
|
||||
- Memoize expensive calculations (score computation)
|
||||
- Lazy load namespace detail views
|
||||
- Debounce search/filter operations
|
||||
- Cache Polaris data with stale-while-revalidate
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Add React.memo to pure components
|
||||
- [ ] Memoize score calculations
|
||||
- [ ] Implement data caching strategy
|
||||
- [ ] Profile component render times
|
||||
|
||||
---
|
||||
|
||||
### 9. Code Quality & Maintainability
|
||||
**Severity:** MEDIUM
|
||||
**Impact:** Easier maintenance, onboarding
|
||||
|
||||
**Recommendations:**
|
||||
- Extract magic strings to constants
|
||||
- Reduce component complexity
|
||||
- Add JSDoc comments for public APIs
|
||||
- Improve code organization
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Create constants file for check IDs
|
||||
- [ ] Split large components (DashboardView, NamespaceDetailView)
|
||||
- [ ] Add comments for complex logic
|
||||
- [ ] Establish code review checklist
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Low Priority / Future Enhancements
|
||||
|
||||
### 10. Documentation
|
||||
**Severity:** LOW
|
||||
**Impact:** Better onboarding, user adoption
|
||||
|
||||
**Gaps:**
|
||||
- No architecture documentation
|
||||
- Limited inline code comments
|
||||
- Missing troubleshooting guide
|
||||
- No contributor guidelines
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Create architecture diagram
|
||||
- [ ] Document component hierarchy
|
||||
- [ ] Add troubleshooting section to README
|
||||
- [ ] Create CONTRIBUTING.md
|
||||
|
||||
---
|
||||
|
||||
### 11. CI/CD Pipeline Optimization
|
||||
**Severity:** LOW
|
||||
**Impact:** Faster feedback, automated releases
|
||||
|
||||
**Opportunities:**
|
||||
- Run tests in parallel
|
||||
- Cache npm dependencies
|
||||
- Add automated security scanning
|
||||
- Implement semantic versioning
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Parallelize test execution
|
||||
- [ ] Add npm cache to GitHub Actions
|
||||
- [ ] Integrate Dependabot
|
||||
- [ ] Add semantic-release
|
||||
|
||||
---
|
||||
|
||||
## Summary & Prioritization
|
||||
|
||||
### Week 1 (Immediate)
|
||||
1. ✅ Fix TypeScript compilation errors
|
||||
2. ✅ Resolve production plugin loading issue
|
||||
3. ✅ Fix failing DashboardView test
|
||||
|
||||
### Week 2 (High Priority)
|
||||
4. Enhance type safety (strict mode)
|
||||
5. Implement security hardening
|
||||
6. Improve error handling and UX
|
||||
|
||||
### Week 3-4 (Medium Priority)
|
||||
7. Increase test coverage to >80%
|
||||
8. Optimize performance (memoization, caching)
|
||||
9. Refactor for maintainability
|
||||
|
||||
### Ongoing (Low Priority)
|
||||
10. Documentation improvements
|
||||
11. CI/CD optimizations
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
**Code Quality:**
|
||||
- ✅ Zero TypeScript errors
|
||||
- ✅ All tests passing
|
||||
- 🎯 Test coverage >80%
|
||||
- 🎯 No high/critical security vulnerabilities
|
||||
|
||||
**Production Readiness:**
|
||||
- ✅ Plugin loads successfully in Headlamp
|
||||
- ✅ All features functional
|
||||
- 🎯 Error rate <1%
|
||||
- 🎯 Average response time <500ms
|
||||
|
||||
**Developer Experience:**
|
||||
- ✅ Clear documentation
|
||||
- ✅ Easy local setup
|
||||
- 🎯 Fast CI/CD (<5 min)
|
||||
- 🎯 Automated releases
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:** Fix TypeScript errors and verify plugin loads
|
||||
2. **Short-term:** Complete Week 1-2 priorities
|
||||
3. **Long-term:** Address medium and low priority items
|
||||
4. **Continuous:** Monitor metrics and iterate
|
||||
|
||||
**Recommended First Action:**
|
||||
Fix the TypeScript compilation errors in `src/index.tsx` by updating the Headlamp plugin API calls.
|
||||
@@ -6,18 +6,29 @@ A [Headlamp](https://headlamp.dev/) plugin that surfaces [Fairwinds Polaris](htt
|
||||
|
||||
## What It Does
|
||||
|
||||
Adds a **Polaris** top-level sidebar section to Headlamp with the following views:
|
||||
Adds a **Polaris** top-level sidebar section to Headlamp with comprehensive security, reliability, and efficiency audit integration:
|
||||
|
||||
- **Overview** -- cluster score as a percentage (color-coded green/amber/red), check summary (pass/warning/danger/skipped counts), and cluster info (nodes, pods, namespaces, controllers)
|
||||
- **Namespaces** -- table of all namespaces with per-namespace score, pass/warning/danger/skipped counts; click a namespace to drill down
|
||||
- **Namespace detail** -- per-namespace score, check counts, and a resource table showing pass/warning/danger per workload
|
||||
- **External link** -- quick jump to the native Polaris dashboard via the Kubernetes service proxy (from namespace detail view)
|
||||
### Main Views
|
||||
|
||||
Data is fetched from the Polaris dashboard API through the Kubernetes service proxy (`/api/v1/namespaces/polaris/services/polaris-dashboard/proxy/results.json`). The plugin is read-only -- it never writes to the cluster.
|
||||
- **Overview Dashboard** -- cluster score with percentage gauge, check distribution charts, top 10 most common failing checks across the cluster, cluster statistics, and last audit time with manual refresh button
|
||||
- **Namespaces** -- table of all namespaces with per-namespace score and check counts; click a namespace to open a detailed side panel (1000px wide, theme-aware)
|
||||
- **Namespace Detail Panel** -- per-namespace score, check counts, resource-level audit results, external Polaris dashboard link, and exemption management
|
||||
|
||||
Results are refreshed on a user-configurable interval (1 / 5 / 10 / 30 minutes, default 5). The setting is available in **Settings > Plugins > Polaris** and persists in the browser's localStorage.
|
||||
### Integrated Features
|
||||
|
||||
Error states are handled explicitly: RBAC denied (403), Polaris not installed (404/503), malformed JSON, and loading.
|
||||
- **App Bar Score Badge** -- cluster Polaris score displayed as a colored chip in the top navigation bar (green ≥80%, yellow ≥50%, red <50%); click to navigate to overview
|
||||
- **Inline Resource Audits** -- Polaris audit results automatically injected into detail views for Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs; shows compact score, failing checks table, and link to full report
|
||||
- **Exemption Management** -- add or remove Polaris exemptions via annotation patches directly from the UI; supports per-check exemptions or exempt-all
|
||||
- **Configurable Dashboard URL** -- supports both Kubernetes service proxy URLs and full HTTP/HTTPS URLs for external Polaris deployments
|
||||
- **Connection Testing** -- test button in settings to verify Polaris dashboard connectivity and show version info
|
||||
|
||||
### Data & Refresh
|
||||
|
||||
Data is fetched from the Polaris dashboard API through the Kubernetes service proxy (`/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`) or custom URLs. The plugin is primarily read-only; it only writes when explicitly applying exemption annotations.
|
||||
|
||||
Results are refreshed on a user-configurable interval (1 / 5 / 10 / 30 minutes, default 5). Settings are available in **Settings > Plugins > Polaris** and persist in browser localStorage.
|
||||
|
||||
Error states are handled explicitly with context-specific messages: RBAC denied (403), Polaris not installed (404/503), malformed JSON, network failures, and CORS issues.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -85,56 +96,7 @@ npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||
|
||||
## Installing Dev/Preview Versions
|
||||
|
||||
Dev preview versions (e.g., `v0.2.0-dev.4`) are published to [GitHub Releases](https://github.com/cpfarhood/headlamp-polaris-plugin/releases) but are **not available via ArtifactHub**. These versions contain experimental features and are intended for testing.
|
||||
|
||||
To install a dev version, use the direct URL method:
|
||||
|
||||
### Sidecar container pattern (recommended)
|
||||
|
||||
Create a ConfigMap with the dev version URL:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
data:
|
||||
plugin.yml: |
|
||||
plugins:
|
||||
- url: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.2.0-dev.4/headlamp-polaris-plugin-0.2.0-dev.4.tar.gz
|
||||
```
|
||||
|
||||
Then configure Headlamp to use a sidecar container that installs from this config:
|
||||
|
||||
```yaml
|
||||
# In Headlamp Helm values or deployment
|
||||
containers:
|
||||
- name: headlamp-plugin-installer
|
||||
image: node:lts-alpine
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /headlamp/plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
volumes:
|
||||
- name: plugins
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
```
|
||||
|
||||
### Manual download
|
||||
|
||||
Browse [GitHub Releases](https://github.com/cpfarhood/headlamp-polaris-plugin/releases), download the dev version tarball, and extract it to your Headlamp plugins directory.
|
||||
|
||||
**Note:** Dev versions are tagged with the `-dev.N` suffix and may introduce breaking changes or unstable features. Use stable versions for production deployments.
|
||||
Dev preview versions are **not currently available** through the Headlamp plugin manager. Stable versions can be installed from ArtifactHub via the plugin manager UI.
|
||||
|
||||
## RBAC / Security Setup
|
||||
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
# Security Policy
|
||||
|
||||
## Overview
|
||||
|
||||
The Headlamp Polaris Plugin is a read-only visualization tool that displays Fairwinds Polaris audit results within the Headlamp UI. Security considerations primarily revolve around Kubernetes RBAC, network policies, and data access controls.
|
||||
|
||||
## Security Model
|
||||
|
||||
### Read-Only Operation
|
||||
|
||||
The plugin performs **only read operations** via the Kubernetes API server's service proxy mechanism:
|
||||
|
||||
- **No write operations**: The plugin never creates, updates, or deletes Kubernetes resources
|
||||
- **No CRD installation**: No custom resource definitions or cluster-level modifications
|
||||
- **No secrets**: The plugin does not read or store Kubernetes secrets
|
||||
- **No PII**: Polaris audit data contains resource metadata but no personally identifiable information
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Browser
|
||||
↓ (HTTPS)
|
||||
Headlamp Pod
|
||||
↓ (in-cluster service account or user token)
|
||||
Kubernetes API Server
|
||||
↓ (service proxy: /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/)
|
||||
Polaris Dashboard Service
|
||||
↓ (returns audit JSON)
|
||||
Plugin Frontend (React)
|
||||
```
|
||||
|
||||
All communication uses Kubernetes authentication and authorization mechanisms. The plugin never stores credentials or bypasses RBAC.
|
||||
|
||||
## RBAC Requirements
|
||||
|
||||
### Minimal Permissions
|
||||
|
||||
The plugin requires only one permission:
|
||||
|
||||
| Verb | API Group | Resource | Resource Name | Namespace |
|
||||
|------|-----------|----------|---------------|-----------|
|
||||
| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` |
|
||||
|
||||
**Example minimal Role:**
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: polaris
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
resourceNames: ["polaris-dashboard"]
|
||||
verbs: ["get"]
|
||||
```
|
||||
|
||||
### RoleBinding Options
|
||||
|
||||
**Option 1: Service Account (Recommended)**
|
||||
|
||||
Bind to the Headlamp service account for all users:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Option 2: OIDC Groups**
|
||||
|
||||
Bind to user groups for OIDC authentication:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: "developers"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**Option 3: Specific Users**
|
||||
|
||||
Bind to individual users:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: User
|
||||
name: "jane@example.com"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### ⚠️ Security Best Practices
|
||||
|
||||
1. **Principle of Least Privilege**: Grant only `services/proxy` access, not broader `services` permissions
|
||||
2. **Namespace Scoping**: Use a namespaced `Role`, not a `ClusterRole`, to limit access to the `polaris` namespace only
|
||||
3. **Resource Name Restriction**: Always specify `resourceNames: ["polaris-dashboard"]` to prevent proxy access to other services
|
||||
4. **Audit Logging**: Enable Kubernetes audit logging to track all service proxy requests
|
||||
5. **Network Policies**: Restrict network access to the Polaris dashboard service (see Network Security below)
|
||||
|
||||
## Network Security
|
||||
|
||||
### Network Policies
|
||||
|
||||
If your cluster uses NetworkPolicies, ensure the Headlamp pod (or more specifically, the Kubernetes API server performing the proxy hop) can reach the Polaris dashboard service.
|
||||
|
||||
**Example NetworkPolicy for Polaris namespace:**
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-api-server-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from API server (adjust based on your cluster setup)
|
||||
- from:
|
||||
- namespaceSelector: {} # API server typically runs in kube-system or no namespace label
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080 # Polaris dashboard default port
|
||||
```
|
||||
|
||||
**Note**: The Kubernetes API server performs the service proxy hop, so network policies should allow traffic from the API server to Polaris, not directly from Headlamp to Polaris.
|
||||
|
||||
### TLS/HTTPS
|
||||
|
||||
- **External Access**: Always access Headlamp over HTTPS, especially when using OIDC authentication
|
||||
- **Internal Communication**: Communication between Headlamp and the Kubernetes API server uses the service account token over the cluster's internal network
|
||||
- **Service Proxy**: The API server → Polaris dashboard communication happens over HTTP within the cluster (ClusterIP service)
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Service Account (Default)
|
||||
|
||||
Headlamp runs with a dedicated service account (`headlamp` in `kube-system`). All users share the same permissions defined by this service account's RBAC bindings.
|
||||
|
||||
**Security Considerations:**
|
||||
- All users have identical access to the plugin
|
||||
- Suitable for trusted internal environments
|
||||
- Simpler RBAC management
|
||||
|
||||
### OIDC Token Authentication
|
||||
|
||||
Headlamp can be configured for OIDC authentication, where each user provides their own bearer token. RBAC is enforced per-user.
|
||||
|
||||
**Security Considerations:**
|
||||
- Fine-grained access control per user
|
||||
- Users without the `polaris-proxy-reader` role will see 403 errors
|
||||
- Requires OIDC provider integration
|
||||
- Suitable for multi-tenant or compliance-focused environments
|
||||
|
||||
**Configuration Example:**
|
||||
|
||||
```yaml
|
||||
config:
|
||||
oidc:
|
||||
clientID: "headlamp"
|
||||
clientSecret: "secret"
|
||||
issuerURL: "https://authentik.example.com/application/o/headlamp/"
|
||||
scopes: "openid profile email groups"
|
||||
```
|
||||
|
||||
When OIDC is enabled, each user's token is used for API requests, including service proxy calls.
|
||||
|
||||
## Vulnerability Reporting
|
||||
|
||||
### Supported Versions
|
||||
|
||||
We apply security updates to the latest release only. Please ensure you are running the most recent version.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| latest | :white_check_mark: |
|
||||
| < latest| :x: |
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in this plugin, please report it via:
|
||||
|
||||
1. **GitHub Security Advisories**: [Report a vulnerability](https://github.com/cpfarhood/headlamp-polaris-plugin/security/advisories/new)
|
||||
2. **Email**: Create a GitHub issue and mark it as "security" if advisories are not available
|
||||
|
||||
**Please do not:**
|
||||
- Open public GitHub issues for security vulnerabilities
|
||||
- Disclose vulnerabilities publicly before a fix is available
|
||||
|
||||
**Response Timeline:**
|
||||
- **Acknowledgment**: Within 48 hours
|
||||
- **Initial Assessment**: Within 1 week
|
||||
- **Fix Timeline**: Depends on severity (critical: 1-2 weeks, high: 2-4 weeks, medium/low: next release cycle)
|
||||
|
||||
## Dependency Security
|
||||
|
||||
### Dependency Scanning
|
||||
|
||||
The project uses:
|
||||
- **npm audit**: Runs automatically during `npm install`
|
||||
- **Dependabot**: GitHub Dependabot monitors dependencies and creates PRs for updates
|
||||
- **GitHub Actions**: CI workflow runs `npm audit` on every commit
|
||||
|
||||
### Updating Dependencies
|
||||
|
||||
Security patches are applied as follows:
|
||||
|
||||
1. **Critical vulnerabilities**: Emergency patch release within 48 hours
|
||||
2. **High severity**: Patched in next minor release (typically within 1-2 weeks)
|
||||
3. **Medium/Low severity**: Included in regular release cycle
|
||||
|
||||
### Headlamp Plugin API
|
||||
|
||||
This plugin depends on `@kinvolk/headlamp-plugin` as a peer dependency. Security updates to Headlamp itself should be applied by upgrading your Headlamp installation.
|
||||
|
||||
**Minimum supported Headlamp version**: v0.26.0
|
||||
|
||||
## Deployment Security
|
||||
|
||||
### Production Checklist
|
||||
|
||||
Before deploying to production, verify:
|
||||
|
||||
- [ ] **RBAC configured**: `polaris-proxy-reader` Role and RoleBinding exist
|
||||
- [ ] **Network policies**: Allow API server → Polaris dashboard traffic
|
||||
- [ ] **TLS enabled**: Headlamp accessible only via HTTPS
|
||||
- [ ] **OIDC configured** (if using per-user auth): Token-based authentication working
|
||||
- [ ] **Audit logging enabled**: Kubernetes API audit logs capture service proxy requests
|
||||
- [ ] **Plugin version**: Running latest release
|
||||
- [ ] **Dependencies audited**: No critical vulnerabilities in npm dependencies
|
||||
- [ ] **Polaris version**: Polaris dashboard is up-to-date
|
||||
|
||||
### Kubernetes Cluster Security
|
||||
|
||||
The plugin's security posture depends on your cluster's security:
|
||||
|
||||
- **API Server Access**: Ensure API server is not publicly accessible without authentication
|
||||
- **Service Account Tokens**: Use projected volume tokens with short expiration (Kubernetes 1.21+)
|
||||
- **Pod Security Standards**: Apply appropriate pod security policies/standards to the Headlamp namespace
|
||||
- **RBAC Auditing**: Regularly review RoleBindings to ensure least privilege
|
||||
|
||||
## Common Security Scenarios
|
||||
|
||||
### Scenario 1: 403 Forbidden Error
|
||||
|
||||
**Symptom**: Plugin shows "403 Forbidden" when loading data
|
||||
|
||||
**Cause**: User or service account lacks `services/proxy` permission on `polaris-dashboard`
|
||||
|
||||
**Resolution**:
|
||||
1. Verify RoleBinding exists in `polaris` namespace
|
||||
2. Check RoleBinding references correct subject (service account, group, or user)
|
||||
3. Confirm Role includes `resourceNames: ["polaris-dashboard"]`
|
||||
|
||||
**Security Note**: This is expected behavior when RBAC is correctly enforced. Do not grant broader permissions to "fix" 403 errors.
|
||||
|
||||
### Scenario 2: Exposing Polaris Dashboard Externally
|
||||
|
||||
**Question**: Can I expose Polaris dashboard via Ingress instead of using service proxy?
|
||||
|
||||
**Recommendation**: **Avoid exposing Polaris dashboard externally**. The service proxy approach:
|
||||
- Enforces Kubernetes RBAC on every request
|
||||
- Avoids exposing internal services to the internet
|
||||
- Prevents authentication bypass attacks
|
||||
|
||||
If you must expose Polaris externally:
|
||||
- Use OAuth2 proxy or similar authentication layer
|
||||
- Configure NetworkPolicies to restrict access
|
||||
- Enable TLS with valid certificates
|
||||
- Consider IP allowlisting
|
||||
|
||||
### Scenario 3: Multi-Tenant Clusters
|
||||
|
||||
**Question**: How do I restrict plugin access in a multi-tenant cluster?
|
||||
|
||||
**Solution**: Use OIDC authentication with per-user RoleBindings:
|
||||
|
||||
```yaml
|
||||
# Bind only to specific groups or users
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: "team-a"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Users not in `team-a` will receive 403 errors when accessing the plugin, preventing unauthorized access to Polaris audit data.
|
||||
|
||||
## Compliance Considerations
|
||||
|
||||
### Data Residency
|
||||
|
||||
All data remains within your Kubernetes cluster. The plugin does not:
|
||||
- Send data to external services
|
||||
- Store data in browser localStorage (except refresh interval preference)
|
||||
- Use third-party analytics or tracking
|
||||
|
||||
### Audit Trail
|
||||
|
||||
All service proxy requests are logged in Kubernetes API audit logs (if enabled):
|
||||
|
||||
```json
|
||||
{
|
||||
"verb": "get",
|
||||
"requestURI": "/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json",
|
||||
"user": {
|
||||
"username": "system:serviceaccount:kube-system:headlamp",
|
||||
"groups": ["system:serviceaccounts", "system:authenticated"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GDPR/Privacy
|
||||
|
||||
The plugin processes only technical metadata (resource names, namespaces, check results). No personal data is collected, stored, or transmitted.
|
||||
|
||||
## Security Updates and Notifications
|
||||
|
||||
### Notification Channels
|
||||
|
||||
Subscribe to security updates via:
|
||||
|
||||
1. **GitHub Watch**: Click "Watch" → "Custom" → "Security alerts"
|
||||
2. **GitHub Releases**: Monitor [releases page](https://github.com/cpfarhood/headlamp-polaris-plugin/releases)
|
||||
3. **ArtifactHub**: Follow package at [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp-polaris-plugin/headlamp-polaris-plugin)
|
||||
|
||||
### Security Patch Process
|
||||
|
||||
When a security vulnerability is identified:
|
||||
|
||||
1. **Private Fix**: Develop fix in private fork
|
||||
2. **Security Advisory**: Publish GitHub Security Advisory
|
||||
3. **Release**: Create new version with fix
|
||||
4. **Notification**: Update advisory with fix version
|
||||
5. **Disclosure**: Public disclosure after fix is available
|
||||
|
||||
## Contact
|
||||
|
||||
- **Security Issues**: [GitHub Security Advisories](https://github.com/cpfarhood/headlamp-polaris-plugin/security/advisories)
|
||||
- **General Questions**: [GitHub Discussions](https://github.com/cpfarhood/headlamp-polaris-plugin/discussions)
|
||||
- **Bug Reports**: [GitHub Issues](https://github.com/cpfarhood/headlamp-polaris-plugin/issues)
|
||||
|
||||
## License
|
||||
|
||||
This plugin is provided under the MIT License. See [LICENSE](LICENSE) for details.
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
version: 0.2.0-dev.5
|
||||
version: 0.3.5
|
||||
name: headlamp-polaris-plugin
|
||||
displayName: Polaris
|
||||
createdAt: "2026-02-05T19:00:00Z"
|
||||
@@ -28,7 +28,7 @@ maintainers:
|
||||
- name: cpfarhood
|
||||
email: "chris@farhood.org"
|
||||
annotations:
|
||||
headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.2.0-dev.5/headlamp-polaris-plugin-0.2.0-dev.5.tar.gz"
|
||||
headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.3.5/headlamp-polaris-plugin-0.3.5.tar.gz"
|
||||
headlamp/plugin/version-compat: ">=0.26"
|
||||
headlamp/plugin/archive-checksum: sha256:cb8d03f52022590fce5565b4f08a3fb99d0e264f3ff6a1c99ab59bf48b33ef79
|
||||
headlamp/plugin/archive-checksum: sha256:09d199ffc2705fae1c69baef39acf5bfa9aa39b7544c9e680a80fafe8c946fa0
|
||||
headlamp/plugin/distro-compat: in-cluster
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Headlamp Plugin Loading Issue - Root Cause and Fix
|
||||
|
||||
## Problem
|
||||
Headlamp v0.39.0 was not loading plugins installed via the plugin manager. Plugins appeared in Settings → Plugins but:
|
||||
- No sidebar entries appeared
|
||||
- No plugin settings were available
|
||||
- Plugin JavaScript was not being executed in the browser
|
||||
|
||||
## Root Cause
|
||||
When `config.watchPlugins: true` (the default), Headlamp treats catalog-managed plugins in `/headlamp/plugins/` as "development directory" plugins. This causes:
|
||||
- Backend serves plugin metadata correctly
|
||||
- Backend logs show "Treating catalog-installed plugin in development directory as user plugin"
|
||||
- **Frontend does NOT execute the plugin JavaScript**
|
||||
- Plugin registrations (`registerSidebarEntry`, `registerRoute`, etc.) never happen
|
||||
|
||||
## Solution
|
||||
Set `config.watchPlugins: false` in the Headlamp HelmRelease values:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
values:
|
||||
config:
|
||||
watchPlugins: false
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
configContent: |
|
||||
plugins:
|
||||
- name: polaris
|
||||
source: https://artifacthub.io/packages/headlamp/polaris/headlamp-polaris-plugin
|
||||
# ... other plugins
|
||||
```
|
||||
|
||||
## Why This Works
|
||||
With `watchPlugins: false`:
|
||||
- Headlamp no longer treats catalog-managed plugins as "development" plugins
|
||||
- Frontend properly loads and executes plugin JavaScript on startup
|
||||
- Plugin registrations happen correctly
|
||||
- All plugin features (sidebar, routes, settings, etc.) work as expected
|
||||
|
||||
## Testing
|
||||
After applying this fix:
|
||||
1. Verify plugins are installed: `kubectl logs -n kube-system <headlamp-pod> -c headlamp-plugin`
|
||||
2. Verify watchPlugins is false: `kubectl logs -n kube-system <headlamp-pod> -c headlamp | grep "Watch Plugins"`
|
||||
3. Hard refresh browser (Cmd+Shift+R / Ctrl+Shift+F5) to clear cached JavaScript
|
||||
4. Verify plugin sidebar entries appear
|
||||
5. Verify plugin functionality works
|
||||
|
||||
## Additional Notes
|
||||
- This appears to be a bug/limitation in Headlamp v0.39.0
|
||||
- The `watchPlugins` feature is intended for development scenarios where plugins are being actively modified
|
||||
- For production deployments with catalog-managed plugins, `watchPlugins: false` is the correct configuration
|
||||
- Once plugins are loaded, subsequent restarts or updates work correctly as long as `watchPlugins` remains false
|
||||
|
||||
## References
|
||||
- Headlamp Helm Chart: https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp
|
||||
- Plugin Manager: https://github.com/headlamp-k8s/headlamp/tree/main/plugins/headlamp-plugin
|
||||
- Issue discovered: 2026-02-11
|
||||
- Fix applied: 2026-02-12
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
# 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
|
||||
@@ -0,0 +1,556 @@
|
||||
# Architecture
|
||||
|
||||
This document describes the architecture, design decisions, and data flow of the Headlamp Polaris Plugin.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [System Architecture](#system-architecture)
|
||||
- [Data Flow](#data-flow)
|
||||
- [Component Hierarchy](#component-hierarchy)
|
||||
- [State Management](#state-management)
|
||||
- [Design Decisions](#design-decisions)
|
||||
- [Integration Points](#integration-points)
|
||||
- [Known Limitations](#known-limitations)
|
||||
|
||||
## Overview
|
||||
|
||||
The Headlamp Polaris Plugin is a **read-only dashboard** that surfaces Fairwinds Polaris audit results within the Headlamp UI. It fetches data from the Polaris dashboard API via the Kubernetes service proxy and presents it in a hierarchical navigation structure.
|
||||
|
||||
**Key Characteristics:**
|
||||
- **Read-only:** No write operations to cluster or Polaris
|
||||
- **Service proxy based:** Uses K8s API server proxy to reach Polaris
|
||||
- **React Context for state:** Shared data fetch across components
|
||||
- **Headlamp plugin API:** Integrates via official plugin system
|
||||
- **Type-safe:** Full TypeScript with strict mode
|
||||
|
||||
## System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Headlamp UI (React) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ App Bar │ │ Sidebar │ │ Routes │ │
|
||||
│ │ (Badge) │ │ (Navigation)│ │ (Views) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────────┼──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Plugin Registry │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────▼──────────────┐ │
|
||||
│ │ Polaris Plugin (This!) │ │
|
||||
│ ├────────────────────────────┤ │
|
||||
│ │ • registerSidebarEntry │ │
|
||||
│ │ • registerRoute │ │
|
||||
│ │ • registerAppBarAction │ │
|
||||
│ │ • registerPluginSettings │ │
|
||||
│ │ • registerDetailsViewSection│ │
|
||||
│ └─────────────┬──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────▼──────────────┐ │
|
||||
│ │ PolarisDataContext │ │
|
||||
│ │ (React Context Provider) │ │
|
||||
│ └─────────────┬──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────┼──────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ┌────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
|
||||
│ │Dashboard │ │ Namespaces │ │ Namespace │ │
|
||||
│ │View │ │ ListView │ │ Detail │ │
|
||||
│ └──────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────▼────────┐
|
||||
│ ApiProxy │
|
||||
│ (Headlamp) │
|
||||
└───────┬────────┘
|
||||
│
|
||||
┌───────▼────────┐
|
||||
│ Kubernetes │
|
||||
│ API Server │
|
||||
└───────┬────────┘
|
||||
│
|
||||
┌───────▼────────┐
|
||||
│ Service Proxy │
|
||||
│ /api/v1/ns/ │
|
||||
│ polaris/svcs/ │
|
||||
│ polaris- │
|
||||
│ dashboard/ │
|
||||
│ proxy/ │
|
||||
└───────┬────────┘
|
||||
│
|
||||
┌───────▼────────┐
|
||||
│ Polaris │
|
||||
│ Dashboard │
|
||||
│ (ClusterIP) │
|
||||
└───────┬────────┘
|
||||
│
|
||||
┌───────▼────────┐
|
||||
│ results.json │
|
||||
│ (AuditData) │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### 1. Initial Load
|
||||
|
||||
```
|
||||
User loads Headlamp
|
||||
↓
|
||||
Headlamp loads plugins
|
||||
↓
|
||||
Plugin registers routes, sidebar, app bar actions
|
||||
↓
|
||||
User navigates to /polaris
|
||||
↓
|
||||
DashboardView mounts
|
||||
↓
|
||||
PolarisDataContext.Provider wraps component
|
||||
↓
|
||||
usePolarisDataContext() hook triggers fetch
|
||||
↓
|
||||
ApiProxy.request() → K8s API → Service Proxy → Polaris
|
||||
↓
|
||||
AuditData returned and cached in Context
|
||||
↓
|
||||
Components receive data and render
|
||||
```
|
||||
|
||||
### 2. Data Refresh
|
||||
|
||||
```
|
||||
User clicks "Refresh" button or auto-refresh interval elapses
|
||||
↓
|
||||
refresh() function called in Context
|
||||
↓
|
||||
setRefreshKey() increments (forces re-fetch)
|
||||
↓
|
||||
useEffect dependency triggers new fetch
|
||||
↓
|
||||
ApiProxy.request() → Polaris Dashboard
|
||||
↓
|
||||
Context state updated with new data
|
||||
↓
|
||||
All consuming components re-render automatically
|
||||
```
|
||||
|
||||
### 3. Navigation Flow
|
||||
|
||||
```
|
||||
User clicks "Polaris" in sidebar
|
||||
↓
|
||||
Route: /c/main/polaris (DashboardView)
|
||||
↓
|
||||
Display cluster score, check distribution
|
||||
↓
|
||||
User clicks "Namespaces" submenu
|
||||
↓
|
||||
Route: /c/main/polaris/namespaces (NamespacesListView)
|
||||
↓
|
||||
Display table of namespaces with scores
|
||||
↓
|
||||
User clicks namespace button in table
|
||||
↓
|
||||
Drawer opens, URL hash updates (#namespace-name)
|
||||
↓
|
||||
NamespaceDetailView renders in drawer
|
||||
↓
|
||||
Display namespace score + resource table
|
||||
```
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
### Plugin Entry Point
|
||||
|
||||
**`src/index.tsx`**
|
||||
- Registers sidebar entries (Polaris → Overview, Namespaces)
|
||||
- Registers routes (`/polaris`, `/polaris/namespaces`)
|
||||
- Registers app bar action (score badge)
|
||||
- Registers plugin settings page
|
||||
- Registers details view section (inline audit)
|
||||
|
||||
### Data Layer
|
||||
|
||||
**`src/api/PolarisDataContext.tsx`**
|
||||
- React Context Provider for shared data
|
||||
- Fetches AuditData from Polaris dashboard
|
||||
- Handles auto-refresh based on user settings
|
||||
- Provides `{ data, loading, error, refresh }` to consumers
|
||||
|
||||
**`src/api/polaris.ts`**
|
||||
- TypeScript types for AuditData schema
|
||||
- Utility functions: `countResults()`, `computeScore()`
|
||||
- Settings management: `getRefreshInterval()`, `setRefreshInterval()`
|
||||
- Constants: `DASHBOARD_URL_DEFAULT`, `INTERVAL_OPTIONS`
|
||||
|
||||
**`src/api/checkMapping.ts`**
|
||||
- Maps Polaris check IDs to human-readable names
|
||||
- Used for display in UI (e.g., "hostIPCSet" → "Host IPC")
|
||||
|
||||
**`src/api/topIssues.ts`**
|
||||
- Aggregates failing checks across cluster
|
||||
- Groups by check ID and severity
|
||||
- Used for top issues dashboard
|
||||
|
||||
### View Components
|
||||
|
||||
**`src/components/DashboardView.tsx`**
|
||||
- **Route:** `/polaris`
|
||||
- **Purpose:** Cluster-wide overview
|
||||
- **Features:**
|
||||
- Cluster score (percentage)
|
||||
- Check distribution (pass/warning/danger/skipped)
|
||||
- Cluster info (Polaris version, last audit time)
|
||||
- Refresh button
|
||||
- **Data:** Uses `usePolarisDataContext()`
|
||||
|
||||
**`src/components/NamespacesListView.tsx`**
|
||||
- **Route:** `/polaris/namespaces`
|
||||
- **Purpose:** List all namespaces with scores
|
||||
- **Features:**
|
||||
- Table with namespace, score, pass/warning/danger counts
|
||||
- Clickable namespace buttons (opens drawer)
|
||||
- Sorted by score (lowest first)
|
||||
- **Data:** Uses `usePolarisDataContext()`, aggregates by namespace
|
||||
|
||||
**`src/components/NamespaceDetailView.tsx`**
|
||||
- **Route:** Drawer on `/polaris/namespaces#<namespace>`
|
||||
- **Purpose:** Namespace-level drill-down
|
||||
- **Features:**
|
||||
- Namespace score
|
||||
- Resource table (kind, name, score, counts)
|
||||
- URL hash navigation
|
||||
- Keyboard shortcuts (Escape to close)
|
||||
- **Data:** Filters `usePolarisDataContext()` by namespace
|
||||
|
||||
### UI Components
|
||||
|
||||
**`src/components/AppBarScoreBadge.tsx`**
|
||||
- **Location:** Headlamp app bar (top-right)
|
||||
- **Purpose:** Quick cluster score visibility
|
||||
- **Features:**
|
||||
- Color-coded badge (green ≥80%, orange ≥50%, red <50%)
|
||||
- Clickable (navigates to `/polaris`)
|
||||
- Shield emoji icon
|
||||
- **Data:** Uses `usePolarisDataContext()`
|
||||
|
||||
**`src/components/PolarisSettings.tsx`**
|
||||
- **Location:** Settings → Plugins → Polaris
|
||||
- **Purpose:** Plugin configuration
|
||||
- **Features:**
|
||||
- Refresh interval selector (1 min to 30 min)
|
||||
- Dashboard URL input (custom Polaris instances)
|
||||
- Connection test button
|
||||
- **Data:** localStorage for persistence
|
||||
|
||||
**`src/components/InlineAuditSection.tsx`**
|
||||
- **Location:** Resource detail pages (Deployment, StatefulSet, etc.)
|
||||
- **Purpose:** Show Polaris audit inline
|
||||
- **Features:**
|
||||
- Pass/warning/danger counts
|
||||
- Check details with messages
|
||||
- Severity badges
|
||||
- **Data:** Uses `usePolarisDataContext()`, filters by resource
|
||||
|
||||
**`src/components/ExemptionManager.tsx`**
|
||||
- **Location:** (Planned feature, UI exists but not fully integrated)
|
||||
- **Purpose:** Manage Polaris exemptions via annotations
|
||||
- **Features:**
|
||||
- View current exemptions
|
||||
- Add exemptions for failing checks
|
||||
- Remove exemptions
|
||||
|
||||
## State Management
|
||||
|
||||
### Why React Context?
|
||||
|
||||
**Decision:** Use React Context instead of Redux/Zustand
|
||||
|
||||
**Rationale:**
|
||||
1. **Simple state:** Single AuditData object shared across views
|
||||
2. **Read-only:** No complex mutations or transactions
|
||||
3. **Headlamp constraints:** Plugin cannot add dependencies (Redux not bundled)
|
||||
4. **Performance:** Data changes infrequently (refresh interval 1-30 min)
|
||||
|
||||
### Context Structure
|
||||
|
||||
```typescript
|
||||
interface PolarisDataContextValue {
|
||||
data: AuditData | null; // Audit results or null if loading/error
|
||||
loading: boolean; // True during initial fetch
|
||||
error: string | null; // Error message if fetch failed
|
||||
refresh: () => void; // Manual refresh function
|
||||
}
|
||||
```
|
||||
|
||||
### Data Fetching Strategy
|
||||
|
||||
1. **Initial fetch:** On first mount of any component using the context
|
||||
2. **Auto-refresh:** Based on user setting (default 5 minutes)
|
||||
3. **Manual refresh:** Via refresh button in UI
|
||||
4. **Caching:** Data persists in context until refresh (no per-route refetch)
|
||||
|
||||
### localStorage Usage
|
||||
|
||||
Settings persisted in localStorage:
|
||||
- **`polaris-plugin-refresh-interval`**: Number (seconds), default 300
|
||||
- **`polaris-plugin-dashboard-url`**: String, default service proxy path
|
||||
|
||||
No sensitive data stored in localStorage.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### 1. Service Proxy vs. Direct Access
|
||||
|
||||
**Decision:** Use Kubernetes service proxy, not direct ClusterIP access
|
||||
|
||||
**Rationale:**
|
||||
- Headlamp already has K8s API credentials (service account or user token)
|
||||
- Service proxy leverages existing RBAC (no new credentials needed)
|
||||
- Works with Headlamp's token auth and OIDC
|
||||
- Simpler deployment (no additional network policies for plugin)
|
||||
|
||||
**Trade-off:**
|
||||
- Requires `get` permission on `services/proxy` resource
|
||||
- Path is longer: `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
|
||||
|
||||
### 2. Two-Level Sidebar Nesting
|
||||
|
||||
**Decision:** Sidebar has "Polaris" → "Overview" and "Namespaces" (2 levels max)
|
||||
|
||||
**Rationale:**
|
||||
- Headlamp sidebar supports 2-level nesting maximum
|
||||
- Deeper nesting (e.g., Polaris → Namespaces → <each namespace>) doesn't work
|
||||
- Sidebar Collapse component is route-based, not click-to-toggle
|
||||
|
||||
**Alternative Considered:**
|
||||
- Dynamic sidebar with namespace entries → rejected (Headlamp limitation)
|
||||
|
||||
**Current Solution:**
|
||||
- Use table in NamespacesListView with clickable namespace buttons
|
||||
- Namespace detail opens in drawer (not new route)
|
||||
|
||||
### 3. Drawer Navigation Instead of Routes
|
||||
|
||||
**Decision:** Namespace detail uses drawer, not dedicated route
|
||||
|
||||
**Rationale:**
|
||||
- Better UX (drawer overlays table, no navigation loss)
|
||||
- URL hash preserves navigation state (`#namespace-name`)
|
||||
- Keyboard shortcuts (Escape to close)
|
||||
- Sidebar doesn't support 3-level nesting for per-namespace routes
|
||||
|
||||
**Implementation:**
|
||||
- URL: `/polaris/namespaces#kube-system`
|
||||
- Drawer controlled by hash presence
|
||||
- `useEffect` watches hash changes
|
||||
|
||||
### 4. No MUI Direct Imports
|
||||
|
||||
**Decision:** Never import from `@mui/material` or `@mui/icons-material`
|
||||
|
||||
**Rationale:**
|
||||
- Headlamp plugin environment doesn't provide full MUI library
|
||||
- Importing MUI causes `createSvgIcon undefined` error
|
||||
- Plugins must use Headlamp CommonComponents only
|
||||
|
||||
**Alternative:**
|
||||
- Use standard HTML elements with inline styles
|
||||
- Use theme-aware CSS variables (`--mui-palette-*`)
|
||||
|
||||
### 5. TypeScript Strict Mode
|
||||
|
||||
**Decision:** Enable all TypeScript strict checks
|
||||
|
||||
**Rationale:**
|
||||
- Catch errors at compile time
|
||||
- Better IDE support and autocomplete
|
||||
- Enforces type safety (no `any`, no implicit unknowns)
|
||||
|
||||
**Impact:**
|
||||
- More verbose code (explicit types required)
|
||||
- Better maintainability and refactorability
|
||||
|
||||
### 6. Auto-Refresh Default: 5 Minutes
|
||||
|
||||
**Decision:** Default refresh interval is 5 minutes (configurable)
|
||||
|
||||
**Rationale:**
|
||||
- Polaris audits typically run every 10-30 minutes
|
||||
- Balance between data freshness and API load
|
||||
- User can configure from 1 minute to 30 minutes
|
||||
|
||||
**Considered:**
|
||||
- WebSocket/SSE for real-time updates → rejected (Polaris dashboard doesn't support)
|
||||
- Shorter default → rejected (unnecessary API calls)
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Headlamp Plugin API
|
||||
|
||||
**Version:** ≥ v0.13.0
|
||||
|
||||
**Registration Functions Used:**
|
||||
|
||||
```typescript
|
||||
// Sidebar navigation
|
||||
registerSidebarEntry({ parent, name, label, url, icon })
|
||||
|
||||
// Routes
|
||||
registerRoute({ path, sidebar, name, exact, component })
|
||||
|
||||
// App bar actions
|
||||
registerAppBarAction(component)
|
||||
|
||||
// Plugin settings
|
||||
registerPluginSettings(name, component, displaySaveButton)
|
||||
|
||||
// Resource detail sections
|
||||
registerDetailsViewSection(component)
|
||||
```
|
||||
|
||||
**Key Changes in v0.13.0:**
|
||||
- `registerDetailsViewSection` now takes 1 argument (component), not 2 (name, component)
|
||||
- `registerAppBarAction` now takes 1 argument (component), not 2 (name, component)
|
||||
|
||||
### Headlamp CommonComponents
|
||||
|
||||
**Used Components:**
|
||||
- `SectionBox` - Card-like container with title
|
||||
- `SectionHeader` - Page header with title
|
||||
- `StatusLabel` - Color-coded status badges
|
||||
- `NameValueTable` - Key-value table layout
|
||||
- `SimpleTable` - Data table with sorting
|
||||
- `Drawer` - Right-side overlay panel
|
||||
- `Loader` - Loading spinner
|
||||
|
||||
**Router:**
|
||||
- `Router.createRouteURL()` - Generate plugin route URLs
|
||||
- React Router's `useHistory()`, `useParams()`, `useLocation()`
|
||||
|
||||
### Kubernetes API (via ApiProxy)
|
||||
|
||||
**Used for:**
|
||||
- Fetching Polaris results: `ApiProxy.request(dashboardUrl + 'results.json')`
|
||||
- No direct K8s API calls (all data from Polaris dashboard)
|
||||
|
||||
**RBAC Required:**
|
||||
- `get` on `services/proxy` for `polaris-dashboard` in `polaris` namespace
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### 1. Sidebar Nesting Depth
|
||||
|
||||
**Limitation:** Headlamp sidebar supports only 2 levels
|
||||
|
||||
**Impact:** Cannot have dynamic per-namespace sidebar entries
|
||||
|
||||
**Workaround:** Use table with drawer navigation
|
||||
|
||||
### 2. Skipped Checks Visibility
|
||||
|
||||
**Limitation:** Skipped checks (severity "ignore") counted but details not shown in dashboard
|
||||
|
||||
**Reason:** Polaris API groups skipped checks but doesn't provide per-check details
|
||||
|
||||
**Impact:** Users see skipped count but can't drill down to specific skipped checks
|
||||
|
||||
**Documented:** README, tooltip on skipped count
|
||||
|
||||
### 3. No Write Operations
|
||||
|
||||
**Limitation:** Plugin cannot modify Polaris configuration or exemptions
|
||||
|
||||
**Reason:** Read-only by design (service proxy only has `get` permission)
|
||||
|
||||
**Impact:** Exemption manager UI exists but requires manual annotation edits
|
||||
|
||||
**Future:** Could add PATCH permission to enable exemption annotations via UI
|
||||
|
||||
### 4. No Real-Time Updates
|
||||
|
||||
**Limitation:** Data refreshes on interval (1-30 minutes), not real-time
|
||||
|
||||
**Reason:** Polaris dashboard doesn't support WebSocket/SSE
|
||||
|
||||
**Impact:** Users may see stale data between refreshes
|
||||
|
||||
**Workaround:** Manual refresh button, configurable interval
|
||||
|
||||
### 5. MUI Import Restrictions
|
||||
|
||||
**Limitation:** Cannot import MUI components directly
|
||||
|
||||
**Reason:** Headlamp plugin environment doesn't provide full MUI bundle
|
||||
|
||||
**Impact:** Must use Headlamp CommonComponents or HTML elements
|
||||
|
||||
**Documented:** CLAUDE.md, CONTRIBUTING.md
|
||||
|
||||
### 6. Single Cluster Support
|
||||
|
||||
**Limitation:** Plugin shows data for current cluster only
|
||||
|
||||
**Reason:** Headlamp's multi-cluster support is route-based (`/c/<cluster>/...`)
|
||||
|
||||
**Impact:** Users must switch clusters in Headlamp to see different cluster's Polaris data
|
||||
|
||||
**Future:** Could enhance to aggregate multi-cluster if Headlamp API supports it
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Bundle Size
|
||||
|
||||
- **Current:** ~27 KB minified (gzip: ~7.6 KB)
|
||||
- **Target:** Keep under 50 KB to ensure fast loading
|
||||
- **Strategy:** No heavy dependencies, tree-shaking enabled
|
||||
|
||||
### Data Fetching
|
||||
|
||||
- **Lazy loading:** Data not fetched until user navigates to plugin
|
||||
- **Caching:** Single fetch shared across all views (React Context)
|
||||
- **Refresh strategy:** User-controlled interval prevents excessive API calls
|
||||
|
||||
### Rendering
|
||||
|
||||
- **React.memo:** Not needed (data changes infrequently)
|
||||
- **Virtual scrolling:** Not needed (namespace/resource lists typically <100 items)
|
||||
- **Component splitting:** Lazy load views if bundle grows significantly
|
||||
|
||||
## Future Architecture Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
1. **WebWorker for data processing**
|
||||
- Offload `countResults()` aggregation for large clusters
|
||||
- Keep UI responsive during heavy computation
|
||||
|
||||
2. **IndexedDB caching**
|
||||
- Cache audit data offline
|
||||
- Show stale data + "refresh available" indicator
|
||||
|
||||
3. **GraphQL/REST API abstraction**
|
||||
- Decouple from Polaris dashboard JSON format
|
||||
- Support multiple backend sources
|
||||
|
||||
4. **Plugin-to-plugin communication**
|
||||
- Integrate with other Headlamp plugins (e.g., policy enforcement)
|
||||
- Shared state between plugins
|
||||
|
||||
5. **Incremental updates**
|
||||
- Fetch only changed namespaces/resources
|
||||
- Reduce bandwidth and processing
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Fairwinds Polaris Documentation](https://polaris.docs.fairwinds.com/)
|
||||
- [React Context API](https://react.dev/reference/react/useContext)
|
||||
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/)
|
||||
@@ -0,0 +1,689 @@
|
||||
# Deployment Guide
|
||||
|
||||
This document provides comprehensive deployment instructions for the Headlamp Polaris Plugin in production Kubernetes environments.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation Methods](#installation-methods)
|
||||
- [Helm Integration](#helm-integration)
|
||||
- [RBAC Configuration](#rbac-configuration)
|
||||
- [Network Policies](#network-policies)
|
||||
- [Plugin Manager Setup](#plugin-manager-setup)
|
||||
- [Production Checklist](#production-checklist)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Components
|
||||
|
||||
1. **Kubernetes Cluster:** v1.19 or later
|
||||
2. **Headlamp:** v0.26 or later (v0.39+ recommended)
|
||||
3. **Polaris:** Deployed and accessible via service
|
||||
4. **RBAC:** Permissions to create Roles and RoleBindings
|
||||
|
||||
### Pre-Deployment Verification
|
||||
|
||||
```bash
|
||||
# Verify Polaris is deployed
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# Verify Polaris dashboard is responding
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Verify Headlamp is deployed
|
||||
kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp
|
||||
```
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Method 1: Headlamp Plugin Manager (Recommended)
|
||||
|
||||
**Best for:** Production deployments, managed updates
|
||||
|
||||
1. **Enable Plugin Manager in Headlamp:**
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: "/headlamp/plugins"
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
```
|
||||
|
||||
2. **Deploy/Update Headlamp:**
|
||||
|
||||
```bash
|
||||
helm upgrade --install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--values headlamp-values.yaml
|
||||
```
|
||||
|
||||
3. **Install Plugin via UI:**
|
||||
- Navigate to Headlamp → Settings → Plugins
|
||||
- Search for "Polaris"
|
||||
- Click "Install"
|
||||
- Refresh browser (Cmd+Shift+R or Ctrl+Shift+R)
|
||||
|
||||
### Method 2: Sidecar Container (Alternative)
|
||||
|
||||
**Best for:** Controlled plugin versions, air-gapped environments
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: "/headlamp/plugins"
|
||||
watchPlugins: false # CRITICAL: Must be false for plugin manager
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
initContainers:
|
||||
- name: install-polaris-plugin
|
||||
image: node:lts-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
npm install -g @kinvolk/headlamp-plugin
|
||||
headlamp-plugin install --config /config/plugin.yml --plugins-dir /plugins
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
- name: plugin-config
|
||||
mountPath: /config
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
emptyDir: {}
|
||||
- name: plugin-config
|
||||
configMap:
|
||||
name: headlamp-plugin-config
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headlamp-plugin-config
|
||||
namespace: kube-system
|
||||
data:
|
||||
plugin.yml: |
|
||||
- name: headlamp-polaris-plugin
|
||||
version: 0.3.4
|
||||
url: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.3.4/headlamp-polaris-plugin-0.3.4.tar.gz
|
||||
```
|
||||
|
||||
### Method 3: Volume Mount (Development)
|
||||
|
||||
**Best for:** Local testing, development
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
config:
|
||||
pluginsDir: "/plugins"
|
||||
|
||||
volumes:
|
||||
- name: plugins
|
||||
hostPath:
|
||||
path: /path/to/plugins
|
||||
type: Directory
|
||||
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /plugins
|
||||
readOnly: true
|
||||
```
|
||||
|
||||
Then manually place `headlamp-polaris-plugin/` in the host path.
|
||||
|
||||
## Helm Integration
|
||||
|
||||
### Complete Helm Values Example
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
replicaCount: 2
|
||||
|
||||
image:
|
||||
repository: ghcr.io/headlamp-k8s/headlamp
|
||||
tag: v0.39.0
|
||||
|
||||
config:
|
||||
baseURL: ""
|
||||
pluginsDir: "/headlamp/plugins"
|
||||
watchPlugins: false # MUST be false for plugin manager
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: headlamp-tls
|
||||
hosts:
|
||||
- headlamp.example.com
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: headlamp
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
|
||||
# OIDC Authentication (optional)
|
||||
env:
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_ID
|
||||
value: "headlamp"
|
||||
- name: HEADLAMP_CONFIG_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: headlamp-oidc
|
||||
key: client-secret
|
||||
- name: HEADLAMP_CONFIG_OIDC_ISSUER_URL
|
||||
value: "https://auth.example.com/realms/kubernetes"
|
||||
- name: HEADLAMP_CONFIG_OIDC_SCOPES
|
||||
value: "openid,profile,email,groups"
|
||||
```
|
||||
|
||||
### FluxCD HelmRelease Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
interval: 30m
|
||||
chart:
|
||||
spec:
|
||||
chart: headlamp
|
||||
version: 0.26.x
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: headlamp
|
||||
namespace: flux-system
|
||||
interval: 12h
|
||||
|
||||
values:
|
||||
config:
|
||||
pluginsDir: "/headlamp/plugins"
|
||||
watchPlugins: false
|
||||
|
||||
pluginsManager:
|
||||
enabled: true
|
||||
repositories:
|
||||
- https://artifacthub.io/packages/search?kind=4
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: headlamp.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
## RBAC Configuration
|
||||
|
||||
### Minimal Role for Plugin
|
||||
|
||||
The plugin requires **read-only** access to the Polaris dashboard service proxy.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: polaris
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
resourceNames: ["polaris-dashboard"]
|
||||
verbs: ["get"]
|
||||
```
|
||||
|
||||
### RoleBinding Options
|
||||
|
||||
#### Option A: Headlamp Service Account (In-Cluster Mode)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
#### Option B: User Groups (Token/OIDC Mode)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: users-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: system:authenticated # All authenticated users
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
#### Option C: Specific Users (Fine-Grained Control)
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: devops-polaris-proxy
|
||||
namespace: polaris
|
||||
subjects:
|
||||
- kind: User
|
||||
name: alice@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: User
|
||||
name: bob@example.com
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: Group
|
||||
name: devops-team
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### Complete RBAC Manifest
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Role: Read-only access to Polaris service proxy
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-proxy-reader
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
resourceNames: ["polaris-dashboard"]
|
||||
verbs: ["get"]
|
||||
|
||||
---
|
||||
# RoleBinding: Grant Headlamp service account access
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-polaris-proxy
|
||||
namespace: polaris
|
||||
labels:
|
||||
app.kubernetes.io/name: headlamp-polaris-plugin
|
||||
app.kubernetes.io/component: rbac
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: polaris-proxy-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Apply with:
|
||||
```bash
|
||||
kubectl apply -f polaris-plugin-rbac.yaml
|
||||
```
|
||||
|
||||
## Network Policies
|
||||
|
||||
### Required Network Access
|
||||
|
||||
The plugin requires network connectivity:
|
||||
- **Headlamp pod** → **Kubernetes API server** (service proxy)
|
||||
- **Kubernetes API server** → **Polaris dashboard service** (port 80)
|
||||
|
||||
### Network Policy Example
|
||||
|
||||
If your `polaris` namespace has strict NetworkPolicies:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-headlamp-to-polaris
|
||||
namespace: polaris
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: polaris
|
||||
app.kubernetes.io/component: dashboard
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Allow from API server (service proxy)
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
component: kube-apiserver
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
```
|
||||
|
||||
**Note:** The API server performs the proxy hop, not the Headlamp pod directly.
|
||||
|
||||
## Plugin Manager Setup
|
||||
|
||||
### Critical Configuration
|
||||
|
||||
**❌ WRONG (Will not load plugins):**
|
||||
```yaml
|
||||
config:
|
||||
watchPlugins: true # Default, treats catalog plugins as dev plugins
|
||||
```
|
||||
|
||||
**✅ CORRECT:**
|
||||
```yaml
|
||||
config:
|
||||
watchPlugins: false # Required for plugin manager catalog plugins
|
||||
```
|
||||
|
||||
### Why `watchPlugins: false` is Required
|
||||
|
||||
- **With `watchPlugins: true`:** Headlamp backend serves plugin metadata, but frontend never executes the JavaScript (treated as development directory plugin)
|
||||
- **Result:** Plugins appear in Settings but no sidebar/routes/settings work
|
||||
- **Fix:** Set `watchPlugins: false` in Headlamp configuration
|
||||
- **Documentation:** See `deployment/PLUGIN_LOADING_FIX.md` for root cause analysis
|
||||
|
||||
### Plugin Manager Verification
|
||||
|
||||
```bash
|
||||
# Check Headlamp config
|
||||
kubectl -n kube-system get configmap headlamp -o yaml | grep watchPlugins
|
||||
|
||||
# Expected output:
|
||||
# watchPlugins: "false"
|
||||
|
||||
# Check plugin is installed
|
||||
kubectl -n kube-system exec -it deployment/headlamp -- ls -la /headlamp/plugins/
|
||||
|
||||
# Expected output:
|
||||
# drwxr-xr-x headlamp-polaris-plugin/
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
|
||||
- [ ] Polaris deployed and running
|
||||
- [ ] Polaris dashboard service exists (`polaris-dashboard` in `polaris` namespace)
|
||||
- [ ] RBAC Role and RoleBinding created
|
||||
- [ ] Headlamp v0.26+ deployed
|
||||
- [ ] `watchPlugins: false` set in Headlamp config
|
||||
|
||||
### Deployment
|
||||
|
||||
- [ ] Plugin installed via plugin manager or sidecar
|
||||
- [ ] Headlamp pods restarted (if config changed)
|
||||
- [ ] Browser cache cleared (Cmd+Shift+R / Ctrl+Shift+R)
|
||||
|
||||
### Post-Deployment Verification
|
||||
|
||||
```bash
|
||||
# 1. Verify Polaris is accessible via service proxy
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion
|
||||
|
||||
# Expected: "1.0" or similar
|
||||
|
||||
# 2. Verify RBAC is correct
|
||||
kubectl auth can-i get services/proxy --as=system:serviceaccount:kube-system:headlamp -n polaris --resource-name=polaris-dashboard
|
||||
|
||||
# Expected: yes
|
||||
|
||||
# 3. Check Headlamp logs
|
||||
kubectl -n kube-system logs deployment/headlamp | grep -i polaris
|
||||
|
||||
# Expected: No errors related to plugin loading
|
||||
|
||||
# 4. Verify plugin files exist
|
||||
kubectl -n kube-system exec -it deployment/headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/
|
||||
|
||||
# Expected: dist/, package.json present
|
||||
```
|
||||
|
||||
### UI Verification
|
||||
|
||||
- [ ] Navigate to Headlamp → Settings → Plugins
|
||||
- [ ] Plugin "headlamp-polaris-plugin" listed
|
||||
- [ ] Sidebar shows "Polaris" entry
|
||||
- [ ] Click "Polaris" → Overview page loads
|
||||
- [ ] Cluster score displays correctly
|
||||
- [ ] Namespaces page shows table
|
||||
- [ ] App bar shows Polaris score badge
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Appearing in Sidebar
|
||||
|
||||
**Symptom:** Plugin listed in Settings → Plugins but no sidebar entry
|
||||
|
||||
**Causes:**
|
||||
1. `watchPlugins: true` (should be `false`)
|
||||
2. Browser cache not cleared
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Fix Headlamp config
|
||||
kubectl -n kube-system edit configmap headlamp
|
||||
# Set watchPlugins: false
|
||||
|
||||
# Restart Headlamp
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
|
||||
# Clear browser cache
|
||||
# Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux)
|
||||
```
|
||||
|
||||
### 403 Forbidden Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, 403 in console
|
||||
|
||||
**Cause:** RBAC missing or incorrect
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify RBAC exists
|
||||
kubectl -n polaris get role polaris-proxy-reader
|
||||
kubectl -n polaris get rolebinding headlamp-polaris-proxy
|
||||
|
||||
# Test permission
|
||||
kubectl auth can-i get services/proxy --as=system:serviceaccount:kube-system:headlamp -n polaris --resource-name=polaris-dashboard
|
||||
|
||||
# If "no", create RBAC (see RBAC Configuration section)
|
||||
```
|
||||
|
||||
### 404 Not Found Error
|
||||
|
||||
**Symptom:** Error loading Polaris data, 404 in console
|
||||
|
||||
**Causes:**
|
||||
1. Polaris not deployed
|
||||
2. Polaris service name wrong
|
||||
3. Polaris namespace wrong
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check Polaris deployment
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
|
||||
# If service doesn't exist, install Polaris:
|
||||
helm install polaris fairwinds-stable/polaris \
|
||||
--namespace polaris \
|
||||
--create-namespace \
|
||||
--set dashboard.enabled=true
|
||||
```
|
||||
|
||||
### Custom Dashboard URL Not Working
|
||||
|
||||
**Symptom:** Error when using custom Polaris URL in settings
|
||||
|
||||
**Causes:**
|
||||
1. URL format incorrect
|
||||
2. CORS not configured on external Polaris
|
||||
3. Network policy blocking external access
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Test URL manually
|
||||
curl -v https://my-polaris.example.com/results.json
|
||||
|
||||
# For external Polaris, check CORS headers
|
||||
# Must allow Headlamp origin
|
||||
```
|
||||
|
||||
### Plugin Shows Old Version
|
||||
|
||||
**Symptom:** Plugin version in Settings doesn't match expected
|
||||
|
||||
**Cause:** Plugin manager hasn't synced from ArtifactHub
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Wait 30 minutes (ArtifactHub sync interval)
|
||||
# Or manually refresh plugin list in Headlamp UI
|
||||
|
||||
# Force Headlamp restart
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
```
|
||||
|
||||
### Network Policy Blocking Access
|
||||
|
||||
**Symptom:** Timeout or connection errors despite correct RBAC
|
||||
|
||||
**Cause:** NetworkPolicy in `polaris` namespace blocking API server
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check NetworkPolicies
|
||||
kubectl -n polaris get networkpolicy
|
||||
|
||||
# Test connectivity from API server (if possible)
|
||||
# Add NetworkPolicy to allow API server → Polaris dashboard (see Network Policies section)
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Least Privilege
|
||||
|
||||
- Grant only `get` on `services/proxy`, not broader permissions
|
||||
- Use `resourceNames` to restrict to specific service (`polaris-dashboard`)
|
||||
- Scope to `polaris` namespace only (Role, not ClusterRole)
|
||||
|
||||
### Audit Logging
|
||||
|
||||
Kubernetes audit logs will record:
|
||||
- User/service account accessing service proxy
|
||||
- Timestamp and response code
|
||||
|
||||
Configure audit policy if needed:
|
||||
```yaml
|
||||
apiVersion: audit.k8s.io/v1
|
||||
kind: Policy
|
||||
rules:
|
||||
- level: Metadata
|
||||
verbs: ["get"]
|
||||
resources:
|
||||
- group: ""
|
||||
resources: ["services/proxy"]
|
||||
namespaces: ["polaris"]
|
||||
```
|
||||
|
||||
### Data Sensitivity
|
||||
|
||||
Polaris audit data may contain:
|
||||
- Resource names and namespaces
|
||||
- Configuration details
|
||||
- Potential security vulnerabilities
|
||||
|
||||
**Recommendation:** Restrict plugin access to authorized users only (not `system:authenticated` group unless appropriate).
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Plugin Upgrade via Plugin Manager
|
||||
|
||||
1. Navigate to Settings → Plugins
|
||||
2. Find "headlamp-polaris-plugin"
|
||||
3. Click "Update" if new version available
|
||||
4. Refresh browser (Cmd+Shift+R / Ctrl+Shift+R)
|
||||
|
||||
### Sidecar Method Upgrade
|
||||
|
||||
1. Update ConfigMap with new version/URL
|
||||
2. Restart Headlamp deployment
|
||||
3. Verify new version in Settings → Plugins
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system edit configmap headlamp-plugin-config
|
||||
# Update version and URL
|
||||
|
||||
kubectl -n kube-system rollout restart deployment/headlamp
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Headlamp Deployment](https://headlamp.dev/docs/latest/installation/)
|
||||
- [Headlamp Helm Chart](https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp)
|
||||
- [Polaris Installation](https://polaris.docs.fairwinds.com/infrastructure-as-code/)
|
||||
- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
|
||||
- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/#manually-constructing-apiserver-proxy-urls)
|
||||
+242
-6
@@ -4,14 +4,20 @@ Playwright-based smoke tests that validate the Polaris plugin against a live Hea
|
||||
|
||||
## CI
|
||||
|
||||
E2E tests run automatically in Gitea Actions on pushes to `main` and pull requests. The workflow (`.gitea/workflows/e2e.yaml`) uses Authentik OIDC for authentication via repo secrets.
|
||||
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.
|
||||
|
||||
### Required Gitea secrets
|
||||
### Required GitHub Secrets
|
||||
|
||||
| Secret | Description |
|
||||
| -------------------- | -------------------------------------------------------------- |
|
||||
| `AUTHENTIK_USERNAME` | Authentik email or username for a CI user with Headlamp access |
|
||||
| `AUTHENTIK_PASSWORD` | Password for that user |
|
||||
Configure these in GitHub repository settings (Settings → Secrets and variables → Actions):
|
||||
|
||||
| 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.
|
||||
|
||||
## Running Locally
|
||||
|
||||
@@ -56,3 +62,233 @@ Set either `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` or `HEADLAMP_TOKEN`. OIDC
|
||||
- **Namespace detail** — Clicking a namespace shows its score and resource table
|
||||
|
||||
These are smoke tests against real cluster data. They verify the plugin loads and renders without errors, not specific data values.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Current Tests (`polaris.spec.ts`)
|
||||
|
||||
1. **`sidebar contains Polaris entry`**
|
||||
- Verifies Polaris appears in the navigation sidebar
|
||||
- Ensures plugin successfully registered sidebar entry
|
||||
|
||||
2. **`overview page renders cluster score`**
|
||||
- Navigates to `/c/main/polaris`
|
||||
- Checks for "Polaris — Overview" heading
|
||||
- Verifies cluster score percentage is displayed
|
||||
- Validates data fetching and rendering
|
||||
|
||||
3. **`namespaces page renders table with namespace buttons`**
|
||||
- Navigates to `/c/main/polaris/namespaces`
|
||||
- Checks for "Polaris — Namespaces" heading
|
||||
- Verifies table is visible with at least one row
|
||||
- Ensures namespace buttons are clickable
|
||||
|
||||
4. **`namespace detail drawer opens from table button`**
|
||||
- Clicks first namespace button in table
|
||||
- Verifies drawer opens with namespace name in heading
|
||||
- Checks "Namespace Score" section is visible
|
||||
- Confirms "Resources" table is displayed
|
||||
- Validates URL hash is updated with namespace name
|
||||
|
||||
5. **`namespace detail drawer closes with Escape key`**
|
||||
- Opens namespace drawer
|
||||
- Presses Escape key
|
||||
- Verifies drawer closes
|
||||
- Checks URL hash is cleared
|
||||
|
||||
6. **`namespace detail drawer opens from URL hash`**
|
||||
- Navigates directly to `/c/main/polaris/namespaces#<namespace>`
|
||||
- Verifies drawer automatically opens
|
||||
- Checks namespace details are displayed
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Cluster Requirements
|
||||
|
||||
1. **Polaris Deployment**
|
||||
```bash
|
||||
# Verify Polaris is running
|
||||
kubectl -n polaris get pods
|
||||
kubectl -n polaris get svc polaris-dashboard
|
||||
```
|
||||
|
||||
2. **Polaris Audit Data**
|
||||
```bash
|
||||
# Check if Polaris has generated audit results
|
||||
kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq '.AuditTime'
|
||||
```
|
||||
|
||||
3. **RBAC Permissions**
|
||||
- Headlamp service account (or test user) needs `get` on `services/proxy` for `polaris-dashboard`
|
||||
- See main README for RBAC setup
|
||||
|
||||
### Local Setup
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
|
||||
# 2. Create .env file (optional, for persistent config)
|
||||
cp .env.example .env
|
||||
|
||||
# 3. Set environment variables
|
||||
export HEADLAMP_URL=https://your-headlamp-instance.com
|
||||
export HEADLAMP_TOKEN=$(kubectl create token headlamp -n kube-system)
|
||||
|
||||
# 4. Run tests
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Run in Headed Mode
|
||||
|
||||
See the browser UI while tests run:
|
||||
|
||||
```bash
|
||||
npm run e2e:headed
|
||||
```
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
Step through tests with Playwright Inspector:
|
||||
|
||||
```bash
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### Generate Trace
|
||||
|
||||
Record full trace for failed tests:
|
||||
|
||||
```bash
|
||||
npx playwright test --trace on
|
||||
npx playwright show-trace test-results/<test-name>/trace.zip
|
||||
```
|
||||
|
||||
### Screenshot on Failure
|
||||
|
||||
Tests automatically capture screenshots on failure in `test-results/`
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Auth fails with "Sign In button not found":**
|
||||
- Check HEADLAMP_URL is correct
|
||||
- Verify Headlamp is accessible
|
||||
- Ensure OIDC is configured if using Authentik
|
||||
|
||||
**Polaris sidebar entry not found:**
|
||||
- Plugin may not be installed: Check Settings → Plugins in Headlamp
|
||||
- Plugin may have failed to load: Check browser console
|
||||
- Clear browser cache and hard refresh
|
||||
|
||||
**Cluster score not displayed:**
|
||||
- Polaris may not have audit data yet
|
||||
- Check Polaris is running: `kubectl -n polaris get pods`
|
||||
- Verify service proxy: `kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json`
|
||||
|
||||
**Namespace table empty:**
|
||||
- Polaris hasn't run audit yet (wait a few minutes)
|
||||
- Check Polaris logs: `kubectl -n polaris logs -l app.kubernetes.io/name=polaris`
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Example: Testing Plugin Settings
|
||||
|
||||
```typescript
|
||||
test('plugin settings page shows Polaris configuration', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Find and click Polaris plugin
|
||||
await page.getByText('headlamp-polaris-plugin').click();
|
||||
|
||||
// Check settings are visible
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible();
|
||||
await expect(page.getByText('Refresh Interval')).toBeVisible();
|
||||
await expect(page.getByText('Dashboard URL')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Testing App Bar Badge
|
||||
|
||||
```typescript
|
||||
test('app bar displays Polaris score badge', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Badge should be visible in app bar
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible();
|
||||
|
||||
// Clicking should navigate to overview
|
||||
await badge.click();
|
||||
await expect(page).toHaveURL(/\/c\/main\/polaris$/);
|
||||
});
|
||||
```
|
||||
|
||||
### Example: Testing Dark Mode
|
||||
|
||||
```typescript
|
||||
test('plugin UI adapts to dark mode', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris');
|
||||
|
||||
// Toggle dark mode
|
||||
await page.getByRole('button', { name: /theme/i }).click();
|
||||
|
||||
// Check background color changes
|
||||
const body = page.locator('body');
|
||||
await expect(body).toHaveCSS('background-color', 'rgb(18, 18, 18)');
|
||||
|
||||
// Plugin components should adapt
|
||||
const sectionBox = page.locator('[class*="MuiPaper"]').first();
|
||||
await expect(sectionBox).not.toHaveCSS('background-color', 'rgb(255, 255, 255)');
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically in GitHub Actions on pushes to `main` and pull requests. See `.github/workflows/e2e.yaml` for workflow configuration.
|
||||
|
||||
### Required Secrets
|
||||
|
||||
Configure these in GitHub repository settings (Settings → Secrets and variables → Actions):
|
||||
|
||||
- `HEADLAMP_URL` (optional): Headlamp instance URL
|
||||
- `AUTHENTIK_USERNAME` + `AUTHENTIK_PASSWORD` (for OIDC auth)
|
||||
- OR `HEADLAMP_TOKEN` (for token-based auth)
|
||||
|
||||
### Workflow Overview
|
||||
|
||||
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
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
You can manually trigger E2E tests from GitHub Actions:
|
||||
1. Go to Actions → E2E Tests
|
||||
2. Click "Run workflow"
|
||||
3. Select branch and run
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use semantic selectors**: `getByRole`, `getByText` over CSS selectors
|
||||
2. **Wait for visibility**: Use `await expect(...).toBeVisible()` instead of `waitForTimeout`
|
||||
3. **Keep tests independent**: Each test should work in isolation
|
||||
4. **Test user flows**: Complete journeys, not just page loads
|
||||
5. **Clean up state**: Close drawers/modals after tests
|
||||
6. **Use storage state**: Reuse auth across tests (already configured)
|
||||
7. **Parallelize carefully**: Currently disabled due to shared state
|
||||
|
||||
## Resources
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
|
||||
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [Project Main README](../README.md)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Polaris app bar badge', () => {
|
||||
test('badge displays cluster score in app bar', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Wait for page to load
|
||||
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible();
|
||||
|
||||
// Badge should be visible in app bar with score percentage
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Badge should show shield emoji
|
||||
await expect(badge).toContainText('🛡️');
|
||||
});
|
||||
|
||||
test('clicking badge navigates to overview page', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Find and click the badge
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
await badge.click();
|
||||
|
||||
// Should navigate to Polaris overview
|
||||
await expect(page).toHaveURL(/\/c\/main\/polaris$/);
|
||||
await expect(page.getByRole('heading', { name: 'Polaris — Overview' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('badge color reflects score level', async ({ page }) => {
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Get the badge
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Extract score from button text
|
||||
const badgeText = await badge.textContent();
|
||||
const scoreMatch = badgeText?.match(/(\d+)%/);
|
||||
expect(scoreMatch).toBeTruthy();
|
||||
|
||||
const score = parseInt(scoreMatch![1]);
|
||||
|
||||
// Check background color matches score level
|
||||
const bgColor = await badge.evaluate(el =>
|
||||
window.getComputedStyle(el).backgroundColor
|
||||
);
|
||||
|
||||
if (score >= 80) {
|
||||
// Green: rgb(76, 175, 80) or #4caf50
|
||||
expect(bgColor).toMatch(/rgb\(76,\s*175,\s*80\)/);
|
||||
} else if (score >= 50) {
|
||||
// Orange: rgb(255, 152, 0) or #ff9800
|
||||
expect(bgColor).toMatch(/rgb\(255,\s*152,\s*0\)/);
|
||||
} else {
|
||||
// Red: rgb(244, 67, 54) or #f44336
|
||||
expect(bgColor).toMatch(/rgb\(244,\s*67,\s*54\)/);
|
||||
}
|
||||
});
|
||||
|
||||
test('badge updates when navigating between clusters', async ({ page }) => {
|
||||
// This test assumes multi-cluster setup; skip if only one cluster
|
||||
await page.goto('/c/main');
|
||||
|
||||
// Get initial badge score
|
||||
const badge = page.getByRole('button', { name: /Polaris: \d+%/ });
|
||||
await expect(badge).toBeVisible({ timeout: 15_000 });
|
||||
const initialScore = await badge.textContent();
|
||||
|
||||
// Try to switch clusters (if available)
|
||||
const clusterSelector = page.getByRole('button', { name: /cluster/i });
|
||||
if (await clusterSelector.isVisible()) {
|
||||
// Note: This part will only work in multi-cluster setups
|
||||
// For single-cluster, this test will just verify badge persists
|
||||
await clusterSelector.click();
|
||||
|
||||
// Select different cluster if available
|
||||
const clusterOptions = page.getByRole('menuitem');
|
||||
const count = await clusterOptions.count();
|
||||
|
||||
if (count > 1) {
|
||||
await clusterOptions.nth(1).click();
|
||||
|
||||
// Badge should update or disappear (if new cluster doesn't have Polaris)
|
||||
// This is just verifying no crash occurs
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Badge should still be functional
|
||||
await expect(badge).toBeEnabled();
|
||||
});
|
||||
});
|
||||
+64
-15
@@ -20,42 +20,91 @@ test.describe('Polaris plugin smoke tests', () => {
|
||||
await expect(page.getByText(/%/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('namespaces page renders table with links', async ({ page }) => {
|
||||
test('namespaces page renders table with namespace buttons', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Polaris \u2014 Namespaces' })).toBeVisible();
|
||||
|
||||
// Table should have at least one row with a namespace link
|
||||
// Table should have at least one row with a namespace button
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const rows = table.locator('tbody tr');
|
||||
await expect(rows.first()).toBeVisible();
|
||||
|
||||
// Each namespace row should contain a link
|
||||
const firstLink = rows.first().locator('a');
|
||||
await expect(firstLink).toBeVisible();
|
||||
// Each namespace row should contain a button (now buttons instead of links for drawer)
|
||||
const firstButton = rows.first().locator('button');
|
||||
await expect(firstButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('namespace detail page renders from table link', async ({ page }) => {
|
||||
test('namespace detail drawer opens from table button', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
// Click the first namespace link in the table
|
||||
// Click the first namespace button in the table
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const firstLink = table.locator('tbody tr').first().locator('a');
|
||||
const namespaceName = await firstLink.textContent();
|
||||
await firstLink.click();
|
||||
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||
const namespaceName = await firstButton.textContent();
|
||||
await firstButton.click();
|
||||
|
||||
// Detail page should show the namespace name in the heading
|
||||
// Drawer should open and show the namespace name in the heading
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).toBeVisible();
|
||||
|
||||
// "Namespace Score" section should be present in drawer
|
||||
await expect(page.getByText('Namespace Score')).toBeVisible();
|
||||
|
||||
// Resources table should exist in drawer
|
||||
await expect(page.getByText('Resources')).toBeVisible();
|
||||
|
||||
// URL hash should be updated with namespace name
|
||||
await expect(page).toHaveURL(/\/polaris\/namespaces#/);
|
||||
});
|
||||
|
||||
test('namespace detail drawer closes with Escape key', async ({ page }) => {
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
|
||||
// Open the drawer by clicking a namespace button
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||
const namespaceName = await firstButton.textContent();
|
||||
await firstButton.click();
|
||||
|
||||
// Verify drawer is open
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).toBeVisible();
|
||||
|
||||
// Press Escape key
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Drawer should close (heading should not be visible anymore)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).not.toBeVisible();
|
||||
|
||||
// URL hash should be cleared
|
||||
await expect(page).toHaveURL(/\/polaris\/namespaces$/);
|
||||
});
|
||||
|
||||
test('namespace detail drawer opens from URL hash', async ({ page }) => {
|
||||
// Get a namespace name first
|
||||
await page.goto('/c/main/polaris/namespaces');
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||
const namespaceName = await firstButton.textContent();
|
||||
|
||||
// Navigate directly to URL with hash
|
||||
await page.goto(`/c/main/polaris/namespaces#${namespaceName}`);
|
||||
|
||||
// Drawer should automatically open with the namespace details
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||
).toBeVisible();
|
||||
|
||||
// "Namespace Score" section should be present
|
||||
await expect(page.getByText('Namespace Score')).toBeVisible();
|
||||
|
||||
// Resources table should exist
|
||||
await expect(page.getByText('Resources')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Polaris plugin settings', () => {
|
||||
test('settings page shows configuration options', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Find Polaris plugin in the list
|
||||
const pluginCard = page.locator('text=headlamp-polaris-plugin').first();
|
||||
await expect(pluginCard).toBeVisible();
|
||||
|
||||
// Click to view settings (if settings are displayed inline, they should already be visible)
|
||||
// Note: Headlamp v0.39.0+ shows settings inline on the plugins page
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('refresh interval setting is configurable', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Navigate to Polaris settings
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Find the refresh interval dropdown
|
||||
const intervalSelect = page.locator('select').filter({ hasText: /minute|second/ });
|
||||
await expect(intervalSelect).toBeVisible();
|
||||
|
||||
// Get current value
|
||||
const currentValue = await intervalSelect.inputValue();
|
||||
|
||||
// Change to a different value
|
||||
const newValue = currentValue === '300' ? '600' : '300';
|
||||
await intervalSelect.selectOption(newValue);
|
||||
|
||||
// Value should be updated
|
||||
await expect(intervalSelect).toHaveValue(newValue);
|
||||
});
|
||||
|
||||
test('dashboard URL setting is configurable', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Navigate to Polaris settings
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Find the dashboard URL input
|
||||
const urlInput = page.getByPlaceholder(/polaris-dashboard/);
|
||||
await expect(urlInput).toBeVisible();
|
||||
|
||||
// Input should have the default proxy URL or custom URL
|
||||
const currentUrl = await urlInput.inputValue();
|
||||
expect(currentUrl).toBeTruthy();
|
||||
|
||||
// Examples text should be visible
|
||||
await expect(page.getByText('Examples:')).toBeVisible();
|
||||
await expect(page.getByText(/K8s proxy:/)).toBeVisible();
|
||||
});
|
||||
|
||||
test('connection test button is available', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Navigate to Polaris settings
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Find and verify test connection button
|
||||
const testButton = page.getByRole('button', { name: /test connection/i });
|
||||
await expect(testButton).toBeVisible();
|
||||
await expect(testButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('connection test works with valid URL', async ({ page }) => {
|
||||
await page.goto('/c/main/settings/plugins');
|
||||
|
||||
// Navigate to Polaris settings
|
||||
await expect(page.getByText('Polaris Settings')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Click test connection
|
||||
const testButton = page.getByRole('button', { name: /test connection/i });
|
||||
await testButton.click();
|
||||
|
||||
// Wait for either success or error message
|
||||
// Note: This will succeed if Polaris is accessible, fail otherwise
|
||||
await page.waitForSelector('text=/Connected successfully|Connection failed/', {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
// Either success or failure is acceptable (depends on environment)
|
||||
const result = await page.textContent('body');
|
||||
expect(result).toMatch(/(Connected successfully|Connection failed)/);
|
||||
});
|
||||
});
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "headlamp-polaris-plugin",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "headlamp-polaris-plugin",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@playwright/test": "^1.58.2"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "headlamp-polaris-plugin",
|
||||
"version": "0.1.6",
|
||||
"version": "0.3.5",
|
||||
"description": "Headlamp plugin for Fairwinds Polaris audit results",
|
||||
"scripts": {
|
||||
"start": "headlamp-plugin start",
|
||||
|
||||
@@ -5,6 +5,7 @@ interface PolarisDataContextValue {
|
||||
data: AuditData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const PolarisDataContext = React.createContext<PolarisDataContextValue | null>(null);
|
||||
@@ -13,7 +14,18 @@ export function PolarisDataProvider(props: { children: React.ReactNode }) {
|
||||
const interval = getRefreshInterval();
|
||||
const state = usePolarisData(interval);
|
||||
|
||||
return <PolarisDataContext.Provider value={state}>{props.children}</PolarisDataContext.Provider>;
|
||||
// Rename triggerRefresh to refresh for consistency
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
data: state.data,
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
refresh: state.triggerRefresh,
|
||||
}),
|
||||
[state]
|
||||
);
|
||||
|
||||
return <PolarisDataContext.Provider value={value}>{props.children}</PolarisDataContext.Provider>;
|
||||
}
|
||||
|
||||
export function usePolarisDataContext(): PolarisDataContextValue {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Mapping of Polaris check IDs to human-readable names and descriptions
|
||||
* Sourced from Polaris documentation
|
||||
*/
|
||||
|
||||
export interface CheckInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'Security' | 'Efficiency' | 'Reliability';
|
||||
defaultSeverity: 'danger' | 'warning' | 'ignore';
|
||||
}
|
||||
|
||||
export const CHECK_MAPPING: Record<string, CheckInfo> = {
|
||||
// Security checks
|
||||
hostIPCSet: {
|
||||
name: 'Host IPC',
|
||||
description: 'Host IPC should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
hostPIDSet: {
|
||||
name: 'Host PID',
|
||||
description: 'Host PID should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
hostNetworkSet: {
|
||||
name: 'Host Network',
|
||||
description: 'Host network should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
hostPortSet: {
|
||||
name: 'Host Port',
|
||||
description: 'Host port should not be configured',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
runAsRootAllowed: {
|
||||
name: 'Run as Root',
|
||||
description: 'Should not be allowed to run as root',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
runAsPrivileged: {
|
||||
name: 'Privileged Container',
|
||||
description: 'Should not run as privileged',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
notReadOnlyRootFilesystem: {
|
||||
name: 'Read-Only Root Filesystem',
|
||||
description: 'Filesystem should be read-only',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
privilegeEscalationAllowed: {
|
||||
name: 'Privilege Escalation',
|
||||
description: 'Privilege escalation should not be allowed',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
dangerousCapabilities: {
|
||||
name: 'Dangerous Capabilities',
|
||||
description: 'Dangerous capabilities should not be allowed',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
insecureCapabilities: {
|
||||
name: 'Insecure Capabilities',
|
||||
description: 'Insecure capabilities should not be allowed',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
sensitiveContainerEnvVar: {
|
||||
name: 'Sensitive Environment Variables',
|
||||
description: 'Sensitive env vars detected',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
sensitiveConfigmapContent: {
|
||||
name: 'Sensitive ConfigMap',
|
||||
description: 'Sensitive ConfigMap content detected',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
automountServiceAccountToken: {
|
||||
name: 'Service Account Token Auto-mount',
|
||||
description: 'Service account token auto-mount',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
tlsSettingsMissing: {
|
||||
name: 'TLS Settings',
|
||||
description: 'TLS settings missing',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
missingNetworkPolicy: {
|
||||
name: 'Network Policy',
|
||||
description: 'Missing NetworkPolicy',
|
||||
category: 'Security',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
|
||||
// Reliability checks
|
||||
tagNotSpecified: {
|
||||
name: 'Image Tag',
|
||||
description: 'Image tag should be specified',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'danger',
|
||||
},
|
||||
pullPolicyNotAlways: {
|
||||
name: 'Pull Policy',
|
||||
description: 'Pull policy should be Always',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
readinessProbeMissing: {
|
||||
name: 'Readiness Probe',
|
||||
description: 'Readiness probe should be configured',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
livenessProbeMissing: {
|
||||
name: 'Liveness Probe',
|
||||
description: 'Liveness probe should be configured',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
deploymentMissingReplicas: {
|
||||
name: 'Deployment Replicas',
|
||||
description: 'Deployment should have multiple replicas',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
priorityClassNotSet: {
|
||||
name: 'Priority Class',
|
||||
description: 'Priority class should be set',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
metadataAndNameMismatched: {
|
||||
name: 'Metadata Mismatch',
|
||||
description: 'Metadata and name should match',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
missingPodDisruptionBudget: {
|
||||
name: 'Pod Disruption Budget',
|
||||
description: 'PodDisruptionBudget should exist',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
pdbDisruptionsIsZero: {
|
||||
name: 'PDB Disruptions',
|
||||
description: 'PDB maxUnavailable should not be zero',
|
||||
category: 'Reliability',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
|
||||
// Efficiency checks
|
||||
cpuRequestsMissing: {
|
||||
name: 'CPU Requests',
|
||||
description: 'CPU requests should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
cpuLimitsMissing: {
|
||||
name: 'CPU Limits',
|
||||
description: 'CPU limits should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
memoryRequestsMissing: {
|
||||
name: 'Memory Requests',
|
||||
description: 'Memory requests should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
memoryLimitsMissing: {
|
||||
name: 'Memory Limits',
|
||||
description: 'Memory limits should be set',
|
||||
category: 'Efficiency',
|
||||
defaultSeverity: 'warning',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get human-readable name for a check ID
|
||||
*/
|
||||
export function getCheckName(checkId: string): string {
|
||||
return CHECK_MAPPING[checkId]?.name || checkId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get check description
|
||||
*/
|
||||
export function getCheckDescription(checkId: string): string {
|
||||
return CHECK_MAPPING[checkId]?.description || 'Unknown check';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get check category
|
||||
*/
|
||||
export function getCheckCategory(checkId: string): 'Security' | 'Efficiency' | 'Reliability' {
|
||||
return CHECK_MAPPING[checkId]?.category || 'Security';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for severity
|
||||
*/
|
||||
export function getSeverityColor(severity: string): string {
|
||||
switch (severity) {
|
||||
case 'danger':
|
||||
return '#f44336';
|
||||
case 'warning':
|
||||
return '#ff9800';
|
||||
case 'ignore':
|
||||
return '#9e9e9e';
|
||||
default:
|
||||
return '#9e9e9e';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status for StatusLabel component
|
||||
*/
|
||||
export function getSeverityStatus(severity: string): 'error' | 'warning' | 'success' {
|
||||
switch (severity) {
|
||||
case 'danger':
|
||||
return 'error';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
+73
-18
@@ -125,11 +125,14 @@ export const INTERVAL_OPTIONS = [
|
||||
{ label: '30 minutes', value: 1800 },
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'polaris-plugin-refresh-interval';
|
||||
const REFRESH_STORAGE_KEY = 'polaris-plugin-refresh-interval';
|
||||
const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
|
||||
|
||||
const URL_STORAGE_KEY = 'polaris-plugin-dashboard-url';
|
||||
const DEFAULT_DASHBOARD_URL = '/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
|
||||
|
||||
export function getRefreshInterval(): number {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const stored = localStorage.getItem(REFRESH_STORAGE_KEY);
|
||||
if (stored !== null) {
|
||||
const parsed = parseInt(stored, 10);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
@@ -140,13 +143,26 @@ export function getRefreshInterval(): number {
|
||||
}
|
||||
|
||||
export function setRefreshInterval(seconds: number): void {
|
||||
localStorage.setItem(STORAGE_KEY, String(seconds));
|
||||
localStorage.setItem(REFRESH_STORAGE_KEY, String(seconds));
|
||||
}
|
||||
|
||||
export function getDashboardUrl(): string {
|
||||
const stored = localStorage.getItem(URL_STORAGE_KEY);
|
||||
if (stored !== null && stored.trim() !== '') {
|
||||
return stored.trim();
|
||||
}
|
||||
return DEFAULT_DASHBOARD_URL;
|
||||
}
|
||||
|
||||
export function setDashboardUrl(url: string): void {
|
||||
localStorage.setItem(URL_STORAGE_KEY, url.trim());
|
||||
}
|
||||
|
||||
// --- Polaris dashboard proxy URL ---
|
||||
|
||||
export const POLARIS_DASHBOARD_PROXY =
|
||||
'/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
|
||||
export function getPolarisProxyUrl(): string {
|
||||
return getDashboardUrl();
|
||||
}
|
||||
|
||||
// --- Score computation ---
|
||||
|
||||
@@ -157,13 +173,20 @@ export function computeScore(counts: ResultCounts): number {
|
||||
|
||||
// --- Data fetching hook ---
|
||||
|
||||
const POLARIS_API_PATH =
|
||||
'/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json';
|
||||
function getPolarisApiPath(): string {
|
||||
const baseUrl = getDashboardUrl();
|
||||
return baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`;
|
||||
}
|
||||
|
||||
function isFullUrl(url: string): boolean {
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
}
|
||||
|
||||
interface PolarisDataState {
|
||||
data: AuditData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
triggerRefresh: () => void;
|
||||
}
|
||||
|
||||
export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState {
|
||||
@@ -172,12 +195,30 @@ export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [tick, setTick] = React.useState(0);
|
||||
|
||||
const triggerRefresh = React.useCallback(() => {
|
||||
setTick(t => t + 1);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const result: AuditData = await ApiProxy.request(POLARIS_API_PATH);
|
||||
const apiPath = getPolarisApiPath();
|
||||
let result: AuditData;
|
||||
|
||||
if (isFullUrl(apiPath)) {
|
||||
// Direct fetch for full URLs
|
||||
const response = await fetch(apiPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
} else {
|
||||
// Kubernetes proxy for relative URLs
|
||||
result = await ApiProxy.request(apiPath);
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setData(result);
|
||||
setError(null);
|
||||
@@ -185,17 +226,31 @@ export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (cancelled) return;
|
||||
const apiPath = getPolarisApiPath();
|
||||
const status = (err as { status?: number }).status;
|
||||
if (status === 403) {
|
||||
setError(
|
||||
'Access denied (403). Check that your RBAC permissions allow proxying to the Polaris service.'
|
||||
);
|
||||
} else if (status === 404 || status === 503) {
|
||||
setError(
|
||||
'Polaris dashboard not reachable. Ensure Polaris is installed in the polaris namespace.'
|
||||
);
|
||||
|
||||
if (isFullUrl(apiPath)) {
|
||||
// Full URL errors
|
||||
if (status === 403) {
|
||||
setError('Access denied (403). Check authentication and CORS configuration.');
|
||||
} else if (status === 404) {
|
||||
setError('Polaris dashboard not found (404). Verify the URL is correct.');
|
||||
} else {
|
||||
setError(`Failed to fetch from ${apiPath}: ${String(err)}`);
|
||||
}
|
||||
} else {
|
||||
setError(`Failed to fetch Polaris data: ${String(err)}`);
|
||||
// Kubernetes proxy errors
|
||||
if (status === 403) {
|
||||
setError(
|
||||
'Access denied (403). Check that your RBAC permissions allow proxying to the Polaris service.'
|
||||
);
|
||||
} else if (status === 404 || status === 503) {
|
||||
setError(
|
||||
'Polaris dashboard not reachable. Ensure Polaris is installed in the configured namespace.'
|
||||
);
|
||||
} else {
|
||||
setError(`Failed to fetch Polaris data: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -216,5 +271,5 @@ export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [refreshIntervalSeconds]);
|
||||
|
||||
return { data, loading, error };
|
||||
return { data, loading, error, triggerRefresh };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { getCheckCategory, getCheckName } from './checkMapping';
|
||||
import { AuditData } from './polaris';
|
||||
|
||||
export interface TopIssue {
|
||||
checkId: string;
|
||||
checkName: string;
|
||||
category: 'Security' | 'Efficiency' | 'Reliability';
|
||||
severity: 'danger' | 'warning';
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the most common failing checks across the cluster
|
||||
* Returns top 10 issues sorted by severity then count
|
||||
*/
|
||||
export function getTopIssues(data: AuditData): TopIssue[] {
|
||||
const issueCounts = new Map<string, { severity: 'danger' | 'warning'; count: number }>();
|
||||
|
||||
// Aggregate all failing checks
|
||||
for (const result of data.Results) {
|
||||
// Pod-level checks
|
||||
if (result.PodResult?.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(result.PodResult.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
const existing = issueCounts.get(checkId);
|
||||
issueCounts.set(checkId, {
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
count: (existing?.count || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container-level checks
|
||||
if (result.PodResult?.ContainerResults) {
|
||||
for (const container of result.PodResult.ContainerResults) {
|
||||
for (const [checkId, checkResult] of Object.entries(container.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
const existing = issueCounts.get(checkId);
|
||||
issueCounts.set(checkId, {
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
count: (existing?.count || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Controller-level checks (if any)
|
||||
if (result.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(result.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
const existing = issueCounts.get(checkId);
|
||||
issueCounts.set(checkId, {
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
count: (existing?.count || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array and format
|
||||
const issues: TopIssue[] = Array.from(issueCounts.entries()).map(([checkId, data]) => ({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
category: getCheckCategory(checkId),
|
||||
severity: data.severity,
|
||||
count: data.count,
|
||||
}));
|
||||
|
||||
// Sort by severity (danger first) then by count (descending)
|
||||
issues.sort((a, b) => {
|
||||
if (a.severity === 'danger' && b.severity !== 'danger') return -1;
|
||||
if (a.severity !== 'danger' && b.severity === 'danger') return 1;
|
||||
return b.count - a.count;
|
||||
});
|
||||
|
||||
// Return top 10
|
||||
return issues.slice(0, 10);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { computeScore, countResults } from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
|
||||
/**
|
||||
* App bar badge showing cluster Polaris score
|
||||
* Clicking navigates to the overview dashboard
|
||||
*/
|
||||
export default function AppBarScoreBadge() {
|
||||
const { data, loading } = usePolarisDataContext();
|
||||
const history = useHistory();
|
||||
|
||||
if (loading || !data) {
|
||||
return null; // Graceful degradation when Polaris unavailable
|
||||
}
|
||||
|
||||
const counts = countResults(data);
|
||||
const score = computeScore(counts);
|
||||
|
||||
// Color based on score
|
||||
const getColor = (score: number): string => {
|
||||
if (score >= 80) return '#4caf50'; // green
|
||||
if (score >= 50) return '#ff9800'; // orange
|
||||
return '#f44336'; // red
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
history.push('/polaris');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginRight: '8px',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '16px',
|
||||
border: 'none',
|
||||
backgroundColor: getColor(score),
|
||||
color: 'white',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
aria-label={`Polaris cluster score: ${score}%`}
|
||||
>
|
||||
<span>🛡️</span>
|
||||
<span>Polaris: {score}%</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,17 @@ vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
SimpleTable: ({ data }: { data: Array<any> }) => (
|
||||
<table data-testid="simple-table">
|
||||
<tbody>
|
||||
{data.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>{JSON.stringify(item)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
PercentageCircle: ({ label }: { label: string }) => (
|
||||
<div data-testid="percentage-circle">{label}</div>
|
||||
),
|
||||
|
||||
@@ -5,11 +5,14 @@ import {
|
||||
PercentageCircle,
|
||||
SectionBox,
|
||||
SectionHeader,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { getSeverityStatus } from '../api/checkMapping';
|
||||
import { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
import { getTopIssues, TopIssue } from '../api/topIssues';
|
||||
|
||||
const COLORS = {
|
||||
pass: '#4caf50',
|
||||
@@ -26,7 +29,6 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
||||
{ name: 'Pass', value: counts.pass, fill: COLORS.pass },
|
||||
{ name: 'Warning', value: counts.warning, fill: COLORS.warning },
|
||||
{ name: 'Danger', value: counts.danger, fill: COLORS.danger },
|
||||
{ name: 'Skipped', value: counts.skipped, fill: COLORS.skipped },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -51,14 +53,6 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
||||
name: 'Danger',
|
||||
value: <StatusLabel status="error">{counts.danger}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Skipped',
|
||||
value: (
|
||||
<span title="Only counts checks with Severity=ignore. Annotation-based exemptions are not included.">
|
||||
{counts.skipped}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
@@ -76,18 +70,70 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatAuditTime(auditTime: string): string {
|
||||
const date = new Date(auditTime);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
export default function DashboardView() {
|
||||
const { data, loading, error } = usePolarisDataContext();
|
||||
const { data, loading, error, refresh } = usePolarisDataContext();
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading Polaris audit data..." />;
|
||||
}
|
||||
|
||||
const counts = data ? countResults(data) : null;
|
||||
const topIssues = data ? getTopIssues(data) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Polaris — Overview" />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<SectionHeader title="Polaris — Overview" />
|
||||
{data && (
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '14px', color: 'var(--mui-palette-text-secondary, #666)' }}>
|
||||
Last updated: {formatAuditTime(data.AuditTime)}
|
||||
</span>
|
||||
<button
|
||||
onClick={refresh}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
border: '1px solid var(--mui-palette-primary-main, #1976d2)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<span>🔄</span>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<SectionBox title="Error">
|
||||
@@ -102,7 +148,35 @@ export default function DashboardView() {
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{data && counts && <OverviewSection data={data} counts={counts} />}
|
||||
{data && counts && (
|
||||
<>
|
||||
<OverviewSection data={data} counts={counts} />
|
||||
|
||||
{topIssues.length > 0 && (
|
||||
<SectionBox title="Top Issues">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Check', getter: (issue: TopIssue) => issue.checkName },
|
||||
{ label: 'Category', getter: (issue: TopIssue) => issue.category },
|
||||
{
|
||||
label: 'Severity',
|
||||
getter: (issue: TopIssue) => (
|
||||
<StatusLabel status={getSeverityStatus(issue.severity)}>
|
||||
{issue.severity}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Affected Workloads',
|
||||
getter: (issue: TopIssue) => String(issue.count),
|
||||
},
|
||||
]}
|
||||
data={topIssues}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!data && !error && (
|
||||
<SectionBox title="No Data">
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { Dialog, NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { getCheckName } from '../api/checkMapping';
|
||||
import { Result } from '../api/polaris';
|
||||
|
||||
interface ExemptionManagerProps {
|
||||
workloadResult: Result;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CheckFailure {
|
||||
checkId: string;
|
||||
checkName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exemption management UI for adding/removing Polaris exemptions
|
||||
* Uses annotation patches on the workload resource
|
||||
*/
|
||||
export default function ExemptionManager({
|
||||
workloadResult,
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
}: ExemptionManagerProps) {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [selectedChecks, setSelectedChecks] = React.useState<Set<string>>(new Set());
|
||||
const [exemptAll, setExemptAll] = React.useState(false);
|
||||
const [applying, setApplying] = React.useState(false);
|
||||
|
||||
// Extract current exemptions from workload metadata
|
||||
const getExemptions = (): string[] => {
|
||||
// This would need to fetch the actual workload from K8s API
|
||||
// For now, return empty array as placeholder
|
||||
return [];
|
||||
};
|
||||
|
||||
// Extract failing checks for this workload
|
||||
const getFailingChecks = (): CheckFailure[] => {
|
||||
const failures: CheckFailure[] = [];
|
||||
|
||||
// Pod-level checks
|
||||
if (workloadResult.PodResult?.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(workloadResult.PodResult.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container checks
|
||||
if (workloadResult.PodResult?.ContainerResults) {
|
||||
for (const container of workloadResult.PodResult.ContainerResults) {
|
||||
for (const [checkId, checkResult] of Object.entries(container.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
// Avoid duplicates
|
||||
if (!failures.some(f => f.checkId === checkId)) {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
};
|
||||
|
||||
const failingChecks = getFailingChecks();
|
||||
const currentExemptions = getExemptions();
|
||||
|
||||
const handleCheckToggle = (checkId: string) => {
|
||||
const newSelected = new Set(selectedChecks);
|
||||
if (newSelected.has(checkId)) {
|
||||
newSelected.delete(checkId);
|
||||
} else {
|
||||
newSelected.add(checkId);
|
||||
}
|
||||
setSelectedChecks(newSelected);
|
||||
};
|
||||
|
||||
const applyExemptions = async () => {
|
||||
setApplying(true);
|
||||
|
||||
try {
|
||||
// Construct the API path based on kind
|
||||
const apiGroup = getApiGroup(kind);
|
||||
const apiVersion = 'v1'; // This would need to be dynamic based on kind
|
||||
const plural = getPlural(kind);
|
||||
|
||||
const patchPath = apiGroup
|
||||
? `/apis/${apiGroup}/${apiVersion}/namespaces/${namespace}/${plural}/${name}`
|
||||
: `/api/v1/namespaces/${namespace}/${plural}/${name}`;
|
||||
|
||||
// Build annotations patch
|
||||
const annotations: Record<string, string> = {};
|
||||
|
||||
if (exemptAll) {
|
||||
annotations['polaris.fairwinds.com/exempt'] = 'true';
|
||||
} else {
|
||||
for (const checkId of selectedChecks) {
|
||||
annotations[`polaris.fairwinds.com/${checkId}-exempt`] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
const patch = {
|
||||
metadata: {
|
||||
annotations,
|
||||
},
|
||||
};
|
||||
|
||||
await ApiProxy.request(patchPath, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/strategic-merge-patch+json',
|
||||
},
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
|
||||
setDialogOpen(false);
|
||||
setSelectedChecks(new Set());
|
||||
setExemptAll(false);
|
||||
|
||||
// Show success message (would need notistack integration)
|
||||
alert('Exemptions applied successfully');
|
||||
} catch (err) {
|
||||
alert(`Failed to apply exemptions: ${String(err)}`);
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionBox title="Exemptions">
|
||||
{currentExemptions.length > 0 ? (
|
||||
<NameValueTable
|
||||
rows={currentExemptions.map(exemption => ({
|
||||
name: exemption,
|
||||
value: (
|
||||
<button
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
backgroundColor: 'var(--mui-palette-error-main, #f44336)',
|
||||
color: 'var(--mui-palette-error-contrastText, #fff)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
onClick={() => {
|
||||
// Remove exemption logic
|
||||
alert('Remove exemption: ' + exemption);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<p>No exemptions configured</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setDialogOpen(true)}
|
||||
disabled={failingChecks.length === 0}
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
padding: '6px 16px',
|
||||
backgroundColor:
|
||||
failingChecks.length === 0
|
||||
? 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||
: 'transparent',
|
||||
color:
|
||||
failingChecks.length === 0
|
||||
? 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
border: '1px solid',
|
||||
borderColor:
|
||||
failingChecks.length === 0
|
||||
? 'var(--mui-palette-divider, #e0e0e0)'
|
||||
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
borderRadius: '4px',
|
||||
cursor: failingChecks.length === 0 ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
Add Exemption
|
||||
</button>
|
||||
</SectionBox>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} title="Add Exemptions">
|
||||
<div style={{ padding: '16px', minWidth: '400px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exemptAll}
|
||||
onChange={e => setExemptAll(e.target.checked)}
|
||||
/>
|
||||
<span>Exempt from all checks</span>
|
||||
</label>
|
||||
|
||||
{!exemptAll && (
|
||||
<>
|
||||
<div style={{ marginTop: '16px', marginBottom: '8px', fontWeight: 600 }}>
|
||||
Select checks to exempt:
|
||||
</div>
|
||||
<div>
|
||||
{failingChecks.map(check => (
|
||||
<label
|
||||
key={check.checkId}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '8px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedChecks.has(check.checkId)}
|
||||
onChange={() => handleCheckToggle(check.checkId)}
|
||||
/>
|
||||
<span>{check.checkName}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ marginTop: '16px', display: 'flex', gap: '8px', justifyContent: 'flex-end' }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setDialogOpen(false)}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={applyExemptions}
|
||||
disabled={applying || (!exemptAll && selectedChecks.size === 0)}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor:
|
||||
applying || (!exemptAll && selectedChecks.size === 0)
|
||||
? 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
color:
|
||||
applying || (!exemptAll && selectedChecks.size === 0)
|
||||
? 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||
: 'var(--mui-palette-primary-contrastText, #fff)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor:
|
||||
applying || (!exemptAll && selectedChecks.size === 0) ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{applying ? 'Applying...' : 'Apply'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper functions to get API info based on kind
|
||||
function getApiGroup(kind: string): string | null {
|
||||
switch (kind) {
|
||||
case 'Deployment':
|
||||
case 'StatefulSet':
|
||||
case 'DaemonSet':
|
||||
return 'apps';
|
||||
case 'Job':
|
||||
case 'CronJob':
|
||||
return 'batch';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlural(kind: string): string {
|
||||
switch (kind) {
|
||||
case 'Deployment':
|
||||
return 'deployments';
|
||||
case 'StatefulSet':
|
||||
return 'statefulsets';
|
||||
case 'DaemonSet':
|
||||
return 'daemonsets';
|
||||
case 'Job':
|
||||
return 'jobs';
|
||||
case 'CronJob':
|
||||
return 'cronjobs';
|
||||
default:
|
||||
return kind.toLowerCase() + 's';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getCheckName, getSeverityStatus } from '../api/checkMapping';
|
||||
import { computeScore, countResultsForItems } from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
import ExemptionManager from './ExemptionManager';
|
||||
|
||||
interface CheckFailure {
|
||||
checkId: string;
|
||||
checkName: string;
|
||||
severity: 'danger' | 'warning';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface InlineAuditSectionProps {
|
||||
resource: any; // KubeObject from Headlamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Polaris audit section for resource detail views
|
||||
* Shows a compact summary of Polaris findings for Deployments, StatefulSets, etc.
|
||||
*/
|
||||
export default function InlineAuditSection({ resource }: InlineAuditSectionProps) {
|
||||
const { data, loading } = usePolarisDataContext();
|
||||
|
||||
if (loading || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is a supported controller kind
|
||||
const supportedKinds = ['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob'];
|
||||
const kind = resource.kind;
|
||||
|
||||
if (!supportedKinds.includes(kind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = resource.metadata?.name;
|
||||
const namespace = resource.metadata?.namespace;
|
||||
|
||||
if (!name || !namespace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find this workload in Polaris audit data
|
||||
const workloadResult = data.Results.find(
|
||||
r => r.Kind === kind && r.Name === name && r.Namespace === namespace
|
||||
);
|
||||
|
||||
if (!workloadResult) {
|
||||
return (
|
||||
<SectionBox title="Polaris Audit">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: 'Polaris dashboard not detected — install Polaris to see audit results',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate score and counts
|
||||
const counts = countResultsForItems([workloadResult]);
|
||||
const score = computeScore(counts);
|
||||
|
||||
// Extract failing checks
|
||||
const failures: CheckFailure[] = [];
|
||||
|
||||
// Pod-level checks
|
||||
if (workloadResult.PodResult?.Results) {
|
||||
for (const [checkId, checkResult] of Object.entries(workloadResult.PodResult.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
message: checkResult.Message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container checks
|
||||
if (workloadResult.PodResult?.ContainerResults) {
|
||||
for (const container of workloadResult.PodResult.ContainerResults) {
|
||||
for (const [checkId, checkResult] of Object.entries(container.Results)) {
|
||||
if (!checkResult.Success && checkResult.Severity !== 'ignore') {
|
||||
// Avoid duplicates
|
||||
if (!failures.some(f => f.checkId === checkId)) {
|
||||
failures.push({
|
||||
checkId,
|
||||
checkName: getCheckName(checkId),
|
||||
severity: checkResult.Severity as 'danger' | 'warning',
|
||||
message: checkResult.Message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by severity
|
||||
failures.sort((a, b) => {
|
||||
if (a.severity === 'danger' && b.severity !== 'danger') return -1;
|
||||
if (a.severity !== 'danger' && b.severity === 'danger') return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionBox title="Polaris Audit">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Score',
|
||||
value: (
|
||||
<StatusLabel status={score >= 80 ? 'success' : score >= 50 ? 'warning' : 'error'}>
|
||||
{score}%
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Summary',
|
||||
value: `${counts.pass} passing, ${counts.warning} warnings, ${counts.danger} dangers`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{failures.length > 0 && (
|
||||
<>
|
||||
<div style={{ marginTop: '16px', marginBottom: '8px', fontWeight: 600 }}>
|
||||
Failing Checks:
|
||||
</div>
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Check', getter: (f: CheckFailure) => f.checkName },
|
||||
{
|
||||
label: 'Severity',
|
||||
getter: (f: CheckFailure) => (
|
||||
<StatusLabel status={getSeverityStatus(f.severity)}>{f.severity}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{ label: 'Message', getter: (f: CheckFailure) => f.message },
|
||||
]}
|
||||
data={failures}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Link
|
||||
to={`/polaris/namespaces#${namespace}`}
|
||||
style={{ color: 'var(--link-color, #1976d2)' }}
|
||||
>
|
||||
View Full Report →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<ExemptionManager
|
||||
workloadResult={workloadResult}
|
||||
namespace={namespace}
|
||||
kind={kind}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
computeScore,
|
||||
countResultsForItems,
|
||||
filterResultsByNamespace,
|
||||
POLARIS_DASHBOARD_PROXY,
|
||||
getPolarisProxyUrl,
|
||||
Result,
|
||||
ResultCounts,
|
||||
} from '../api/polaris';
|
||||
@@ -89,7 +89,7 @@ export default function NamespaceDetailView() {
|
||||
{
|
||||
name: 'Polaris Dashboard',
|
||||
value: (
|
||||
<a href={POLARIS_DASHBOARD_PROXY} target="_blank" rel="noopener noreferrer">
|
||||
<a href={getPolarisProxyUrl()} target="_blank" rel="noopener noreferrer">
|
||||
View in Polaris Dashboard
|
||||
</a>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
@@ -117,7 +118,7 @@ describe('NamespacesListView', () => {
|
||||
expect(screen.getByText('No Polaris audit results found.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders namespace rows with correct scores and links', () => {
|
||||
it('renders namespace rows with correct scores and buttons', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-a',
|
||||
@@ -157,12 +158,14 @@ describe('NamespacesListView', () => {
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
|
||||
// Namespace links
|
||||
const alphaLink = screen.getByText('alpha');
|
||||
expect(alphaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/alpha');
|
||||
// Namespace buttons (now buttons instead of links for drawer)
|
||||
const alphaButton = screen.getByText('alpha');
|
||||
expect(alphaButton).toBeInTheDocument();
|
||||
expect(alphaButton.tagName).toBe('BUTTON');
|
||||
|
||||
const betaLink = screen.getByText('beta');
|
||||
expect(betaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/beta');
|
||||
const betaButton = screen.getByText('beta');
|
||||
expect(betaButton).toBeInTheDocument();
|
||||
expect(betaButton.tagName).toBe('BUTTON');
|
||||
});
|
||||
|
||||
it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => {
|
||||
@@ -216,4 +219,74 @@ describe('NamespacesListView', () => {
|
||||
const errorScore = scoreLabels.find(el => el.textContent === '0%');
|
||||
expect(errorScore).toHaveAttribute('data-status', 'error');
|
||||
});
|
||||
|
||||
it('opens drawer when namespace button is clicked and URL hash is updated', async () => {
|
||||
const user = userEvent.setup();
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-a',
|
||||
Namespace: 'alpha',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderWithRouter(<NamespacesListView />);
|
||||
|
||||
// Click the namespace button
|
||||
const alphaButton = screen.getByText('alpha');
|
||||
await user.click(alphaButton);
|
||||
|
||||
// Drawer should open (check for the panel title)
|
||||
expect(screen.getByText(/Polaris — alpha/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('initializes drawer from URL hash', () => {
|
||||
const data = makeAuditData([
|
||||
makeResult({
|
||||
Name: 'deploy-a',
|
||||
Namespace: 'test-ns',
|
||||
Results: {
|
||||
c1: {
|
||||
ID: 'c1',
|
||||
Message: '',
|
||||
Details: [],
|
||||
Success: true,
|
||||
Severity: 'warning',
|
||||
Category: 'X',
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
mockUsePolarisDataContext.mockReturnValue({
|
||||
data,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Render with initial hash in URL
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/polaris/namespaces#test-ns']}>
|
||||
<NamespacesListView />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Drawer should be open with the namespace from hash
|
||||
expect(screen.getByText(/Polaris — test-ns/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Router } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
Loader,
|
||||
NameValueTable,
|
||||
@@ -7,13 +6,16 @@ import {
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
computeScore,
|
||||
countResultsForItems,
|
||||
filterResultsByNamespace,
|
||||
getNamespaces,
|
||||
getPolarisProxyUrl,
|
||||
Result,
|
||||
ResultCounts,
|
||||
} from '../api/polaris';
|
||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||
|
||||
@@ -32,9 +34,230 @@ interface NamespaceRow {
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
export default function NamespacesListView() {
|
||||
function resourceCounts(result: Result): ResultCounts {
|
||||
return countResultsForItems([result]);
|
||||
}
|
||||
|
||||
interface NamespaceDetailPanelProps {
|
||||
namespace: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function NamespaceDetailPanel({ namespace, onClose }: NamespaceDetailPanelProps) {
|
||||
const { data, loading, error } = usePolarisDataContext();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Loader title={`Loading Polaris data for ${namespace}...`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<SectionBox title="Error">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="error">{error}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<SectionBox title="No Data">
|
||||
<NameValueTable rows={[{ name: 'Status', value: 'No Polaris audit results found.' }]} />
|
||||
</SectionBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const results = filterResultsByNamespace(data, namespace);
|
||||
const counts = countResultsForItems(results);
|
||||
const score = computeScore(counts);
|
||||
const status = scoreStatus(score);
|
||||
|
||||
const countsPerResource = new Map<string, ResultCounts>();
|
||||
for (const r of results) {
|
||||
countsPerResource.set(`${r.Namespace}/${r.Kind}/${r.Name}`, resourceCounts(r));
|
||||
}
|
||||
|
||||
function getResourceCounts(row: Result): ResultCounts {
|
||||
return countsPerResource.get(`${row.Namespace}/${row.Kind}/${row.Name}`) ?? resourceCounts(row);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '1000px',
|
||||
backgroundColor: 'var(--mui-palette-background-default)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
boxShadow: '-2px 0 8px rgba(0,0,0,0.15)',
|
||||
overflowY: 'auto',
|
||||
zIndex: 1200,
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, color: 'var(--mui-palette-text-primary)' }}>
|
||||
Polaris — {namespace}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
padding: '0 8px',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
aria-label="Close panel"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SectionBox title="External">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Polaris Dashboard',
|
||||
value: (
|
||||
<a href={getPolarisProxyUrl()} target="_blank" rel="noopener noreferrer">
|
||||
View in Polaris Dashboard
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Namespace Score">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Score',
|
||||
value: <StatusLabel status={status}>{score}%</StatusLabel>,
|
||||
},
|
||||
{ name: 'Total Checks', value: String(counts.total) },
|
||||
{
|
||||
name: 'Pass',
|
||||
value: <StatusLabel status="success">{counts.pass}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Warning',
|
||||
value: <StatusLabel status="warning">{counts.warning}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Danger',
|
||||
value: <StatusLabel status="error">{counts.danger}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Skipped',
|
||||
value: (
|
||||
<span title="Only counts checks with Severity=ignore. Annotation-based exemptions are not included.">
|
||||
{counts.skipped}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Resources">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Name', getter: (row: Result) => row.Name },
|
||||
{ label: 'Kind', getter: (row: Result) => row.Kind },
|
||||
{
|
||||
label: 'Pass',
|
||||
getter: (row: Result) => (
|
||||
<StatusLabel status="success">{getResourceCounts(row).pass}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Warning',
|
||||
getter: (row: Result) => (
|
||||
<StatusLabel status="warning">{getResourceCounts(row).warning}</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
getter: (row: Result) => (
|
||||
<StatusLabel status="error">{getResourceCounts(row).danger}</StatusLabel>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={results}
|
||||
emptyMessage={`No resources found in namespace "${namespace}".`}
|
||||
/>
|
||||
</SectionBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NamespacesListView() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { data, loading, error } = usePolarisDataContext();
|
||||
|
||||
// Initialize from URL hash
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(
|
||||
location.hash.slice(1) || null
|
||||
);
|
||||
|
||||
// Sync drawer state when URL hash changes (browser back/forward)
|
||||
useEffect(() => {
|
||||
const hashNs = location.hash.slice(1);
|
||||
setSelectedNamespace(hashNs || null);
|
||||
}, [location.hash]);
|
||||
|
||||
const openNamespace = (ns: string) => {
|
||||
setSelectedNamespace(ns);
|
||||
history.push(`${location.pathname}#${ns}`);
|
||||
};
|
||||
|
||||
const closeNamespace = () => {
|
||||
setSelectedNamespace(null);
|
||||
history.push(location.pathname);
|
||||
};
|
||||
|
||||
// Handle keyboard navigation (Escape key closes drawer)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && selectedNamespace) {
|
||||
closeNamespace();
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedNamespace) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedNamespace]);
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading Polaris audit data..." />;
|
||||
}
|
||||
@@ -92,13 +315,20 @@ export default function NamespacesListView() {
|
||||
{
|
||||
label: 'Namespace',
|
||||
getter: (row: NamespaceRow) => (
|
||||
<Link
|
||||
to={Router.createRouteURL('polaris-namespace', {
|
||||
namespace: row.namespace,
|
||||
})}
|
||||
<button
|
||||
onClick={() => openNamespace(row.namespace)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'var(--link-color, #1976d2)',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline',
|
||||
padding: 0,
|
||||
font: 'inherit',
|
||||
}}
|
||||
>
|
||||
{row.namespace}
|
||||
</Link>
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -130,6 +360,25 @@ export default function NamespacesListView() {
|
||||
emptyMessage="No namespaces found in Polaris audit data."
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
{selectedNamespace && (
|
||||
<>
|
||||
<div
|
||||
onClick={closeNamespace}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 1100,
|
||||
}}
|
||||
aria-label="Close panel backdrop"
|
||||
/>
|
||||
<NamespaceDetailPanel namespace={selectedNamespace} onClose={closeNamespace} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { getRefreshInterval, INTERVAL_OPTIONS, setRefreshInterval } from '../api/polaris';
|
||||
import {
|
||||
AuditData,
|
||||
getDashboardUrl,
|
||||
getRefreshInterval,
|
||||
INTERVAL_OPTIONS,
|
||||
setDashboardUrl,
|
||||
setRefreshInterval,
|
||||
} from '../api/polaris';
|
||||
|
||||
interface PluginSettingsProps {
|
||||
data?: { [key: string]: string | number | boolean };
|
||||
@@ -10,13 +22,61 @@ interface PluginSettingsProps {
|
||||
export default function PolarisSettings(props: PluginSettingsProps) {
|
||||
const { data, onDataChange } = props;
|
||||
const currentInterval = (data?.refreshInterval as number) ?? getRefreshInterval();
|
||||
const currentUrl = (data?.dashboardUrl as string) ?? getDashboardUrl();
|
||||
const [testing, setTesting] = React.useState(false);
|
||||
const [testResult, setTestResult] = React.useState<{ success: boolean; message: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
function handleIntervalChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const seconds = Number(e.target.value);
|
||||
setRefreshInterval(seconds);
|
||||
onDataChange?.({ ...data, refreshInterval: seconds });
|
||||
}
|
||||
|
||||
function handleUrlChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const url = e.target.value;
|
||||
setDashboardUrl(url);
|
||||
onDataChange?.({ ...data, dashboardUrl: url });
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
const baseUrl = currentUrl;
|
||||
const apiPath = baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`;
|
||||
const isFullUrl = apiPath.startsWith('http://') || apiPath.startsWith('https://');
|
||||
|
||||
let result: AuditData;
|
||||
|
||||
if (isFullUrl) {
|
||||
const response = await fetch(apiPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
result = await response.json();
|
||||
} else {
|
||||
result = await ApiProxy.request(apiPath);
|
||||
}
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `Connected successfully! Version: ${
|
||||
result.PolarisOutputVersion
|
||||
}, Last audit: ${new Date(result.AuditTime).toLocaleString()}`,
|
||||
});
|
||||
} catch (err) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: `Connection failed: ${String(err)}`,
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionBox title="Polaris Settings">
|
||||
<NameValueTable
|
||||
@@ -24,7 +84,7 @@ export default function PolarisSettings(props: PluginSettingsProps) {
|
||||
{
|
||||
name: 'Refresh Interval',
|
||||
value: (
|
||||
<select value={currentInterval} onChange={handleChange}>
|
||||
<select value={currentInterval} onChange={handleIntervalChange}>
|
||||
{INTERVAL_OPTIONS.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
@@ -33,6 +93,74 @@ export default function PolarisSettings(props: PluginSettingsProps) {
|
||||
</select>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Dashboard URL',
|
||||
value: (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={currentUrl}
|
||||
onChange={handleUrlChange}
|
||||
placeholder="/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px 8px',
|
||||
border: '1px solid var(--mui-palette-divider, #e0e0e0)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
|
||||
color: 'var(--mui-palette-text-primary, #000)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--mui-palette-text-secondary, #666)',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
Examples:
|
||||
<br />• K8s proxy:{' '}
|
||||
<code>/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/</code>
|
||||
<br />• Full URL: <code>https://my-polaris.example.com</code>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Connection Test',
|
||||
value: (
|
||||
<div>
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={testing}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: testing
|
||||
? 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
color: testing
|
||||
? 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||
: 'var(--mui-palette-primary-contrastText, #fff)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: testing ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{testing ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
{testResult && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<StatusLabel status={testResult.success ? 'success' : 'error'}>
|
||||
{testResult.message}
|
||||
</StatusLabel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
+24
-10
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
registerAppBarAction,
|
||||
registerDetailsViewSection,
|
||||
registerPluginSettings,
|
||||
registerRoute,
|
||||
registerSidebarEntry,
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
import { PolarisDataProvider } from './api/PolarisDataContext';
|
||||
import AppBarScoreBadge from './components/AppBarScoreBadge';
|
||||
import DashboardView from './components/DashboardView';
|
||||
import NamespaceDetailView from './components/NamespaceDetailView';
|
||||
import InlineAuditSection from './components/InlineAuditSection';
|
||||
import NamespacesListView from './components/NamespacesListView';
|
||||
import PolarisSettings from './components/PolarisSettings';
|
||||
|
||||
@@ -62,16 +65,27 @@ registerRoute({
|
||||
),
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/polaris/ns/:namespace',
|
||||
sidebar: 'polaris-namespaces',
|
||||
name: 'polaris-namespace',
|
||||
exact: true,
|
||||
component: () => (
|
||||
// Register plugin settings
|
||||
registerPluginSettings('headlamp-polaris-plugin', PolarisSettings, true);
|
||||
|
||||
// Register details view section for supported controller types
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
const supportedKinds = ['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'CronJob'];
|
||||
|
||||
if (!supportedKinds.includes(resource?.kind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PolarisDataProvider>
|
||||
<NamespaceDetailView />
|
||||
<InlineAuditSection resource={resource} />
|
||||
</PolarisDataProvider>
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
registerPluginSettings('polaris', PolarisSettings, true);
|
||||
// Register app bar score badge
|
||||
registerAppBarAction(() => (
|
||||
<PolarisDataProvider>
|
||||
<AppBarScoreBadge />
|
||||
</PolarisDataProvider>
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user