PSA/helm/templates/appliance-bootstrap-configmap.yaml
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

394 lines
16 KiB
YAML

{{- 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}" <<SQL
INSERT INTO license_state (edition_choice, trial_started_at, license_token, appliance_id, check_in_url, appliance_credential, updated_at)
SELECT '${edition_choice}', ${trial_expr}, ${license_token_sql}, ${appliance_id_sql}, ${check_in_url_sql}, ${appliance_credential_sql}, NOW()
WHERE NOT EXISTS (SELECT 1 FROM license_state);
SQL
log "license_state seeded (edition_choice=${edition_choice})"
}
if is_enabled "${SETUP_RUN_SEEDS:-true}"; then
ensure_initial_tenant
resolve_seeds_dir
if [ -n "${SEEDS_DIR:-}" ] && [ ! -d "${SEEDS_DIR}" ]; then
log "ERROR: Configured seed directory does not exist: ${SEEDS_DIR}"
exit 1
fi
log "Running seeds from ${SEEDS_DIR:-./seeds/dev}"
NODE_ENV=migration timeout 300 npx knex seed:run --knexfile /app/server/knexfile.cjs --verbose
if ! check_seeds_status; then
log "ERROR: Appliance bootstrap finished seed execution but no admin users exist"
exit 1
fi
else
log "SETUP_RUN_SEEDS is disabled; skipping seeds"
fi
log "Appliance setup completed"
{{- end }}