From 7e942c7d7b08ed3d41c942d05ea7e8405e026f9a Mon Sep 17 00:00:00 2001 From: Aaron Lippold Date: Tue, 30 Dec 2025 11:52:44 -0500 Subject: [PATCH 01/13] bd sync: 2025-12-30 11:52:44 --- .beads/metadata.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .beads/metadata.json diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..c787975 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} \ No newline at end of file From 0c129c965579e1b5ff31bebd42b0eac55c7909ad Mon Sep 17 00:00:00 2001 From: Nicholas Shumar Date: Thu, 26 Feb 2026 18:30:41 -0500 Subject: [PATCH 02/13] removed beads directory Signed-off-by: Nicholas Shumar --- .beads/metadata.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .beads/metadata.json diff --git a/.beads/metadata.json b/.beads/metadata.json deleted file mode 100644 index c787975..0000000 --- a/.beads/metadata.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "database": "beads.db", - "jsonl_export": "issues.jsonl" -} \ No newline at end of file From 9467f67a90f4845670c66d3a31ce67df0008216b Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Tue, 28 Apr 2026 21:58:43 -0400 Subject: [PATCH 03/13] Rename Heimdall2 to Heimdall in README Resolves #35 --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9ad436f..38c0958 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# heimdall2-helm +# heimdall-helm A Helm chart for the [MITRE SAF Heimdall application](https://github.com/mitre/heimdall2). @@ -14,7 +14,7 @@ You can clone this repo, enter the repository folder and then execute something ./start_heimdall2.sh ``` -The script will spin up Heimdall2 using the example [values.yaml](heimdall2/values.yaml) values file. You will need +The script will spin up Heimdall using the example [values.yaml](heimdall2/values.yaml) values file. You will need to provide your own if you want to configure other settings, and ingress, etc. Look at the [values.yaml](heimdall2/values.yaml) file for what to place in your own. @@ -35,7 +35,7 @@ The start_heimdall.sh script generates some of these values for you, and demonst ## To install via MITRE chart repository ``` -helm repo add heimdall2-helm https://mitre.github.io/heimdall2-helm/ +helm repo add heimdall2-helm https://mitre.github.io/heimdall-helm/ helm repo update helm search repo heimdall2 wget https://raw.githubusercontent.com/mitre/heimdall2-helm/main/values.yaml @@ -44,11 +44,11 @@ helm install heimdall heimdall2-helm/heimdall --namespace heimdall --create-name watch -n 15 kubectl get pods -n heimdall ``` -Give it time for Heimdall2 to come fully up. It has to "migrate" data, and the frontend site needs to build. It takes a few minutes. +Give it time for Heimdall to come fully up. It has to "migrate" data, and the frontend site needs to build. It takes a few minutes. -## Accessing Heimdall2 +## Accessing Heimdall -If you've spun up Heimdall2 using the [start_heimdall2.sh](start_heimdall2.sh) script, you can access it in your +If you've spun up Heimdall using the [start_heimdall2.sh](start_heimdall2.sh) script, you can access it in your browser via exposing via `kubectl port-forward` like so ``` From f22869d4bd17bd094600288445ab05e1273e9554 Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Tue, 28 Apr 2026 23:16:51 -0400 Subject: [PATCH 04/13] respect user supplied filename instead of forcing them to use certs.pem when certificates is enabled but the system certs approach is disabled Signed-off-by: Amndeep Singh Mann --- CHANGELOG | 23 +++++++++++++++++++ heimdall2/Chart.yaml | 2 +- heimdall2/templates/heimdall-statefulset.yaml | 4 ++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f7edeb3..1502e90 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,26 @@ +# 3.3.4 +Updated license +Repo was renamed to heimdall-helm and corresponding documentation updates +Make stateful set respect the filename provided by the user when they have certificates enabled but the system certs approach disabled + +# 3.3.3 +Updated default postgres version to 17 + +# 3.3.2 +Added support for the OIDC_USES_VERIFIED_EMAIL envvar + +# 3.3.1 +Updated license + +# 3.3.0 +Reworked certificates support + +# 3.2.1 +Minor fixes to the gateway functionality + +# 3.2.0 +Add SOPS secret support + # 3.1.10 Add external interfaces variables, specifically variables for splunk and tenable urls @georgedias diff --git a/heimdall2/Chart.yaml b/heimdall2/Chart.yaml index 793bce6..10bd3f7 100644 --- a/heimdall2/Chart.yaml +++ b/heimdall2/Chart.yaml @@ -3,7 +3,7 @@ name: heimdall description: Heimdall is a security results visualization tool that lets you view, store, and compare security scan results. type: application -version: 3.3.3 +version: 3.3.4 appVersion: "release-latest" annotations: diff --git a/heimdall2/templates/heimdall-statefulset.yaml b/heimdall2/templates/heimdall-statefulset.yaml index 51d5f1b..910b3bc 100644 --- a/heimdall2/templates/heimdall-statefulset.yaml +++ b/heimdall2/templates/heimdall-statefulset.yaml @@ -93,9 +93,9 @@ spec: env: {{- if and (not .Values.certs.systemCertsApproach.enabled) .Values.certs.enabled }} - name: NODE_EXTRA_CA_CERTS - value: /home/node/certs/certs.pem + value: /home/node/certs/{{ (index .Values.certs.certificates 0).filename }} - name: SSL_CERT_FILE - value: /home/node/certs/certs.pem + value: /home/node/certs/{{ (index .Values.certs.certificates 0).filename }} {{- end }} - name: NODE_ENV value: {{ .Values.nodeEnv | quote }} From 064d72756e89a68a88a5a68c06d829b6234808cb Mon Sep 17 00:00:00 2001 From: Daniel Medina Date: Tue, 1 Sep 2026 16:12:30 -0400 Subject: [PATCH 05/13] Update tenableHostUrl info --- heimdall2/values.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/heimdall2/values.yaml b/heimdall2/values.yaml index 0012d48..2774092 100644 --- a/heimdall2/values.yaml +++ b/heimdall2/values.yaml @@ -42,6 +42,10 @@ databaseUsername: postgres # splunkHostUrl: default empty string (ex. https://your.splunk.domain.com) # tenableHostUrl: default empty string (ex. https://your.tenable.domain.com) +# For multiple allowed hosts, use a YAML block scalar with one host per line: +# tenableHostUrl: | +# https://tenable-prod.example.com +# https://tenable-staging.example.com # forceTenableFrontend: default false (If true, the frontend will use Tenable.SC Lite features # ldapEnabled: default false From abb437a7f340b475e6b9e2bdea710735b21e5480 Mon Sep 17 00:00:00 2001 From: DMedina6 <158589619+DMedina6@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:56:54 -0400 Subject: [PATCH 06/13] Remove indents on additional comments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- heimdall2/values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/heimdall2/values.yaml b/heimdall2/values.yaml index 2774092..1953f6e 100644 --- a/heimdall2/values.yaml +++ b/heimdall2/values.yaml @@ -42,10 +42,10 @@ databaseUsername: postgres # splunkHostUrl: default empty string (ex. https://your.splunk.domain.com) # tenableHostUrl: default empty string (ex. https://your.tenable.domain.com) -# For multiple allowed hosts, use a YAML block scalar with one host per line: -# tenableHostUrl: | -# https://tenable-prod.example.com -# https://tenable-staging.example.com +# For multiple allowed hosts, use a YAML block scalar with one host per line: +# tenableHostUrl: | +# https://tenable-prod.example.com +# https://tenable-staging.example.com # forceTenableFrontend: default false (If true, the frontend will use Tenable.SC Lite features # ldapEnabled: default false From bdb3c0123f257830ab04832ac5d314f485e7754f Mon Sep 17 00:00:00 2001 From: Daniel Medina Date: Thu, 10 Sep 2026 15:41:05 -0400 Subject: [PATCH 07/13] Bump version --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1502e90..a002021 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +# 3.3.5 +Updated tenableHostUrl to reflect Heimdall implementation changes allowing multiple hosts separate by new-lines + # 3.3.4 Updated license Repo was renamed to heimdall-helm and corresponding documentation updates From 3015cad6c685135f3c1b5bdd9b1d351b16ba1014 Mon Sep 17 00:00:00 2001 From: Daniel Medina Date: Thu, 10 Sep 2026 16:24:14 -0400 Subject: [PATCH 08/13] Chart yaml bump version --- heimdall2/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heimdall2/Chart.yaml b/heimdall2/Chart.yaml index 10bd3f7..55154af 100644 --- a/heimdall2/Chart.yaml +++ b/heimdall2/Chart.yaml @@ -3,7 +3,7 @@ name: heimdall description: Heimdall is a security results visualization tool that lets you view, store, and compare security scan results. type: application -version: 3.3.4 +version: 3.3.5 appVersion: "release-latest" annotations: From 8e64b1e70e4d1e82b375c3d5d09db1b0b32c6300 Mon Sep 17 00:00:00 2001 From: ashimgautam1 Date: Wed, 23 Sep 2026 15:53:48 -0400 Subject: [PATCH 09/13] Add Helm lifecycle tests to develop CI --- .github/ct.yaml | 44 +++ .github/workflows/test.yml | 290 ++++++++++++++++++ .gitignore | 5 +- heimdall/tests/lifecycle/01-minimal.sh | 22 ++ .../tests/lifecycle/02-embedded-postgres.sh | 29 ++ .../lifecycle/03-github-service-postgres.sh | 45 +++ heimdall/tests/lifecycle/04-sops.sh | 55 ++++ heimdall/tests/lifecycle/05-vault-secret.sh | 39 +++ .../tests/lifecycle/06-configmap-certs.sh | 51 +++ heimdall/tests/lifecycle/README.md | 60 ++++ heimdall/tests/lifecycle/lib.sh | 94 ++++++ heimdall/tests/lifecycle/run-all.sh | 25 ++ 12 files changed, 758 insertions(+), 1 deletion(-) create mode 100644 .github/ct.yaml create mode 100644 .github/workflows/test.yml create mode 100755 heimdall/tests/lifecycle/01-minimal.sh create mode 100755 heimdall/tests/lifecycle/02-embedded-postgres.sh create mode 100755 heimdall/tests/lifecycle/03-github-service-postgres.sh create mode 100755 heimdall/tests/lifecycle/04-sops.sh create mode 100755 heimdall/tests/lifecycle/05-vault-secret.sh create mode 100755 heimdall/tests/lifecycle/06-configmap-certs.sh create mode 100644 heimdall/tests/lifecycle/README.md create mode 100755 heimdall/tests/lifecycle/lib.sh create mode 100755 heimdall/tests/lifecycle/run-all.sh diff --git a/.github/ct.yaml b/.github/ct.yaml new file mode 100644 index 0000000..07300be --- /dev/null +++ b/.github/ct.yaml @@ -0,0 +1,44 @@ +# chart-testing (ct) configuration +# https://github.com/helm/chart-testing + +# Target branches for detecting changed charts +target-branch: develop +remote: origin + +# Chart directories to process +chart-dirs: + - . + +# Chart repositories (required for dependencies) +chart-repos: + - bitnami=https://charts.bitnami.com/bitnami + +# Charts to exclude from testing +excluded-charts: [] + +# Helm version to use +helm-version: v3.22.0 + +# Enable chart schema validation +validate-chart-schema: true + +# Enable values schema validation (values.schema.json) +validate-maintainers: false + +# Check version increments +check-version-increment: true + +# Upgrade testing +upgrade: false # Will enable in separate upgrade test job + +# Additional Helm install arguments +# Timeout increased to 15min for CI environment (PostgreSQL + Heimdall startup) +helm-extra-args: --timeout 900s + +# Namespace for chart installation +# NOTE: Do NOT set a fixed namespace. Chart-testing needs to create random +# namespaces for each test to ensure isolation and proper cleanup. +# When namespace is empty, ct automatically creates/deletes test namespaces. + +# Release name +release-label: app.kubernetes.io/instance diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6b141cf --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,290 @@ +name: Test + +on: + pull_request: + branches: + - develop + paths: + - 'heimdall/**' + - '.github/workflows/test.yml' + - '.github/ct.yaml' + push: + branches: + - develop + paths: + - 'heimdall/**' + - '.github/workflows/test.yml' + - '.github/ct.yaml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + # Stage 1: Lint (< 10s) + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + cache-dependency-path: '' # Disable cache (no Python deps in repo) + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.8.0 + + - name: Run chart-testing (lint) + run: ct lint --config .github/ct.yaml + + # Stage 2: Unit Tests (< 60s) + unit-test: + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + + - name: Install helm-unittest plugin + run: helm plugin install https://github.com/helm-unittest/helm-unittest --version=v1.0.3 + + - name: Run unit tests + run: helm unittest ./heimdall + + # Stage 3: Environment Variables Validation (< 10s) + env-vars-validation: + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + cache-dependency-path: '' # Disable cache (no Python deps in repo) + + - name: Install PyYAML + run: pip install pyyaml + + - name: Validate env vars schema compliance + run: python heimdall/tests/scripts/test_env_vars_schema_compliance.py + + - name: Validate env vars against templates + run: python heimdall/tests/scripts/validate_env_vars_against_templates.py + + # Stage 4: Schema Validation (< 5s) + schema-validation: + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + + - name: Add Bitnami Helm repository + run: helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Build chart dependencies + run: helm dependency build ./heimdall + + - name: Validate values.schema.json + run: | + helm template heimdall ./heimdall + + - name: Test invalid values are rejected + run: | + # Test that invalid nodeEnv is rejected + if helm template heimdall ./heimdall --set nodeEnv=invalid 2>&1 | grep -q "values don't meet the specifications"; then + echo "✓ Schema validation working - rejected invalid nodeEnv" + else + echo "✗ Schema validation failed - should reject invalid nodeEnv" + exit 1 + fi + + # Stage 5: Integration Tests (2-5 min) + integration-test: + runs-on: ubuntu-latest + needs: [unit-test, env-vars-validation, schema-validation] + strategy: + matrix: + k8s-version: + - v1.30.0 # Latest stable + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.8.0 + + - name: Create kind cluster (Kubernetes ${{ matrix.k8s-version }}) + uses: helm/kind-action@v1.10.0 + with: + node_image: kindest/node:${{ matrix.k8s-version }} + cluster_name: heimdall-test + wait: 120s + + - name: List changed charts + id: list-changed + run: | + changed=$(ct list-changed --config .github/ct.yaml --target-branch develop) + if [[ -n "$changed" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Run chart-testing (install) + if: steps.list-changed.outputs.changed == 'true' + run: ct install --config .github/ct.yaml + + # Stage 6: Template Rendering Tests + template-test: + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + + - name: Add Bitnami Helm repository + run: helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Update chart dependencies + run: helm dependency update ./heimdall + + - name: Test template rendering (embedded PostgreSQL) + run: | + helm template heimdall ./heimdall > /tmp/heimdall-embedded.yaml + echo "✓ Template rendered successfully with embedded PostgreSQL" + + - name: Test template rendering (external database) + run: | + helm template heimdall ./heimdall \ + --set postgresql.enabled=false \ + --set externalDatabase.host=db.example.com \ + --set externalDatabase.database=heimdall_prod \ + --set externalDatabase.username=heimdall_user \ + > /tmp/heimdall-external.yaml + echo "✓ Template rendered successfully with external database" + + - name: Test template rendering (with ingress) + run: | + helm template heimdall ./heimdall \ + --set heimdall.ingress.enabled=true \ + --set 'heimdall.ingress.hosts[0].host=heimdall.example.com' \ + --set 'heimdall.ingress.hosts[0].paths[0].path=/' \ + > /tmp/heimdall-ingress.yaml + echo "✓ Template rendered successfully with ingress enabled" + + - name: Verify rendered manifests are valid YAML + run: | + # Verify YAML syntax is valid + cat /tmp/heimdall-embedded.yaml | grep -q "apiVersion:" + cat /tmp/heimdall-external.yaml | grep -q "apiVersion:" + cat /tmp/heimdall-ingress.yaml | grep -q "apiVersion:" + echo "✓ All manifests contain valid Kubernetes resources" + + # Stage 7: Runtime Lifecycle Scenarios + lifecycle-test: + runs-on: ubuntu-latest + needs: lint + timeout-minutes: 30 + services: + postgres: + image: postgres:17 + env: + POSTGRES_DB: heimdall + POSTGRES_USER: postgres + POSTGRES_PASSWORD: lifecycle-postgres-password + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d heimdall" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + + - name: Install SOPS and age + env: + SOPS_VERSION: 3.13.3 + AGE_VERSION: 1.3.2 + AGE_SHA256: cbe24006683f8eb669266162894b9a522a1af52f2665fbc63a4bb032ed26ac10 + run: | + curl --fail --silent --show-error --location --remote-name \ + "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.amd64" + curl --fail --silent --show-error --location --remote-name \ + "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.checksums.txt" + sha256sum --check --ignore-missing "sops-v${SOPS_VERSION}.checksums.txt" + sudo install --mode 0755 "sops-v${SOPS_VERSION}.linux.amd64" /usr/local/bin/sops + curl --fail --silent --show-error --location --remote-name \ + "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-amd64.tar.gz" + echo "${AGE_SHA256} age-v${AGE_VERSION}-linux-amd64.tar.gz" | sha256sum --check + tar --extract --gzip --file "age-v${AGE_VERSION}-linux-amd64.tar.gz" + sudo install --mode 0755 age/age age/age-keygen /usr/local/bin/ + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: heimdall-test + wait: 120s + + - name: Locate GitHub PostgreSQL service + id: postgres-service + run: | + host="$(docker network inspect kind --format '{{range .IPAM.Config}}{{println .Gateway}}{{end}}' \ + | awk '/\./ {print; exit}')" + test -n "$host" + echo "host=$host" >> "$GITHUB_OUTPUT" + + - name: Show tool versions + run: | + helm version --short + kubectl version --client + kind version + sops --version + age --version + + - name: Run lifecycle scenarios + env: + EXTERNAL_POSTGRES_HOST: ${{ steps.postgres-service.outputs.host }} + EXTERNAL_POSTGRES_PORT: 5432 + EXTERNAL_POSTGRES_PASSWORD: lifecycle-postgres-password + run: heimdall/tests/lifecycle/run-all.sh diff --git a/.gitignore b/.gitignore index 46a9907..619bef0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .env .cr-release-packages/ .cr-index/ -.DS_Store \ No newline at end of file +.DS_Store + +# Lifecycle test artifacts +heimdall/tests/lifecycle/.generated/ diff --git a/heimdall/tests/lifecycle/01-minimal.sh b/heimdall/tests/lifecycle/01-minimal.sh new file mode 100755 index 0000000..198c8ea --- /dev/null +++ b/heimdall/tests/lifecycle/01-minimal.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Bare-minimum values: install, validate, and uninstall cleanly. +source "$(dirname "$0")/lib.sh" +NS=lifecycle-minimal +trap 'cleanup_ns "$NS"' EXIT + +log "Scenario 1: bare-minimum lifecycle" +gen_min_values +helm lint "$CHART" -f "$GEN/min.yaml" +helm install "$RELEASE" "$CHART" -n "$NS" --create-namespace -f "$GEN/min.yaml" +wait_ready "$NS" +check_http "$NS" + +[[ -z "$(kubectl get pvc -n "$NS" -o name)" ]] || fail "unexpected PVC with persistence disabled" + +log "Spin down" +helm uninstall "$RELEASE" -n "$NS" +kubectl wait --for=delete pod --all -n "$NS" --timeout=120s +left="$(kubectl get all,configmap,secret,pvc,serviceaccount,networkpolicy,pdb \ + -n "$NS" -l app.kubernetes.io/instance="$RELEASE" -o name 2>/dev/null || true)" +[[ -z "$left" ]] || fail "resources remained after uninstall: $left" +pass "install, HTTP validation, and clean uninstall" diff --git a/heimdall/tests/lifecycle/02-embedded-postgres.sh b/heimdall/tests/lifecycle/02-embedded-postgres.sh new file mode 100755 index 0000000..f5ff1f7 --- /dev/null +++ b/heimdall/tests/lifecycle/02-embedded-postgres.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Embedded PostgreSQL: PVC binds and data survives a pod restart. +source "$(dirname "$0")/lib.sh" +NS=lifecycle-embedded-pg +trap 'cleanup_ns "$NS"' EXIT + +log "Scenario 2: embedded PostgreSQL with persistence" +gen_min_values +helm install "$RELEASE" "$CHART" -n "$NS" --create-namespace -f "$GEN/min.yaml" \ + --set postgresql.primary.persistence.enabled=true \ + --set postgresql.primary.persistence.storageClass="$STORAGE_CLASS" +wait_ready "$NS" + +pvc="$(kubectl get pvc -n "$NS" -l app.kubernetes.io/instance="$RELEASE" -o jsonpath='{.items[0].metadata.name}')" +[[ -n "$pvc" ]] || fail "PostgreSQL PVC was not created" +[[ "$(kubectl get pvc "$pvc" -n "$NS" -o jsonpath='{.status.phase}')" == Bound ]] \ + || fail "PostgreSQL PVC is not Bound" +pass "PostgreSQL PVC is Bound" + +psql_in "$NS" "create table lifecycle_persistence(value int); insert into lifecycle_persistence values (42);" >/dev/null +kubectl delete pod -n "$NS" "$RELEASE-postgresql-0" +kubectl wait --for=condition=ready pod "$RELEASE-postgresql-0" -n "$NS" --timeout="$WAIT_TIMEOUT" +[[ "$(psql_in "$NS" 'select value from lifecycle_persistence;')" == 42 ]] \ + || fail "database data did not survive the PostgreSQL pod restart" +pass "database data survived a PostgreSQL pod restart" + +tables="$(psql_in "$NS" "select count(*) from information_schema.tables where table_schema='public';")" +(( tables > 1 )) || fail "Heimdall database migrations were not detected" +pass "Heimdall database schema was migrated" diff --git a/heimdall/tests/lifecycle/03-github-service-postgres.sh b/heimdall/tests/lifecycle/03-github-service-postgres.sh new file mode 100755 index 0000000..f22795f --- /dev/null +++ b/heimdall/tests/lifecycle/03-github-service-postgres.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# External PostgreSQL supplied by a GitHub Actions service container. +source "$(dirname "$0")/lib.sh" +NS=lifecycle-service-pg +EXTERNAL_POSTGRES_HOST="${EXTERNAL_POSTGRES_HOST:-}" +EXTERNAL_POSTGRES_PORT="${EXTERNAL_POSTGRES_PORT:-5432}" +EXTERNAL_POSTGRES_PASSWORD="${EXTERNAL_POSTGRES_PASSWORD:-lifecycle-postgres-password}" +trap 'cleanup_ns "$NS"' EXIT + +[[ -n "$EXTERNAL_POSTGRES_HOST" ]] \ + || fail "EXTERNAL_POSTGRES_HOST is required; see README.md for local and CI examples" + +log "Scenario 3: external PostgreSQL at $EXTERNAL_POSTGRES_HOST:$EXTERNAL_POSTGRES_PORT" +gen_min_values +kubectl create namespace "$NS" +kubectl create secret generic external-postgres -n "$NS" \ + --from-literal=password="$EXTERNAL_POSTGRES_PASSWORD" + +cat > "$GEN/external-postgres.yaml" </dev/null +public_key="$(awk '/public key/ {print $4}' "$GEN/sops-key.txt")" + +cat > "$GEN/sops-plain.env" < "$GEN/sops-encrypted.env" +grep -q 'ENC\[AES256_GCM' "$GEN/sops-encrypted.env" || fail "SOPS output is not encrypted" +pass "secret values were encrypted with SOPS and age" + +kubectl create namespace "$NS" +SOPS_AGE_KEY_FILE="$GEN/sops-key.txt" sops --decrypt --input-type dotenv --output-type dotenv \ + "$GEN/sops-encrypted.env" > "$GEN/sops-decrypted.env" +kubectl create secret generic "$SECRET_NAME" -n "$NS" --from-env-file="$GEN/sops-decrypted.env" + +cat > "$GEN/sops-values.yaml" < "$GEN/vault-values.yaml" </dev/null + +cat > "$GEN/configmap-values.yaml" </dev/null \ + || fail "custom CA ConfigMap is missing" + +env_output="$(kubectl exec -n "$NS" "$RELEASE-0" -c heimdall-front -- env)" +grep -qx 'NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/ca-bundle.pem' <<<"$env_output" \ + || fail "NODE_EXTRA_CA_CERTS is not set" +kubectl exec -n "$NS" "$RELEASE-0" -c heimdall-front -- \ + grep -q 'BEGIN CERTIFICATE' /usr/local/share/ca-certificates/ca-bundle.pem \ + || fail "generated CA bundle does not contain a certificate" +pass "baseline ConfigMap and custom CA bundle work" diff --git a/heimdall/tests/lifecycle/README.md b/heimdall/tests/lifecycle/README.md new file mode 100644 index 0000000..82aab07 --- /dev/null +++ b/heimdall/tests/lifecycle/README.md @@ -0,0 +1,60 @@ +# Lifecycle tests + +These end-to-end scenarios install the chart in a disposable Kubernetes cluster and validate the +runtime behaviors required by the Helm testing ticket. Every scenario uses its own namespace and +cleans it up on exit. + +| Script | Scenario | +|---|---| +| `01-minimal.sh` | Bare-minimum install, HTTP validation, and clean uninstall | +| `02-embedded-postgres.sh` | Embedded PostgreSQL with persistent data across a pod restart | +| `03-github-service-postgres.sh` | External PostgreSQL supplied by a GitHub Actions service container | +| `04-sops.sh` | SOPS/age encryption with an externally managed Kubernetes Secret | +| `05-vault-secret.sh` | `existingSecret` contract used by Vault or External Secrets integrations | +| `06-configmap-certs.sh` | Baseline ConfigMap values and custom CA certificate bundle | + +The Vault scenario validates the chart boundary after Vault has synchronized a Kubernetes Secret. It +does not deploy Vault or an External Secrets operator. + +## Requirements + +Install `helm`, `kubectl`, `openssl`, `curl`, `sops`, and `age`, and use a disposable cluster such as +[kind](https://kind.sigs.k8s.io/). The current context must start with `kind-` or `k3d-` unless +`ALLOW_ANY_CONTEXT=1` is explicitly set. + +## Run locally + +Create a cluster and a local PostgreSQL container for scenario 3: + +```bash +kind create cluster --name heimdall-test +docker run --detach --rm --name heimdall-lifecycle-postgres \ + --env POSTGRES_DB=heimdall \ + --env POSTGRES_USER=postgres \ + --env POSTGRES_PASSWORD=lifecycle-postgres-password \ + --publish 15432:5432 \ + postgres:17 +``` + +On Docker Desktop, run all scenarios with: + +```bash +EXTERNAL_POSTGRES_HOST=host.docker.internal \ +EXTERNAL_POSTGRES_PORT=15432 \ +heimdall/tests/lifecycle/run-all.sh +``` + +Run selected scenarios with `ONLY`, for example: + +```bash +ONLY="01 04 05" heimdall/tests/lifecycle/run-all.sh +``` + +Clean up the local resources afterward: + +```bash +docker stop heimdall-lifecycle-postgres +kind delete cluster --name heimdall-test +``` + +Generated keys, values, and certificates are written to `.generated/`, which is gitignored. diff --git a/heimdall/tests/lifecycle/lib.sh b/heimdall/tests/lifecycle/lib.sh new file mode 100755 index 0000000..afbf7ea --- /dev/null +++ b/heimdall/tests/lifecycle/lib.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Shared helpers for lifecycle tests. Source this file; do not run it directly. + +set -euo pipefail + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CHART="$(cd "$TESTS_DIR/../.." && pwd)" +GEN="$TESTS_DIR/.generated" +RELEASE="heimdall" +WAIT_TIMEOUT="${WAIT_TIMEOUT:-600s}" +STORAGE_CLASS="${STORAGE_CLASS:-standard}" + +mkdir -p "$GEN" + +log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } +pass() { printf '\033[32mPASS\033[0m %s\n' "$*"; } +fail() { printf '\033[31mFAIL\033[0m %s\n' "$*" >&2; exit 1; } + +require() { + local tool + for tool in "$@"; do + command -v "$tool" >/dev/null || fail "missing required tool: $tool" + done +} + +check_context() { + local ctx + ctx="$(kubectl config current-context)" + log "kubectl context: $ctx" + if [[ "$ctx" != kind-* && "$ctx" != k3d-* && "${ALLOW_ANY_CONTEXT:-0}" != 1 ]]; then + fail "context '$ctx' is not kind/k3d; set ALLOW_ANY_CONTEXT=1 to override" + fi + kubectl wait --for=condition=Ready node --all --timeout=120s \ + || fail "cluster nodes are not Ready" +} + +gen_min_values() { + DB_PASSWORD="$(openssl rand -hex 24)" + export DB_PASSWORD + cat > "$GEN/min.yaml" </dev/null 2>&1 || true + kubectl delete namespace "$namespace" --wait=false >/dev/null 2>&1 || true +} + +wait_ready() { + local namespace="$1" + kubectl wait --for=condition=ready pod --all -n "$namespace" --timeout="$WAIT_TIMEOUT" \ + || { + kubectl get pods -n "$namespace" -o wide || true + kubectl describe pods -n "$namespace" | tail -80 || true + fail "pods in $namespace did not become Ready" + } +} + +check_http() { + local namespace="$1" port="${2:-18081}" code=000 pf + kubectl port-forward -n "$namespace" "service/$RELEASE" "$port:3000" >/dev/null 2>&1 & + pf=$! + for _ in $(seq 1 30); do + code="$(curl --silent --output /dev/null --write-out '%{http_code}' "http://localhost:$port/" || true)" + [[ "$code" == 200 ]] && break + sleep 2 + done + kill "$pf" >/dev/null 2>&1 || true + wait "$pf" 2>/dev/null || true + [[ "$code" == 200 ]] || fail "GET / returned $code (expected 200)" + pass "application serves HTTP 200" +} + +psql_in() { + local namespace="$1" sql="$2" + kubectl exec -n "$namespace" "$RELEASE-postgresql-0" -- \ + env PGPASSWORD="$DB_PASSWORD" psql -U postgres -d heimdall -tA -c "$sql" +} diff --git a/heimdall/tests/lifecycle/run-all.sh b/heimdall/tests/lifecycle/run-all.sh new file mode 100755 index 0000000..533ccc8 --- /dev/null +++ b/heimdall/tests/lifecycle/run-all.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Run every lifecycle scenario against the current disposable Kubernetes context. +# Use ONLY="01 04" to select scenario prefixes. +source "$(dirname "$0")/lib.sh" +require helm kubectl openssl curl +check_context + +log "Build chart dependencies" +helm dependency build "$CHART" + +failed=() +for scenario in "$TESTS_DIR"/0*.sh; do + base="$(basename "$scenario")" + if [[ -n "${ONLY:-}" && ! " $ONLY " == *" ${base:0:2} "* ]]; then + continue + fi + bash "$scenario" || failed+=("$base") +done + +echo +if ((${#failed[@]})); then + printf '\033[31mFAILED:\033[0m %s\n' "${failed[*]}" + exit 1 +fi +printf '\033[32mAll lifecycle scenarios passed\033[0m\n' From 1910876b8abf08b58caed6d69062a2970b914696 Mon Sep 17 00:00:00 2001 From: ashimgautam1 Date: Wed, 23 Sep 2026 16:01:51 -0400 Subject: [PATCH 10/13] Keep lifecycle harness outside packaged chart --- .github/workflows/test.yml | 4 +++- .gitignore | 2 +- {heimdall/tests => tests}/lifecycle/01-minimal.sh | 0 {heimdall/tests => tests}/lifecycle/02-embedded-postgres.sh | 0 .../tests => tests}/lifecycle/03-github-service-postgres.sh | 0 {heimdall/tests => tests}/lifecycle/04-sops.sh | 0 {heimdall/tests => tests}/lifecycle/05-vault-secret.sh | 0 {heimdall/tests => tests}/lifecycle/06-configmap-certs.sh | 0 {heimdall/tests => tests}/lifecycle/README.md | 4 ++-- {heimdall/tests => tests}/lifecycle/lib.sh | 3 ++- {heimdall/tests => tests}/lifecycle/run-all.sh | 0 11 files changed, 8 insertions(+), 5 deletions(-) rename {heimdall/tests => tests}/lifecycle/01-minimal.sh (100%) rename {heimdall/tests => tests}/lifecycle/02-embedded-postgres.sh (100%) rename {heimdall/tests => tests}/lifecycle/03-github-service-postgres.sh (100%) rename {heimdall/tests => tests}/lifecycle/04-sops.sh (100%) rename {heimdall/tests => tests}/lifecycle/05-vault-secret.sh (100%) rename {heimdall/tests => tests}/lifecycle/06-configmap-certs.sh (100%) rename {heimdall/tests => tests}/lifecycle/README.md (95%) rename {heimdall/tests => tests}/lifecycle/lib.sh (97%) rename {heimdall/tests => tests}/lifecycle/run-all.sh (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b141cf..8205b39 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ on: - develop paths: - 'heimdall/**' + - 'tests/lifecycle/**' - '.github/workflows/test.yml' - '.github/ct.yaml' push: @@ -13,6 +14,7 @@ on: - develop paths: - 'heimdall/**' + - 'tests/lifecycle/**' - '.github/workflows/test.yml' - '.github/ct.yaml' workflow_dispatch: @@ -287,4 +289,4 @@ jobs: EXTERNAL_POSTGRES_HOST: ${{ steps.postgres-service.outputs.host }} EXTERNAL_POSTGRES_PORT: 5432 EXTERNAL_POSTGRES_PASSWORD: lifecycle-postgres-password - run: heimdall/tests/lifecycle/run-all.sh + run: tests/lifecycle/run-all.sh diff --git a/.gitignore b/.gitignore index 619bef0..f4fb68b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ .DS_Store # Lifecycle test artifacts -heimdall/tests/lifecycle/.generated/ +tests/lifecycle/.generated/ diff --git a/heimdall/tests/lifecycle/01-minimal.sh b/tests/lifecycle/01-minimal.sh similarity index 100% rename from heimdall/tests/lifecycle/01-minimal.sh rename to tests/lifecycle/01-minimal.sh diff --git a/heimdall/tests/lifecycle/02-embedded-postgres.sh b/tests/lifecycle/02-embedded-postgres.sh similarity index 100% rename from heimdall/tests/lifecycle/02-embedded-postgres.sh rename to tests/lifecycle/02-embedded-postgres.sh diff --git a/heimdall/tests/lifecycle/03-github-service-postgres.sh b/tests/lifecycle/03-github-service-postgres.sh similarity index 100% rename from heimdall/tests/lifecycle/03-github-service-postgres.sh rename to tests/lifecycle/03-github-service-postgres.sh diff --git a/heimdall/tests/lifecycle/04-sops.sh b/tests/lifecycle/04-sops.sh similarity index 100% rename from heimdall/tests/lifecycle/04-sops.sh rename to tests/lifecycle/04-sops.sh diff --git a/heimdall/tests/lifecycle/05-vault-secret.sh b/tests/lifecycle/05-vault-secret.sh similarity index 100% rename from heimdall/tests/lifecycle/05-vault-secret.sh rename to tests/lifecycle/05-vault-secret.sh diff --git a/heimdall/tests/lifecycle/06-configmap-certs.sh b/tests/lifecycle/06-configmap-certs.sh similarity index 100% rename from heimdall/tests/lifecycle/06-configmap-certs.sh rename to tests/lifecycle/06-configmap-certs.sh diff --git a/heimdall/tests/lifecycle/README.md b/tests/lifecycle/README.md similarity index 95% rename from heimdall/tests/lifecycle/README.md rename to tests/lifecycle/README.md index 82aab07..5b1f274 100644 --- a/heimdall/tests/lifecycle/README.md +++ b/tests/lifecycle/README.md @@ -41,13 +41,13 @@ On Docker Desktop, run all scenarios with: ```bash EXTERNAL_POSTGRES_HOST=host.docker.internal \ EXTERNAL_POSTGRES_PORT=15432 \ -heimdall/tests/lifecycle/run-all.sh +tests/lifecycle/run-all.sh ``` Run selected scenarios with `ONLY`, for example: ```bash -ONLY="01 04 05" heimdall/tests/lifecycle/run-all.sh +ONLY="01 04 05" tests/lifecycle/run-all.sh ``` Clean up the local resources afterward: diff --git a/heimdall/tests/lifecycle/lib.sh b/tests/lifecycle/lib.sh similarity index 97% rename from heimdall/tests/lifecycle/lib.sh rename to tests/lifecycle/lib.sh index afbf7ea..61e1537 100755 --- a/heimdall/tests/lifecycle/lib.sh +++ b/tests/lifecycle/lib.sh @@ -4,7 +4,8 @@ set -euo pipefail TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CHART="$(cd "$TESTS_DIR/../.." && pwd)" +REPO_ROOT="$(cd "$TESTS_DIR/../.." && pwd)" +CHART="$REPO_ROOT/heimdall" GEN="$TESTS_DIR/.generated" RELEASE="heimdall" WAIT_TIMEOUT="${WAIT_TIMEOUT:-600s}" diff --git a/heimdall/tests/lifecycle/run-all.sh b/tests/lifecycle/run-all.sh similarity index 100% rename from heimdall/tests/lifecycle/run-all.sh rename to tests/lifecycle/run-all.sh From e8557ccc39141eb9d37b47bb7040778d9b8c91a6 Mon Sep 17 00:00:00 2001 From: ashimgautam1 Date: Wed, 23 Sep 2026 16:06:36 -0400 Subject: [PATCH 11/13] Initialize lifecycle chart dependencies --- tests/lifecycle/run-all.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lifecycle/run-all.sh b/tests/lifecycle/run-all.sh index 533ccc8..b33f2a5 100755 --- a/tests/lifecycle/run-all.sh +++ b/tests/lifecycle/run-all.sh @@ -6,6 +6,7 @@ require helm kubectl openssl curl check_context log "Build chart dependencies" +helm repo add bitnami https://charts.bitnami.com/bitnami --force-update helm dependency build "$CHART" failed=() From 2d736873df7e4c39084cc54bafaa77d0c25e2069 Mon Sep 17 00:00:00 2001 From: ashimgautam1 Date: Thu, 24 Sep 2026 12:34:18 -0400 Subject: [PATCH 12/13] Address workflow branch review feedback --- .github/ct.yaml | 2 +- .github/workflows/test.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ct.yaml b/.github/ct.yaml index 07300be..657bff0 100644 --- a/.github/ct.yaml +++ b/.github/ct.yaml @@ -2,7 +2,7 @@ # https://github.com/helm/chart-testing # Target branches for detecting changed charts -target-branch: develop +target-branch: main remote: origin # Chart directories to process diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8205b39..7d8d5b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test +name: Test Helm Chart on: pull_request: @@ -11,6 +11,7 @@ on: - '.github/ct.yaml' push: branches: + - main - develop paths: - 'heimdall/**' @@ -156,7 +157,7 @@ jobs: - name: List changed charts id: list-changed run: | - changed=$(ct list-changed --config .github/ct.yaml --target-branch develop) + changed=$(ct list-changed --config .github/ct.yaml --target-branch ${{ github.event.repository.default_branch }}) if [[ -n "$changed" ]]; then echo "changed=true" >> "$GITHUB_OUTPUT" fi From e365786677ce88d011114caa8b8966da206badca Mon Sep 17 00:00:00 2001 From: ashimgautam1 Date: Thu, 24 Sep 2026 14:54:59 -0400 Subject: [PATCH 13/13] Adapt seven-stage test workflow for main --- .github/workflows/test.yml | 186 ++++++---- .gitignore | 1 + tests/README.md | 340 ++++++++++++++++++ tests/lifecycle/02-embedded-postgres.sh | 6 +- tests/lifecycle/03-github-service-postgres.sh | 40 +-- tests/lifecycle/04-sops.sh | 48 +-- tests/lifecycle/05-vault-secret.sh | 52 +-- tests/lifecycle/06-configmap-certs.sh | 71 ++-- tests/lifecycle/README.md | 53 ++- tests/lifecycle/lib.sh | 48 ++- tests/lifecycle/run-all.sh | 4 - tests/unit/__snapshot__/.gitkeep | 1 + tests/unit/secrets_test.yaml | 45 +++ 13 files changed, 679 insertions(+), 216 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/unit/__snapshot__/.gitkeep create mode 100644 tests/unit/secrets_test.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d8d5b0..d00af78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,19 +3,21 @@ name: Test Helm Chart on: pull_request: branches: - - develop + - main paths: - - 'heimdall/**' + - 'heimdall2/**' - 'tests/lifecycle/**' + - 'tests/unit/**' - '.github/workflows/test.yml' - '.github/ct.yaml' push: branches: - main - - develop + - hh-lifecycle-tests paths: - - 'heimdall/**' + - 'heimdall2/**' - 'tests/lifecycle/**' + - 'tests/unit/**' - '.github/workflows/test.yml' - '.github/ct.yaml' workflow_dispatch: @@ -24,7 +26,7 @@ permissions: contents: read jobs: - # Stage 1: Lint (< 10s) + # Stage 1: Lint lint: runs-on: ubuntu-latest steps: @@ -42,15 +44,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.x' - cache-dependency-path: '' # Disable cache (no Python deps in repo) - name: Set up chart-testing uses: helm/chart-testing-action@v2.8.0 - - name: Run chart-testing (lint) + - name: Run chart-testing lint run: ct lint --config .github/ct.yaml - # Stage 2: Unit Tests (< 60s) + # Stage 2: Unit Tests unit-test: runs-on: ubuntu-latest needs: lint @@ -67,9 +68,9 @@ jobs: run: helm plugin install https://github.com/helm-unittest/helm-unittest --version=v1.0.3 - name: Run unit tests - run: helm unittest ./heimdall + run: helm unittest --strict -f '../tests/unit/*_test.yaml' ./heimdall2 - # Stage 3: Environment Variables Validation (< 10s) + # Stage 3: Environment Variables Validation env-vars-validation: runs-on: ubuntu-latest needs: lint @@ -77,22 +78,67 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.22.0 + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - cache-dependency-path: '' # Disable cache (no Python deps in repo) - name: Install PyYAML run: pip install pyyaml - - name: Validate env vars schema compliance - run: python heimdall/tests/scripts/test_env_vars_schema_compliance.py - - - name: Validate env vars against templates - run: python heimdall/tests/scripts/validate_env_vars_against_templates.py - - # Stage 4: Schema Validation (< 5s) + - name: Render chart + run: | + helm template heimdall ./heimdall2 \ + --set-string jwtSecret=env-test-jwt \ + --set-string databasePassword=env-test-db \ + --set-string apiKeySecret=env-test-api \ + --set-string adminPassword=env-test-admin \ + --set-string externalUrl=http://localhost:3000 \ + --set heimdall.ingress.enabled=false \ + > /tmp/heimdall-env.yaml + + - name: Validate required environment variables + run: | + python - <<'PY' + import yaml + + with open('/tmp/heimdall-env.yaml', encoding='utf-8') as stream: + documents = [document for document in yaml.safe_load_all(stream) if document] + + statefulset = next( + document for document in documents + if document.get('kind') == 'StatefulSet' + and document.get('metadata', {}).get('name') == 'heimdall' + ) + environment = statefulset['spec']['template']['spec']['containers'][0]['env'] + by_name = {entry['name']: entry for entry in environment} + required = { + 'NODE_ENV', 'DATABASE_HOST', 'DATABASE_PORT', 'DATABASE_NAME', + 'DATABASE_USERNAME', 'DATABASE_PASSWORD', 'JWT_SECRET', + 'API_KEY_SECRET', 'ADMIN_PASSWORD', 'EXTERNAL_URL', + } + missing = sorted(required - by_name.keys()) + if missing: + raise SystemExit(f'Missing environment variables: {missing}') + + secret_backed = { + 'DATABASE_USERNAME', 'DATABASE_PASSWORD', 'JWT_SECRET', + 'API_KEY_SECRET', 'ADMIN_PASSWORD', + } + invalid = sorted( + name for name in secret_backed + if by_name[name].get('valueFrom', {}).get('secretKeyRef', {}).get('name') != 'heimdall' + ) + if invalid: + raise SystemExit(f'Variables not backed by the Heimdall Secret: {invalid}') + PY + + # Stage 4: Schema Validation schema-validation: runs-on: ubuntu-latest needs: lint @@ -105,34 +151,36 @@ jobs: with: version: v3.22.0 - - name: Add Bitnami Helm repository - run: helm repo add bitnami https://charts.bitnami.com/bitnami - - - name: Build chart dependencies - run: helm dependency build ./heimdall - - - name: Validate values.schema.json + - name: Validate chart and values run: | - helm template heimdall ./heimdall - - - name: Test invalid values are rejected + helm lint --strict ./heimdall2 \ + --set-string jwtSecret=schema-test-jwt \ + --set-string databasePassword=schema-test-db \ + --set-string apiKeySecret=schema-test-api \ + --set-string externalUrl=http://localhost:3000 + helm template heimdall ./heimdall2 \ + --set-string jwtSecret=schema-test-jwt \ + --set-string databasePassword=schema-test-db \ + --set-string apiKeySecret=schema-test-api \ + --set-string externalUrl=http://localhost:3000 \ + > /dev/null + + - name: Report values schema coverage run: | - # Test that invalid nodeEnv is rejected - if helm template heimdall ./heimdall --set nodeEnv=invalid 2>&1 | grep -q "values don't meet the specifications"; then - echo "✓ Schema validation working - rejected invalid nodeEnv" + if [[ -f heimdall2/values.schema.json ]]; then + echo 'values.schema.json is present and was validated by Helm' else - echo "✗ Schema validation failed - should reject invalid nodeEnv" - exit 1 + echo '::notice::main does not currently include heimdall2/values.schema.json; Helm lint and rendering validation completed' fi - # Stage 5: Integration Tests (2-5 min) + # Stage 5: Integration Tests integration-test: runs-on: ubuntu-latest needs: [unit-test, env-vars-validation, schema-validation] strategy: matrix: k8s-version: - - v1.30.0 # Latest stable + - v1.30.0 steps: - name: Checkout uses: actions/checkout@v4 @@ -157,14 +205,20 @@ jobs: - name: List changed charts id: list-changed run: | - changed=$(ct list-changed --config .github/ct.yaml --target-branch ${{ github.event.repository.default_branch }}) - if [[ -n "$changed" ]]; then - echo "changed=true" >> "$GITHUB_OUTPUT" + if [[ "${{ github.event_name }}" == pull_request ]]; then + changed=$(ct list-changed --config .github/ct.yaml --target-branch ${{ github.event.repository.default_branch }}) + if [[ -n "$changed" ]]; then + echo 'changed=true' >> "$GITHUB_OUTPUT" + fi + else + echo 'changed=true' >> "$GITHUB_OUTPUT" fi - - name: Run chart-testing (install) + - name: Run chart-testing install if: steps.list-changed.outputs.changed == 'true' - run: ct install --config .github/ct.yaml + run: | + ct install --config .github/ct.yaml --all \ + --helm-extra-set-args '--set-string jwtSecret=integration-test-jwt --set-string databasePassword=integration-test-db --set-string apiKeySecret=integration-test-api --set-string externalUrl=http://localhost:3000 --set heimdall.ingress.enabled=false' # Stage 6: Template Rendering Tests template-test: @@ -179,43 +233,47 @@ jobs: with: version: v3.22.0 - - name: Add Bitnami Helm repository - run: helm repo add bitnami https://charts.bitnami.com/bitnami - - - name: Update chart dependencies - run: helm dependency update ./heimdall - - name: Test template rendering (embedded PostgreSQL) run: | - helm template heimdall ./heimdall > /tmp/heimdall-embedded.yaml - echo "✓ Template rendered successfully with embedded PostgreSQL" + helm template heimdall ./heimdall2 \ + --set-string jwtSecret=template-test-jwt \ + --set-string databasePassword=template-test-db \ + --set-string apiKeySecret=template-test-api \ + --set-string externalUrl=http://localhost:3000 \ + --set heimdall.ingress.enabled=false \ + > /tmp/heimdall-embedded.yaml - name: Test template rendering (external database) run: | - helm template heimdall ./heimdall \ + helm template heimdall ./heimdall2 \ --set postgresql.enabled=false \ - --set externalDatabase.host=db.example.com \ - --set externalDatabase.database=heimdall_prod \ - --set externalDatabase.username=heimdall_user \ + --set-string databaseHost=db.example.com \ + --set databasePort=5432 \ + --set-string databaseName=heimdall_prod \ + --set-string databaseUsername=heimdall_user \ + --set-string databasePassword=template-test-db \ + --set-string jwtSecret=template-test-jwt \ + --set-string apiKeySecret=template-test-api \ + --set-string externalUrl=http://localhost:3000 \ + --set heimdall.ingress.enabled=false \ > /tmp/heimdall-external.yaml - echo "✓ Template rendered successfully with external database" - name: Test template rendering (with ingress) run: | - helm template heimdall ./heimdall \ + helm template heimdall ./heimdall2 \ + --set-string jwtSecret=template-test-jwt \ + --set-string databasePassword=template-test-db \ + --set-string apiKeySecret=template-test-api \ + --set-string externalUrl=https://heimdall.example.com \ --set heimdall.ingress.enabled=true \ - --set 'heimdall.ingress.hosts[0].host=heimdall.example.com' \ - --set 'heimdall.ingress.hosts[0].paths[0].path=/' \ + --set-string 'heimdall.ingress.hosts[0].host=heimdall.example.com' \ > /tmp/heimdall-ingress.yaml - echo "✓ Template rendered successfully with ingress enabled" - - name: Verify rendered manifests are valid YAML + - name: Verify rendered manifests run: | - # Verify YAML syntax is valid - cat /tmp/heimdall-embedded.yaml | grep -q "apiVersion:" - cat /tmp/heimdall-external.yaml | grep -q "apiVersion:" - cat /tmp/heimdall-ingress.yaml | grep -q "apiVersion:" - echo "✓ All manifests contain valid Kubernetes resources" + grep -q '^kind: StatefulSet$' /tmp/heimdall-embedded.yaml + grep -q 'value: "db.example.com"' /tmp/heimdall-external.yaml + grep -q '^kind: Ingress$' /tmp/heimdall-ingress.yaml # Stage 7: Runtime Lifecycle Scenarios lifecycle-test: @@ -290,4 +348,6 @@ jobs: EXTERNAL_POSTGRES_HOST: ${{ steps.postgres-service.outputs.host }} EXTERNAL_POSTGRES_PORT: 5432 EXTERNAL_POSTGRES_PASSWORD: lifecycle-postgres-password + CERTS_IMAGE: ${{ vars.MITRE_ARTIFACTORY_URL != '' && format('{0}/docker/ubi8/ubi', vars.MITRE_ARTIFACTORY_URL) || 'registry.access.redhat.com/ubi8/ubi' }} + CERTS_IMAGE_TAG: latest run: tests/lifecycle/run-all.sh diff --git a/.gitignore b/.gitignore index f4fb68b..ab1a0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ # Lifecycle test artifacts tests/lifecycle/.generated/ +tests/.generated/ diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..9d2fa3f --- /dev/null +++ b/tests/README.md @@ -0,0 +1,340 @@ +# Testing the Heimdall Helm chart + +The CI workflow in [`.github/workflows/test.yml`](../.github/workflows/test.yml) implements seven +independent test stages for the `heimdall2/` chart on `main`. Chart-testing settings are defined in +[`.github/ct.yaml`](../.github/ct.yaml). + +Run all commands in this guide from the repository root. + +## Implemented test coverage + +| Stage | CI job | Coverage | +|---|---|---| +| 1 | `lint` | Helm chart linting through `ct lint` | +| 2 | `unit-test` | Helm template unit tests for Secret creation, keys, values, encoding, and externally managed mode | +| 3 | `env-vars-validation` | Required StatefulSet environment variables and Secret references | +| 4 | `schema-validation` | Strict Helm linting and values/template rendering | +| 5 | `integration-test` | Installation into Kubernetes 1.30 with `ct install`, followed by the chart's Helm connection test | +| 6 | `template-test` | Rendering with embedded PostgreSQL, external PostgreSQL, and ingress | +| 7 | `lifecycle-test` | Runtime scenarios for minimum values, PostgreSQL, SOPS, Vault-compatible Secrets, and certificate ConfigMaps | + +Stage 4 currently cannot perform JSON Schema rejection tests because `main` does not contain +`heimdall2/values.schema.json`. It validates the chart with strict Helm linting and rendering, and CI +reports the missing schema as a notice. + +## Local requirements + +The stages collectively require: + +- Docker Desktop +- Helm 3.22.0 +- `kubectl` +- `kind` +- `ct` (Helm chart-testing) +- Python 3 and PyYAML +- The `helm-unittest` plugin +- SOPS and age for Stage 7 +- `openssl` and `curl` for Stage 7 + +Confirm the main tools are available: + +```bash +helm version --short +kubectl version --client +kind version +ct version +python3 --version +sops --version +age --version +``` + +## Stage 1: lint + +The CI command is: + +```bash +ct lint --config .github/ct.yaml +``` + +That command only processes charts changed relative to `main`. A test-only branch may therefore +report `No chart changes detected`. To force local validation of the chart, run: + +```bash +ct lint --config .github/ct.yaml --all +``` + +Expected result: `All charts linted successfully`. + +## Stage 2: unit tests + +The unit suite is in [`tests/unit/secrets_test.yaml`](unit/secrets_test.yaml). It contains four tests +covering the generated Kubernetes Secret and externally managed Secret mode. + +If the plugin is not already installed, install the version used by CI: + +```bash +helm plugin install https://github.com/helm-unittest/helm-unittest --version=v1.0.3 +``` + +Run Stage 2 independently: + +```bash +helm unittest --strict \ + -f '../tests/unit/*_test.yaml' \ + ./heimdall2 +``` + +Expected result: + +```text +Test Suites: 1 passed, 1 total +Tests: 4 passed, 4 total +``` + +## Stage 3: environment-variable validation + +Create an isolated Python environment so local Python packages are not modified: + +```bash +python3 -m venv /tmp/heimdall-ci-python +source /tmp/heimdall-ci-python/bin/activate +pip install pyyaml +``` + +Render the chart: + +```bash +helm template heimdall ./heimdall2 \ + --set-string jwtSecret=env-test-jwt \ + --set-string databasePassword=env-test-db \ + --set-string apiKeySecret=env-test-api \ + --set-string adminPassword=env-test-admin \ + --set-string externalUrl=http://localhost:3000 \ + --set heimdall.ingress.enabled=false \ + > /tmp/heimdall-env.yaml +``` + +Validate the rendered StatefulSet: + +```bash +python - <<'PY' +import yaml + +with open('/tmp/heimdall-env.yaml', encoding='utf-8') as stream: + documents = [document for document in yaml.safe_load_all(stream) if document] + +statefulset = next( + document for document in documents + if document.get('kind') == 'StatefulSet' + and document.get('metadata', {}).get('name') == 'heimdall' +) +environment = statefulset['spec']['template']['spec']['containers'][0]['env'] +by_name = {entry['name']: entry for entry in environment} +required = { + 'NODE_ENV', 'DATABASE_HOST', 'DATABASE_PORT', 'DATABASE_NAME', + 'DATABASE_USERNAME', 'DATABASE_PASSWORD', 'JWT_SECRET', + 'API_KEY_SECRET', 'ADMIN_PASSWORD', 'EXTERNAL_URL', +} +missing = sorted(required - by_name.keys()) +if missing: + raise SystemExit(f'Missing environment variables: {missing}') + +secret_backed = { + 'DATABASE_USERNAME', 'DATABASE_PASSWORD', 'JWT_SECRET', + 'API_KEY_SECRET', 'ADMIN_PASSWORD', +} +invalid = sorted( + name for name in secret_backed + if by_name[name].get('valueFrom', {}).get('secretKeyRef', {}).get('name') != 'heimdall' +) +if invalid: + raise SystemExit(f'Variables not backed by the Heimdall Secret: {invalid}') + +print('Stage 3 passed') +PY +``` + +Clean up the Python environment: + +```bash +deactivate +``` + +## Stage 4: schema and values validation + +Run strict Helm linting: + +```bash +helm lint --strict ./heimdall2 \ + --set-string jwtSecret=schema-test-jwt \ + --set-string databasePassword=schema-test-db \ + --set-string apiKeySecret=schema-test-api \ + --set-string externalUrl=http://localhost:3000 +``` + +Validate values processing and template rendering: + +```bash +helm template heimdall ./heimdall2 \ + --set-string jwtSecret=schema-test-jwt \ + --set-string databasePassword=schema-test-db \ + --set-string apiKeySecret=schema-test-api \ + --set-string externalUrl=http://localhost:3000 \ + > /dev/null +``` + +Both commands must exit successfully. Helm will automatically validate `values.schema.json` if one +is added to the chart in the future. + +## Stage 5: Kubernetes integration + +This stage installs the chart into Kubernetes and runs the chart's Helm test hook. Docker Desktop +must be running. + +Create an isolated cluster matching CI's Kubernetes version: + +```bash +kind create cluster \ + --name heimdall-ci-v130 \ + --image kindest/node:v1.30.0 +``` + +Run the integration test. Local testing uses `--all` so it does not depend on Git history: + +```bash +ct install \ + --config .github/ct.yaml \ + --all \ + --helm-extra-set-args \ + '--set-string jwtSecret=integration-test-jwt --set-string databasePassword=integration-test-db --set-string apiKeySecret=integration-test-api --set-string externalUrl=http://localhost:3000 --set heimdall.ingress.enabled=false' +``` + +Expected result: `All charts installed successfully`. Chart-testing creates and removes an isolated +namespace automatically. + +Delete the temporary cluster: + +```bash +kind delete cluster --name heimdall-ci-v130 +``` + +If another local cluster was active before this test, restore its context afterward. For example: + +```bash +kubectl config use-context kind-heimdall-test +``` + +## Stage 6: template rendering + +Render the embedded PostgreSQL configuration: + +```bash +helm template heimdall ./heimdall2 \ + --set-string jwtSecret=template-test-jwt \ + --set-string databasePassword=template-test-db \ + --set-string apiKeySecret=template-test-api \ + --set-string externalUrl=http://localhost:3000 \ + --set heimdall.ingress.enabled=false \ + > /tmp/heimdall-embedded.yaml +``` + +Render the external PostgreSQL configuration: + +```bash +helm template heimdall ./heimdall2 \ + --set postgresql.enabled=false \ + --set-string databaseHost=db.example.com \ + --set databasePort=5432 \ + --set-string databaseName=heimdall_prod \ + --set-string databaseUsername=heimdall_user \ + --set-string databasePassword=template-test-db \ + --set-string jwtSecret=template-test-jwt \ + --set-string apiKeySecret=template-test-api \ + --set-string externalUrl=http://localhost:3000 \ + --set heimdall.ingress.enabled=false \ + > /tmp/heimdall-external.yaml +``` + +Render the ingress configuration: + +```bash +helm template heimdall ./heimdall2 \ + --set-string jwtSecret=template-test-jwt \ + --set-string databasePassword=template-test-db \ + --set-string apiKeySecret=template-test-api \ + --set-string externalUrl=https://heimdall.example.com \ + --set heimdall.ingress.enabled=true \ + --set-string 'heimdall.ingress.hosts[0].host=heimdall.example.com' \ + > /tmp/heimdall-ingress.yaml +``` + +Check the rendered resources: + +```bash +grep -q '^kind: StatefulSet$' /tmp/heimdall-embedded.yaml +grep -q 'value: "db.example.com"' /tmp/heimdall-external.yaml +grep -q '^kind: Ingress$' /tmp/heimdall-ingress.yaml +echo 'Stage 6 passed' +``` + +## Stage 7: runtime lifecycle scenarios + +The six runtime scenarios are documented in +[`tests/lifecycle/README.md`](lifecycle/README.md). They cover: + +1. Bare-minimum install, HTTP validation, and uninstall. +2. Persistent embedded PostgreSQL and data survival after a pod restart. +3. External PostgreSQL supplied through a GitHub Actions service container. +4. SOPS/age encryption and an externally managed Kubernetes Secret. +5. The external Secret contract used by a Vault integration. +6. Certificate ConfigMap injection and the UBI-based system trust store. + +For a complete local run, create a Kind cluster and external PostgreSQL container: + +```bash +kind create cluster --name heimdall-test +docker run --detach --rm --name heimdall-lifecycle-postgres \ + --env POSTGRES_DB=heimdall \ + --env POSTGRES_USER=postgres \ + --env POSTGRES_PASSWORD=lifecycle-postgres-password \ + --publish 15432:5432 \ + postgres:17 +``` + +Run all scenarios through Docker Desktop and MITRE's UBI mirror: + +```bash +EXTERNAL_POSTGRES_HOST=host.docker.internal \ +EXTERNAL_POSTGRES_PORT=15432 \ +EXTERNAL_POSTGRES_PASSWORD=lifecycle-postgres-password \ +CERTS_IMAGE="/docker/ubi8/ubi" \ +CERTS_IMAGE_TAG=latest \ +tests/lifecycle/run-all.sh +``` + +CI reads the Artifactory base URL from the GitHub Actions repository variable +`MITRE_ARTIFACTORY_URL`. If the variable is not configured, the workflow falls back to the chart's +public UBI image. + +Run one or more scenarios independently with `ONLY`: + +```bash +ONLY='01' tests/lifecycle/run-all.sh +ONLY='03 05' \ + EXTERNAL_POSTGRES_HOST=host.docker.internal \ + EXTERNAL_POSTGRES_PORT=15432 \ + tests/lifecycle/run-all.sh +ONLY='06' \ + CERTS_IMAGE="/docker/ubi8/ubi" \ + tests/lifecycle/run-all.sh +``` + +Clean up the local resources: + +```bash +docker stop heimdall-lifecycle-postgres +kind delete cluster --name heimdall-test +``` + +Every lifecycle script uses its own namespace and attempts to remove it on exit. Generated values, +keys, and certificates are stored under `tests/lifecycle/.generated/` and are ignored by Git. diff --git a/tests/lifecycle/02-embedded-postgres.sh b/tests/lifecycle/02-embedded-postgres.sh index f5ff1f7..9c66963 100755 --- a/tests/lifecycle/02-embedded-postgres.sh +++ b/tests/lifecycle/02-embedded-postgres.sh @@ -7,11 +7,11 @@ trap 'cleanup_ns "$NS"' EXIT log "Scenario 2: embedded PostgreSQL with persistence" gen_min_values helm install "$RELEASE" "$CHART" -n "$NS" --create-namespace -f "$GEN/min.yaml" \ - --set postgresql.primary.persistence.enabled=true \ - --set postgresql.primary.persistence.storageClass="$STORAGE_CLASS" + --set postgresql.persistence.enabled=true \ + --set postgresql.persistence.storageClassName="$STORAGE_CLASS" wait_ready "$NS" -pvc="$(kubectl get pvc -n "$NS" -l app.kubernetes.io/instance="$RELEASE" -o jsonpath='{.items[0].metadata.name}')" +pvc="$RELEASE-db-data" [[ -n "$pvc" ]] || fail "PostgreSQL PVC was not created" [[ "$(kubectl get pvc "$pvc" -n "$NS" -o jsonpath='{.status.phase}')" == Bound ]] \ || fail "PostgreSQL PVC is not Bound" diff --git a/tests/lifecycle/03-github-service-postgres.sh b/tests/lifecycle/03-github-service-postgres.sh index f22795f..70da9da 100755 --- a/tests/lifecycle/03-github-service-postgres.sh +++ b/tests/lifecycle/03-github-service-postgres.sh @@ -11,35 +11,29 @@ trap 'cleanup_ns "$NS"' EXIT || fail "EXTERNAL_POSTGRES_HOST is required; see README.md for local and CI examples" log "Scenario 3: external PostgreSQL at $EXTERNAL_POSTGRES_HOST:$EXTERNAL_POSTGRES_PORT" -gen_min_values -kubectl create namespace "$NS" -kubectl create secret generic external-postgres -n "$NS" \ - --from-literal=password="$EXTERNAL_POSTGRES_PASSWORD" - cat > "$GEN/external-postgres.yaml" </dev/null public_key="$(awk '/public key/ {print $4}' "$GEN/sops-key.txt")" cat > "$GEN/sops-plain.env" < "$GEN/sops-encrypted.env" @@ -28,28 +29,31 @@ SOPS_AGE_KEY_FILE="$GEN/sops-key.txt" sops --decrypt --input-type dotenv --outpu kubectl create secret generic "$SECRET_NAME" -n "$NS" --from-env-file="$GEN/sops-decrypted.env" cat > "$GEN/sops-values.yaml" < "$GEN/vault-values.yaml" </dev/null - -cat > "$GEN/configmap-values.yaml" < "$GEN/configmap-values.yaml" helm install "$RELEASE" "$CHART" -n "$NS" --create-namespace -f "$GEN/configmap-values.yaml" wait_ready "$NS" - -[[ "$(kubectl get configmap "$RELEASE-config" -n "$NS" \ - -o jsonpath='{.data.CLASSIFICATION_BANNER_TEXT}')" == 'LIFECYCLE TEST' ]] \ - || fail "baseline configuration value is missing" -kubectl get configmap "$RELEASE-ca-certs" -n "$NS" >/dev/null \ - || fail "custom CA ConfigMap is missing" - +kubectl get configmap heimdall-cacerts -n "$NS" >/dev/null \ + || fail "certificate ConfigMap is missing" env_output="$(kubectl exec -n "$NS" "$RELEASE-0" -c heimdall-front -- env)" -grep -qx 'NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/ca-bundle.pem' <<<"$env_output" \ - || fail "NODE_EXTRA_CA_CERTS is not set" +grep -qx 'NODE_EXTRA_CA_CERTS=/home/node/certs/lifecycle-ca.pem' <<<"$env_output" \ + || fail "NODE_EXTRA_CA_CERTS does not use the configured filename" +grep -qx 'SSL_CERT_FILE=/home/node/certs/lifecycle-ca.pem' <<<"$env_output" \ + || fail "SSL_CERT_FILE does not use the configured filename" kubectl exec -n "$NS" "$RELEASE-0" -c heimdall-front -- \ - grep -q 'BEGIN CERTIFICATE' /usr/local/share/ca-certificates/ca-bundle.pem \ - || fail "generated CA bundle does not contain a certificate" -pass "baseline ConfigMap and custom CA bundle work" + cat /home/node/certs/lifecycle-ca.pem | diff - "$GEN/ca.pem" >/dev/null \ + || fail "mounted certificate differs from the supplied certificate" +pass "direct certificate injection works" + +log "System trust-store approach with $CERTS_IMAGE:$CERTS_IMAGE_TAG" +helm upgrade "$RELEASE" "$CHART" -n "$NS" -f "$GEN/configmap-values.yaml" \ + --set certs.systemCertsApproach.enabled=true \ + --set certs.systemCertsApproach.image.repository="$CERTS_IMAGE" \ + --set-string certs.systemCertsApproach.image.tag="$CERTS_IMAGE_TAG" +kubectl rollout status statefulset/"$RELEASE" -n "$NS" --timeout="$WAIT_TIMEOUT" +wait_ready "$NS" +[[ "$(kubectl get pod "$RELEASE-0" -n "$NS" -o jsonpath='{.spec.initContainers[0].name}')" \ + == setup-certs ]] || fail "setup-certs init container is missing" +certificate_count="$(kubectl exec -n "$NS" "$RELEASE-0" -c heimdall-front -- \ + awk '/BEGIN CERT/{count++} END{print count}' /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem)" +(( certificate_count > 146 )) \ + || fail "trust bundle has $certificate_count certificates (expected more than 146)" +pass "system trust store contains the injected certificate" diff --git a/tests/lifecycle/README.md b/tests/lifecycle/README.md index 5b1f274..0b10288 100644 --- a/tests/lifecycle/README.md +++ b/tests/lifecycle/README.md @@ -1,20 +1,21 @@ # Lifecycle tests -These end-to-end scenarios install the chart in a disposable Kubernetes cluster and validate the -runtime behaviors required by the Helm testing ticket. Every scenario uses its own namespace and -cleans it up on exit. +These end-to-end scenarios install the `heimdall2/` chart from `main` into a disposable Kubernetes +cluster. Each scenario uses its own namespace and cleans it up when finished. | Script | Scenario | |---|---| -| `01-minimal.sh` | Bare-minimum install, HTTP validation, and clean uninstall | -| `02-embedded-postgres.sh` | Embedded PostgreSQL with persistent data across a pod restart | +| `01-minimal.sh` | Bare-minimum values: install, HTTP validation, and clean uninstall | +| `02-embedded-postgres.sh` | In-chart PostgreSQL with persistent data across a pod restart | | `03-github-service-postgres.sh` | External PostgreSQL supplied by a GitHub Actions service container | -| `04-sops.sh` | SOPS/age encryption with an externally managed Kubernetes Secret | -| `05-vault-secret.sh` | `existingSecret` contract used by Vault or External Secrets integrations | -| `06-configmap-certs.sh` | Baseline ConfigMap values and custom CA certificate bundle | +| `04-sops.sh` | SOPS/age encryption and the chart's externally managed Secret mode | +| `05-vault-secret.sh` | Kubernetes Secret contract used by a Vault integration | +| `06-configmap-certs.sh` | Certificate ConfigMap with direct and system trust-store injection | -The Vault scenario validates the chart boundary after Vault has synchronized a Kubernetes Secret. It -does not deploy Vault or an External Secrets operator. +The chart on `main` does not have a generic `existingSecret` value. It uses `sops.enabled` to suppress +creation of its built-in Secret and expects an external controller to create a Secret named after the +Helm release. The Vault scenario validates that boundary; it does not deploy Vault or an External +Secrets operator. ## Requirements @@ -24,7 +25,7 @@ Install `helm`, `kubectl`, `openssl`, `curl`, `sops`, and `age`, and use a dispo ## Run locally -Create a cluster and a local PostgreSQL container for scenario 3: +Create a cluster and an external PostgreSQL container for scenario 3: ```bash kind create cluster --name heimdall-test @@ -36,7 +37,7 @@ docker run --detach --rm --name heimdall-lifecycle-postgres \ postgres:17 ``` -On Docker Desktop, run all scenarios with: +On Docker Desktop, run every scenario with: ```bash EXTERNAL_POSTGRES_HOST=host.docker.internal \ @@ -50,11 +51,35 @@ Run selected scenarios with `ONLY`, for example: ONLY="01 04 05" tests/lifecycle/run-all.sh ``` -Clean up the local resources afterward: +Clean up afterward: ```bash docker stop heimdall-lifecycle-postgres kind delete cluster --name heimdall-test ``` -Generated keys, values, and certificates are written to `.generated/`, which is gitignored. +## MITRE Artifactory images + +MITRE Artifactory's Docker virtual repository proxies the official PostgreSQL image and includes the +PostgreSQL server and `psql` client: + +```text +/docker/postgres:17 +``` + +The UBI image used by the system-certificate scenario is also available through Artifactory: + +```text +/docker/ubi8/ubi:latest +``` + +Use the UBI mirror without changing the chart defaults: + +```bash +CERTS_IMAGE="/docker/ubi8/ubi" \ +CERTS_IMAGE_TAG=latest \ +tests/lifecycle/06-configmap-certs.sh +``` + +Options include `STORAGE_CLASS` (default `standard`) and `WAIT_TIMEOUT` (default `600s`). Generated +keys, values, and certificates are written to `tests/lifecycle/.generated/`, which is gitignored. diff --git a/tests/lifecycle/lib.sh b/tests/lifecycle/lib.sh index 61e1537..d6b5e1c 100755 --- a/tests/lifecycle/lib.sh +++ b/tests/lifecycle/lib.sh @@ -5,7 +5,7 @@ set -euo pipefail TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$TESTS_DIR/../.." && pwd)" -CHART="$REPO_ROOT/heimdall" +CHART="$REPO_ROOT/heimdall2" GEN="$TESTS_DIR/.generated" RELEASE="heimdall" WAIT_TIMEOUT="${WAIT_TIMEOUT:-600s}" @@ -25,35 +25,31 @@ require() { } check_context() { - local ctx - ctx="$(kubectl config current-context)" - log "kubectl context: $ctx" - if [[ "$ctx" != kind-* && "$ctx" != k3d-* && "${ALLOW_ANY_CONTEXT:-0}" != 1 ]]; then - fail "context '$ctx' is not kind/k3d; set ALLOW_ANY_CONTEXT=1 to override" + local context + context="$(kubectl config current-context)" + log "kubectl context: $context" + if [[ "$context" != kind-* && "$context" != k3d-* && "${ALLOW_ANY_CONTEXT:-0}" != 1 ]]; then + fail "context '$context' is not kind/k3d; set ALLOW_ANY_CONTEXT=1 to override" fi kubectl wait --for=condition=Ready node --all --timeout=120s \ || fail "cluster nodes are not Ready" } gen_min_values() { - DB_PASSWORD="$(openssl rand -hex 24)" + DB_PASSWORD="$(openssl rand -hex 33)" export DB_PASSWORD cat > "$GEN/min.yaml" </dev/null 2>&1 & - pf=$! + port_forward_pid=$! for _ in $(seq 1 30); do code="$(curl --silent --output /dev/null --write-out '%{http_code}' "http://localhost:$port/" || true)" [[ "$code" == 200 ]] && break sleep 2 done - kill "$pf" >/dev/null 2>&1 || true - wait "$pf" 2>/dev/null || true + kill "$port_forward_pid" >/dev/null 2>&1 || true + wait "$port_forward_pid" 2>/dev/null || true [[ "$code" == 200 ]] || fail "GET / returned $code (expected 200)" pass "application serves HTTP 200" } @@ -91,5 +87,5 @@ check_http() { psql_in() { local namespace="$1" sql="$2" kubectl exec -n "$namespace" "$RELEASE-postgresql-0" -- \ - env PGPASSWORD="$DB_PASSWORD" psql -U postgres -d heimdall -tA -c "$sql" + env PGPASSWORD="$DB_PASSWORD" psql -U postgres -d heimdall-database -tA -c "$sql" } diff --git a/tests/lifecycle/run-all.sh b/tests/lifecycle/run-all.sh index b33f2a5..7fd0e18 100755 --- a/tests/lifecycle/run-all.sh +++ b/tests/lifecycle/run-all.sh @@ -5,10 +5,6 @@ source "$(dirname "$0")/lib.sh" require helm kubectl openssl curl check_context -log "Build chart dependencies" -helm repo add bitnami https://charts.bitnami.com/bitnami --force-update -helm dependency build "$CHART" - failed=() for scenario in "$TESTS_DIR"/0*.sh; do base="$(basename "$scenario")" diff --git a/tests/unit/__snapshot__/.gitkeep b/tests/unit/__snapshot__/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/unit/__snapshot__/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/unit/secrets_test.yaml b/tests/unit/secrets_test.yaml new file mode 100644 index 0000000..dccb0e4 --- /dev/null +++ b/tests/unit/secrets_test.yaml @@ -0,0 +1,45 @@ +suite: Heimdall Secret +templates: + - templates/secrets.yaml +set: + jwtSecret: unit-test-jwt + databaseUsername: unit-test-user + databasePassword: unit-test-password + apiKeySecret: unit-test-api-key + adminPassword: unit-test-admin-password +tests: + - it: renders the application Secret + asserts: + - isKind: + of: Secret + - equal: + path: metadata.name + value: RELEASE-NAME-heimdall + + - it: stores string values under the keys consumed by the StatefulSet + asserts: + - equal: + path: stringData.jwtSecret + value: unit-test-jwt + - equal: + path: stringData.databaseUsername + value: unit-test-user + - equal: + path: stringData.apiKeySecret + value: unit-test-api-key + - equal: + path: stringData.adminPassword + value: unit-test-admin-password + + - it: base64-encodes the database password + asserts: + - equal: + path: data.databasePassword + value: dW5pdC10ZXN0LXBhc3N3b3Jk + + - it: omits the chart-managed Secret in externally managed mode + set: + sops.enabled: true + asserts: + - hasDocuments: + count: 0