diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 4811050..c70e4ae 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -11,7 +11,7 @@ permissions: contents: read env: - E2E_NAMESPACE: headlamp-e2e + E2E_NAMESPACE: default E2E_RELEASE: headlamp-e2e jobs: diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml index 5c571f3..730cabc 100644 --- a/artifacthub-pkg.yml +++ b/artifacthub-pkg.yml @@ -11,6 +11,7 @@ description: >- `polaris-dashboard` service in the `polaris` namespace. license: Apache-2.0 homeURL: "https://github.com/privilegedescalation/headlamp-polaris-plugin" +appVersion: "5.0" category: security keywords: - polaris @@ -24,6 +25,43 @@ links: url: "https://github.com/privilegedescalation/headlamp-polaris-plugin" - name: Polaris url: "https://polaris.docs.fairwinds.com/" +install: | + ## Installation + + ### Prerequisites + + 1. [Headlamp](https://headlamp.dev) v0.26.0 or later + 2. [Fairwinds Polaris](https://polaris.docs.fairwinds.com/) installed and the dashboard running in your cluster + + ### Install via Headlamp Plugin Catalog + + 1. Open Headlamp and navigate to **Settings → Plugin Catalog** + 2. Search for **"Polaris"** + 3. Click **Install** and restart Headlamp when prompted + + The plugin is sourced directly from [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp/headlamp-polaris). + + ## Usage + + After installation, the Polaris plugin adds: + - A **cluster score badge** in the Headlamp app bar + - A **Polaris** section in the sidebar with the full dashboard and namespace drill-downs + - An **inline audit panel** on Deployment, StatefulSet, DaemonSet, Job, and CronJob detail pages + + For more information, see the [README](https://github.com/privilegedescalation/headlamp-polaris-plugin/blob/main/README.md). +changes: + - kind: added + description: ExemptionManager — apply Polaris annotation exemptions directly from the resource detail page + - kind: added + description: Inline audit section on workload detail pages with per-check pass/fail breakdown + - kind: added + description: Namespace drill-down view with per-resource score list and filterable check table + - kind: added + description: App bar score badge showing overall cluster Polaris score + - kind: added + description: PolarisSettings page for configuring dashboard refresh interval + - kind: changed + description: Stable public API — routes, sidebar entries, settings schema, and app bar action are frozen maintainers: - name: privilegedescalation email: "chris@farhood.org" @@ -31,4 +69,4 @@ annotations: headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-polaris-plugin/releases/download/v0.7.2/headlamp-polaris-0.7.2.tar.gz" headlamp/plugin/version-compat: ">=0.26" headlamp/plugin/archive-checksum: sha256:ce75449a05d3d3dd3c546db36a2257fae3e4601e466108182e64310a1a4f6d71 - headlamp/plugin/distro-compat: in-cluster + headlamp/plugin/distro-compat: "in-cluster,web,desktop" diff --git a/deployment/e2e-ci-runner-rbac.yaml b/deployment/e2e-ci-runner-rbac.yaml index 511d6dd..d806f6b 100644 --- a/deployment/e2e-ci-runner-rbac.yaml +++ b/deployment/e2e-ci-runner-rbac.yaml @@ -2,20 +2,19 @@ # RBAC for the GitHub Actions CI runner to manage the E2E Headlamp instance. # CI-only test fixture — NOT for production use. # -# Grants the ARC runner service account permissions in the headlamp-e2e -# namespace to deploy and tear down a dedicated Headlamp instance via Helm. +# Grants the ARC runner service account permissions in the default namespace +# to deploy and tear down a dedicated Headlamp instance via Helm. +# E2E resources run in `default` — nothing persists beyond a test run. # -# No kube-system access needed — E2E tests use a separate namespace. # Plugin is loaded via ConfigMap volume mount — no custom Docker images. # # Prerequisites: -# kubectl create namespace headlamp-e2e # kubectl apply -f deployment/e2e-ci-runner-rbac.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: e2e-ci-runner - namespace: headlamp-e2e + namespace: default rules: # Helm needs to manage these resources for the Headlamp chart - apiGroups: ["apps"] @@ -36,7 +35,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: e2e-ci-runner-binding - namespace: headlamp-e2e + namespace: default subjects: - kind: ServiceAccount name: runners-privilegedescalation-gha-rs-no-permission @@ -45,29 +44,3 @@ roleRef: kind: Role name: e2e-ci-runner apiGroup: rbac.authorization.k8s.io ---- -# ClusterRole to allow the runner SA to verify the headlamp-e2e namespace -# exists before attempting namespaced operations. kubectl get namespace is a -# cluster-scoped operation not coverable by a namespaced Role. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: e2e-ci-namespace-reader -rules: - - apiGroups: [""] - resources: ["namespaces"] - verbs: ["get"] - resourceNames: ["headlamp-e2e"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: e2e-ci-namespace-reader-binding -subjects: - - kind: ServiceAccount - name: runners-privilegedescalation-gha-rs-no-permission - namespace: arc-runners -roleRef: - kind: ClusterRole - name: e2e-ci-namespace-reader - apiGroup: rbac.authorization.k8s.io diff --git a/deployment/headlamp-e2e-values.yaml b/deployment/headlamp-e2e-values.yaml index 1c968a2..37d6539 100644 --- a/deployment/headlamp-e2e-values.yaml +++ b/deployment/headlamp-e2e-values.yaml @@ -7,7 +7,7 @@ # # Usage: # helm install headlamp-e2e headlamp/headlamp \ -# -n headlamp-e2e --create-namespace \ +# -n default \ # -f deployment/headlamp-e2e-values.yaml \ # --set image.registry=ghcr.io \ # --set image.repository=headlamp-k8s/headlamp \ diff --git a/e2e/README.md b/e2e/README.md index 8b7e446..b93d390 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -275,7 +275,6 @@ No custom Docker images, no PVCs, no kubectl exec/cp, no patching of existing de One-time setup by a cluster admin: ```bash -kubectl create namespace headlamp-e2e kubectl apply -f deployment/e2e-ci-runner-rbac.yaml ``` diff --git a/package-lock.json b/package-lock.json index 887daa4..1523bc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@testing-library/user-event": "^14.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^3.2.4", "jsdom": "^24.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -37,6 +38,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.7.2", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", @@ -431,6 +446,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", @@ -4846,6 +4871,40 @@ "vitest": "3.2.4" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -5651,6 +5710,35 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", diff --git a/package.json b/package.json index ad2c18e..6e657fb 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@testing-library/user-event": "^14.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^3.2.4", "jsdom": "^24.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/scripts/deploy-e2e-headlamp.sh b/scripts/deploy-e2e-headlamp.sh index 5599aa1..08512cc 100755 --- a/scripts/deploy-e2e-headlamp.sh +++ b/scripts/deploy-e2e-headlamp.sh @@ -5,14 +5,17 @@ # a ConfigMap volume mount. No custom Docker images — the plugin is built # in CI and injected as a ConfigMap. # +# E2E resources are deployed to the `default` namespace. Nothing persists +# beyond the test run — teardown cleans up all created resources. +# # Prerequisites: # - Plugin built (dist/ exists with plugin-main.js + package.json) # - kubectl configured with cluster access # - Helm 3 installed -# - E2E namespace pre-created by cluster admin (see deployment/e2e-ci-runner-rbac.yaml) +# - RBAC applied: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml # # Environment: -# E2E_NAMESPACE — namespace for E2E Headlamp (default: headlamp-e2e) +# E2E_NAMESPACE — namespace for E2E Headlamp (default: default) # E2E_RELEASE — Helm release name (default: headlamp-e2e) # HEADLAMP_VERSION — Headlamp image tag (default: latest) set -euo pipefail @@ -20,7 +23,7 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" DIST_DIR="$REPO_ROOT/dist" -E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-e2e}" +E2E_NAMESPACE="${E2E_NAMESPACE:-default}" E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" HEADLAMP_VERSION="${HEADLAMP_VERSION:-latest}" @@ -29,21 +32,19 @@ if [ ! -d "$DIST_DIR" ]; then exit 1 fi +# --- Preflight: verify RBAC before touching the cluster --- +echo "Checking RBAC permissions in namespace '${E2E_NAMESPACE}'..." +if ! kubectl auth can-i delete configmaps -n "$E2E_NAMESPACE" --quiet 2>/dev/null; then + echo "ERROR: Missing RBAC — cannot delete configmaps in namespace '${E2E_NAMESPACE}'." >&2 + echo " Apply RBAC first: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml" >&2 + exit 1 +fi + echo "=== E2E Headlamp Deployment ===" echo " Image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}" echo " Namespace: $E2E_NAMESPACE" echo " Release: $E2E_RELEASE" -# --- Verify namespace exists (must be pre-created by cluster admin) --- -echo "" -echo "Verifying namespace ${E2E_NAMESPACE} exists..." -if ! kubectl get namespace "$E2E_NAMESPACE" >/dev/null 2>&1; then - echo "ERROR: Namespace ${E2E_NAMESPACE} does not exist." >&2 - echo "A cluster admin must create it first: kubectl create namespace ${E2E_NAMESPACE}" >&2 - echo "Then apply RBAC: kubectl apply -f deployment/e2e-ci-runner-rbac.yaml" >&2 - exit 1 -fi - # --- Create ConfigMap from built plugin --- echo "" echo "Creating ConfigMap with plugin files..." diff --git a/scripts/teardown-e2e-headlamp.sh b/scripts/teardown-e2e-headlamp.sh index d1a0025..b936851 100755 --- a/scripts/teardown-e2e-headlamp.sh +++ b/scripts/teardown-e2e-headlamp.sh @@ -4,21 +4,15 @@ # Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh. # # Environment: -# E2E_NAMESPACE — namespace to clean up (default: headlamp-e2e) +# E2E_NAMESPACE — namespace to clean up (default: default) # E2E_RELEASE — Helm release to uninstall (default: headlamp-e2e) set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-e2e}" +E2E_NAMESPACE="${E2E_NAMESPACE:-default}" E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}" -# Exit early if the namespace does not exist — nothing to tear down. -if ! kubectl get namespace "$E2E_NAMESPACE" >/dev/null 2>&1; then - echo "Namespace $E2E_NAMESPACE does not exist, nothing to tear down." - exit 0 -fi - echo "=== E2E Headlamp Teardown ===" echo " Namespace: $E2E_NAMESPACE" echo " Release: $E2E_RELEASE" @@ -32,9 +26,6 @@ kubectl delete configmap headlamp-polaris-plugin -n "$E2E_NAMESPACE" --ignore-no echo "Cleaning up service account..." kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found -# Note: namespace is NOT deleted — it is managed by a cluster admin. -# The runner SA only has namespace-scoped permissions (see deployment/e2e-ci-runner-rbac.yaml). - # Clean up local env file rm -f "$REPO_ROOT/.env.e2e" diff --git a/src/components/ExemptionManager.test.tsx b/src/components/ExemptionManager.test.tsx new file mode 100644 index 0000000..c164549 --- /dev/null +++ b/src/components/ExemptionManager.test.tsx @@ -0,0 +1,432 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { makeResult } from '../test-utils'; + +const { mockApiRequest } = vi.hoisted(() => ({ mockApiRequest: vi.fn() })); + +vi.mock('@kinvolk/headlamp-plugin/lib', () => ({ + ApiProxy: { request: mockApiRequest }, +})); + +vi.mock('@mui/material/styles', () => ({ + useTheme: () => ({ + palette: { + primary: { main: '#1976d2', contrastText: '#fff' }, + action: { disabledBackground: '#e0e0e0', disabled: '#9e9e9e' }, + divider: '#e0e0e0', + }, + }), +})); + +vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({ + SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => ( +