#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "$script_dir/../.." && pwd)" local_bundle_dir="$repo_root/deploy/local-bundle" compose_file="$local_bundle_dir/docker-compose.yml" local_bundle_env_file="$local_bundle_dir/.env" if [[ -f "$local_bundle_env_file" ]]; then set -a # shellcheck disable=SC1090 source "$local_bundle_env_file" set +a fi config_repo_dir="${CONFIG_REPO_DIR:-$repo_root/config-repo}" runtime_dir="${LOCAL_JAR_RUNTIME_DIR:-$repo_root/build/local-jar-runtime}" pid_dir="$runtime_dir/pids" log_dir="$runtime_dir/logs" if [[ "$config_repo_dir" != /* ]]; then config_repo_dir="$repo_root/$config_repo_dir" fi if [[ "$runtime_dir" != /* ]]; then runtime_dir="$repo_root/$runtime_dir" pid_dir="$runtime_dir/pids" log_dir="$runtime_dir/logs" fi config_service="spring-cloud-config-server" app_services=( pcp-srpring-boot-admin-server pcp-stations-service pcp-satellite-catalog-service pcp-request-service pcp-route-processing-service pcp-ballistics-service slots-service pcp-complex-mission-service pcp-mission-planing-service pcp-coverage-scheme-service pcp-dynamic-plan-service tle-monitoring-service pcp-tgu-service pcp-ui-service ) all_services=("$config_service" "${app_services[@]}") databases=( pcp_ballistics pcp_requests pcp_route_processing pcp_missions pcp_slots pcp_complex_plan pcp_satellite_catalog pcp_stations pcp_tgu pcp_tle pcp_satellites ) declare -A service_ports=( [spring-cloud-config-server]="${CONFIG_SERVER_PORT:-38888}" [pcp-srpring-boot-admin-server]="${PCP_ADMIN_PORT:-7000}" [tle-monitoring-service]="${PCP_TLE_MONITORING_PORT:-7001}" [pcp-complex-mission-service]="${PCP_COMPLEX_MISSION_PORT:-7002}" [pcp-ballistics-service]="${PCP_BALLISTICS_PORT:-7003}" [pcp-route-processing-service]="${PCP_ROUTE_PROCESSING_PORT:-7004}" [pcp-request-service]="${PCP_REQUEST_PORT:-7005}" [slots-service]="${PCP_SLOTS_PORT:-7006}" [pcp-ui-service]="${PCP_UI_PORT:-7008}" [pcp-stations-service]="${PCP_STATIONS_PORT:-7009}" [pcp-mission-planing-service]="${PCP_MISSION_PORT:-7010}" [pcp-tgu-service]="${PCP_TGU_PORT:-7011}" [pcp-coverage-scheme-service]="${PCP_COVERAGE_SCHEME_PORT:-7012}" [pcp-satellite-catalog-service]="${PCP_SATELLITE_CATALOG_PORT:-7013}" [pcp-dynamic-plan-service]="${PCP_DYNAMIC_PLAN_PORT:-7014}" ) gradle_max_workers="${GRADLE_MAX_WORKERS:-1}" gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}" kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}" config_server_port="${service_ports[$config_service]}" config_server_uri="${LOCAL_CONFIG_SERVER_URI:-http://localhost:$config_server_port}" service_profiles="${LOCAL_SERVICE_PROFILES:-local}" config_profiles="${LOCAL_CONFIG_PROFILES:-$service_profiles}" postgres_host="${PCP_POSTGRES_HOST:-localhost}" postgres_port="${PCP_POSTGRES_PORT:-5432}" kafka_host="${PCP_KAFKA_HOST:-localhost}" kafka_port="${PCP_KAFKA_PORT:-29092}" gradle_common_args=( "--no-parallel" "--max-workers=$gradle_max_workers" "-Dorg.gradle.jvmargs=$gradle_jvmargs" "-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs" ) usage() { cat <<'EOF' Usage: ./deploy/local-jar/run-services.sh [command] Commands: start Build bootJar artifacts and start local infra, config-server and services. restart Stop local jar processes, then run start. stop Stop jar processes started by this script. status Show pid and log status for local jar processes. build Build bootJar artifacts only. infra-up Start local Postgres/Kafka via deploy/local-bundle/docker-compose.yml. infra-down Stop local Postgres/Kafka containers. logs NAME Tail log for a service. Useful environment: START_INFRA=false Do not start Postgres/Kafka containers during start. INIT_DB=false Do not create local Postgres databases during start. WAIT_FOR_APPS=false Do not wait for each app /actuator/health. CONFIG_REPO_DIR=... Config repository used by spring-cloud-config-server. JAVA_OPTS="-Xmx1g" Extra JVM options for every jar process. LOCAL_SERVICE_PROFILES=local,foo EOF } require_command() { local command_name="$1" if ! command -v "$command_name" >/dev/null 2>&1; then echo "$command_name is required." >&2 exit 1 fi } compose() { if docker compose version >/dev/null 2>&1; then docker compose --project-directory "$local_bundle_dir" -f "$compose_file" "$@" elif command -v docker-compose >/dev/null 2>&1; then docker-compose --project-directory "$local_bundle_dir" -f "$compose_file" "$@" else echo "docker compose or docker-compose is required." >&2 exit 1 fi } http_get() { local url="$1" if command -v curl >/dev/null 2>&1; then curl -fsS "$url" >/dev/null elif command -v wget >/dev/null 2>&1; then wget -q -O /dev/null "$url" else return 2 fi } wait_for_http() { local name="$1" local url="$2" local attempts="${3:-90}" local attempt for ((attempt = 1; attempt <= attempts; attempt++)); do if http_get "$url"; then echo "$name is ready: $url" return 0 fi sleep 2 done echo "$name is not ready after $((attempts * 2)) seconds: $url" >&2 exit 1 } wait_for_postgres() { local attempt local postgres_user="${POSTGRES_USER:-postgres}" for attempt in {1..60}; do if compose exec -T postgres pg_isready -U "$postgres_user" -d postgres >/dev/null 2>&1; then echo "Postgres is ready." return 0 fi sleep 2 done echo "Postgres is not ready after 120 seconds." >&2 exit 1 } wait_for_kafka() { local attempt for attempt in {1..60}; do if compose exec -T kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then echo "Kafka is ready." return 0 fi sleep 2 done echo "Kafka is not ready after 120 seconds." >&2 exit 1 } create_database_if_missing() { local db_name="$1" local postgres_user="${POSTGRES_USER:-postgres}" local exists exists="$( compose exec -T postgres psql -U "$postgres_user" -d postgres -tAc \ "SELECT 1 FROM pg_database WHERE datname = '$db_name'" | tr -d '[:space:]' )" if [[ "$exists" == "1" ]]; then echo "Database already exists: $db_name" return 0 fi echo "Creating database: $db_name" compose exec -T postgres createdb -U "$postgres_user" "$db_name" } start_infra() { require_command docker echo "Starting local Postgres and Kafka..." compose up -d postgres kafka wait_for_postgres wait_for_kafka } init_databases() { echo "Initializing local service databases..." for db_name in "${databases[@]}"; do create_database_if_missing "$db_name" done } build_services() { echo "Building bootJar artifacts..." echo "Gradle max workers: $gradle_max_workers" echo "Gradle JVM args: $gradle_jvmargs" echo "Kotlin daemon JVM args: $kotlin_daemon_jvmargs" for service_name in "${all_services[@]}"; do echo "Building bootJar for $service_name..." "$repo_root/gradlew" "${gradle_common_args[@]}" ":services:$service_name:bootJar" done } service_pid_file() { local service_name="$1" printf '%s/%s.pid' "$pid_dir" "$service_name" } service_log_file() { local service_name="$1" printf '%s/%s.log' "$log_dir" "$service_name" } is_pid_running() { local pid="$1" [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1 } read_service_pid() { local service_name="$1" local pid_file pid_file="$(service_pid_file "$service_name")" if [[ -f "$pid_file" ]]; then tr -d '[:space:]' < "$pid_file" fi } find_boot_jar() { local service_name="$1" local jar_dir="$repo_root/services/$service_name/build/libs" local jar_count local jar_file if [[ ! -d "$jar_dir" ]]; then echo "Missing build/libs for $service_name. Run build first." >&2 exit 1 fi jar_count="$(find "$jar_dir" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)" if [[ "$jar_count" != "1" ]]; then echo "Expected exactly one bootJar for $service_name, found $jar_count:" >&2 find "$jar_dir" -maxdepth 1 -type f -name '*.jar' -print >&2 || true exit 1 fi jar_file="$(find "$jar_dir" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)" printf '%s' "$jar_file" } start_service() { local service_name="$1" local jar_file local pid_file local log_file local existing_pid local port="${service_ports[$service_name]}" local java_cmd=(java) local parsed_java_opts=() pid_file="$(service_pid_file "$service_name")" log_file="$(service_log_file "$service_name")" existing_pid="$(read_service_pid "$service_name" || true)" if is_pid_running "$existing_pid"; then echo "$service_name is already running with pid $existing_pid" return 0 fi mkdir -p "$pid_dir" "$log_dir" jar_file="$(find_boot_jar "$service_name")" if [[ -n "${JAVA_OPTS:-}" ]]; then read -r -a parsed_java_opts <<< "$JAVA_OPTS" java_cmd+=("${parsed_java_opts[@]}") fi java_cmd+=(-jar "$jar_file") echo "Starting $service_name on port $port..." if [[ "$service_name" == "$config_service" ]]; then ( cd "$repo_root" env \ SPRING_PROFILES_ACTIVE=local \ CONFIG_NATIVE_SEARCH_LOCATIONS="file:$config_repo_dir" \ SERVER_PORT="$config_server_port" \ "${java_cmd[@]}" > "$log_file" 2>&1 & echo "$!" > "$pid_file" ) else ( cd "$repo_root" env \ CONFIG_SERVER_URI="$config_server_uri" \ CONFIG_SERVER_FAIL_FAST=true \ SPRING_PROFILES_ACTIVE="$service_profiles" \ SPRING_CLOUD_CONFIG_PROFILE="$config_profiles" \ SPRING_CLOUD_CONFIG_LABEL= \ SPRING_KAFKA_ADMIN_AUTO_CREATE=true \ SPRING_KAFKA_LISTENER_MISSING_TOPICS_FATAL=false \ CAMUNDA_CLIENT_ENABLED=false \ PLANNING_EXTERNAL_UPDATE_ENABLED=false \ TLE_POLLING_ENABLED=false \ PCP_POSTGRES_HOST="$postgres_host" \ PCP_POSTGRES_PORT="$postgres_port" \ PCP_KAFKA_HOST="$kafka_host" \ PCP_KAFKA_PORT="$kafka_port" \ PCP_PORTS_CONFIG="$config_server_port" \ PCP_PORTS_ADMIN="${service_ports[pcp-srpring-boot-admin-server]}" \ PCP_PORTS_TLE_MONITORING="${service_ports[tle-monitoring-service]}" \ PCP_PORTS_COMPLEX_MISSION_SERVICE="${service_ports[pcp-complex-mission-service]}" \ PCP_PORTS_BALLISTICS="${service_ports[pcp-ballistics-service]}" \ PCP_PORTS_ROUTE_PROCESSING="${service_ports[pcp-route-processing-service]}" \ PCP_PORTS_REQUEST="${service_ports[pcp-request-service]}" \ PCP_PORTS_SLOTS="${service_ports[slots-service]}" \ PCP_PORTS_UI="${service_ports[pcp-ui-service]}" \ PCP_PORTS_STATIONS="${service_ports[pcp-stations-service]}" \ PCP_PORTS_MISSION="${service_ports[pcp-mission-planing-service]}" \ PCP_PORTS_TGU="${service_ports[pcp-tgu-service]}" \ PCP_PORTS_COVERAGE_SCHEME="${service_ports[pcp-coverage-scheme-service]}" \ PCP_PORTS_SATELLITE_CATALOG="${service_ports[pcp-satellite-catalog-service]}" \ PCP_PORTS_DYNAMIC_PLAN="${service_ports[pcp-dynamic-plan-service]}" \ "${java_cmd[@]}" > "$log_file" 2>&1 & echo "$!" > "$pid_file" ) fi echo "$service_name log: $log_file" } start_config_server() { if [[ ! -d "$config_repo_dir" ]]; then echo "Config repo directory does not exist: $config_repo_dir" >&2 exit 1 fi start_service "$config_service" wait_for_http "$config_service" "$config_server_uri/actuator/health" wait_for_http "$config_service config-repo lookup" "$config_server_uri/pcp-request-service/local" } wait_for_service_health() { local service_name="$1" local port="${service_ports[$service_name]}" wait_for_http "$service_name" "http://localhost:$port/actuator/health" "${APP_HEALTH_ATTEMPTS:-90}" } start_app_services() { for service_name in "${app_services[@]}"; do start_service "$service_name" if [[ "${WAIT_FOR_APPS:-true}" == "true" ]]; then wait_for_service_health "$service_name" fi done } stop_service() { local service_name="$1" local pid_file local pid local attempt pid_file="$(service_pid_file "$service_name")" pid="$(read_service_pid "$service_name" || true)" if ! is_pid_running "$pid"; then rm -f "$pid_file" echo "$service_name is not running." return 0 fi echo "Stopping $service_name with pid $pid..." kill "$pid" >/dev/null 2>&1 || true for attempt in {1..30}; do if ! is_pid_running "$pid"; then rm -f "$pid_file" return 0 fi sleep 1 done echo "Force stopping $service_name with pid $pid..." kill -9 "$pid" >/dev/null 2>&1 || true rm -f "$pid_file" } stop_services() { local service_name local idx for ((idx = ${#app_services[@]} - 1; idx >= 0; idx--)); do service_name="${app_services[$idx]}" stop_service "$service_name" done stop_service "$config_service" } status_services() { local service_name local pid local log_file printf '%-36s %-10s %s\n' "service" "pid" "log" for service_name in "${all_services[@]}"; do pid="$(read_service_pid "$service_name" || true)" log_file="$(service_log_file "$service_name")" if is_pid_running "$pid"; then printf '%-36s %-10s %s\n' "$service_name" "$pid" "$log_file" else printf '%-36s %-10s %s\n' "$service_name" "-" "$log_file" fi done } tail_logs() { local service_name="${1:-}" local log_file if [[ -z "$service_name" ]]; then echo "Service name is required for logs command." >&2 exit 1 fi log_file="$(service_log_file "$service_name")" if [[ ! -f "$log_file" ]]; then echo "Log file does not exist: $log_file" >&2 exit 1 fi tail -f "$log_file" } start_all() { require_command java build_services if [[ "${START_INFRA:-true}" == "true" ]]; then start_infra if [[ "${INIT_DB:-true}" == "true" ]]; then init_databases fi else echo "Skipping local infra startup. Expecting Postgres at $postgres_host:$postgres_port and Kafka at $kafka_host:$kafka_port." fi start_config_server start_app_services echo "Local jar startup requested." status_services } command="${1:-start}" case "$command" in start) start_all ;; restart) stop_services start_all ;; stop) stop_services ;; status) status_services ;; build) build_services ;; infra-up) start_infra if [[ "${INIT_DB:-true}" == "true" ]]; then init_databases fi ;; infra-down) require_command docker compose stop kafka postgres ;; logs) tail_logs "${2:-}" ;; help|-h|--help) usage ;; *) usage >&2 exit 1 ;; esac