# Lifecycle: pull, ordered start (deps → migrate → app), first admin, health, # plus update/down/logs/status/clean. Wraps `docker compose` with the project's # env file and generated compose file. PROJECT="${MCMA_PROJECT:-mcma}" dc() { docker compose \ --project-name "$PROJECT" \ --project-directory "$BOOTSTRAP_DIR" \ --env-file "$ENV_FILE" \ -f "$COMPOSE_FILE" "$@" } # backing_services — the embedded dependencies present in the compose file that # must be healthy/complete before migrations (subset of db redis minio*). backing_services() { local all want s out="" all="$(dc config --services 2>/dev/null)" for want in db redis minio minio-setup; do while IFS= read -r s; do [[ "$s" == "$want" ]] && out+="$want " done <<<"$all" done printf '%s' "${out% }" } # app_services — everything that is not a one-shot/backing dependency wait. ensure_media_dir() { # MEDIA_HOST_PATH is read from the generated env file. local p p="$(grep -E '^MEDIA_HOST_PATH=' "$ENV_FILE" | cut -d= -f2-)" [[ -n "$p" ]] || return 0 # Resolve relative paths against the project directory (compose does too). [[ "$p" = /* ]] || p="${BOOTSTRAP_DIR}/${p#./}" mkdir -p "$p" } lifecycle_pull() { ui_info "$(t pull_images "$(grep -E '^MCMA_IMAGE_TAG=' "$ENV_FILE" | cut -d= -f2-)")" ui_dim "$(t pull_hint)" dc pull } # wait_api_healthy — poll the running API's liveness endpoint over the compose # network (no host port / TLS assumptions). Returns non-zero on timeout. wait_api_healthy() { ui_info "$(t checking_health)" dc run --rm --no-deps -T api python -c ' import urllib.request, time, sys for _ in range(60): try: if urllib.request.urlopen("http://api:8000/health", timeout=2).status == 200: sys.exit(0) except Exception: pass time.sleep(2) sys.exit(1) ' } # lifecycle_start CREATE_ADMIN(yes|no) # Ordered, fail-loud startup. Never starts the backend over a broken DB. lifecycle_start() { local create_admin="${1:-no}" ensure_media_dir lifecycle_pull local deps; deps="$(backing_services)" if [[ -n "$deps" ]]; then ui_info "$(t starting_deps)" # shellcheck disable=SC2086 dc up -d --wait $deps fi ui_info "$(t running_migrations)" if ! dc run --rm --no-deps -T api alembic upgrade head; then ui_err "$(t err_migrations_failed)" exit 1 fi if [[ "$create_admin" == "yes" ]]; then ui_info "$(t creating_admin)" # Password passed via --password to avoid interactive prompts; not echoed. dc run --rm --no-deps -T api mcma create-admin "$CFG_ADMIN_USER" --password "$CFG_ADMIN_PASS" fi ui_info "$(t starting_app)" dc up -d if ! wait_api_healthy; then ui_err "$(t err_health_timeout)" exit 1 fi } # -- management commands --------------------------------------------------- lifecycle_update() { lifecycle_pull local deps; deps="$(backing_services)" if [[ -n "$deps" ]]; then # shellcheck disable=SC2086 dc up -d --wait $deps fi ui_info "$(t running_migrations)" if ! dc run --rm --no-deps -T api alembic upgrade head; then ui_err "$(t err_migrations_failed)"; exit 1 fi dc up -d wait_api_healthy || { ui_err "$(t err_health_timeout)"; exit 1; } ui_ok "$(t done_title)" } lifecycle_up() { ensure_media_dir; dc up -d; } lifecycle_down() { dc down; } lifecycle_logs() { dc logs -f --tail=100; } lifecycle_status() { dc ps echo if wait_api_healthy >/dev/null 2>&1; then ui_ok "API: ok"; else ui_warn "API: not ready"; fi } lifecycle_clean() { dc down if ui_yesno "$(t clean_volumes_confirm)" "no"; then dc down -v fi if ui_yesno "$(t clean_confirm)" "yes"; then rm -f "$COMPOSE_FILE" "$CADDYFILE" "$ENV_FILE" ui_ok "$(t clean_done)" fi }