PSA/helm/templates/jobs.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

453 lines
20 KiB
YAML

{{- if and (not .Values.devEnv.enabled) (or .Values.setup.runMigrations .Values.setup.runSeeds) }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "sebastian.fullname" . }}-bootstrap
namespace: {{ include "sebastian.namespace" . }}
{{- if not .Values.setup.applianceBootstrap.enabled }}
# SaaS: run as a post-install/upgrade hook. NOT for the appliance — the alga
# core Deployment's wait-for-bootstrap init blocks readiness until this job
# creates the `users` table, but a post-install hook only fires AFTER the
# install succeeds, which can't happen while readiness is blocked. That
# deadlocks the appliance install, so there it runs as a regular concurrent Job.
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
{{- end }}
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
name: {{ include "sebastian.fullname" . }}-bootstrap
spec:
{{- if .Values.setup.image.is_private }}
imagePullSecrets:
- name: "{{ .Values.setup.image.credentials }}"
{{- end }}
containers:
- name: bootstrap
image: "{{ .Values.setup.image.name }}:{{ .Values.setup.image.tag }}"
imagePullPolicy: {{ .Values.setup.pullPolicy }}
{{- if .Values.setup.applianceBootstrap.enabled }}
command: ["/bin/sh", "/bootstrap/appliance-bootstrap.sh"]
{{- else }}
command: ["/bin/sh", "-ec"]
args:
- |
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_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 -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
pg_password="$(get_admin_password)"
if [ -z "$pg_password" ]; then
log "ERROR: No admin database password available"
exit 1
fi
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
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
;;
*"could not translate host name"*|*"Name or service not known"*|*"Temporary failure in name resolution"* )
log "ERROR: PostgreSQL DNS resolution failed for ${pg_admin_host}"
log "ERROR: ${output}"
exit 1
;;
*)
log "PostgreSQL is unavailable at ${pg_admin_host}:${pg_admin_port} - sleeping"
;;
esac
sleep 1
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$'
}
wait_for_postgres
preflight_database_state
log "Creating databases and roles"
timeout 120 node /app/server/setup/create_database.js
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
if is_enabled "${SETUP_RUN_SEEDS:-true}"; then
if check_seeds_status; then
log "Seeds already exist; skipping"
else
log "Running seeds"
NODE_ENV=migration timeout 300 npx knex seed:run --knexfile /app/server/knexfile.cjs --verbose
fi
else
log "SETUP_RUN_SEEDS is disabled; skipping seeds"
fi
log "Setup completed"
{{- end }}
env:
- name: APP_NAME
value: "{{ .Values.nameOverride }}"
- name: APP_ENV
value: "{{ .Values.env | default "production" }}"
- name: NODE_ENV
value: "{{ .Values.env | default "production" }}"
- name: EDITION
value: "{{ .Values.edition | default "community" }}"
- name: SETUP_RUN_MIGRATIONS
value: {{ ternary "true" "false" .Values.setup.runMigrations | quote }}
- name: SETUP_RUN_SEEDS
value: {{ ternary "true" "false" .Values.setup.runSeeds | quote }}
- name: BOOTSTRAP_MODE
value: {{ .Values.bootstrap.mode | default "recover" | quote }}
- name: BOOTSTRAP_WAIT_TIMEOUT_SECONDS
value: {{ .Values.setup.applianceBootstrap.waitTimeoutSeconds | default 300 | quote }}
- name: BOOTSTRAP_WAIT_RETRY_SECONDS
value: {{ .Values.setup.applianceBootstrap.retryIntervalSeconds | default 2 | quote }}
- name: BOOTSTRAP_LOCK_TIMEOUT_SECONDS
value: {{ .Values.setup.applianceBootstrap.lockTimeoutSeconds | default 1800 | quote }}
- name: BOOTSTRAP_LOCK_STALE_SECONDS
value: {{ .Values.setup.applianceBootstrap.lockStaleSeconds | default 120 | quote }}
- name: BOOTSTRAP_LOCK_HEARTBEAT_SECONDS
value: {{ .Values.setup.applianceBootstrap.lockHeartbeatSeconds | default 10 | quote }}
{{- if .Values.setup.applianceBootstrap.enabled }}
- name: SEEDS_DIR
value: "/app/ee/server/seeds/onboarding"
{{- end }}
# Secret provider configuration for setup jobs
- name: SECRET_READ_CHAIN
value: "{{ .Values.secrets_provider.readChain | default "env,filesystem" }}"
- name: SECRET_WRITE_PROVIDER
value: "{{ .Values.secrets_provider.writeProvider | default "filesystem" }}"
{{- if .Values.secrets_provider.envPrefix }}
- name: SECRET_ENV_PREFIX
value: "{{ .Values.secrets_provider.envPrefix }}"
{{- end }}
{{- if .Values.setup.applianceBootstrap.enabled }}
# Appliance bootstrap hashes the initial admin password with PBKDF2
# peppered by getSecret('nextauth_secret','NEXTAUTH_SECRET'); getSecret
# consults the env provider FIRST, which looks up the *lowercase*
# secret name. Bind both names to the shared cluster secret so the
# bootstrap and the server resolve the identical pepper.
- name: nextauth_secret
valueFrom:
secretKeyRef:
name: "{{ include "sebastian.fullname" . }}-secrets"
key: NEXTAUTH_SECRET
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: "{{ include "sebastian.fullname" . }}-secrets"
key: NEXTAUTH_SECRET
# PBKDF2 params MUST match the server deployment (.Values.crypto),
# else the bootstrap hashes with the hardcoded defaults (10000/64/
# 12/sha512) while the server verifies with the configured values --
# a permanent "Invalid email or password" at first login.
- name: SALT_BYTES
value: "{{ .Values.crypto.salt_bytes }}"
- name: ITERATIONS
value: "{{ .Values.crypto.iteration }}"
- name: KEY_LENGTH
value: "{{ .Values.crypto.key_length }}"
- name: ALGORITHM
value: "{{ .Values.crypto.algorithm }}"
{{- end }}
# Vault configuration for setup jobs (only if vault is used)
{{- if or (contains "vault" .Values.secrets_provider.readChain) (eq .Values.secrets_provider.writeProvider "vault") }}
{{- if .Values.secrets_provider.vault.addr }}
- name: VAULT_ADDR
value: "{{ .Values.secrets_provider.vault.addr }}"
{{- end }}
{{- if .Values.secrets_provider.vault.token }}
- name: VAULT_TOKEN
value: "{{ .Values.secrets_provider.vault.token }}"
{{- end }}
{{- if .Values.secrets_provider.vault.appSecretPath }}
- name: VAULT_APP_SECRET_PATH
value: "{{ .Values.secrets_provider.vault.appSecretPath }}"
{{- end }}
{{- if .Values.secrets_provider.vault.tenantSecretPathTemplate }}
- name: VAULT_TENANT_SECRET_PATH_TEMPLATE
value: "{{ .Values.secrets_provider.vault.tenantSecretPathTemplate }}"
{{- end }}
{{- end }}
{{- if .Values.db.enabled }}
- name: DB_TYPE
value: postgres
- name: DB_NAME
value: "postgres"
- name: DB_HOST
value: "db.{{ include "sebastian.namespace" . }}.svc.cluster.local"
- name: DB_HOST_ADMIN
value: "db.{{ include "sebastian.namespace" . }}.svc.cluster.local"
- name: DB_PORT
value: "5432"
- name: DB_PORT_ADMIN
value: "5432"
- name: DB_USER
value: "postgres"
- name: DB_PASSWORD_ADMIN
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SUPERUSER
- name: DB_USER_ADMIN
value: "postgres"
- name: DB_PASSWORD_SUPERUSER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SUPERUSER
- name: DB_USER_HOCUSPOCUS
value: "hocuspocus_user"
- name: DB_NAME_HOCUSPOCUS
value: "hocuspocus"
- name: DB_PASSWORD_HOCUSPOCUS
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_HOCUSPOCUS
- name: DB_USER_SERVER
value: "app_user"
- name: DB_USER_PGBOUNCER
value: "app_user_pgbouncer"
- name: DB_NAME_SERVER
value: "server"
- name: DB_PASSWORD_SERVER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_SERVER
- name: DB_PASSWORD_PGBOUNCER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD_PGBOUNCER
{{- else }}
- name: DB_TYPE
value: "{{ .Values.config.db.type }}"
- name: DB_HOST
value: "{{ .Values.config.db.host }}"
- name: DB_HOST_ADMIN
value: "{{ .Values.config.db.adminHost | default .Values.config.db.host }}"
- name: DB_PORT
value: "{{ .Values.config.db.port }}"
- name: DB_PORT_ADMIN
value: "{{ .Values.config.db.adminPort | default .Values.config.db.port }}"
- name: DB_USER
value: "{{ .Values.config.db.user }}"
- name: DB_PASSWORD_ADMIN
{{- if and .Values.config.db.server_password_admin_secret.name .Values.config.db.server_password_admin_secret.key }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.db.server_password_admin_secret.name | quote }}
key: {{ .Values.config.db.server_password_admin_secret.key | quote }}
{{- else }}
value: {{ .Values.config.db.server_admin_password | quote }}
{{- end }}
- name: DB_USER_ADMIN
value: "{{ .Values.config.db.adminUser | default .Values.config.db.user }}"
- name: DB_PASSWORD_SUPERUSER
{{- if and .Values.config.db.server_password_admin_secret.name .Values.config.db.server_password_admin_secret.key }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.db.server_password_admin_secret.name | quote }}
key: {{ .Values.config.db.server_password_admin_secret.key | quote }}
{{- else }}
value: {{ .Values.config.db.server_admin_password | quote }}
{{- end }}
- name: DB_USER_HOCUSPOCUS
value: "{{ .Values.config.db.hocuspocus_user | default "hocuspocus_user" }}"
- name: DB_NAME_HOCUSPOCUS
value: "{{ .Values.config.db.hocuspocus_database }}"
- name: DB_PASSWORD_HOCUSPOCUS
{{- if .Values.config.db.hocuspocus_password_secret }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.db.hocuspocus_password_secret.name | quote }}
key: {{ .Values.config.db.hocuspocus_password_secret.key | quote }}
{{- else }}
value: {{ .Values.config.db.password | quote }}
{{- end }}
- name: DB_USER_SERVER
value: "{{ .Values.config.db.user }}"
{{- if .Values.config.db.pgbouncer_user }}
- name: DB_USER_PGBOUNCER
value: "{{ .Values.config.db.pgbouncer_user }}"
{{- end }}
- name: DB_NAME_SERVER
value: "{{ .Values.config.db.server_database }}"
- name: DB_PASSWORD_SERVER
{{- if and .Values.config.db.server_password_secret.name .Values.config.db.server_password_secret.key }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.db.server_password_secret.name | quote }}
key: {{ .Values.config.db.server_password_secret.key | quote }}
{{- else }}
value: {{ .Values.config.db.server_password | quote }}
{{- end }}
{{- if .Values.config.db.pgbouncer_password_secret }}
- name: DB_PASSWORD_PGBOUNCER
valueFrom:
secretKeyRef:
name: {{ .Values.config.db.pgbouncer_password_secret.name | quote }}
key: {{ .Values.config.db.pgbouncer_password_secret.key | quote }}
{{- else if .Values.config.db.pgbouncer_password }}
- name: DB_PASSWORD_PGBOUNCER
value: {{ .Values.config.db.pgbouncer_password | quote }}
{{- end }}
{{- end }}
{{- if .Values.setup.applianceBootstrap.enabled }}
envFrom:
- secretRef:
name: appliance-initial-tenant
optional: true
- secretRef:
name: appliance-license-seed
optional: true
volumeMounts:
- name: appliance-bootstrap
mountPath: /bootstrap
readOnly: true
{{- end }}
restartPolicy: Never
{{- if .Values.setup.applianceBootstrap.enabled }}
volumes:
- name: appliance-bootstrap
configMap:
name: {{ include "sebastian.fullname" . }}-appliance-bootstrap
defaultMode: 0755
{{- end }}
{{- if .Values.setup.applianceBootstrap.enabled }}
# Appliance: this runs concurrently with db-0 coming up (not a post-install
# hook), so allow retries while Postgres becomes reachable.
backoffLimit: 6
{{- else }}
backoffLimit: 0
{{- end }}
{{- end }}