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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
394 lines
16 KiB
YAML
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 }}
|