{{- if and (not .Values.devEnv.enabled) .Values.setup.applianceBootstrap.enabled }} apiVersion: v1 kind: ConfigMap metadata: name: {{ include "sebastian.fullname" . }}-appliance-bootstrap namespace: {{ include "sebastian.namespace" . }} labels: {{- include "sebastian.labels" . | nindent 4 }} data: appliance-bootstrap.sh: | #!/bin/sh set -eu log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" } is_enabled() { case "${1:-true}" in [Ff][Aa][Ll][Ss][Ee]|0|[Nn][Oo]|[Oo][Ff][Ff]) return 1 ;; *) return 0 ;; esac } get_admin_password() { local password="" if [ -f /run/secrets/postgres_password ]; then password="$(cat /run/secrets/postgres_password 2>/dev/null)" fi if [ -z "$password" ] && [ -n "${DB_PASSWORD_ADMIN:-}" ]; then password="${DB_PASSWORD_ADMIN}" fi if [ -z "$password" ] && [ -n "${DB_PASSWORD_SUPERUSER:-}" ]; then password="${DB_PASSWORD_SUPERUSER}" fi echo "$password" | tr -d '[:space:]' } psql_admin() { local database="$1" shift local pg_admin_host="${DB_HOST_ADMIN:-${DB_HOST:-postgres}}" local pg_admin_port="${DB_PORT_ADMIN:-${DB_PORT:-5432}}" local pg_admin_user="${DB_USER_ADMIN:-${DB_USER:-postgres}}" local pg_password pg_password="$(get_admin_password)" PGPASSWORD="${pg_password}" psql -v ON_ERROR_STOP=1 -h "${pg_admin_host}" -p "${pg_admin_port}" -U "${pg_admin_user}" -d "${database}" "$@" } psql_quiet() { local database="$1" shift local pg_admin_host="${DB_HOST_ADMIN:-${DB_HOST:-postgres}}" local pg_admin_port="${DB_PORT_ADMIN:-${DB_PORT:-5432}}" local pg_admin_user="${DB_USER_ADMIN:-${DB_USER:-postgres}}" local pg_password pg_password="$(get_admin_password)" PGPASSWORD="${pg_password}" psql -v ON_ERROR_STOP=1 -h "${pg_admin_host}" -p "${pg_admin_port}" -U "${pg_admin_user}" -d "${database}" "$@" 2>&1 } wait_for_postgres() { local pg_admin_host="${DB_HOST_ADMIN:-${DB_HOST:-postgres}}" local pg_admin_port="${DB_PORT_ADMIN:-${DB_PORT:-5432}}" local pg_admin_user="${DB_USER_ADMIN:-${DB_USER:-postgres}}" local pg_password local output local timeout_seconds="${BOOTSTRAP_WAIT_TIMEOUT_SECONDS:-300}" local retry_seconds="${BOOTSTRAP_WAIT_RETRY_SECONDS:-2}" local started_at local now pg_password="$(get_admin_password)" if [ -z "$pg_password" ]; then log "ERROR: No admin database password available" exit 1 fi started_at="$(date +%s)" while true; do output="$(PGPASSWORD="${pg_password}" psql -h "${pg_admin_host}" -p "${pg_admin_port}" -U "${pg_admin_user}" -d postgres -c '\q' 2>&1)" && break now="$(date +%s)" case "$output" in *"password authentication failed"*|*"no password supplied"*|*"SASL authentication failed"*|*"role \""* ) log "ERROR: PostgreSQL authentication failed for ${pg_admin_user} at ${pg_admin_host}:${pg_admin_port}" log "ERROR: Existing appliance data may not match the current db-credentials secret. Reuse the original credentials in recover mode or wipe appliance data before a fresh install." exit 1 ;; *) if [ $((now - started_at)) -ge "${timeout_seconds}" ]; then log "ERROR: PostgreSQL did not become reachable within ${timeout_seconds}s at ${pg_admin_host}:${pg_admin_port}" log "ERROR: ${output}" exit 1 fi log "PostgreSQL is not ready yet at ${pg_admin_host}:${pg_admin_port} - retrying" ;; esac sleep "${retry_seconds}" done log "PostgreSQL is up and running" } database_exists() { local database_name="$1" psql_quiet postgres -Atqc "SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname='${database_name}');" | grep -q '^t$' } users_table_exists() { psql_quiet "${DB_NAME_SERVER:-server}" -Atqc "SELECT to_regclass('public.users') IS NOT NULL;" | grep -q '^t$' } seed_row_count() { psql_quiet "${DB_NAME_SERVER:-server}" -Atqc "SELECT COUNT(*) FROM users;" | tr -d '[:space:]' } preflight_database_state() { local server_db=false local hocuspocus_db=false local users_table=false local user_count=0 if database_exists "${DB_NAME_SERVER:-server}"; then server_db=true if users_table_exists; then users_table=true user_count="$(seed_row_count)" fi fi if database_exists "${DB_NAME_HOCUSPOCUS:-hocuspocus}"; then hocuspocus_db=true fi log "Database preflight: bootstrap_mode=${BOOTSTRAP_MODE:-recover} server_db=${server_db} hocuspocus_db=${hocuspocus_db} users_table=${users_table} user_count=${user_count}" if [ "${BOOTSTRAP_MODE:-recover}" = "fresh" ] && { [ "$server_db" = "true" ] || [ "$hocuspocus_db" = "true" ] || [ "$user_count" -gt 0 ]; }; then log "ERROR: Existing application database state detected while bootstrap.mode=fresh." log "ERROR: Wipe persisted appliance data before a fresh install or rerun in recover mode." exit 1 fi } check_seeds_status() { local pg_admin_host="${DB_HOST_ADMIN:-${DB_HOST:-postgres}}" local pg_admin_port="${DB_PORT_ADMIN:-${DB_PORT:-5432}}" local pg_admin_user="${DB_USER_ADMIN:-${DB_USER:-postgres}}" local pg_password pg_password="$(get_admin_password)" PGPASSWORD="${pg_password}" psql -h "${pg_admin_host}" -p "${pg_admin_port}" -U "${pg_admin_user}" -d "${DB_NAME_SERVER:-server}" -tAc "SELECT EXISTS (SELECT 1 FROM users LIMIT 1);" 2>/dev/null | grep -q '^t$' } require_initial_tenant_value() { local name="$1" eval "value=\${${name}:-}" if [ -z "${value}" ]; then log "ERROR: ${name} is required to create the initial appliance tenant" log "ERROR: Reopen appliance setup and provide the initial company/admin fields." exit 1 fi } ensure_initial_tenant() { if check_seeds_status; then log "Initial user already exists; skipping initial tenant creation" return 0 fi require_initial_tenant_value INITIAL_TENANT_NAME require_initial_tenant_value INITIAL_ADMIN_FIRST_NAME require_initial_tenant_value INITIAL_ADMIN_LAST_NAME require_initial_tenant_value INITIAL_ADMIN_EMAIL require_initial_tenant_value INITIAL_ADMIN_PASSWORD log "Creating initial appliance tenant and admin user" # INITIAL_TENANT_ID (set when an install code was redeemed) makes # create-tenant adopt the registry-minted tenant id; empty => DB-generated. INITIAL_ADMIN_PASSWORD="${INITIAL_ADMIN_PASSWORD}" INITIAL_TENANT_ID="${INITIAL_TENANT_ID:-}" npx tsx /app/server/scripts/create-tenant.ts \ --tenant "${INITIAL_TENANT_NAME}" \ --companyName "${INITIAL_TENANT_NAME}" \ --clientName "${INITIAL_TENANT_NAME}" \ --email "${INITIAL_ADMIN_EMAIL}" \ --firstName "${INITIAL_ADMIN_FIRST_NAME}" \ --lastName "${INITIAL_ADMIN_LAST_NAME}" \ --productCode psa log "Initial appliance tenant and admin user created" } resolve_seeds_dir() { if [ -n "${SEEDS_DIR:-}" ] && [ -d "${SEEDS_DIR}/psa" ] && ! find "${SEEDS_DIR}" -maxdepth 1 -name '*.cjs' -print -quit | grep -q .; then SEEDS_DIR="${SEEDS_DIR}/psa" export SEEDS_DIR fi } ensure_database() { local db_name="$1" psql_admin postgres -v db_name="${db_name}" <<'SQL' SELECT CASE WHEN EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db_name') THEN NULL ELSE format('CREATE DATABASE %I', :'db_name') END \gexec SQL } ensure_role() { local role_name="$1" local role_password="$2" local encryption="${3:-scram-sha-256}" psql_admin postgres -v role_name="${role_name}" -v role_password="${role_password}" -v role_encryption="${encryption}" <<'SQL' SET password_encryption = :'role_encryption'; SELECT CASE WHEN EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'role_name') THEN format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'role_name', :'role_password') ELSE format('CREATE ROLE %I WITH LOGIN PASSWORD %L', :'role_name', :'role_password') END \gexec SQL } grant_server_privileges() { local role_name="$1" local db_name="${DB_NAME_SERVER:-server}" psql_admin postgres -v db_name="${db_name}" -v role_name="${role_name}" <<'SQL' SELECT format('GRANT CONNECT ON DATABASE %I TO %I', :'db_name', :'role_name') \gexec SQL psql_admin "${db_name}" -v role_name="${role_name}" <<'SQL' SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'role_name') \gexec SELECT format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO %I', :'role_name') \gexec SELECT format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO %I', :'role_name') \gexec SELECT format('GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO %I', :'role_name') \gexec SELECT format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON SEQUENCES TO %I', :'role_name') \gexec SQL } grant_hocuspocus_privileges() { local role_name="$1" local db_name="${DB_NAME_HOCUSPOCUS:-hocuspocus}" psql_admin postgres -v db_name="${db_name}" -v role_name="${role_name}" <<'SQL' SELECT format('GRANT CONNECT ON DATABASE %I TO %I', :'db_name', :'role_name') \gexec SQL psql_admin "${db_name}" -v role_name="${role_name}" <<'SQL' SELECT format('GRANT ALL PRIVILEGES ON SCHEMA public TO %I', :'role_name') \gexec SELECT format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO %I', :'role_name') \gexec SELECT format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO %I', :'role_name') \gexec SQL } configure_server_database() { psql_admin "${DB_NAME_SERVER:-server}" -v app_env="${APP_ENV:-production}" <<'SQL' CREATE EXTENSION IF NOT EXISTS vector; SELECT format('ALTER DATABASE %I SET app.environment = %L', current_database(), :'app_env') \gexec GRANT ALL PRIVILEGES ON SCHEMA public TO postgres; SQL psql_admin postgres -v db_name="${DB_NAME_SERVER:-server}" <<'SQL' SELECT format('GRANT ALL PRIVILEGES ON DATABASE %I TO postgres', :'db_name') \gexec SQL } wait_for_postgres preflight_database_state log "Creating appliance databases and roles" ensure_database "${DB_NAME_HOCUSPOCUS:-hocuspocus}" ensure_role "${DB_USER_HOCUSPOCUS:-hocuspocus_user}" "${DB_PASSWORD_HOCUSPOCUS:?DB_PASSWORD_HOCUSPOCUS is required}" "scram-sha-256" grant_hocuspocus_privileges "${DB_USER_HOCUSPOCUS:-hocuspocus_user}" ensure_database "${DB_NAME_SERVER:-server}" ensure_role "${DB_USER_SERVER:-app_user}" "${DB_PASSWORD_SERVER:?DB_PASSWORD_SERVER is required}" "scram-sha-256" if [ -n "${DB_USER_PGBOUNCER:-}" ] && [ -n "${DB_PASSWORD_PGBOUNCER:-}" ] && [ "${DB_USER_PGBOUNCER}" != "${DB_USER_SERVER:-app_user}" ]; then ensure_role "${DB_USER_PGBOUNCER}" "${DB_PASSWORD_PGBOUNCER}" "scram-sha-256" fi configure_server_database grant_server_privileges "${DB_USER_SERVER:-app_user}" if [ -n "${DB_USER_PGBOUNCER:-}" ] && [ "${DB_USER_PGBOUNCER}" != "${DB_USER_SERVER:-app_user}" ]; then grant_server_privileges "${DB_USER_PGBOUNCER}" fi if is_enabled "${SETUP_RUN_MIGRATIONS:-true}"; then pg_admin_host="${DB_HOST_ADMIN:-${DB_HOST:-postgres}}" pg_admin_port="${DB_PORT_ADMIN:-${DB_PORT:-5432}}" pg_admin_user="${DB_USER_ADMIN:-${DB_USER:-postgres}}" pg_password="$(get_admin_password)" log "Creating pgboss schema" PGPASSWORD="${pg_password}" psql -h "${pg_admin_host}" -p "${pg_admin_port}" -U "${pg_admin_user}" -d "${DB_NAME_SERVER:-server}" -c 'CREATE SCHEMA IF NOT EXISTS pgboss;' log "Granting necessary permissions" PGPASSWORD="${pg_password}" psql -h "${pg_admin_host}" -p "${pg_admin_port}" -U "${pg_admin_user}" -d "${DB_NAME_SERVER:-server}" -c 'GRANT ALL ON SCHEMA public TO postgres;' log "Running migrations" NODE_ENV=migration timeout 300 npx knex migrate:latest --knexfile /app/server/knexfile.cjs --verbose else log "SETUP_RUN_MIGRATIONS is disabled; skipping migrations" fi # Seed license_state from appliance-license-seed Secret (F082-F084). # ALWAYS seeds a row on a self-hosted/appliance install (defaulting to an # 'ee' 15-day trial when EDITION_CHOICE is absent). A missing/empty # appliance-license-seed Secret previously left license_state empty, which # silently bricks licensing: getLicenseStatus returns selfHostMode=false, so # the in-app License page shows "only available for self-hosted" with NO # airgap form (the form only renders once a row exists, so there is no UI # path to apply a license) and addUser's seat check is skipped entirely. # Idempotent: WHERE NOT EXISTS never resets an existing entitlement. if [ -z "${EDITION_CHOICE:-}" ]; then log "WARNING: EDITION_CHOICE unset (appliance-license-seed Secret missing/empty); defaulting license_state to an 'ee' trial so the License page and seat enforcement stay usable" fi { pg_admin_host="${DB_HOST_ADMIN:-${DB_HOST:-postgres}}" pg_admin_port="${DB_PORT_ADMIN:-${DB_PORT:-5432}}" pg_admin_user="${DB_USER_ADMIN:-${DB_USER:-postgres}}" pg_password="$(get_admin_password)" edition_choice="${EDITION_CHOICE:-ee}" license_token="${LICENSE_TOKEN:-}" # Registry edition from an install-code redeem (essentials|pro|premium); # empty on the manual editionChoice/licenseKey path. install_edition="${INSTALL_EDITION:-}" # Connected-appliance refresh fields (paid install-code redeem only). appliance_id="${APPLIANCE_ID:-}" check_in_url="${CHECK_IN_URL:-}" appliance_credential="${APPLIANCE_CREDENTIAL:-}" log "Seeding license_state (edition_choice=${edition_choice}${install_edition:+ install_edition=${install_edition}})" # trial_started_at: NOW() only for an 'ee' install with no license AND not an # explicit essentials registration (essentials should resolve to essentials # tier, not auto-start a premium trial; the operator can start one later). if [ "${edition_choice}" = "ee" ] && [ -z "${license_token}" ] && [ "${install_edition}" != "essentials" ]; then trial_expr="NOW()" else trial_expr="NULL" fi # Build a SQL value (NULL if empty, else single-quoted + escaped). sql_value() { if [ -z "$1" ]; then echo "NULL" else echo "'$(echo "$1" | sed "s/'/''/g")'" fi } license_token_sql="$(sql_value "${license_token}")" appliance_id_sql="$(sql_value "${appliance_id}")" check_in_url_sql="$(sql_value "${check_in_url}")" appliance_credential_sql="$(sql_value "${appliance_credential}")" PGPASSWORD="${pg_password}" psql -v ON_ERROR_STOP=1 -h "${pg_admin_host}" -p "${pg_admin_port}" \ -U "${pg_admin_user}" -d "${DB_NAME_SERVER:-server}" <