[infra] Unify self-hosted deployment paths in getting-started.sh

Make getting-started.sh the single entry point for all self-hosted
deployments. The wizard now asks two independent questions - identity
provider (built-in vs standalone OIDC) and architecture (combined
netbird-server vs separate management/signal/relay containers) - and
renders the matching Docker Compose deployment with full reverse-proxy
parity (built-in Traefik, external Traefik, Nginx, NPM, Caddy, manual).

Highlights:
- setup.env contract: every wizard answer is persisted; --non-interactive
  re-renders idempotently from the file (IaC), --render-only generates
  without starting services. Secrets are generated once and appended so
  re-renders never rotate them.
- Split architecture renders a modern management.json (embedded Dex or
  external OIDC via PKCE), drops coturn entirely (the relay container
  serves STUN via NB_ENABLE_STUN), and optionally adds a PostgreSQL
  container when the postgres engine is selected without a DSN.
- The standalone-IdP path is framed around its real differentiator:
  multi-account support. The built-in IdP supports external SSO
  connectors but enforces single account mode.
- configure.sh, getting-started-with-dex.sh and
  getting-started-with-zitadel.sh print deprecation banners; their
  templates are frozen pending removal.
- New tests/test-render.sh validates all 8 combos (JSON validity,
  compose config, idempotent re-render, combined+external rejection)
  and runs in CI as the test-render-matrix job.
This commit is contained in:
jnfrati
2026-06-11 13:34:30 +02:00
parent 079bce3c2f
commit 8e05834384
7 changed files with 2045 additions and 212 deletions
+196
View File
@@ -0,0 +1,196 @@
#!/bin/bash
# Render-validation tests for getting-started.sh.
# Runs the script in --non-interactive --render-only mode for every supported
# architecture / IdP / reverse-proxy combination and validates the generated
# files. Requires jq; uses `docker compose config` when docker is available.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GETTING_STARTED="$SCRIPT_DIR/../getting-started.sh"
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
FAILURES=0
compose_validate() {
if docker compose version &>/dev/null; then
docker compose -f "$1" config -q
return $?
fi
if command -v docker-compose &>/dev/null; then
docker-compose -f "$1" config -q
return $?
fi
# Without docker fall back to a structural sanity check
grep -q "^services:" "$1"
return $?
}
write_oidc_fixture() {
# A file:// OpenID discovery document stands in for a real IdP
cat > "$1" <<EOF
{
"issuer": "https://idp.example.org/realms/netbird",
"authorization_endpoint": "https://idp.example.org/realms/netbird/protocol/openid-connect/auth",
"token_endpoint": "https://idp.example.org/realms/netbird/protocol/openid-connect/token",
"jwks_uri": "https://idp.example.org/realms/netbird/protocol/openid-connect/certs",
"device_authorization_endpoint": "https://idp.example.org/realms/netbird/protocol/openid-connect/auth/device"
}
EOF
}
run_case() {
local name="$1"
shift
local expected_files=("$@")
local case_dir="$WORK_DIR/$name"
mkdir -p "$case_dir"
# setup.env content is provided on stdin
cat > "$case_dir/setup.env"
echo "--- case: $name"
if ! (cd "$case_dir" && bash "$GETTING_STARTED" --non-interactive --render-only > render.log 2>&1); then
echo "FAIL($name): render exited non-zero"
tail -5 "$case_dir/render.log" | sed 's/^/ /'
FAILURES=$((FAILURES + 1))
return 0
fi
for f in "${expected_files[@]}"; do
if [[ ! -f "$case_dir/$f" ]]; then
echo "FAIL($name): expected file $f was not generated"
FAILURES=$((FAILURES + 1))
fi
done
if [[ -f "$case_dir/management.json" ]] && ! jq . "$case_dir/management.json" > /dev/null; then
echo "FAIL($name): management.json is not valid JSON"
FAILURES=$((FAILURES + 1))
fi
if [[ -f "$case_dir/docker-compose.yml" ]] && ! compose_validate "$case_dir/docker-compose.yml"; then
echo "FAIL($name): docker-compose.yml failed validation"
FAILURES=$((FAILURES + 1))
fi
# Re-rendering from the persisted setup.env must be byte-identical (idempotent)
local checksums_before checksums_after
checksums_before=$(cd "$case_dir" && sha256sum docker-compose.yml dashboard.env config.yaml management.json 2>/dev/null || true)
if ! (cd "$case_dir" && bash "$GETTING_STARTED" --non-interactive --render-only > render2.log 2>&1); then
echo "FAIL($name): re-render exited non-zero"
FAILURES=$((FAILURES + 1))
return 0
fi
checksums_after=$(cd "$case_dir" && sha256sum docker-compose.yml dashboard.env config.yaml management.json 2>/dev/null || true)
if [[ "$checksums_before" != "$checksums_after" ]]; then
echo "FAIL($name): re-render from setup.env is not idempotent"
FAILURES=$((FAILURES + 1))
fi
return 0
}
OIDC_FIXTURE="$WORK_DIR/openid-configuration-fixture.json"
write_oidc_fixture "$OIDC_FIXTURE"
run_case combined-embedded-traefik docker-compose.yml config.yaml dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="combined"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="0"
NETBIRD_TRAEFIK_ACME_EMAIL="admin@example.org"
EOF
run_case combined-embedded-nginx docker-compose.yml config.yaml dashboard.env nginx-netbird.conf <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="combined"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="2"
EOF
run_case split-embedded-traefik docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="0"
NETBIRD_TRAEFIK_ACME_EMAIL="admin@example.org"
EOF
run_case split-embedded-external-traefik docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="embedded"
NETBIRD_REVERSE_PROXY_TYPE="1"
NETBIRD_TRAEFIK_ENTRYPOINT="websecure"
NETBIRD_TRAEFIK_CERTRESOLVER="letsencrypt"
EOF
run_case split-external-nginx-postgres docker-compose.yml management.json dashboard.env nginx-netbird.conf <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_AUTH_CLIENT_SECRET="some-secret"
NETBIRD_REVERSE_PROXY_TYPE="2"
NETBIRD_STORE_CONFIG_ENGINE="postgres"
EOF
run_case split-external-manual docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="split"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_REVERSE_PROXY_TYPE="5"
NETBIRD_BIND_LOCALHOST_ONLY="false"
EOF
# Auto-forced split must be accepted when no architecture is given with external IdP
run_case split-external-defaulted docker-compose.yml management.json dashboard.env <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_REVERSE_PROXY_TYPE="5"
EOF
# Invalid combination must fail: combined + external IdP
echo "--- case: combined-external-rejected"
REJECT_DIR="$WORK_DIR/combined-external-rejected"
mkdir -p "$REJECT_DIR"
cat > "$REJECT_DIR/setup.env" <<EOF
NETBIRD_DOMAIN="netbird.example.org"
NETBIRD_ARCHITECTURE="combined"
NETBIRD_IDP_MODE="external"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="file://$OIDC_FIXTURE"
NETBIRD_AUTH_CLIENT_ID="netbird"
NETBIRD_REVERSE_PROXY_TYPE="5"
EOF
if (cd "$REJECT_DIR" && bash "$GETTING_STARTED" --non-interactive --render-only > render.log 2>&1); then
echo "FAIL(combined-external-rejected): combined + external IdP was not rejected"
FAILURES=$((FAILURES + 1))
fi
# Spot-check rendered content
SPLIT_EXT="$WORK_DIR/split-external-nginx-postgres"
if [[ -f "$SPLIT_EXT/management.json" ]]; then
[[ $(jq -r '.HttpConfig.AuthIssuer' "$SPLIT_EXT/management.json") == "https://idp.example.org/realms/netbird" ]] || { echo "FAIL: external AuthIssuer mismatch"; FAILURES=$((FAILURES + 1)); }
[[ $(jq -r '.EmbeddedIdP' "$SPLIT_EXT/management.json") == "null" ]] || { echo "FAIL: external mode must not configure EmbeddedIdP"; FAILURES=$((FAILURES + 1)); }
[[ $(jq -r '.StoreConfig.Engine' "$SPLIT_EXT/management.json") == "postgres" ]] || { echo "FAIL: postgres engine not set"; FAILURES=$((FAILURES + 1)); }
grep -q "postgres:" "$SPLIT_EXT/docker-compose.yml" || { echo "FAIL: postgres container missing"; FAILURES=$((FAILURES + 1)); }
fi
SPLIT_EMB="$WORK_DIR/split-embedded-traefik"
if [[ -f "$SPLIT_EMB/management.json" ]]; then
[[ $(jq -r '.EmbeddedIdP.Enabled' "$SPLIT_EMB/management.json") == "true" ]] || { echo "FAIL: embedded IdP not enabled in split mode"; FAILURES=$((FAILURES + 1)); }
[[ $(jq -r '.Relay.Addresses[0]' "$SPLIT_EMB/management.json") == "rels://netbird.example.org:443" ]] || { echo "FAIL: relay address mismatch"; FAILURES=$((FAILURES + 1)); }
fi
echo ""
if [[ $FAILURES -gt 0 ]]; then
echo "$FAILURES test(s) failed"
exit 1
fi
echo "All render tests passed"