Compare commits

..

3 Commits

Author SHA1 Message Date
Chris Farhood 7d56a903c8 fix(e2e): add waitForSidebar helper and networkidle waits for reliability
Add waitForSidebar helper function with explicit sidebar visibility wait
and networkidle state to ensure page is fully loaded before assertions.
This addresses flaky E2E tests where elements were not consistently
found due to timing issues during page transitions.
2026-05-05 06:50:21 +00:00
Chris Farhood ef7183a2d6 Fix E2E workflow: use pnpm-capable reusable workflow branch
The reusable plugin-e2e.yaml@main lacks pnpm support. Switching to
the PR branch that has pnpm detector, Corepack setup, and pnpm commands.

Will revert to @main once PR #141 merges.

- PRI-619 E2E fix

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 06:10:20 +00:00
Chris Farhood 6d48470691 Add E2E test infrastructure for tns-csi plugin
Scaffolded via e2e-scaffold.sh (proactive improvement).
- playwright.config.ts, e2e/auth.setup.ts, e2e/tns-csi.spec.ts
- scripts/deploy-e2e-headlamp.sh, scripts/teardown-e2e-headlamp.sh
- .github/workflows/e2e.yaml uses reusable workflow
- @playwright/test ^1.58.2 devDep

- PRI-639

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 05:14:51 +00:00
27 changed files with 1032 additions and 919 deletions
+4 -202
View File
@@ -2,210 +2,12 @@ name: CI
on: on:
push: push:
branches: ['**'] branches: [main]
pull_request: pull_request:
branches: [main, dev, uat] branches: [main]
workflow_dispatch: workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
timeout-minutes: 10
container: node:22-slim
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Python
run: apt-get update && apt-get install -y --no-install-recommends python3 python3-yaml
- name: Validate artifacthub-pkg.yml
run: |
python3 - <<'EOF'
import sys, re
try:
import yaml
except ImportError:
print("::warning::PyYAML not available, skipping artifacthub-pkg.yml validation")
sys.exit(0)
try:
with open("artifacthub-pkg.yml") as f:
pkg = yaml.safe_load(f)
except FileNotFoundError:
print("::error::artifacthub-pkg.yml not found")
sys.exit(1)
except yaml.YAMLError as e:
print(f"::error::artifacthub-pkg.yml is invalid YAML: {e}")
sys.exit(1)
errors = []
for field in ["version", "name", "description", "homeURL"]:
if not pkg.get(field):
errors.append(f"Missing required field: {field}")
version = pkg.get("version", "")
if version and not re.match(r'^\d+\.\d+\.\d+$', str(version)):
errors.append(f"version '{version}' is not SemVer (expected X.Y.Z)")
annotations = pkg.get("annotations", {}) or {}
archive_url = annotations.get("headlamp/plugin/archive-url", "")
archive_checksum = annotations.get("headlamp/plugin/archive-checksum", "")
if not archive_url:
errors.append("Missing annotation: headlamp/plugin/archive-url")
if not archive_checksum:
errors.append("Missing annotation: headlamp/plugin/archive-checksum")
elif not re.match(r'^sha256:[0-9a-f]{64}$', str(archive_checksum)):
errors.append(f"archive-checksum has unexpected format: '{archive_checksum}' (expected sha256:<64 hex chars>)")
if errors:
for e in errors:
print(f"::error::{e}")
sys.exit(1)
print(f"artifacthub-pkg.yml valid: name={pkg['name']} version={pkg['version']}")
EOF
- name: Detect package manager
id: pkg-manager
run: |
if [ -f "pnpm-lock.yaml" ]; then
echo "manager=pnpm" >> $GITHUB_OUTPUT
PM=$(python3 -c "import json,sys; d=json.load(open('package.json')); print('true' if d.get('packageManager','').startswith('pnpm@') else 'false')" 2>/dev/null || echo "false")
echo "has_package_manager=$PM" >> $GITHUB_OUTPUT
else
echo "manager=npm" >> $GITHUB_OUTPUT
echo "has_package_manager=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '22'
cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }}
- name: Setup pnpm (via Corepack, reads version from packageManager field)
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'true'
run: |
npm install -g corepack
corepack enable pnpm
corepack install
- name: Setup pnpm (version latest)
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'false'
uses: pnpm/action-setup@v5
with:
run_install: false
version: latest
- name: Get pnpm store directory
id: pnpm-store
if: steps.pkg-manager.outputs.manager == 'pnpm'
run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
if: steps.pkg-manager.outputs.manager == 'pnpm'
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Validate pnpm lockfile freshness
if: steps.pkg-manager.outputs.manager == 'pnpm'
run: |
if [ ! -f "pnpm-lock.yaml" ]; then
echo "No pnpm-lock.yaml found, skipping lockfile freshness check"
exit 0
fi
if ! grep -q 'overrides:' pnpm-lock.yaml 2>/dev/null; then
echo "No overrides section in pnpm-lock.yaml, skipping lockfile freshness check"
exit 0
fi
echo "Detected pnpm-lock.yaml with overrides section. Checking lockfile freshness..."
ERR_FILE=$(mktemp)
if pnpm install --frozen-lockfile 2>&1 | tee "$ERR_FILE"; then
echo "Lockfile is fresh."
else
if grep -q "CONFIG_MISMATCH\|EBADLOCKFILE\|ERR_PNPM_LOCKFILE" "$ERR_FILE"; then
echo ""
echo "::error::pnpm-lock.yaml is out of sync with package.json overrides."
echo "::error::Run 'pnpm install' to regenerate the lockfile and commit the updated pnpm-lock.yaml."
rm -f "$ERR_FILE"
exit 1
fi
rm -f "$ERR_FILE"
echo "::warning::Install failed with a different error. Will retry in the Install dependencies step."
fi
- name: Install dependencies
run: |
max_attempts=3
attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts"
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile && break
else
npm ci && break
fi
if [ $attempt -lt $max_attempts ]; then
echo "::warning::Install step failed on attempt $attempt. Retrying in 5 seconds..."
sleep 5
fi
attempt=$((attempt + 1))
done
if [ $attempt -gt $max_attempts ]; then
echo "::error::Install step failed after $max_attempts attempts."
exit 1
fi
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Lint
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run lint
else
npm run lint
fi
- name: Type-check
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run tsc
else
npm run tsc
fi
- name: Format check
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm run format:check
else
npm run format:check
fi
- name: Run tests
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
pnpm test
else
npm test
fi
- name: Security audit
run: |
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
npx audit-ci --pnpm --audit-level=high --config ./audit-ci.jsonc
else
npx audit-ci --npm --audit-level=high --config ./audit-ci.jsonc
fi
+11 -107
View File
@@ -1,116 +1,20 @@
name: Promotion Gate name: Dual Approval (CTO + QA)
# dev PRs: no gate (engineer self-merges). # Calls the shared dual-approval-check workflow.
# uat PRs: QA approval required. # Passes when both privilegedescalation-cto and privilegedescalation-qa
# main PRs: UAT approval required (uat→main promotions). # have approved the PR. Add "Dual Approval (CTO + QA)" to required_status_checks
# in branch protection to enforce this gate.
on: on:
pull_request_review: pull_request_review:
types: [submitted, dismissed] types: [submitted, dismissed]
pull_request: pull_request:
branches: [uat, main] branches: [main]
types: [opened, reopened, synchronize] types: [opened, reopened, synchronize]
jobs: jobs:
promotion-gate: dual-approval:
name: Promotion Gate uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main
runs-on: ubuntu-latest secrets: inherit
container: ubuntu:latest with:
timeout-minutes: 5 pr_number: ${{ github.event.pull_request.number }}
steps:
- name: Install dependencies
run: apt-get update -qq && apt-get install -y --no-install-recommends ca-certificates curl jq
- name: Check promotion approval
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.base_ref }}
run: |
if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ]; then
echo "::notice::No PR number in context. Skipping promotion gate."
exit 0
fi
echo "Checking promotion gate for PR #${PR_NUMBER} targeting ${BASE_REF} in ${REPO}"
if [ -z "${BASE_REF}" ] && [ -n "${PR_NUMBER}" ] && [ "${PR_NUMBER}" != "null" ]; then
BASE_REF=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Accept: application/json" \
"https://git.farh.net/api/v1/repos/${REPO}/pulls/${PR_NUMBER}" | jq -r '.base.ref')
echo "BASE_REF was empty; resolved from PR #${PR_NUMBER} API: ${BASE_REF}"
fi
# Determine required reviewer based on target branch
case "${BASE_REF}" in
dev)
echo "Target is dev — no review required. Engineers self-merge."
exit 0
;;
uat)
REQUIRED_REVIEWER="pe_regina"
GATE_NAME="QA"
;;
main)
REQUIRED_REVIEWER="pe_regina"
GATE_NAME="QA"
# For plugin repos (Pipeline A), UAT approval is needed for uat→main
# Check if the source branch is uat
SOURCE_REF=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Accept: application/json" \
"https://git.farh.net/api/v1/repos/${REPO}/pulls/${PR_NUMBER}" | jq -r '.head.ref')
if [ "${SOURCE_REF}" = "uat" ]; then
REQUIRED_REVIEWER="pe_patty"
GATE_NAME="UAT"
fi
;;
*)
echo "::notice::Target branch '${BASE_REF}' has no promotion gate configured."
exit 0
;;
esac
echo "Required reviewer: ${REQUIRED_REVIEWER} (${GATE_NAME})"
# For uat→main promotions, pe_patty may not be able to review (bot account).
# Accept pe_nancy (CTO) as a valid alternative reviewer.
ALT_REVIEWER=""
if [ "${REQUIRED_REVIEWER}" = "pe_patty" ]; then
ALT_REVIEWER="pe_nancy"
fi
REVIEWS=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Accept: application/json" \
"https://git.farh.net/api/v1/repos/${REPO}/pulls/${PR_NUMBER}/reviews")
if [ -z "${REVIEWS}" ] || [ "${REVIEWS}" = "null" ]; then
echo "::warning::Could not fetch reviews for PR #${PR_NUMBER}."
exit 1
fi
REVIEWER_APPROVED=$(echo "${REVIEWS}" | jq -r --arg user "${REQUIRED_REVIEWER}" \
'[.[] | select(.user.login == $user)] | last | if .state then .state == "APPROVED" else false end')
echo "${GATE_NAME} (${REQUIRED_REVIEWER}) approved: ${REVIEWER_APPROVED}"
# Fallback: check if CTO approved as alternative for uat→main
if [ "${REVIEWER_APPROVED}" != "true" ] && [ -n "${ALT_REVIEWER}" ]; then
REVIEWER_APPROVED=$(echo "${REVIEWS}" | jq -r --arg user "${ALT_REVIEWER}" \
'[.[] | select(.user.login == $user)] | last | if .state then .state == "APPROVED" else false end')
if [ "${REVIEWER_APPROVED}" = "true" ]; then
echo "CTO (${ALT_REVIEWER}) approved as fallback for UAT gate."
fi
fi
if [ "${REVIEWER_APPROVED}" = "true" ]; then
echo "Promotion gate passed: ${GATE_NAME} has approved."
else
echo "Promotion gate failed: waiting for ${GATE_NAME} approval from ${REQUIRED_REVIEWER}."
exit 1
fi
+23
View File
@@ -0,0 +1,23 @@
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: e2e-${{ github.repository }}
cancel-in-progress: false
jobs:
e2e:
uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@hugh/add-pnpm-support-plugin-e2e
with:
node-version: '22'
headlamp-version: v0.40.1
e2e-namespace: headlamp-dev
+6
View File
@@ -5,3 +5,9 @@ dist/
.env .env
.env.local .env.local
.eslintcache .eslintcache
# E2E
e2e/.auth/
.env.e2e
playwright-report/
test-results/
-4
View File
@@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- **docs: namespace references** — Updated all documentation, README, and ArtifactHub metadata to explicitly reference the `headlamp` namespace instead of generic "controller pod" language. RBAC examples now clearly scope `pods/proxy` access to `kube-system` where the tns-csi controller runs.
## [1.0.0] - 2026-03-24 ## [1.0.0] - 2026-03-24
### Added ### Added
+24
View File
@@ -0,0 +1,24 @@
# Installation Policy
## Approved Installation Method
**The ONLY approved method for installing this plugin is via [Artifact Hub](https://artifacthub.io/) using the Headlamp plugin installer.**
No other installation method is acceptable. This includes but is not limited to:
- Direct installation from GitHub release assets
- Manual npm pack / tarball extraction
- initContainer workarounds that bypass Artifact Hub
- Direct file copy or sidecar injection
## Enforcement
All deployment configurations, CI/CD pipelines, and documentation MUST reference Artifact Hub as the sole plugin distribution channel. Any pull request that introduces an alternative installation method will be rejected.
## Rationale
Artifact Hub provides verified checksums, consistent versioning, and a standard discovery mechanism for the CNCF ecosystem. Bypassing it introduces security and integrity risks.
---
*This policy is set by the CTO and approved by the CEO of Privileged Escalation.*
+4 -12
View File
@@ -63,16 +63,12 @@ config:
pluginsManager: pluginsManager:
sources: sources:
- name: tns-csi - name: tns-csi
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
``` ```
> See [Plugin Installation Policy](https://git.farh.net/privilegedescalation/privilegedescalation.com/wiki/Plugin-Installation-Policy) for approved installation methods.
## RBAC / Security Setup ## RBAC / Security Setup
The plugin reads from the Kubernetes API and the tns-csi controller pod's Prometheus endpoint (deployed in `kube-system`). The Benchmark page additionally creates and deletes Jobs and PVCs. The plugin reads from the Kubernetes API and the tns-csi controller pod's Prometheus endpoint. The Benchmark page additionally creates and deletes Jobs and PVCs.
### Minimal read-only permissions ### Minimal read-only permissions
@@ -94,10 +90,6 @@ rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["pods/log"] resources: ["pods/log"]
verbs: ["get"] verbs: ["get"]
- apiGroups: [""]
resources: ["pods/proxy"]
verbs: ["get"]
resourceNames: ["pods"]
``` ```
### Additional permissions for Benchmark page ### Additional permissions for Benchmark page
@@ -113,13 +105,13 @@ rules:
### Metrics access ### Metrics access
The plugin fetches Prometheus metrics from the tns-csi controller pod via the Kubernetes pod proxy sub-resource in `kube-system`. Grant `get` on `pods/proxy` scoped to `kube-system`: The plugin fetches Prometheus metrics from the tns-csi controller pod via the Kubernetes pod proxy sub-resource. Grant `get` on `pods/proxy` in `kube-system`:
```yaml ```yaml
- apiGroups: [""] - apiGroups: [""]
resources: ["pods/proxy"] resources: ["pods/proxy"]
verbs: ["get"] verbs: ["get"]
# Scope to kube-system where the tns-csi controller runs # Optionally scope to the controller pod namespace
``` ```
Apply the role and bind it to your Headlamp service account with a ClusterRoleBinding. Apply the role and bind it to your Headlamp service account with a ClusterRoleBinding.
+3 -3
View File
@@ -91,7 +91,7 @@ metadata:
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system # adjust to your Headlamp namespace
roleRef: roleRef:
kind: ClusterRole kind: ClusterRole
name: headlamp-tns-csi-reader name: headlamp-tns-csi-reader
@@ -143,7 +143,7 @@ The Kubernetes API server performs the pod proxy hop, so policies should permit
### Service Account (Default) ### Service Account (Default)
Headlamp runs with a dedicated service account (`headlamp` in the namespace where Headlamp is installed). All users share the same RBAC permissions. Headlamp runs with a dedicated service account (`headlamp` in `kube-system`). All users share the same RBAC permissions.
**Security Considerations:** **Security Considerations:**
- All users have identical access to plugin functionality including Benchmark - All users have identical access to plugin functionality including Benchmark
@@ -223,7 +223,7 @@ All API requests are logged in Kubernetes API audit logs (if enabled). Pod proxy
"verb": "get", "verb": "get",
"requestURI": "/api/v1/namespaces/kube-system/pods/<controller-pod>/proxy/metrics", "requestURI": "/api/v1/namespaces/kube-system/pods/<controller-pod>/proxy/metrics",
"user": { "user": {
"username": "system:serviceaccount:<your-namespace>:headlamp" "username": "system:serviceaccount:kube-system:headlamp"
} }
} }
``` ```
+3 -3
View File
@@ -1,4 +1,4 @@
version: "1.0.3" version: "1.0.0"
name: headlamp-tns-csi-plugin name: headlamp-tns-csi-plugin
displayName: TrueNAS CSI (tns-csi) displayName: TrueNAS CSI (tns-csi)
description: >- description: >-
@@ -63,7 +63,7 @@ changes:
description: "GitHub App token secrets passed to release workflow" description: "GitHub App token secrets passed to release workflow"
annotations: annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.3/headlamp-tns-csi-plugin-1.0.3.tar.gz" headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz"
headlamp/plugin/archive-checksum: sha256:8a032919de65f9ed45a06f4110083cceb11b91625d97f7b49075adecf38e3adc headlamp/plugin/archive-checksum: sha256:e38846ceb17a79438f8aecc50f22920b0efa7456f3ebb3e628d89856af83ad01
headlamp/plugin/version-compat: ">=0.20.0" headlamp/plugin/version-compat: ">=0.20.0"
headlamp/plugin/distro-compat: "in-cluster,web,app" headlamp/plugin/distro-compat: "in-cluster,web,app"
-20
View File
@@ -1,20 +0,0 @@
{
// Allowlist for inherited dev-dependency CVEs from @kinvolk/headlamp-plugin
// CTO decision (PRI-854): these high-severity vulns are dev/build-time only,
// trace to @kinvolk/headlamp-plugin transitive deps (Picomatch, Vite, lodash),
// and do NOT ship in production plugin artifacts.
"allowlist": [
{
"id": "GHSA-hhpm-516h-p3p6",
"reason": "Picomatch ReDoS: devDependency only, does not ship in production plugin bundle"
},
{
"id": "GHSA-36xf-7xpp-53w5",
"reason": "Vite arbitrary file read: devDependency only, does not ship in production plugin bundle"
},
{
"id": "GHSA-jf8v-p3pp-93qh",
"reason": "lodash code injection via _.template: devDependency only, does not ship in production plugin bundle"
}
]
}
+1 -1
View File
@@ -67,7 +67,7 @@ Welcome to the Headlamp TNS-CSI Plugin documentation.
## External Links ## External Links
- **[GitHub Repository](https://github.com/privilegedescalation/headlamp-tns-csi-plugin)** - **[GitHub Repository](https://github.com/privilegedescalation/headlamp-tns-csi-plugin)**
- **[Artifact Hub](https://artifacthub.io/packages/headlamp/tns-csi/headlamp-tns-csi-plugin)** - **[Artifact Hub](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin)**
- **[tns-csi Driver](https://github.com/fenio/tns-csi)** - **[tns-csi Driver](https://github.com/fenio/tns-csi)**
- **[kbench](https://github.com/longhorn/kbench)** - **[kbench](https://github.com/longhorn/kbench)**
- **[Headlamp](https://headlamp.dev/)** - **[Headlamp](https://headlamp.dev/)**
+1 -1
View File
@@ -28,7 +28,7 @@ The TNS-CSI plugin is a single-page React application bundled as a Headlamp plug
│ HTTPS │ HTTPS
┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ Headlamp Pod (headlamp namespace) │ │ Headlamp Pod (kube-system) │
│ │ │ │
│ Headlamp UI server + API proxy │ │ Headlamp UI server + API proxy │
│ (forwards requests using service account token │ │ (forwards requests using service account token │
+10 -10
View File
@@ -9,11 +9,11 @@ helm repo add headlamp https://headlamp-k8s.github.io/headlamp/
helm repo update helm repo update
helm install headlamp headlamp/headlamp \ helm install headlamp headlamp/headlamp \
--namespace <your-namespace> \ --namespace kube-system \
--create-namespace \ --create-namespace \
--set config.pluginsDir=/headlamp/plugins \ --set config.pluginsDir=/headlamp/plugins \
--set pluginsManager.sources[0].name=tns-csi \ --set pluginsManager.sources[0].name=tns-csi \
--set pluginsManager.sources[0].url=https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz --set pluginsManager.sources[0].url=https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
``` ```
## Complete values.yaml Example ## Complete values.yaml Example
@@ -27,7 +27,7 @@ config:
pluginsManager: pluginsManager:
sources: sources:
- name: tns-csi - name: tns-csi
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
serviceAccount: serviceAccount:
name: headlamp name: headlamp
@@ -44,7 +44,7 @@ Apply:
```bash ```bash
helm install headlamp headlamp/headlamp \ helm install headlamp headlamp/headlamp \
--namespace <your-namespace> \ --namespace kube-system \
-f headlamp-values.yaml -f headlamp-values.yaml
``` ```
@@ -55,7 +55,7 @@ apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository kind: HelmRepository
metadata: metadata:
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
spec: spec:
interval: 12h interval: 12h
url: https://headlamp-k8s.github.io/headlamp/ url: https://headlamp-k8s.github.io/headlamp/
@@ -64,7 +64,7 @@ apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease kind: HelmRelease
metadata: metadata:
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
spec: spec:
interval: 1h interval: 1h
chart: chart:
@@ -74,14 +74,14 @@ spec:
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
values: values:
config: config:
pluginsDir: /headlamp/plugins pluginsDir: /headlamp/plugins
pluginsManager: pluginsManager:
sources: sources:
- name: tns-csi - name: tns-csi
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
``` ```
## RBAC Manifest (Apply Separately) ## RBAC Manifest (Apply Separately)
@@ -122,7 +122,7 @@ metadata:
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
roleRef: roleRef:
kind: ClusterRole kind: ClusterRole
name: headlamp-tns-csi-reader name: headlamp-tns-csi-reader
@@ -136,7 +136,7 @@ To upgrade to a new plugin version, update the `url` in your values and apply:
```bash ```bash
helm upgrade headlamp headlamp/headlamp \ helm upgrade headlamp headlamp/headlamp \
--namespace <your-namespace> \ --namespace kube-system \
-f headlamp-values.yaml -f headlamp-values.yaml
``` ```
+11 -8
View File
@@ -22,7 +22,7 @@ config:
pluginsManager: pluginsManager:
sources: sources:
- name: tns-csi - name: tns-csi
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
``` ```
**Via FluxCD HelmRelease:** **Via FluxCD HelmRelease:**
@@ -32,7 +32,7 @@ apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease kind: HelmRelease
metadata: metadata:
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
spec: spec:
chart: chart:
spec: spec:
@@ -46,7 +46,7 @@ spec:
pluginsManager: pluginsManager:
sources: sources:
- name: tns-csi - name: tns-csi
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
``` ```
### Method 2: Manual Tarball Install ### Method 2: Manual Tarball Install
@@ -55,10 +55,13 @@ Download and extract the plugin directly:
```bash ```bash
# Download the release tarball # Download the release tarball
wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
# Verify the checksum
echo "14a3e8c13d0b894a41aa1cfccbcb1f6af09dcbb8fd95c7040a540987ea2096a7 tns-csi-0.2.4.tar.gz" | sha256sum --check
# Extract into your Headlamp plugins directory # Extract into your Headlamp plugins directory
tar xzf tns-csi-1.0.0.tar.gz -C /headlamp/plugins/ tar xzf tns-csi-0.2.4.tar.gz -C /headlamp/plugins/
``` ```
The plugin directory should appear as `/headlamp/plugins/tns-csi/`. The plugin directory should appear as `/headlamp/plugins/tns-csi/`.
@@ -78,7 +81,7 @@ initContainers:
- -c - -c
- | - |
wget -O /tmp/plugin.tar.gz \ wget -O /tmp/plugin.tar.gz \
https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
tar xzf /tmp/plugin.tar.gz -C /headlamp/plugins/ tar xzf /tmp/plugin.tar.gz -C /headlamp/plugins/
volumeMounts: volumeMounts:
- name: plugins - name: plugins
@@ -95,10 +98,10 @@ cd headlamp-tns-csi-plugin
npm install npm install
npm run build npm run build
npm run package npm run package
# Produces tns-csi-<version>.tar.gz # Produces tns-csi-0.2.4.tar.gz
# Extract to your Headlamp plugins directory # Extract to your Headlamp plugins directory
tar xzf tns-csi-<version>.tar.gz -C /headlamp/plugins/ tar xzf tns-csi-0.2.4.tar.gz -C /headlamp/plugins/
``` ```
Or use `headlamp-plugin extract` for automatic placement: Or use `headlamp-plugin extract` for automatic placement:
+4 -4
View File
@@ -28,13 +28,13 @@ config:
pluginsManager: pluginsManager:
sources: sources:
- name: tns-csi - name: tns-csi
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v1.0.0/tns-csi-1.0.0.tar.gz url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.4/tns-csi-0.2.4.tar.gz
``` ```
Then upgrade your Headlamp release: Then upgrade your Headlamp release:
```bash ```bash
helm upgrade headlamp headlamp/headlamp -f values.yaml -n <your-namespace> helm upgrade headlamp headlamp/headlamp -f values.yaml -n kube-system
``` ```
## Step 2: Configure RBAC ## Step 2: Configure RBAC
@@ -70,7 +70,7 @@ metadata:
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
roleRef: roleRef:
kind: ClusterRole kind: ClusterRole
name: headlamp-tns-csi-reader name: headlamp-tns-csi-reader
@@ -78,7 +78,7 @@ roleRef:
EOF EOF
``` ```
Adjust `name: headlamp` and `namespace: <your-namespace>` to match your Headlamp service account. Adjust `name: headlamp` and `namespace: kube-system` to match your Headlamp service account.
## Step 3: Verify ## Step 3: Verify
+2 -2
View File
@@ -77,7 +77,7 @@ If a page shows a loading spinner indefinitely:
1. **Check browser console** for errors (F12 → Console) 1. **Check browser console** for errors (F12 → Console)
2. **Check network tab** for failed API requests (look for 403, 404, 500) 2. **Check network tab** for failed API requests (look for 403, 404, 500)
3. **Check Headlamp pod logs**: `kubectl logs -n <your-namespace> -l app.kubernetes.io/name=headlamp` 3. **Check Headlamp pod logs**: `kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp`
4. **Try refreshing** — the watch connection may have been interrupted 4. **Try refreshing** — the watch connection may have been interrupted
## Common API Errors ## Common API Errors
@@ -102,7 +102,7 @@ Look for errors related to `tns-csi`, `headlamp-plugin`, or Kubernetes API paths
**Headlamp pod logs:** **Headlamp pod logs:**
```bash ```bash
kubectl logs -n <your-namespace> -l app.kubernetes.io/name=headlamp --tail=100 kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp --tail=100
``` ```
**tns-csi controller logs:** **tns-csi controller logs:**
+2 -2
View File
@@ -8,10 +8,10 @@ The Benchmark page requires permissions to create and delete Jobs and PVCs:
```bash ```bash
kubectl auth can-i create jobs -n <benchmark-namespace> \ kubectl auth can-i create jobs -n <benchmark-namespace> \
--as=system:serviceaccount:<your-namespace>:headlamp --as=system:serviceaccount:kube-system:headlamp
kubectl auth can-i create persistentvolumeclaims -n <benchmark-namespace> \ kubectl auth can-i create persistentvolumeclaims -n <benchmark-namespace> \
--as=system:serviceaccount:<your-namespace>:headlamp --as=system:serviceaccount:kube-system:headlamp
``` ```
Apply the additional permissions if missing — see [RBAC Issues](rbac.md) or [SECURITY.md](../../SECURITY.md). Apply the additional permissions if missing — see [RBAC Issues](rbac.md) or [SECURITY.md](../../SECURITY.md).
+1 -1
View File
@@ -47,7 +47,7 @@ This requires `get` on `pods/proxy` in `kube-system`:
```bash ```bash
kubectl auth can-i get pods/proxy \ kubectl auth can-i get pods/proxy \
-n kube-system \ -n kube-system \
--as=system:serviceaccount:<your-namespace>:headlamp --as=system:serviceaccount:kube-system:headlamp
``` ```
### 5. Network Policies ### 5. Network Policies
+3 -3
View File
@@ -11,16 +11,16 @@ Use `kubectl auth can-i` to check specific permissions:
```bash ```bash
# Check if the Headlamp service account can list StorageClasses # Check if the Headlamp service account can list StorageClasses
kubectl auth can-i list storageclasses \ kubectl auth can-i list storageclasses \
--as=system:serviceaccount:<your-namespace>:headlamp --as=system:serviceaccount:kube-system:headlamp
# Check pod proxy access (for metrics) # Check pod proxy access (for metrics)
kubectl auth can-i get pods/proxy \ kubectl auth can-i get pods/proxy \
-n kube-system \ -n kube-system \
--as=system:serviceaccount:<your-namespace>:headlamp --as=system:serviceaccount:kube-system:headlamp
# Check snapshot access # Check snapshot access
kubectl auth can-i list volumesnapshots \ kubectl auth can-i list volumesnapshots \
--as=system:serviceaccount:<your-namespace>:headlamp --as=system:serviceaccount:kube-system:headlamp
``` ```
### Applying the Required RBAC ### Applying the Required RBAC
+2 -2
View File
@@ -47,7 +47,7 @@ metadata:
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: headlamp # adjust to your Headlamp service account name name: headlamp # adjust to your Headlamp service account name
namespace: <your-namespace> namespace: kube-system # adjust to your Headlamp namespace
roleRef: roleRef:
kind: ClusterRole kind: ClusterRole
name: headlamp-tns-csi-reader name: headlamp-tns-csi-reader
@@ -99,7 +99,7 @@ metadata:
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: headlamp name: headlamp
namespace: <your-namespace> namespace: kube-system
roleRef: roleRef:
kind: Role kind: Role
name: headlamp-tns-csi-benchmark name: headlamp-tns-csi-benchmark
+69
View File
@@ -0,0 +1,69 @@
import { test as setup, expect, Page } from '@playwright/test';
const AUTH_STATE_PATH = 'e2e/.auth/state.json';
async function authenticateWithOIDC(page: Page, username: string, password: string): Promise<void> {
await page.goto('/');
await page.waitForURL('**/login');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: /sign in/i }).click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
await popup.waitForLoadState('networkidle');
const usernameField = popup.getByRole('textbox', { name: /email or username/i });
await usernameField.waitFor({ state: 'visible', timeout: 15_000 });
await usernameField.fill(username);
await popup.getByRole('button', { name: /log in/i }).click();
await popup.waitForLoadState('networkidle');
const passwordField = popup.getByRole('textbox', { name: /password/i });
await passwordField.waitFor({ state: 'visible', timeout: 15_000 });
await passwordField.fill(password);
await popup.getByRole('button', { name: /continue|log in/i }).click();
await popup.waitForEvent('close', { timeout: 15_000 });
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
async function authenticateWithToken(page: Page, token: string): Promise<void> {
await page.goto('/');
await page.waitForURL(/\/(login|token)$/);
if (page.url().includes('/login')) {
const useTokenBtn = page.getByRole('button', { name: /use a token/i });
await useTokenBtn.waitFor({ state: 'visible', timeout: 15_000 });
await useTokenBtn.click();
await page.waitForURL('**/token');
}
await page.getByRole('textbox', { name: /id token/i }).fill(token);
await page.getByRole('button', { name: /authenticate/i }).click();
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
setup('authenticate with Headlamp', async ({ page }) => {
const username = process.env.AUTHENTIK_USERNAME;
const password = process.env.AUTHENTIK_PASSWORD;
const token = process.env.HEADLAMP_TOKEN;
if (username && password) {
await authenticateWithOIDC(page, username, password);
} else if (token) {
await authenticateWithToken(page, token);
} else {
throw new Error(
'Set AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD for OIDC auth, or HEADLAMP_TOKEN for token auth'
);
}
await page.context().storageState({ path: AUTH_STATE_PATH });
});
+50
View File
@@ -0,0 +1,50 @@
import { test, expect } from '@playwright/test';
async function waitForSidebar(page: import('@playwright/test').Page) {
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await page.waitForLoadState('networkidle');
return sidebar;
}
test.describe('TNS CSI plugin smoke tests', () => {
test('sidebar contains TNS CSI entry', async ({ page }) => {
await page.goto('/');
const sidebar = await waitForSidebar(page);
await expect(sidebar.getByRole('button', { name: /tns.csi/i })).toBeVisible();
});
test('TNS CSI sidebar entry navigates to TNS CSI view', async ({ page }) => {
await page.goto('/');
const sidebar = await waitForSidebar(page);
const entry = sidebar.getByRole('button', { name: /tns.csi/i });
await expect(entry).toBeVisible();
await entry.click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/tns-csi/);
await expect(page.getByRole('heading', { name: /tns.csi.*overview/i })).toBeVisible();
});
test('TNS CSI page renders content', async ({ page }) => {
await page.goto('/c/main/tns-csi');
await waitForSidebar(page);
await expect(page.getByRole('heading', { name: /tns.csi.*overview/i })).toBeVisible({
timeout: 15_000,
});
const hasTable = await page.locator('table').first().isVisible().catch(() => false);
const hasContent = await page.locator('[class*="Mui"]').first().isVisible().catch(() => false);
expect(hasTable || hasContent).toBe(true);
});
test('plugin settings page shows TNS CSI plugin entry', async ({ page }) => {
await page.goto('/settings/plugins');
await page.waitForLoadState('networkidle');
const pluginEntry = page.locator('text=/tns.csi/i').first();
await expect(pluginEntry).toBeVisible({ timeout: 30_000 });
});
});
+7 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "tns-csi", "name": "tns-csi",
"version": "1.0.3", "version": "1.0.0",
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking", "description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -23,7 +23,9 @@
"format": "prettier --write src/", "format": "prettier --write src/",
"format:check": "prettier --check src/", "format:check": "prettier --check src/",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest" "test:watch": "vitest",
"e2e": "playwright test",
"e2e:headed": "playwright test --headed"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.0.0", "react": "^18.0.0",
@@ -46,12 +48,12 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vitest": "^3.2.4" "vitest": "^3.2.4",
"@playwright/test": "^1.58.2"
}, },
"overrides": { "overrides": {
"tar": "^7.5.11", "tar": "^7.5.11",
"undici": "^7.24.3", "undici": "^7.24.3",
"vite": ">=6.4.2", "vite": ">=6.4.2"
"elliptic": ">=6.6.1"
} }
} }
+27
View File
@@ -0,0 +1,27 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30_000,
expect: { timeout: 10_000 },
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
reporter: 'list',
use: {
baseURL: process.env.HEADLAMP_URL || (() => { throw new Error('HEADLAMP_URL is required — run scripts/deploy-e2e-headlamp.sh first'); })(),
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/, timeout: 60_000 },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'e2e/.auth/state.json',
},
dependencies: ['setup'],
},
],
});
+567 -529
View File
File diff suppressed because it is too large Load Diff
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DIST_DIR="$REPO_ROOT/dist"
E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}"
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}"
HEADLAMP_VERSION="${HEADLAMP_VERSION:-latest}"
if [ ! -d "$DIST_DIR" ]; then
echo "ERROR: dist/ not found. Run 'pnpm build' first." >&2
exit 1
fi
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
exit 1
fi
echo "=== E2E Headlamp Deployment ==="
echo " Image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}"
echo " Namespace: $E2E_NAMESPACE"
echo " Release: $E2E_RELEASE"
echo ""
echo "Creating ConfigMap with plugin files..."
kubectl delete configmap headlamp-tns-csi-plugin -n "$E2E_NAMESPACE" --ignore-not-found
kubectl create configmap headlamp-tns-csi-plugin -n "$E2E_NAMESPACE" --from-file="$DIST_DIR" --from-file=package.json="$REPO_ROOT/package.json"
echo ""
echo "Removing any existing E2E deployment (clean-start)..."
kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait
kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait
kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found --wait
echo ""
echo "Deploying Headlamp E2E instance..."
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
template:
metadata:
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
serviceAccountName: ${E2E_RELEASE}
automountServiceAccountToken: true
securityContext: {}
containers:
- name: headlamp
image: ghcr.io/headlamp-k8s/headlamp:${HEADLAMP_VERSION}
imagePullPolicy: IfNotPresent
securityContext:
runAsNonRoot: true
privileged: false
runAsUser: 100
runAsGroup: 101
args:
- "-in-cluster"
- "-in-cluster-context-name=main"
- "-plugins-dir=/headlamp/plugins"
ports:
- name: http
containerPort: 4466
protocol: TCP
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: tns-csi-plugin
mountPath: /headlamp/plugins/headlamp-tns-csi
readOnly: true
volumes:
- name: tns-csi-plugin
configMap:
name: headlamp-tns-csi-plugin
---
apiVersion: v1
kind: Service
metadata:
name: ${E2E_RELEASE}
namespace: ${E2E_NAMESPACE}
labels:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
EOF
echo "Waiting for rollout..."
kubectl rollout status "deployment/${E2E_RELEASE}" -n "$E2E_NAMESPACE" --timeout=120s
SVC_URL="http://${E2E_RELEASE}.${E2E_NAMESPACE}.svc.cluster.local"
echo ""
echo "Waiting for ${SVC_URL} to be reachable..."
ATTEMPTS=0
MAX_ATTEMPTS=24
until curl -sf --max-time 5 "${SVC_URL}" -o /dev/null 2>/dev/null; do
ATTEMPTS=$((ATTEMPTS + 1))
if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
echo "ERROR: ${SVC_URL} not reachable after $((MAX_ATTEMPTS * 5))s" >&2
exit 1
fi
echo " [${ATTEMPTS}/${MAX_ATTEMPTS}] not yet reachable, retrying in 5s..."
sleep 5
done
echo ""
echo "E2E Headlamp is ready at: ${SVC_URL}"
echo ""
echo "Creating service account token for E2E auth..."
kubectl create serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
TOKEN=$(kubectl create token headlamp-e2e-test -n "$E2E_NAMESPACE" --duration=1h 2>/dev/null || echo "")
if [ -n "$TOKEN" ]; then
echo "HEADLAMP_URL=${SVC_URL}" > "$REPO_ROOT/.env.e2e"
echo "HEADLAMP_TOKEN=${TOKEN}" >> "$REPO_ROOT/.env.e2e"
echo "Wrote .env.e2e with HEADLAMP_URL and HEADLAMP_TOKEN"
else
echo " WARNING: Could not generate token."
fi
echo ""
echo "E2E deployment complete."
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
E2E_NAMESPACE="${E2E_NAMESPACE:-headlamp-dev}"
E2E_RELEASE="${E2E_RELEASE:-headlamp-e2e}"
echo "=== E2E Headlamp Teardown ==="
echo " Namespace: $E2E_NAMESPACE"
echo " Release: $E2E_RELEASE"
echo "Removing Headlamp Deployment, Service, and ServiceAccount..."
kubectl delete deployment "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found
kubectl delete service "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found
kubectl delete serviceaccount "${E2E_RELEASE}" -n "$E2E_NAMESPACE" --ignore-not-found
echo "Cleaning up ConfigMap..."
kubectl delete configmap headlamp-tns-csi-plugin -n "$E2E_NAMESPACE" --ignore-not-found
echo "Cleaning up test service account..."
kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found
if [ -f "$REPO_ROOT/.env.e2e" ]; then
rm "$REPO_ROOT/.env.e2e"
echo "Removed .env.e2e"
fi
echo ""
echo "E2E teardown complete."