#!/bin/sh # Assemble runnable PHP backend from ac-workspace submodules (overlay on legacy seed). set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)" # shellcheck source=/dev/null . "$ROOT/scripts/lib/common.sh" load_cluster_env WS="${AC_WORKSPACE_ROOT:-/var/www/ac/workspace}" LAB_SEED="${ROOT}/lab-seeds/backend" LEGACY="$LAB_SEED" COMPOSED="${COMPOSE_BACKEND:-/var/www/ac/composed/backend}" PLATFORM_ROOT="${AC_PLATFORM_ROOT:-/var/www/ac/platform}" APP_PASS="$(read_cred mariadb_app password)" DB_HOST="$PRIMARY_DB_HOST" if is_primary_node; then DB_HOST=127.0.0.1 fi [ -d "$WS/.git" ] || die "missing ac-workspace at $WS (run deploy-ac-workspace.sh first)" overlay_src() { _repo="$1" _dir="${WS}/${_repo}/src" [ -d "$_dir" ] || { log "skip src ${_repo} (no src/)"; return 0; } log "overlay src ${_repo}" cp -a "$_dir"/. "${COMPOSED}/src/" } overlay_api() { _repo="$1" _dir="${WS}/${_repo}/public/api" [ -d "$_dir" ] || return 0 log "overlay api ${_repo}" mkdir -p "${COMPOSED}/public/api" cp -a "$_dir"/. "${COMPOSED}/public/api/" } overlay_views() { _repo="$1" _dir="${WS}/${_repo}/views" [ -d "$_dir" ] || return 0 log "overlay views ${_repo}" mkdir -p "${COMPOSED}/views" cp -a "$_dir"/. "${COMPOSED}/views/" } if [ ! -f "${COMPOSED}/public/index.php" ] || [ "${FORCE_COMPOSE_SEED:-0}" = "1" ]; then [ -d "$LEGACY/public" ] || die "backend seed missing at $LEGACY (sync ac-deploy lab-seeds to NFS /shared/cluster)" log "seed composed backend from ${LEGACY}" mkdir -p "$(dirname "$COMPOSED")" rm -rf "$COMPOSED" mkdir -p "$COMPOSED" cp -a "${LEGACY}/." "$COMPOSED/" fi mkdir -p "$PLATFORM_ROOT" if [ -d "${WS}/ac-platform-php/platform" ]; then log "sync platform helpers → ${PLATFORM_ROOT}" cp -a "${WS}/ac-platform-php/platform/." "$PLATFORM_ROOT/" fi if [ -d "${WS}/ac-platform-web/assets" ]; then log "sync platform-web assets" mkdir -p "${COMPOSED}/public/assets" cp -a "${WS}/ac-platform-web/assets/." "${COMPOSED}/public/assets/" if [ -f "${WS}/ac-platform-web/src/AnalyticsHead.php" ]; then cp -a "${WS}/ac-platform-web/src/AnalyticsHead.php" "${COMPOSED}/src/" fi fi if [ -f "${WS}/ac-be-auth/public/assets/js/two_factor.js" ]; then log "sync ac-be-auth two_factor.js" mkdir -p "${COMPOSED}/public/assets/js" cp -a "${WS}/ac-be-auth/public/assets/js/two_factor.js" "${COMPOSED}/public/assets/js/" fi overlay_src ac-platform-php overlay_src ac-ms-identity overlay_src ac-ms-rbac overlay_src ac-ms-devices overlay_src ac-ms-issues overlay_src ac-ms-tickets overlay_src ac-ms-graphs overlay_src ac-ms-remote-access # Lab-seed PHP not yet split into ac-ms-* repos — NFS seed stays authoritative after overlays. overlay_lab_seed_src() { _f="$1" if [ -f "${LEGACY}/src/${_f}" ]; then log "overlay lab-seed src ${_f}" cp -a "${LEGACY}/src/${_f}" "${COMPOSED}/src/" fi } overlay_lab_seed_src ShortLinksRepository.php overlay_lab_seed_src UrlShortenerDatabase.php overlay_api ac-ms-identity overlay_api ac-ms-rbac overlay_api ac-ms-devices overlay_api ac-ms-issues overlay_api ac-ms-tickets overlay_api ac-ms-graphs overlay_api ac-ms-remote-access overlay_views ac-be-auth overlay_views ac-be-issues overlay_views ac-be-tickets overlay_views ac-be-graphs overlay_views ac-be-remote-access overlay_views ac-be-access ISSUES_BASE="${APP_BASE_PATH:-/app/androidcast_project/issues}" PUBLIC_ORIGIN_CFG="${LAB_PUBLIC_ORIGIN:-https://acl0.f0xx.org}" load_secrets_lab AUTH_KEY="${AUTH_ENCRYPTION_KEY:-lab-cluster-dev-32-char-min-secret!!}" _MSMTP_PASS="$(read_secret MSMTP_APP_PASSWORD 2>/dev/null || true)" [ -n "$_MSMTP_PASS" ] || _MSMTP_PASS="${MSMTP_APP_PASSWORD:-}" MAIL_TRANSPORT="${MAIL_TRANSPORT:-sendmail}" _mail_from_secret="$(read_secret MAIL_FROM 2>/dev/null || true)" [ -n "$_mail_from_secret" ] || _mail_from_secret="${MAIL_FROM:-}" MAIL_REPLY="${MAIL_REPLY_TO:-bestcastr@gmail.com}" SMTP_HOST="${SMTP_HOST:-127.0.0.1}" SMTP_PORT="${SMTP_PORT:-587}" SMTP_ENC="${SMTP_ENCRYPTION:-tls}" SMTP_USER="${SMTP_USER:-${MSMTP_USER:-}}" SMTP_PASS="${SMTP_PASS:-}" if [ -n "$_MSMTP_PASS" ]; then MAIL_TRANSPORT=smtp SMTP_HOST=smtp.gmail.com SMTP_USER="${MSMTP_USER:-bestcastr@gmail.com}" SMTP_PASS="$_MSMTP_PASS" fi MAIL_FROM="$(normalize_mail_from "$_mail_from_secret" "$SMTP_USER")" [ -n "$MAIL_FROM" ] || MAIL_FROM="Android Cast Lab <${SMTP_USER:-bestcastr@gmail.com}>" unset _MSMTP_PASS _mail_from_secret MAIL_FROM_PHP="$(printf '%s' "$MAIL_FROM" | sed "s/'/\\\\'/g")" AUTH_BEARER_ID="$(read_secret URL_SHORTENER_AUTH_BEARER_ID 2>/dev/null || true)" [ -n "$AUTH_BEARER_ID" ] || AUTH_BEARER_ID="${URL_SHORTENER_AUTH_BEARER_ID:-0}" API_BEARER_TOKEN="$(read_secret URL_SHORTENER_API_BEARER_TOKEN 2>/dev/null || true)" API_BEARER_PHP="" if [ -n "$API_BEARER_TOKEN" ]; then API_BEARER_PHP="$(printf '%s' "$API_BEARER_TOKEN" | sed "s/'/\\\\'/g")" fi WG_ENDPOINT_CFG="${WG_ENDPOINT:-}" WG_SERVER_PUBLIC_KEY_CFG="${WG_SERVER_PUBLIC_KEY:-}" WG_PROVISION="${WG_PROVISION_PEERS:-0}" if [ -f /etc/wireguard/keys/server.publickey ] && [ -z "$WG_SERVER_PUBLIC_KEY_CFG" ]; then WG_SERVER_PUBLIC_KEY_CFG="$(cat /etc/wireguard/keys/server.publickey)" fi if [ -z "$WG_ENDPOINT_CFG" ] && [ -n "$WG_SERVER_PUBLIC_KEY_CFG" ]; then WG_ENDPOINT_CFG="${CAST01_HOST:-cast01}.${CAST_DOMAIN:-intra.raptor.org}:51820" WG_PROVISION=1 fi WG_PROVISION_PHP=false RSSH_PROVISION_PHP=false [ "$WG_PROVISION" = "1" ] && WG_PROVISION_PHP=true RSSH_BASTION_HOST_CFG="${RSSH_BASTION_HOST:-}" RSSH_BASTION_PORT_CFG="${RSSH_BASTION_PORT:-22}" if [ -z "$RSSH_BASTION_HOST_CFG" ] && [ -n "$WG_SERVER_PUBLIC_KEY_CFG" ]; then RSSH_BASTION_HOST_CFG="${CAST01_HOST:-cast01}.${CAST_DOMAIN:-intra.raptor.org}" fi RSSH_PROVISION="${RSSH_PROVISION_USERS:-0}" if [ -n "$RSSH_BASTION_HOST_CFG" ]; then RSSH_PROVISION=1 fi [ "$RSSH_PROVISION" = "1" ] && RSSH_PROVISION_PHP=true URL_SHORTENER_API_BASE_CFG="${URL_SHORTENER_API_BASE:-${SHORT_LINKS_PUBLIC_BASE}}" URL_SHORTENER_API_HOST_CFG="${URL_SHORTENER_API_HOST:-}" mkdir -p "${COMPOSED}/config" log "write composed config.php base_path=${ISSUES_BASE}" cat > "${COMPOSED}/config/config.php" < 'Android Cast Issues', 'base_path' => '${ISSUES_BASE}', 'public_origin' => '${PUBLIC_ORIGIN_CFG}', 'db' => [ 'driver' => 'mariadb', 'sqlite_path' => __DIR__ . '/../data/crashes.sqlite', 'mysql' => [ 'host' => '${DB_HOST}', 'port' => ${PRIMARY_DB_PORT}, 'socket' => '', 'database' => 'androidcast_crashes', 'username' => 'androidcast', 'password' => '${APP_PASS}', 'charset' => 'utf8mb4', ], ], 'session_name' => 'ac_crash_sess', 'session_cookie_path' => '${APP_HUB_PATH}', 'debug' => true, 'rbac' => ['default_company_slug' => 'default', 'default_company_id' => 1], 'analytics' => ['enabled' => false, 'measurement_id' => '', 'debug' => false], 'remote_access' => [ 'wg_endpoint' => '${WG_ENDPOINT_CFG}', 'wg_server_public_key' => '${WG_SERVER_PUBLIC_KEY_CFG}', 'wg_interface' => 'wg0', 'wg_server_allowed_ips' => '172.200.2.1/32', 'wg_client_ip_prefix' => '172.200.2.', 'wg_client_ip_min' => 10, 'wg_client_ip_max' => 250, 'wg_set_prefix' => 'sudo -n ', 'provision_peers' => ${WG_PROVISION_PHP:-false}, 'require_wg_tools' => ${WG_PROVISION_PHP:-false}, 'session_ttl_s' => 3600, 'min_poll_interval_s' => 45, 'rssh' => [ 'bastion_host' => '${RSSH_BASTION_HOST_CFG}', 'bastion_port' => ${RSSH_BASTION_PORT_CFG:-22}, 'local_forward_port' => 8022, 'remote_port_min' => 18000, 'remote_port_max' => 18999, 'provision_users' => ${RSSH_PROVISION_PHP:-false}, 'bastion_user_script' => __DIR__ . '/../scripts/rssh_bastion_user.sh', ], ], 'auth' => [ 'encryption_key' => '${AUTH_KEY}', 'max_attempts' => 8, 'lockout_window_minutes' => 15, ], 'mail' => [ 'transport' => '${MAIL_TRANSPORT}', 'from' => '${MAIL_FROM_PHP}', 'reply_to' => '${MAIL_REPLY}', 'smtp' => [ 'host' => '${SMTP_HOST}', 'port' => ${SMTP_PORT}, 'encryption' => '${SMTP_ENC}', 'username' => '${SMTP_USER}', 'password' => '${SMTP_PASS}', ], ], 'url_shortener' => [ 'enabled' => true, 'public_base' => '${SHORT_LINKS_PUBLIC_BASE}', 'api_base' => '${URL_SHORTENER_API_BASE_CFG}', 'api_host' => '${URL_SHORTENER_API_HOST_CFG}', 'api_bearer_token' => '${API_BEARER_PHP}', 'auth_bearer_id' => ${AUTH_BEARER_ID:-0}, 'db' => [ 'mysql' => [ 'host' => '${DB_HOST}', 'port' => ${PRIMARY_DB_PORT}, 'socket' => '', 'database' => 'url_shortener', 'username' => 'androidcast', 'password' => '${APP_PASS}', 'charset' => 'utf8mb4', ], ], ], ]; PHP BUILD_CFG="${WS}/ac-be-builder/config/config.php" if [ -f "${COMPOSED}/config/config.php" ]; then log "write ac-be-builder config.php (shared cluster session + DB)" cat > "$BUILD_CFG" <<'PHP' '/app/androidcast_project/', 'crashes' => '/app/androidcast_project/issues/', 'git' => '/app/androidcast_project/git/', 'tickets' => '/app/androidcast_project/issues/?view=tickets', ]; $config['build'] = array_merge([ 'docker_image' => 'androidcast-ci:latest', 'docker_bin' => '/usr/bin/docker', 'ci_version' => '00.01.00.1000', 'repo_root' => '/var/www/ac/workspace', 'artifacts_root' => '/var/www/ac/broadcast/builds', 'ota_mount' => '/var/www/ac/broadcast/ota-artifacts', 'ota_base_url' => 'https://apps.f0xx.org', 'runner_script' => '/var/www/ac/workspace/ac-scripts/docker-build-runner.sh', 'isolate_source' => true, 'git_clone_depth' => 1, 'pipeline_config' => '/var/www/ac/workspace/ac-scripts/build.config.yml', 'default_channels' => ['stable', 'staging', 'dev', 'nightly'], ], $config['build'] ?? []); return $config; PHP chown nginx:nginx "$BUILD_CFG" 2>/dev/null || true fi if command -v composer >/dev/null 2>&1; then log "composer dump-autoload (optional)" cat > "${COMPOSED}/composer.json" <<'JSON' { "name": "androidcast/lab-composed-backend", "require": { "php": ">=8.1" }, "autoload": { "classmap": ["src/"] } } JSON (cd "$COMPOSED" && composer dump-autoload -o 2>/dev/null) || true fi chown -R nginx:nginx "$(dirname "$COMPOSED")" "$PLATFORM_ROOT" 2>/dev/null || true log "compose-lab-backend_ok $(host_short) → ${COMPOSED}"