247 lines
6.7 KiB
Bash
247 lines
6.7 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
remote43_require_file() {
|
||
|
|
local path="$1"
|
||
|
|
local label="$2"
|
||
|
|
[[ -f "$path" ]] || {
|
||
|
|
echo "missing $label: $path" >&2
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
remote43_random_hex() {
|
||
|
|
local bytes="${1:?bytes required}"
|
||
|
|
python3 - "$bytes" <<'PY'
|
||
|
|
import secrets
|
||
|
|
import sys
|
||
|
|
|
||
|
|
print(secrets.token_hex(int(sys.argv[1])))
|
||
|
|
PY
|
||
|
|
}
|
||
|
|
|
||
|
|
remote43_write_env_file() {
|
||
|
|
local path="$1"
|
||
|
|
shift
|
||
|
|
: > "$path"
|
||
|
|
while [[ $# -gt 0 ]]; do
|
||
|
|
local key="$1"
|
||
|
|
local value="$2"
|
||
|
|
shift 2
|
||
|
|
case "$value" in
|
||
|
|
*$'\n'*)
|
||
|
|
echo "env value for $key must not contain newlines" >&2
|
||
|
|
return 1
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
printf '%s=%s\n' "$key" "$value" >> "$path"
|
||
|
|
done
|
||
|
|
}
|
||
|
|
|
||
|
|
render_remote43_host_env() {
|
||
|
|
local pg_container="$1"
|
||
|
|
local redis_container="$2"
|
||
|
|
local db_password="$3"
|
||
|
|
local db_name="$4"
|
||
|
|
local admin_email="$5"
|
||
|
|
local admin_password="$6"
|
||
|
|
local jwt_secret="$7"
|
||
|
|
local totp_key="$8"
|
||
|
|
|
||
|
|
cat <<EOF
|
||
|
|
AUTO_SETUP=true
|
||
|
|
DATABASE_HOST=$pg_container
|
||
|
|
DATABASE_PORT=5432
|
||
|
|
DATABASE_USER=sub2api
|
||
|
|
DATABASE_PASSWORD=$db_password
|
||
|
|
DATABASE_DBNAME=$db_name
|
||
|
|
REDIS_HOST=$redis_container
|
||
|
|
REDIS_PORT=6379
|
||
|
|
ADMIN_EMAIL=$admin_email
|
||
|
|
ADMIN_PASSWORD=$admin_password
|
||
|
|
JWT_SECRET=$jwt_secret
|
||
|
|
TOTP_ENCRYPTION_KEY=$totp_key
|
||
|
|
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||
|
|
SECURITY_URL_ALLOWLIST_MODE=disabled
|
||
|
|
EOF
|
||
|
|
}
|
||
|
|
|
||
|
|
render_remote43_crm_env() {
|
||
|
|
local crm_port="$1"
|
||
|
|
local sqlite_dsn="$2"
|
||
|
|
local admin_token="$3"
|
||
|
|
|
||
|
|
cat <<EOF
|
||
|
|
SUB2API_CRM_LISTEN_ADDR=127.0.0.1:$crm_port
|
||
|
|
SUB2API_CRM_SQLITE_DSN=$sqlite_dsn
|
||
|
|
SUB2API_CRM_ADMIN_TOKEN=$admin_token
|
||
|
|
SUB2API_CRM_RECONCILE_WORKER_ENABLED=false
|
||
|
|
EOF
|
||
|
|
}
|
||
|
|
|
||
|
|
render_remote43_bootstrap_script() {
|
||
|
|
local remote_root="$1"
|
||
|
|
local host_env_file="$2"
|
||
|
|
local crm_env_file="$3"
|
||
|
|
local host_binary_name="$4"
|
||
|
|
local crm_binary_name="$5"
|
||
|
|
local data_dir="$6"
|
||
|
|
local crm_db_file="$7"
|
||
|
|
local crm_pid_file="$8"
|
||
|
|
local crm_log_file="$9"
|
||
|
|
local app_container="${10}"
|
||
|
|
local pg_container="${11}"
|
||
|
|
local redis_container="${12}"
|
||
|
|
local network_name="${13}"
|
||
|
|
local host_image="${14}"
|
||
|
|
local pg_image="${15}"
|
||
|
|
local redis_image="${16}"
|
||
|
|
local db_password="${17}"
|
||
|
|
local db_name="${18}"
|
||
|
|
local host_port="${19}"
|
||
|
|
local crm_port="${20}"
|
||
|
|
local host_container_port="${21}"
|
||
|
|
|
||
|
|
local remote_root_q host_env_q crm_env_q host_binary_q crm_binary_q
|
||
|
|
local data_dir_q crm_db_q crm_pid_q crm_log_q app_q pg_q redis_q
|
||
|
|
local network_q host_image_q pg_image_q redis_image_q db_password_q db_name_q
|
||
|
|
local host_port_q crm_port_q host_container_port_q
|
||
|
|
remote_root_q="$(printf '%q' "$remote_root")"
|
||
|
|
host_env_q="$(printf '%q' "$host_env_file")"
|
||
|
|
crm_env_q="$(printf '%q' "$crm_env_file")"
|
||
|
|
host_binary_q="$(printf '%q' "$host_binary_name")"
|
||
|
|
crm_binary_q="$(printf '%q' "$crm_binary_name")"
|
||
|
|
data_dir_q="$(printf '%q' "$data_dir")"
|
||
|
|
crm_db_q="$(printf '%q' "$crm_db_file")"
|
||
|
|
crm_pid_q="$(printf '%q' "$crm_pid_file")"
|
||
|
|
crm_log_q="$(printf '%q' "$crm_log_file")"
|
||
|
|
app_q="$(printf '%q' "$app_container")"
|
||
|
|
pg_q="$(printf '%q' "$pg_container")"
|
||
|
|
redis_q="$(printf '%q' "$redis_container")"
|
||
|
|
network_q="$(printf '%q' "$network_name")"
|
||
|
|
host_image_q="$(printf '%q' "$host_image")"
|
||
|
|
pg_image_q="$(printf '%q' "$pg_image")"
|
||
|
|
redis_image_q="$(printf '%q' "$redis_image")"
|
||
|
|
db_password_q="$(printf '%q' "$db_password")"
|
||
|
|
db_name_q="$(printf '%q' "$db_name")"
|
||
|
|
host_port_q="$(printf '%q' "$host_port")"
|
||
|
|
crm_port_q="$(printf '%q' "$crm_port")"
|
||
|
|
host_container_port_q="$(printf '%q' "$host_container_port")"
|
||
|
|
|
||
|
|
cat <<EOF
|
||
|
|
#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
REMOTE_ROOT=$remote_root_q
|
||
|
|
HOST_ENV_FILE=$host_env_q
|
||
|
|
CRM_ENV_FILE=$crm_env_q
|
||
|
|
HOST_BINARY="\$REMOTE_ROOT/$host_binary_q"
|
||
|
|
CRM_BINARY="\$REMOTE_ROOT/$crm_binary_q"
|
||
|
|
DATA_DIR=$data_dir_q
|
||
|
|
CRM_DB_FILE=$crm_db_q
|
||
|
|
CRM_PID_FILE=$crm_pid_q
|
||
|
|
CRM_LOG_FILE=$crm_log_q
|
||
|
|
APP_CONTAINER=$app_q
|
||
|
|
PG_CONTAINER=$pg_q
|
||
|
|
REDIS_CONTAINER=$redis_q
|
||
|
|
NETWORK_NAME=$network_q
|
||
|
|
HOST_IMAGE=$host_image_q
|
||
|
|
PG_IMAGE=$pg_image_q
|
||
|
|
REDIS_IMAGE=$redis_image_q
|
||
|
|
DB_PASSWORD=$db_password_q
|
||
|
|
DB_NAME=$db_name_q
|
||
|
|
HOST_PORT=$host_port_q
|
||
|
|
CRM_PORT=$crm_port_q
|
||
|
|
HOST_CONTAINER_PORT=$host_container_port_q
|
||
|
|
|
||
|
|
mkdir -p "\$REMOTE_ROOT" "\$DATA_DIR"
|
||
|
|
chmod 755 "\$HOST_BINARY" "\$CRM_BINARY"
|
||
|
|
|
||
|
|
rm -f "\$DATA_DIR/install.lock" "\$DATA_DIR/config.yaml" "\$DATA_DIR/.installed"
|
||
|
|
rm -f "\$CRM_DB_FILE"
|
||
|
|
|
||
|
|
if [[ -f "\$CRM_PID_FILE" ]]; then
|
||
|
|
kill "\$(cat "\$CRM_PID_FILE")" >/dev/null 2>&1 || true
|
||
|
|
rm -f "\$CRM_PID_FILE"
|
||
|
|
fi
|
||
|
|
rm -f "\$CRM_LOG_FILE"
|
||
|
|
|
||
|
|
sudo -n docker rm -f "\$APP_CONTAINER" "\$PG_CONTAINER" "\$REDIS_CONTAINER" >/dev/null 2>&1 || true
|
||
|
|
sudo -n docker network inspect "\$NETWORK_NAME" >/dev/null 2>&1 || sudo -n docker network create "\$NETWORK_NAME" >/dev/null
|
||
|
|
|
||
|
|
sudo -n docker run -d --name "\$PG_CONTAINER" --network "\$NETWORK_NAME" \\
|
||
|
|
-e POSTGRES_USER=sub2api \\
|
||
|
|
-e POSTGRES_PASSWORD="\$DB_PASSWORD" \\
|
||
|
|
-e POSTGRES_DB="\$DB_NAME" \\
|
||
|
|
"\$PG_IMAGE" >/dev/null
|
||
|
|
|
||
|
|
sudo -n docker run -d --name "\$REDIS_CONTAINER" --network "\$NETWORK_NAME" \\
|
||
|
|
"\$REDIS_IMAGE" >/dev/null
|
||
|
|
|
||
|
|
sleep 10
|
||
|
|
|
||
|
|
sudo -n docker run -d --name "\$APP_CONTAINER" --network "\$NETWORK_NAME" \\
|
||
|
|
-p "127.0.0.1:\$HOST_PORT:\$HOST_CONTAINER_PORT" \\
|
||
|
|
--env-file "\$HOST_ENV_FILE" \\
|
||
|
|
-v "\$DATA_DIR:/app/data" \\
|
||
|
|
-v "\$HOST_BINARY:/app/sub2api:ro" \\
|
||
|
|
"\$HOST_IMAGE" /app/sub2api >/dev/null
|
||
|
|
|
||
|
|
python3 - "\$HOST_ENV_FILE" "\$HOST_PORT" <<'PY'
|
||
|
|
import json
|
||
|
|
import pathlib
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
env_path = pathlib.Path(sys.argv[1])
|
||
|
|
host_port = sys.argv[2]
|
||
|
|
values = {}
|
||
|
|
for line in env_path.read_text(encoding='utf-8').splitlines():
|
||
|
|
if '=' not in line:
|
||
|
|
continue
|
||
|
|
key, value = line.split('=', 1)
|
||
|
|
values[key] = value
|
||
|
|
|
||
|
|
payload = json.dumps({
|
||
|
|
'email': values['ADMIN_EMAIL'],
|
||
|
|
'password': values['ADMIN_PASSWORD'],
|
||
|
|
'turnstile_token': '',
|
||
|
|
}, ensure_ascii=False)
|
||
|
|
url = f"http://127.0.0.1:{host_port}/api/v1/auth/login"
|
||
|
|
for _ in range(60):
|
||
|
|
result = subprocess.run(
|
||
|
|
['curl', '-fsS', '-H', 'Content-Type: application/json', '-X', 'POST', url, '-d', payload],
|
||
|
|
text=True,
|
||
|
|
capture_output=True,
|
||
|
|
)
|
||
|
|
if result.returncode == 0 and 'access_token' in result.stdout:
|
||
|
|
raise SystemExit(0)
|
||
|
|
time.sleep(2)
|
||
|
|
raise SystemExit(f'host login did not become ready on {url}')
|
||
|
|
PY
|
||
|
|
|
||
|
|
nohup bash -lc 'set -a; source "\$1"; set +a; exec "\$2"' _ "\$CRM_ENV_FILE" "\$CRM_BINARY" >"\$CRM_LOG_FILE" 2>&1 &
|
||
|
|
echo \$! > "\$CRM_PID_FILE"
|
||
|
|
|
||
|
|
python3 - "\$CRM_PORT" <<'PY'
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
url = f"http://127.0.0.1:{sys.argv[1]}/healthz"
|
||
|
|
for _ in range(30):
|
||
|
|
result = subprocess.run(['curl', '-fsS', url], text=True, capture_output=True)
|
||
|
|
if result.returncode == 0 and result.stdout.strip() == 'ok':
|
||
|
|
raise SystemExit(0)
|
||
|
|
time.sleep(1)
|
||
|
|
raise SystemExit(f'crm healthz did not become ready on {url}')
|
||
|
|
PY
|
||
|
|
|
||
|
|
printf 'host_base=http://127.0.0.1:%s\n' "\$HOST_PORT"
|
||
|
|
printf 'crm_base=http://127.0.0.1:%s\n' "\$CRM_PORT"
|
||
|
|
printf 'remote_host_env=%s\n' "\$HOST_ENV_FILE"
|
||
|
|
printf 'crm_log=%s\n' "\$CRM_LOG_FILE"
|
||
|
|
EOF
|
||
|
|
}
|