245 lines
7.9 KiB
Bash
245 lines
7.9 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
META_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
ROOT="${GOVOPLAN_CORE_ROOT:-$META_ROOT/../govoplan-core}"
|
|
ROOT="$(cd "$ROOT" && pwd)"
|
|
PROFILE_ROOT="$META_ROOT/dev/production-like"
|
|
PROFILE_ENV="${GOVOPLAN_PRODUCTION_LIKE_ENV:-$PROFILE_ROOT/.env}"
|
|
if [ ! -f "$PROFILE_ENV" ]; then
|
|
PROFILE_ENV="$PROFILE_ROOT/.env.example"
|
|
fi
|
|
|
|
WEBUI_ROOT="$ROOT/webui"
|
|
VENV_ROOT="${GOVOPLAN_VENV_ROOT:-$META_ROOT/.venv}"
|
|
PYTHON="${PYTHON:-$VENV_ROOT/bin/python}"
|
|
NODE_BIN="${NODE_BIN:-/home/zemion/.nvm/versions/node/v22.22.3/bin}"
|
|
NPM="${NPM:-$NODE_BIN/npm}"
|
|
DOCKER="${DOCKER:-docker}"
|
|
|
|
BACKEND_HOST="${GOVOPLAN_BACKEND_HOST:-127.0.0.1}"
|
|
BACKEND_PORT="${GOVOPLAN_BACKEND_PORT:-8000}"
|
|
FRONTEND_HOST="${GOVOPLAN_FRONTEND_HOST:-127.0.0.1}"
|
|
FRONTEND_PORT="${GOVOPLAN_FRONTEND_PORT:-5173}"
|
|
OPEN_BROWSER="${OPEN_BROWSER:-1}"
|
|
STOP_DEPENDENCIES_ON_EXIT="${GOVOPLAN_STOP_PROFILE_DEPENDENCIES_ON_EXIT:-0}"
|
|
|
|
LOG_DIR="$META_ROOT/runtime/production-like/logs"
|
|
BACKEND_LOG="$LOG_DIR/backend.log"
|
|
WORKER_LOG="$LOG_DIR/worker.log"
|
|
FRONTEND_LOG="$LOG_DIR/frontend.log"
|
|
BACKEND_URL="http://$BACKEND_HOST:$BACKEND_PORT"
|
|
FRONTEND_URL="http://$FRONTEND_HOST:$FRONTEND_PORT"
|
|
|
|
backend_pid=""
|
|
worker_pid=""
|
|
frontend_pid=""
|
|
|
|
fail() {
|
|
printf 'launch-production-like-dev: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
[ -f "$PROFILE_ENV" ] || fail "profile env file not found: $PROFILE_ENV"
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$PROFILE_ENV"
|
|
set +a
|
|
|
|
export APP_ENV="${APP_ENV:-staging}"
|
|
export GOVOPLAN_INSTALL_PROFILE="${GOVOPLAN_INSTALL_PROFILE:-production-like}"
|
|
export ENABLED_MODULES="${ENABLED_MODULES:-tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,poll,scheduling,notifications,docs,ops}"
|
|
export DATABASE_URL="${DATABASE_URL:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL:-postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}"
|
|
export GOVOPLAN_DATABASE_URL_PGTOOLS="${GOVOPLAN_DATABASE_URL_PGTOOLS:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL_PGTOOLS:-postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}"
|
|
export REDIS_URL="${REDIS_URL:-${GOVOPLAN_PRODUCTION_LIKE_REDIS_URL:-redis://127.0.0.1:56379/0}}"
|
|
export CELERY_ENABLED="${CELERY_ENABLED:-true}"
|
|
export CELERY_QUEUES="${CELERY_QUEUES:-send_email,append_sent,default}"
|
|
export FILE_STORAGE_BACKEND="${FILE_STORAGE_BACKEND:-local}"
|
|
export FILE_STORAGE_LOCAL_ROOT="${FILE_STORAGE_LOCAL_ROOT:-$META_ROOT/runtime/production-like/files}"
|
|
export DEV_AUTO_MIGRATE_ENABLED="${DEV_AUTO_MIGRATE_ENABLED:-false}"
|
|
export DEV_BOOTSTRAP_ENABLED="${DEV_BOOTSTRAP_ENABLED:-true}"
|
|
export CORS_ORIGINS="${CORS_ORIGINS:-http://127.0.0.1:$FRONTEND_PORT,http://localhost:$FRONTEND_PORT}"
|
|
|
|
if [ -z "${MASTER_KEY_B64:-}" ]; then
|
|
MASTER_KEY_B64="$("$PYTHON" - <<'PY'
|
|
from cryptography.fernet import Fernet
|
|
print(Fernet.generate_key().decode())
|
|
PY
|
|
)"
|
|
export MASTER_KEY_B64
|
|
fi
|
|
|
|
"$PYTHON" -m govoplan_core.commands.config validate --profile production-like
|
|
|
|
compose() {
|
|
"$DOCKER" compose --env-file "$PROFILE_ENV" -f "$PROFILE_ROOT/docker-compose.yml" "$@"
|
|
}
|
|
|
|
port_is_free() {
|
|
"$PYTHON" - "$1" "$2" <<'PY'
|
|
import socket
|
|
import sys
|
|
|
|
host = sys.argv[1]
|
|
port = int(sys.argv[2])
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
try:
|
|
sock.bind((host, port))
|
|
except OSError:
|
|
raise SystemExit(1)
|
|
PY
|
|
}
|
|
|
|
wait_for_tcp() {
|
|
"$PYTHON" - "$1" "$2" <<'PY'
|
|
import socket
|
|
import sys
|
|
import time
|
|
|
|
host = sys.argv[1]
|
|
port = int(sys.argv[2])
|
|
deadline = time.monotonic() + 90
|
|
last_error = None
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with socket.create_connection((host, port), timeout=2):
|
|
raise SystemExit(0)
|
|
except OSError as exc:
|
|
last_error = exc
|
|
time.sleep(1)
|
|
print(f"Timed out waiting for {host}:{port}: {last_error}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
PY
|
|
}
|
|
|
|
wait_for_url() {
|
|
"$PYTHON" - "$1" <<'PY'
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
|
|
url = sys.argv[1]
|
|
deadline = time.monotonic() + 90
|
|
last_error = None
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=2) as response:
|
|
if 200 <= response.status < 500:
|
|
raise SystemExit(0)
|
|
except Exception as exc: # noqa: BLE001 - printed only on timeout.
|
|
last_error = exc
|
|
time.sleep(1)
|
|
print(f"Timed out waiting for {url}: {last_error}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
PY
|
|
}
|
|
|
|
cleanup() {
|
|
if [ -n "${frontend_pid:-}" ] && kill -0 "$frontend_pid" 2>/dev/null; then
|
|
kill "$frontend_pid" 2>/dev/null || true
|
|
fi
|
|
if [ -n "${backend_pid:-}" ] && kill -0 "$backend_pid" 2>/dev/null; then
|
|
kill "$backend_pid" 2>/dev/null || true
|
|
fi
|
|
if [ -n "${worker_pid:-}" ] && kill -0 "$worker_pid" 2>/dev/null; then
|
|
kill "$worker_pid" 2>/dev/null || true
|
|
fi
|
|
if [ "$STOP_DEPENDENCIES_ON_EXIT" = "1" ]; then
|
|
compose down >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
[ -x "$PYTHON" ] || fail "Python virtualenv not found at $PYTHON. Run: cd $META_ROOT && python3 -m venv .venv && ./.venv/bin/python -m pip install --upgrade pip && ./.venv/bin/python -m pip install -r requirements-dev.txt"
|
|
[ -x "$NPM" ] || fail "npm not found at $NPM. Set NODE_BIN or NPM to your Node installation."
|
|
[ -f "$WEBUI_ROOT/package.json" ] || fail "WebUI package.json not found at $WEBUI_ROOT/package.json"
|
|
[ -d "$WEBUI_ROOT/node_modules" ] || fail "WebUI node_modules missing. Run: cd $WEBUI_ROOT && PATH=$NODE_BIN:\$PATH $NPM install"
|
|
|
|
mkdir -p "$LOG_DIR" "$FILE_STORAGE_LOCAL_ROOT"
|
|
: > "$BACKEND_LOG"
|
|
: > "$WORKER_LOG"
|
|
: > "$FRONTEND_LOG"
|
|
|
|
port_is_free "$BACKEND_HOST" "$BACKEND_PORT" || fail "$BACKEND_URL is already in use"
|
|
port_is_free "$FRONTEND_HOST" "$FRONTEND_PORT" || fail "$FRONTEND_URL is already in use"
|
|
|
|
printf 'Starting production-like dependencies through Docker Compose\n'
|
|
compose up -d
|
|
|
|
wait_for_tcp 127.0.0.1 "${GOVOPLAN_PRODUCTION_LIKE_POSTGRES_PORT:-55433}"
|
|
wait_for_tcp 127.0.0.1 "${GOVOPLAN_PRODUCTION_LIKE_REDIS_PORT:-56379}"
|
|
|
|
printf 'Running migrations and development bootstrap against %s\n' "$DATABASE_URL"
|
|
(
|
|
cd "$ROOT"
|
|
"$PYTHON" -m govoplan_core.commands.init_db --database-url "$DATABASE_URL" --with-dev-data
|
|
)
|
|
|
|
printf 'Starting GovOPlaN backend at %s\n' "$BACKEND_URL"
|
|
(
|
|
cd "$ROOT"
|
|
"$PYTHON" -m govoplan_core.devserver --host "$BACKEND_HOST" --port "$BACKEND_PORT"
|
|
) >"$BACKEND_LOG" 2>&1 &
|
|
backend_pid="$!"
|
|
|
|
printf 'Waiting for %s/health\n' "$BACKEND_URL"
|
|
wait_for_url "$BACKEND_URL/health" || {
|
|
tail -n 80 "$BACKEND_LOG" >&2 || true
|
|
fail "backend did not become healthy"
|
|
}
|
|
|
|
printf 'Starting Celery worker for queues %s\n' "$CELERY_QUEUES"
|
|
(
|
|
cd "$ROOT"
|
|
"$PYTHON" -m celery -A govoplan_core.celery_app:celery worker \
|
|
--queues "$CELERY_QUEUES" \
|
|
--hostname "govoplan-production-like@%h" \
|
|
--loglevel INFO
|
|
) >"$WORKER_LOG" 2>&1 &
|
|
worker_pid="$!"
|
|
|
|
printf 'Starting GovOPlaN WebUI at %s\n' "$FRONTEND_URL"
|
|
(
|
|
cd "$WEBUI_ROOT"
|
|
PATH="$WEBUI_ROOT/node_modules/.bin:$NODE_BIN:$PATH" \
|
|
CHOKIDAR_USEPOLLING="${GOVOPLAN_FRONTEND_USE_POLLING:-1}" \
|
|
CHOKIDAR_INTERVAL="${GOVOPLAN_FRONTEND_POLLING_INTERVAL:-500}" \
|
|
VITE_API_PROXY_TARGET="$BACKEND_URL" \
|
|
"$NPM" run dev -- --force
|
|
) >"$FRONTEND_LOG" 2>&1 &
|
|
frontend_pid="$!"
|
|
|
|
printf 'Waiting for %s\n' "$FRONTEND_URL"
|
|
wait_for_url "$FRONTEND_URL" || {
|
|
tail -n 80 "$FRONTEND_LOG" >&2 || true
|
|
fail "frontend did not become reachable"
|
|
}
|
|
|
|
if [ "$OPEN_BROWSER" = "1" ] && command -v xdg-open >/dev/null 2>&1; then
|
|
xdg-open "$FRONTEND_URL" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
GovOPlaN production-like development profile is running.
|
|
Web UI: $FRONTEND_URL
|
|
API: $BACKEND_URL/api/v1
|
|
Health: $BACKEND_URL/health
|
|
Ops: $BACKEND_URL/api/v1/ops/status
|
|
DB: $DATABASE_URL
|
|
Redis: $REDIS_URL
|
|
Files: $FILE_STORAGE_LOCAL_ROOT
|
|
|
|
Logs:
|
|
Backend: $BACKEND_LOG
|
|
Worker: $WORKER_LOG
|
|
WebUI: $FRONTEND_LOG
|
|
|
|
Docker dependencies remain running after Ctrl+C unless GOVOPLAN_STOP_PROFILE_DEPENDENCIES_ON_EXIT=1.
|
|
Press Ctrl+C to stop API, worker, and WebUI.
|
|
EOF
|
|
|
|
wait
|