diff --git a/.gitignore b/.gitignore index 39d4a73..9f819b2 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ out/ /pcp_zip.zip /pcp-chatgpt-context*.zip /deploy/local-bundle/build/ +/deploy/local-bundle/.env /deploy/local-bundle/pcp-local-bundle-*.tar.gz ### Node / Vite ### @@ -69,3 +70,4 @@ out/ /services/pcp-tgu-ops-ui/yarn-debug.log* /services/pcp-tgu-ops-ui/yarn-error.log* /services/pcp-tgu-ops-ui/pnpm-debug.log* +.gstack/ diff --git a/create-service-dbs.sh b/create-service-dbs.sh old mode 100644 new mode 100755 index 33534d6..fa446cb --- a/create-service-dbs.sh +++ b/create-service-dbs.sh @@ -1,4 +1,3 @@ -cat > create-service-dbs.sh <<'BASH' #!/usr/bin/env bash set -euo pipefail @@ -124,4 +123,3 @@ done echo echo "All requested databases processed." -BASH diff --git a/deploy/local-bundle/.env.example b/deploy/local-bundle/.env.example index a93158d..6ad9046 100644 --- a/deploy/local-bundle/.env.example +++ b/deploy/local-bundle/.env.example @@ -1,5 +1,18 @@ COMPOSE_PROJECT_NAME=pcp-local +USE_EXTERNAL_INFRA=false +PCP_DOCKER_NETWORK=pcp-local_default +PCP_DOCKER_NETWORK_EXTERNAL=false + +# Existing allinone infra example: +# USE_EXTERNAL_INFRA=true +# PCP_DOCKER_NETWORK=allinone_camunda +# PCP_DOCKER_NETWORK_EXTERNAL=true +# EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1 +# EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1 +# CAMUNDA_CLIENT_ENABLED=true +# CAMUNDA_GRPC_ADDRESS=http://orchestration:26500 + PCP_IMAGE_TAG=local POSTGRES_IMAGE=postgis/postgis:16-3.4 @@ -8,14 +21,24 @@ KAFKA_IMAGE=apache/kafka:3.7.2 POSTGRES_USER=postgres POSTGRES_PASSWORD=password POSTGRES_PORT=5432 +PCP_POSTGRES_HOST=postgres +PCP_POSTGRES_PORT=5432 +EXTERNAL_POSTGRES_CONTAINER= KAFKA_PORT=29092 +PCP_KAFKA_HOST=kafka +PCP_KAFKA_PORT=9092 +EXTERNAL_KAFKA_CONTAINER= + +CAMUNDA_CLIENT_ENABLED=false +CAMUNDA_GRPC_ADDRESS=http://host.docker.internal:26500 + CONFIG_SERVER_PORT=38888 PCP_ADMIN_PORT=7000 -PCP_TLE_MONITORING_PORT=7001 PCP_COMPLEX_MISSION_PORT=7002 PCP_BALLISTICS_PORT=7003 +PCP_ROUTE_PROCESSING_PORT=7004 PCP_REQUEST_PORT=7005 PCP_SLOTS_PORT=7006 PCP_UI_PORT=7008 diff --git a/deploy/local-bundle/README-local.md b/deploy/local-bundle/README-local.md index fc8b1ae..1840177 100644 --- a/deploy/local-bundle/README-local.md +++ b/deploy/local-bundle/README-local.md @@ -2,6 +2,9 @@ Локальный bundle предназначен для запуска PCP на машине с Docker без GitLab Config Repo, Kubernetes и Helm. +В bundle входят backend-сервисы PCP, кроме `tle-monitoring-service`; отдельный frontend +`pcp-tgu-ops-ui` также не включается. + ## Состав - `docker-compose.yml` - локальная схема запуска. @@ -15,6 +18,72 @@ - `scripts/stop.sh` - остановка compose stack. - `scripts/status.sh` - статус контейнеров. +## Локальный запуск без архива + +Для запуска на той же машине, где собираются images, архив и `docker save` не нужны. +Используйте один скрипт: + +```bash +./deploy/local-bundle/run-local.sh start +``` + +Скрипт: + +- создаёт `.env` из `.env.example`, если его ещё нет; +- собирает `bootJar` в Gradle offline mode; +- собирает локальные Docker images `pcp/:`; +- запускает `docker compose`. + +Для вашего allinone Postgres/Kafka/Camunda перед первым запуском настройте +`deploy/local-bundle/.env`: + +```env +USE_EXTERNAL_INFRA=true +PCP_DOCKER_NETWORK=allinone_camunda +PCP_DOCKER_NETWORK_EXTERNAL=true +PCP_POSTGRES_HOST=postgres +PCP_POSTGRES_PORT=5432 +PCP_KAFKA_HOST=kafka +PCP_KAFKA_PORT=9092 +EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1 +EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1 +CAMUNDA_CLIENT_ENABLED=true +CAMUNDA_GRPC_ADDRESS=http://orchestration:26500 +``` + +Команды: + +```bash +./deploy/local-bundle/run-local.sh start +./deploy/local-bundle/run-local.sh status +./deploy/local-bundle/run-local.sh logs +./deploy/local-bundle/run-local.sh stop +``` + +## Пересборка одного сервиса + +После первого запуска всего стека можно пересобирать и перезапускать только один сервис: + +```bash +./deploy/local-bundle/services/pcp-request-service.sh +``` + +Это выполнит: + +- `bootJar` только для `pcp-request-service`; +- `docker build` только для `pcp/pcp-request-service:`; +- `docker compose up -d --no-deps --force-recreate pcp-request-service`. + +Доступны wrapper-скрипты в `deploy/local-bundle/services/` для каждого сервиса. +То же самое можно вызвать через общий скрипт: + +```bash +./deploy/local-bundle/run-service.sh pcp-request-service +./deploy/local-bundle/run-service.sh pcp-request-service build +./deploy/local-bundle/run-service.sh pcp-request-service logs +./deploy/local-bundle/run-service.sh pcp-request-service stop +``` + ## Сборка архива Из корня репозитория: @@ -38,6 +107,7 @@ deploy/local-bundle/build/pcp-local-bundle-.tar.gz Скрипт сборки: - собирает `bootJar` для разворачиваемых сервисов; +- запускает Gradle в offline mode по умолчанию; - собирает Docker images сервисов; - сохраняет PCP service images в один `images/pcp-services.tar`; - по умолчанию не сохраняет публичные infra images, чтобы не раздувать архив; @@ -61,6 +131,41 @@ KOTLIN_DAEMON_JVMARGS="-Xmx3g -XX:MaxMetaspaceSize=768m" \ ./deploy/local-bundle/build-local-bundle.sh ``` +Gradle offline mode включён по умолчанию: + +```bash +GRADLE_OFFLINE=true ./deploy/local-bundle/build-local-bundle.sh +``` + +Если нужно один раз прогреть локальный Gradle cache, временно отключите offline mode: + +```bash +GRADLE_OFFLINE=false ./deploy/local-bundle/build-local-bundle.sh +``` + +Важно: `GRADLE_OFFLINE=true` отключает сетевой резолв зависимостей Gradle, но Docker всё равно +должен иметь локально доступный base image. Для local bundle сервисные Dockerfile собираются с +локальным override: + +```text +LOCAL_BUNDLE_RUNTIME_IMAGE=bellsoft/liberica-openjre-alpine:21.0.5 +``` + +Поэтому перед полностью offline Docker-сборкой подтяните или импортируйте: + +```bash +docker pull bellsoft/liberica-openjre-alpine:21.0.5 +``` + +Для полностью переносимого архива с infra images заранее подтяните Postgres/Kafka и включите их +сохранение: + +```bash +docker pull postgis/postgis:16-3.4 +docker pull apache/kafka:3.7.2 +INCLUDE_INFRA_IMAGES=true ./deploy/local-bundle/build-local-bundle.sh +``` + ## Запуск на локальной машине Распаковать архив: @@ -72,6 +177,26 @@ cd pcp-local-bundle- Проверить `.env` и при необходимости изменить порты. +Если Postgres/Kafka/Camunda уже подняты внешним allinone compose, например в сети +`allinone_camunda`, включите external infra mode в `.env`: + +```env +USE_EXTERNAL_INFRA=true +PCP_DOCKER_NETWORK=allinone_camunda +PCP_DOCKER_NETWORK_EXTERNAL=true +PCP_POSTGRES_HOST=postgres +PCP_POSTGRES_PORT=5432 +PCP_KAFKA_HOST=kafka +PCP_KAFKA_PORT=9092 +EXTERNAL_POSTGRES_CONTAINER=allinone-postgres-1 +EXTERNAL_KAFKA_CONTAINER=allinone-kafka-1 +CAMUNDA_CLIENT_ENABLED=true +CAMUNDA_GRPC_ADDRESS=http://orchestration:26500 +``` + +В этом режиме `scripts/start.sh` не поднимает свои `postgres` и `kafka`, а сервисы PCP +подключаются к уже существующим контейнерам через общую Docker network. + Импортировать images: ```bash @@ -109,9 +234,9 @@ cd pcp-local-bundle- - Config Server: `http://localhost:38888` - Spring Boot Admin: `http://localhost:7000` -- TLE Monitoring: `http://localhost:7001` - Complex Mission: `http://localhost:7002` - Ballistics: `http://localhost:7003` +- Route Processing: `http://localhost:7004` - Request Service: `http://localhost:7005` - Slots Service: `http://localhost:7006` - UI: `http://localhost:7008` @@ -178,13 +303,13 @@ SPRING_CLOUD_CONFIG_LABEL= - `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` Таблицы, схемы и seed data вручную не создаются. Схемы создаются существующими Flyway migrations внутри сервисов при старте приложений. diff --git a/deploy/local-bundle/build-local-bundle.sh b/deploy/local-bundle/build-local-bundle.sh index 974578c..8455e27 100755 --- a/deploy/local-bundle/build-local-bundle.sh +++ b/deploy/local-bundle/build-local-bundle.sh @@ -26,6 +26,7 @@ service_names=( pcp-satellite-catalog-service pcp-ballistics-service pcp-request-service + pcp-route-processing-service slots-service pcp-complex-mission-service pcp-mission-planing-service @@ -33,13 +34,14 @@ service_names=( pcp-dynamic-plan-service pcp-stations-service pcp-ui-service - tle-monitoring-service pcp-tgu-service ) 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}" +gradle_offline="${GRADLE_OFFLINE:-true}" +runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}" include_infra_images="${INCLUDE_INFRA_IMAGES:-false}" gradle_common_args=( @@ -49,6 +51,10 @@ gradle_common_args=( "-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs" ) +if [[ "$gradle_offline" == "true" ]]; then + gradle_common_args+=("--offline") +fi + infra_images=( "${POSTGRES_IMAGE:-postgis/postgis:16-3.4}" "${KAFKA_IMAGE:-apache/kafka:3.7.2}" @@ -117,9 +123,11 @@ 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" +echo "Gradle offline mode: $gradle_offline" +echo "Local bundle runtime image: $runtime_image" for service_name in "${service_names[@]}"; do echo "Building bootJar for $service_name..." - "$repo_root/gradlew" "${gradle_common_args[@]}" ":services:$service_name:bootJar" + "$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar" done rm -rf "$dist_dir" "$docker_contexts_dir" "$archive_path" @@ -132,7 +140,11 @@ for service_name in "${service_names[@]}"; do context_dir="$docker_contexts_dir/$service_name" copy_single_boot_jar "$service_name" "$context_dir" - docker build --file "$context_dir/Dockerfile" --tag "$image_name" "$context_dir" + docker build \ + --build-arg "RUNTIME_IMAGE=$runtime_image" \ + --file "$context_dir/Dockerfile" \ + --tag "$image_name" \ + "$context_dir" service_images+=("$image_name") done diff --git a/deploy/local-bundle/docker-compose.yml b/deploy/local-bundle/docker-compose.yml index dd52411..8a5437d 100644 --- a/deploy/local-bundle/docker-compose.yml +++ b/deploy/local-bundle/docker-compose.yml @@ -8,9 +8,17 @@ x-pcp-service-env: &pcp-service-env SPRING_CLOUD_CONFIG_LABEL: "" SPRING_KAFKA_ADMIN_AUTO_CREATE: "true" SPRING_KAFKA_LISTENER_AUTO_STARTUP: "true" + PCP_POSTGRES_HOST: ${PCP_POSTGRES_HOST:-postgres} + PCP_POSTGRES_PORT: ${PCP_POSTGRES_PORT:-5432} + PCP_KAFKA_HOST: ${PCP_KAFKA_HOST:-kafka} + PCP_KAFKA_PORT: ${PCP_KAFKA_PORT:-9092} + CAMUNDA_CLIENT_ENABLED: ${CAMUNDA_CLIENT_ENABLED:-false} + CAMUNDA_GRPC_ADDRESS: ${CAMUNDA_GRPC_ADDRESS:-http://host.docker.internal:26500} x-pcp-service-common: &pcp-service-common restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: spring-cloud-config-server: condition: service_started @@ -64,11 +72,8 @@ services: spring-cloud-config-server: image: pcp/spring-cloud-config-server:${PCP_IMAGE_TAG:-local} restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - kafka: - condition: service_started + extra_hosts: + - "host.docker.internal:host-gateway" environment: SPRING_PROFILES_ACTIVE: local CONFIG_NATIVE_SEARCH_LOCATIONS: file:/config-repo @@ -86,14 +91,6 @@ services: ports: - "${PCP_ADMIN_PORT:-7000}:7000" - tle-monitoring-service: - <<: *pcp-service-common - image: pcp/tle-monitoring-service:${PCP_IMAGE_TAG:-local} - environment: - <<: *pcp-service-env - ports: - - "${PCP_TLE_MONITORING_PORT:-7001}:7001" - pcp-complex-mission-service: <<: *pcp-service-common image: pcp/pcp-complex-mission-service:${PCP_IMAGE_TAG:-local} @@ -118,6 +115,14 @@ services: ports: - "${PCP_REQUEST_PORT:-7005}:7005" + pcp-route-processing-service: + <<: *pcp-service-common + image: pcp/pcp-route-processing-service:${PCP_IMAGE_TAG:-local} + environment: + <<: *pcp-service-env + ports: + - "${PCP_ROUTE_PROCESSING_PORT:-7004}:7004" + slots-service: <<: *pcp-service-common image: pcp/slots-service:${PCP_IMAGE_TAG:-local} @@ -193,3 +198,8 @@ services: volumes: postgres-data: kafka-data: + +networks: + default: + name: ${PCP_DOCKER_NETWORK:-pcp-local_default} + external: ${PCP_DOCKER_NETWORK_EXTERNAL:-false} diff --git a/deploy/local-bundle/run-local.sh b/deploy/local-bundle/run-local.sh new file mode 100755 index 0000000..5771eaa --- /dev/null +++ b/deploy/local-bundle/run-local.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../.." && pwd)" +env_file="$script_dir/.env" +env_example_file="$script_dir/.env.example" +docker_contexts_dir="$script_dir/build/local-docker-contexts" + +service_names=( + spring-cloud-config-server + pcp-srpring-boot-admin-server + pcp-satellite-catalog-service + pcp-ballistics-service + pcp-request-service + pcp-route-processing-service + slots-service + pcp-complex-mission-service + pcp-mission-planing-service + pcp-coverage-scheme-service + pcp-dynamic-plan-service + pcp-stations-service + pcp-ui-service + pcp-tgu-service +) + +ensure_env_file() { + if [[ ! -f "$env_file" && -f "$env_example_file" ]]; then + cp "$env_example_file" "$env_file" + echo "Created $env_file from .env.example" + fi +} + +ensure_source_config_repo_dir() { + if grep -q '^CONFIG_REPO_DIR=' "$env_file"; then + sed -i "s#^CONFIG_REPO_DIR=.*#CONFIG_REPO_DIR=$repo_root/config-repo#" "$env_file" + else + printf '\nCONFIG_REPO_DIR=%s/config-repo\n' "$repo_root" >> "$env_file" + fi +} + +load_env() { + set -a + # shellcheck disable=SC1090 + source "$env_file" + set +a +} + +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 "$script_dir" -f "$script_dir/docker-compose.yml" "$@" + elif command -v docker-compose >/dev/null 2>&1; then + docker-compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@" + else + echo "docker compose or docker-compose is required." >&2 + exit 1 + fi +} + +copy_single_boot_jar() { + local service_name="$1" + local context_dir="$2" + local jar_count + local jar_file + + jar_count="$( + find "$repo_root/services/$service_name/build/libs" -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 "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true + exit 1 + fi + + jar_file="$( + find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1 + )" + + mkdir -p "$context_dir/build/libs" + cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile" + cp "$jar_file" "$context_dir/build/libs/app.jar" +} + +build_images() { + local gradle_max_workers="${GRADLE_MAX_WORKERS:-1}" + local gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}" + local kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}" + local gradle_offline="${GRADLE_OFFLINE:-true}" + local runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}" + local image_tag="${PCP_IMAGE_TAG:-local}" + local service_name + local image_name + local context_dir + local gradle_common_args=( + "--no-parallel" + "--max-workers=$gradle_max_workers" + "-Dorg.gradle.jvmargs=$gradle_jvmargs" + "-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs" + ) + + if [[ "$gradle_offline" == "true" ]]; then + gradle_common_args+=("--offline") + fi + + echo "Building local Docker images..." + echo "Gradle offline mode: $gradle_offline" + echo "Local image tag: $image_tag" + echo "Runtime image: $runtime_image" + + for service_name in "${service_names[@]}"; do + echo "Building bootJar for $service_name..." + "$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar" + done + + rm -rf "$docker_contexts_dir" + mkdir -p "$docker_contexts_dir" + + for service_name in "${service_names[@]}"; do + image_name="pcp/$service_name:$image_tag" + context_dir="$docker_contexts_dir/$service_name" + + copy_single_boot_jar "$service_name" "$context_dir" + echo "Building Docker image $image_name..." + docker build \ + --build-arg "RUNTIME_IMAGE=$runtime_image" \ + --file "$context_dir/Dockerfile" \ + --tag "$image_name" \ + "$context_dir" + done +} + +usage() { + cat <<'EOF' +Usage: + ./deploy/local-bundle/run-local.sh [command] + +Commands: + start Build service images locally and start docker compose. + restart Stop compose stack, then build and start. + build Build service images locally, without starting compose. + stop Stop compose stack. + status Show compose status. + logs Follow compose logs. +EOF +} + +start_local() { + build_images + "$script_dir/scripts/start.sh" +} + +command="${1:-start}" +case "$command" in + help|-h|--help) + usage + exit 0 + ;; +esac + +ensure_env_file +ensure_source_config_repo_dir +load_env +require_command docker + +case "$command" in + start) + start_local + ;; + restart) + "$script_dir/scripts/stop.sh" + start_local + ;; + build) + build_images + ;; + stop) + "$script_dir/scripts/stop.sh" + ;; + status) + "$script_dir/scripts/status.sh" + ;; + logs) + compose logs -f + ;; + *) + usage >&2 + exit 1 + ;; +esac diff --git a/deploy/local-bundle/run-service.sh b/deploy/local-bundle/run-service.sh new file mode 100755 index 0000000..684999f --- /dev/null +++ b/deploy/local-bundle/run-service.sh @@ -0,0 +1,242 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../.." && pwd)" +env_file="$script_dir/.env" +env_example_file="$script_dir/.env.example" +docker_contexts_dir="$script_dir/build/local-docker-contexts" + +service_names=( + spring-cloud-config-server + pcp-srpring-boot-admin-server + pcp-satellite-catalog-service + pcp-ballistics-service + pcp-request-service + pcp-route-processing-service + slots-service + pcp-complex-mission-service + pcp-mission-planing-service + pcp-coverage-scheme-service + pcp-dynamic-plan-service + pcp-stations-service + pcp-ui-service + pcp-tgu-service +) + +usage() { + cat <<'EOF' +Usage: + ./deploy/local-bundle/run-service.sh SERVICE [command] + +Commands: + restart Build SERVICE bootJar, rebuild its Docker image and recreate only this compose service. + build Build SERVICE bootJar and Docker image, without restarting container. + stop Stop SERVICE container. + logs Follow SERVICE logs. + +Examples: + ./deploy/local-bundle/run-service.sh pcp-request-service + ./deploy/local-bundle/run-service.sh pcp-request-service build +EOF +} + +ensure_env_file() { + if [[ ! -f "$env_file" && -f "$env_example_file" ]]; then + cp "$env_example_file" "$env_file" + echo "Created $env_file from .env.example" + fi +} + +ensure_source_config_repo_dir() { + if grep -q '^CONFIG_REPO_DIR=' "$env_file"; then + sed -i "s#^CONFIG_REPO_DIR=.*#CONFIG_REPO_DIR=$repo_root/config-repo#" "$env_file" + else + printf '\nCONFIG_REPO_DIR=%s/config-repo\n' "$repo_root" >> "$env_file" + fi +} + +load_env() { + set -a + # shellcheck disable=SC1090 + source "$env_file" + set +a +} + +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 "$script_dir" -f "$script_dir/docker-compose.yml" "$@" + elif command -v docker-compose >/dev/null 2>&1; then + docker-compose --project-directory "$script_dir" -f "$script_dir/docker-compose.yml" "$@" + else + echo "docker compose or docker-compose is required." >&2 + exit 1 + fi +} + +is_known_service() { + local service_name="$1" + local known_service_name + + for known_service_name in "${service_names[@]}"; do + if [[ "$known_service_name" == "$service_name" ]]; then + return 0 + fi + done + + return 1 +} + +copy_single_boot_jar() { + local service_name="$1" + local context_dir="$2" + local jar_count + local jar_file + + jar_count="$( + find "$repo_root/services/$service_name/build/libs" -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 "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' -print >&2 || true + exit 1 + fi + + jar_file="$( + find "$repo_root/services/$service_name/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1 + )" + + mkdir -p "$context_dir/build/libs" + cp "$repo_root/services/$service_name/Dockerfile" "$context_dir/Dockerfile" + cp "$jar_file" "$context_dir/build/libs/app.jar" +} + +build_service_image() { + local service_name="$1" + local gradle_max_workers="${GRADLE_MAX_WORKERS:-1}" + local gradle_jvmargs="${GRADLE_JVMARGS:--Xmx2g -Dfile.encoding=UTF-8}" + local kotlin_daemon_jvmargs="${KOTLIN_DAEMON_JVMARGS:--Xmx3g -XX:MaxMetaspaceSize=768m}" + local gradle_offline="${GRADLE_OFFLINE:-true}" + local runtime_image="${LOCAL_BUNDLE_RUNTIME_IMAGE:-bellsoft/liberica-openjre-alpine:21.0.5}" + local image_tag="${PCP_IMAGE_TAG:-local}" + local image_name="pcp/$service_name:$image_tag" + local context_dir="$docker_contexts_dir/$service_name" + local gradle_common_args=( + "--no-parallel" + "--max-workers=$gradle_max_workers" + "-Dorg.gradle.jvmargs=$gradle_jvmargs" + "-Pkotlin.daemon.jvmargs=$kotlin_daemon_jvmargs" + ) + + if [[ "$gradle_offline" == "true" ]]; then + gradle_common_args+=("--offline") + fi + + echo "Building bootJar for $service_name..." + "$repo_root/gradlew" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar" + + rm -rf "$context_dir" + copy_single_boot_jar "$service_name" "$context_dir" + + echo "Building Docker image $image_name..." + docker build \ + --build-arg "RUNTIME_IMAGE=$runtime_image" \ + --file "$context_dir/Dockerfile" \ + --tag "$image_name" \ + "$context_dir" +} + +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_config_server() { + local attempt + local url="http://localhost:${CONFIG_SERVER_PORT:-38888}/actuator/health" + + for attempt in {1..90}; do + if http_get "$url"; then + echo "spring-cloud-config-server is ready: $url" + return 0 + fi + sleep 2 + done + + echo "spring-cloud-config-server is not ready after 180 seconds: $url" >&2 + exit 1 +} + +restart_service() { + local service_name="$1" + + build_service_image "$service_name" + + if [[ "$service_name" == "spring-cloud-config-server" ]]; then + compose up -d --no-deps --force-recreate "$service_name" + wait_for_config_server + return 0 + fi + + compose up -d spring-cloud-config-server + wait_for_config_server + compose up -d --no-deps --force-recreate "$service_name" +} + +service_name="${1:-}" +command="${2:-restart}" + +if [[ -z "$service_name" || "$service_name" == "help" || "$service_name" == "-h" || "$service_name" == "--help" ]]; then + usage + exit 0 +fi + +if ! is_known_service "$service_name"; then + echo "Unknown service: $service_name" >&2 + echo "Known services:" >&2 + printf ' %s\n' "${service_names[@]}" >&2 + exit 1 +fi + +ensure_env_file +ensure_source_config_repo_dir +load_env +require_command docker + +case "$command" in + restart) + restart_service "$service_name" + ;; + build) + build_service_image "$service_name" + ;; + stop) + compose stop "$service_name" + ;; + logs) + compose logs -f "$service_name" + ;; + help|-h|--help) + usage + ;; + *) + usage >&2 + exit 1 + ;; +esac diff --git a/deploy/local-bundle/scripts/import-images.sh b/deploy/local-bundle/scripts/import-images.sh index f48ad24..25fb0cc 100755 --- a/deploy/local-bundle/scripts/import-images.sh +++ b/deploy/local-bundle/scripts/import-images.sh @@ -25,6 +25,10 @@ ensure_env_file() { } ensure_env_file +set -a +# shellcheck disable=SC1091 +source "$bundle_dir/.env" +set +a if [[ ! -d "$images_dir" ]]; then echo "Images directory not found: $images_dir" >&2 @@ -49,7 +53,12 @@ if [[ -f "$images_dir/public-images.txt" ]]; then fi cd "$bundle_dir" -echo "Starting postgres, kafka and spring-cloud-config-server..." -compose up -d postgres kafka spring-cloud-config-server +if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then + echo "Using external infrastructure. Starting spring-cloud-config-server only." + compose up -d spring-cloud-config-server +else + echo "Starting postgres, kafka and spring-cloud-config-server..." + compose up -d postgres kafka spring-cloud-config-server +fi echo "Images imported and base services requested." diff --git a/deploy/local-bundle/scripts/init-db.sh b/deploy/local-bundle/scripts/init-db.sh index 1fa3d8f..cae5684 100755 --- a/deploy/local-bundle/scripts/init-db.sh +++ b/deploy/local-bundle/scripts/init-db.sh @@ -43,6 +43,26 @@ wait_for_postgres() { exit 1 } +wait_for_external_postgres() { + local container_name="${EXTERNAL_POSTGRES_CONTAINER:-}" + local attempt + + if [[ -z "$container_name" ]]; then + echo "EXTERNAL_POSTGRES_CONTAINER is required when USE_EXTERNAL_INFRA=true and SKIP_DB_INIT is not true." >&2 + exit 1 + fi + + for attempt in {1..60}; do + if docker exec "$container_name" pg_isready -U "${POSTGRES_USER:-postgres}" -d postgres >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + + echo "External Postgres is not ready after 120 seconds: $container_name" >&2 + exit 1 +} + create_database_if_missing() { local db_name="$1" local exists @@ -61,16 +81,35 @@ create_database_if_missing() { compose exec -T postgres createdb -U "${POSTGRES_USER:-postgres}" "$db_name" } +create_external_database_if_missing() { + local db_name="$1" + local container_name="${EXTERNAL_POSTGRES_CONTAINER:-}" + local exists + + exists="$( + docker exec "$container_name" psql -U "${POSTGRES_USER:-postgres}" -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" + docker exec "$container_name" createdb -U "${POSTGRES_USER:-postgres}" "$db_name" +} + 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 ) @@ -78,12 +117,20 @@ ensure_env_file load_env cd "$bundle_dir" -compose up -d postgres -wait_for_postgres +if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then + wait_for_external_postgres -for db_name in "${databases[@]}"; do - create_database_if_missing "$db_name" -done + for db_name in "${databases[@]}"; do + create_external_database_if_missing "$db_name" + done +else + compose up -d postgres + wait_for_postgres + + for db_name in "${databases[@]}"; do + create_database_if_missing "$db_name" + done +fi echo "Database initialization completed. Tables are left to service Flyway migrations." echo "No schema objects or seed data were created by this script." diff --git a/deploy/local-bundle/scripts/start.sh b/deploy/local-bundle/scripts/start.sh index 32fe041..f8a24e5 100755 --- a/deploy/local-bundle/scripts/start.sh +++ b/deploy/local-bundle/scripts/start.sh @@ -72,11 +72,34 @@ wait_for_kafka() { exit 1 } +wait_for_external_kafka() { + local container_name="${EXTERNAL_KAFKA_CONTAINER:-}" + local attempt + + if [[ -z "$container_name" ]]; then + echo "External Kafka container is not configured; skipping Kafka readiness check." + return 0 + fi + + for attempt in {1..60}; do + if docker exec "$container_name" kafka-topics --bootstrap-server "${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" --list >/dev/null 2>&1 || + docker exec "$container_name" kafka-topics.sh --bootstrap-server "${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" --list >/dev/null 2>&1; then + echo "External Kafka is ready in container $container_name." + return 0 + fi + sleep 2 + done + + echo "External Kafka is not ready after 120 seconds: $container_name" >&2 + exit 1 +} + app_services=( pcp-srpring-boot-admin-server pcp-satellite-catalog-service pcp-ballistics-service pcp-request-service + pcp-route-processing-service slots-service pcp-complex-mission-service pcp-mission-planing-service @@ -84,7 +107,6 @@ app_services=( pcp-dynamic-plan-service pcp-stations-service pcp-ui-service - tle-monitoring-service pcp-tgu-service ) @@ -93,9 +115,16 @@ load_env cd "$bundle_dir" -echo "Starting infrastructure..." -compose up -d postgres kafka -wait_for_kafka +if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then + echo "Using external infrastructure. Skipping local postgres/kafka startup." + echo "Postgres endpoint for services: ${PCP_POSTGRES_HOST:-postgres}:${PCP_POSTGRES_PORT:-5432}" + echo "Kafka endpoint for services: ${PCP_KAFKA_HOST:-kafka}:${PCP_KAFKA_PORT:-9092}" + wait_for_external_kafka +else + echo "Starting infrastructure..." + compose up -d postgres kafka + wait_for_kafka +fi if [[ "${SKIP_DB_INIT:-false}" != "true" ]]; then "$script_dir/init-db.sh" @@ -106,7 +135,10 @@ compose up -d spring-cloud-config-server wait_for_http "spring-cloud-config-server" "http://localhost:${CONFIG_SERVER_PORT:-38888}/actuator/health" echo "Starting application services..." -compose up -d "${app_services[@]}" +for service_name in "${app_services[@]}"; do + echo "Starting $service_name..." + compose up -d "$service_name" +done echo "Application startup requested." compose ps diff --git a/deploy/local-bundle/services/pcp-ballistics-service.sh b/deploy/local-bundle/services/pcp-ballistics-service.sh new file mode 100755 index 0000000..cf0366e --- /dev/null +++ b/deploy/local-bundle/services/pcp-ballistics-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-ballistics-service "$@" diff --git a/deploy/local-bundle/services/pcp-complex-mission-service.sh b/deploy/local-bundle/services/pcp-complex-mission-service.sh new file mode 100755 index 0000000..5e1c1fc --- /dev/null +++ b/deploy/local-bundle/services/pcp-complex-mission-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-complex-mission-service "$@" diff --git a/deploy/local-bundle/services/pcp-coverage-scheme-service.sh b/deploy/local-bundle/services/pcp-coverage-scheme-service.sh new file mode 100755 index 0000000..692959f --- /dev/null +++ b/deploy/local-bundle/services/pcp-coverage-scheme-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-coverage-scheme-service "$@" diff --git a/deploy/local-bundle/services/pcp-dynamic-plan-service.sh b/deploy/local-bundle/services/pcp-dynamic-plan-service.sh new file mode 100755 index 0000000..ede4158 --- /dev/null +++ b/deploy/local-bundle/services/pcp-dynamic-plan-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-dynamic-plan-service "$@" diff --git a/deploy/local-bundle/services/pcp-mission-planing-service.sh b/deploy/local-bundle/services/pcp-mission-planing-service.sh new file mode 100755 index 0000000..b9ea620 --- /dev/null +++ b/deploy/local-bundle/services/pcp-mission-planing-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-mission-planing-service "$@" diff --git a/deploy/local-bundle/services/pcp-request-service.sh b/deploy/local-bundle/services/pcp-request-service.sh new file mode 100755 index 0000000..39c1680 --- /dev/null +++ b/deploy/local-bundle/services/pcp-request-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-request-service "$@" diff --git a/deploy/local-bundle/services/pcp-route-processing-service.sh b/deploy/local-bundle/services/pcp-route-processing-service.sh new file mode 100755 index 0000000..82cd037 --- /dev/null +++ b/deploy/local-bundle/services/pcp-route-processing-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-route-processing-service "$@" diff --git a/deploy/local-bundle/services/pcp-satellite-catalog-service.sh b/deploy/local-bundle/services/pcp-satellite-catalog-service.sh new file mode 100755 index 0000000..3620854 --- /dev/null +++ b/deploy/local-bundle/services/pcp-satellite-catalog-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-satellite-catalog-service "$@" diff --git a/deploy/local-bundle/services/pcp-srpring-boot-admin-server.sh b/deploy/local-bundle/services/pcp-srpring-boot-admin-server.sh new file mode 100755 index 0000000..c2310d5 --- /dev/null +++ b/deploy/local-bundle/services/pcp-srpring-boot-admin-server.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-srpring-boot-admin-server "$@" diff --git a/deploy/local-bundle/services/pcp-stations-service.sh b/deploy/local-bundle/services/pcp-stations-service.sh new file mode 100755 index 0000000..c79d12f --- /dev/null +++ b/deploy/local-bundle/services/pcp-stations-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-stations-service "$@" diff --git a/deploy/local-bundle/services/pcp-tgu-service.sh b/deploy/local-bundle/services/pcp-tgu-service.sh new file mode 100755 index 0000000..a179740 --- /dev/null +++ b/deploy/local-bundle/services/pcp-tgu-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-tgu-service "$@" diff --git a/deploy/local-bundle/services/pcp-ui-service.sh b/deploy/local-bundle/services/pcp-ui-service.sh new file mode 100755 index 0000000..5a3c3ab --- /dev/null +++ b/deploy/local-bundle/services/pcp-ui-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" pcp-ui-service "$@" diff --git a/deploy/local-bundle/services/slots-service.sh b/deploy/local-bundle/services/slots-service.sh new file mode 100755 index 0000000..d3ef6d2 --- /dev/null +++ b/deploy/local-bundle/services/slots-service.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" slots-service "$@" diff --git a/deploy/local-bundle/services/spring-cloud-config-server.sh b/deploy/local-bundle/services/spring-cloud-config-server.sh new file mode 100755 index 0000000..1fa0025 --- /dev/null +++ b/deploy/local-bundle/services/spring-cloud-config-server.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "$script_dir/run-service.sh" spring-cloud-config-server "$@" diff --git a/deploy/local-jar/README.md b/deploy/local-jar/README.md new file mode 100644 index 0000000..83dc930 --- /dev/null +++ b/deploy/local-jar/README.md @@ -0,0 +1,79 @@ +# Local jar runner + +Скрипт запускает backend-сервисы как Spring Boot jar'ы на локальной машине. +Spring Cloud Config Server стартует первым и читает конфигурацию из локального +`config-repo`. + +## Запуск + +Из корня репозитория: + +```bash +./deploy/local-jar/run-services.sh start +``` + +Что делает `start`: + +- собирает `bootJar` для `spring-cloud-config-server` и всех Spring Boot сервисов; +- поднимает локальные `postgres` и `kafka` через `deploy/local-bundle/docker-compose.yml`; +- создаёт сервисные базы данных, если их ещё нет; +- запускает `spring-cloud-config-server` на `http://localhost:38888`; +- проверяет lookup конфигурации из `config-repo`; +- запускает остальные jar'ы в заданной последовательности. + +Runtime-файлы создаются в `build/local-jar-runtime`: + +- `pids/` - PID процессов; +- `logs/` - stdout/stderr каждого сервиса. + +## Команды + +```bash +./deploy/local-jar/run-services.sh status +./deploy/local-jar/run-services.sh logs pcp-request-service +./deploy/local-jar/run-services.sh stop +./deploy/local-jar/run-services.sh restart +``` + +Только сборка: + +```bash +./deploy/local-jar/run-services.sh build +``` + +Только инфраструктура: + +```bash +./deploy/local-jar/run-services.sh infra-up +./deploy/local-jar/run-services.sh infra-down +``` + +## Настройки + +По умолчанию config-server использует: + +```text +SPRING_PROFILES_ACTIVE=local +CONFIG_NATIVE_SEARCH_LOCATIONS=file:/config-repo +SERVER_PORT=38888 +``` + +Сервисы используют: + +```text +SPRING_PROFILES_ACTIVE=local +SPRING_CLOUD_CONFIG_PROFILE=local +SPRING_CLOUD_CONFIG_LABEL= +CONFIG_SERVER_URI=http://localhost:38888 +``` + +Полезные override'ы: + +```bash +START_INFRA=false ./deploy/local-jar/run-services.sh start +INIT_DB=false ./deploy/local-jar/run-services.sh start +WAIT_FOR_APPS=false ./deploy/local-jar/run-services.sh start +JAVA_OPTS="-Xmx1g" ./deploy/local-jar/run-services.sh start +CONFIG_REPO_DIR=/path/to/config-repo ./deploy/local-jar/run-services.sh start +LOCAL_SERVICE_PROFILES=local,foo ./deploy/local-jar/run-services.sh start +``` diff --git a/deploy/local-jar/run-services.sh b/deploy/local-jar/run-services.sh new file mode 100755 index 0000000..d602f5f --- /dev/null +++ b/deploy/local-jar/run-services.sh @@ -0,0 +1,538 @@ +#!/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 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 diff --git a/docs/аудит-проекта-2026-06-06.md b/docs/аудит-проекта-2026-06-06.md new file mode 100644 index 0000000..3062d0b --- /dev/null +++ b/docs/аудит-проекта-2026-06-06.md @@ -0,0 +1,173 @@ +# Аудит проекта observatio-terrae + +Дата: 2026-06-06. Автор: Claude (Opus 4.8). +Охват: фронтенд `pcp-tgu-ops-ui` + бэкенд-сервисы. Сервис `tle-monitoring-service` по +указанию исключён из анализа. + +Метод: статический разбор кода, прогон сборки/тестов фронта, выборочное чтение бэкенд-сервисов +(акцент на недавно дорабатываемых: tgu, mission-planing, complex-mission, request), проверка +конфигов, CORS, обработки ошибок и гигиены репозитория. + +## Сводка по состоянию + +Базовое здоровье — хорошее. Фронтенд проходит строгую типизацию (`tsc -b`, exit 0) и все +тесты (53 теста в 12 файлах, зелёные); в исходниках нет `console.*`, `any`, `@ts-ignore`. +Бэкенд использует общий `GlobalExceptionHandler` из `pcp-types-lib`, нет `printStackTrace`, +нет `@Disabled`/`@Ignore` тестов. Найденные проблемы — это в основном технический долг, +копипаст-артефакты и один реальный риск надёжности в Camunda-воркерах. + +Приоритеты: **B1** (надёжность), **R1/C1** (гигиена/конфиги), затем рефакторинг-долг. + +--- + +## Бэкенд + +### B1 (высокий). Camunda job-воркеры молча проглатывают исключения +Файл: [FlowService.kt](../services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/FlowService.kt) + +Все воркеры объявлены с `autoComplete = false` и обёрнуты в: + +```kotlin +catch (ex : Exception) { logger.warn(ex.message) } +``` + +Если исключение возникает до `client.newCompleteCommand(...).send()`, задача **не завершается и +не падает**: воркер «съел» ошибку. Camunda не получает ни complete, ни fail/incident — BPMN-процесс +зависает молча до таймаута активации, без видимого инцидента и без явной диагностики. Плюс +`logger.warn(ex.message)` теряет stack trace, а `ex.message` может быть `null`. + +Рекомендация: в catch завершать задачу через `client.newFailCommand(job.key)` (с retries) либо +бросать исключение наверх, чтобы Camunda создала incident; логировать как `logger.error(msg, ex)` +со стек-трейсом. Это касается `surveyPlan`, `surveyDrop`, `sendPlan`, `confirmPlan` и прочих +воркеров файла. + +### B2 (средний). Симуляционная заглушка `Thread.sleep(5000)` в проде-пути +Файл: [FlowService.kt:97](../services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/FlowService.kt#L97) + +В `sendPlan` стоит `Thread.sleep(5000)` с комментарием «Иммитация сообщения о получении +подтверждения» — синхронно блокирует поток job-воркера на 5 с и эмулирует приём подтверждения +плана. Для MVP-демо допустимо (см. практику осознанных заглушек), но это нужно снять при +интеграции с реальной шиной подтверждений; пока имитация маскирует отсутствие реального канала и +держит поток пула воркеров. + +### B3 (средний). Несогласованные пакеты — копипаст-артефакты +Базовые пакеты ряда сервисов не соответствуют их имени (видимо, форки шаблона): + +| Сервис | Пакет | Проблема | +|---|---|---| +| pcp-ballistics-service | `space.nstart.pcp.pcp_request_service` | пакет от request-сервиса | +| pcp-complex-mission-service | `space.nstart.pcp.pcp_satellites_service` | чужое имя | +| pcp-ui-service | `space.nstart.pcp.slots_service` | совпадает со slots-service | +| slots-service | `space.nstart.pcp.slots_service` | тот же пакет, что у ui-service | +| pcp-tgu-service | `space.nstart.pcp_tgu_service` | пропущен сегмент `.pcp.` | +| pcp-request-service | `org.nstart.dep265.requestservice` | другой корень (`org` вместо `space`) | + +Последствия: grep/навигация по пакету выводит на «не тот» сервис, IDE-импорты обманчивы, + +ui-service и slots-service неотличимы по пакету. Переименование пакетов затрагивает много файлов — +делать осознанно, отдельной задачей; новые сервисы заводить уже с корректным пакетом. + +### B4 (низкий). Опечатка «srpring» растиражирована +`pcp-srpring-boot-admin-server` — опечатка в имени каталога и пакета, продублированная в +`settings.gradle.kts`, `build.gradle.kts`, `.gitlab-ci.yml`, `deploy/local-bundle/docker-compose.yml`, +`config-repo/application-docker-local.yaml`. Косметика, но фиксируется во всей инфраструктуре, потому +исправлять дорого — оставить как известный долг или чинить вместе с B3. + +### B5 (низкий). Потеря stack trace при логировании +~9 мест вида `logger.warn(ex.message)` / `logger.error(ex.message)` логируют только текст без +исключения. Для диагностики предпочтительно `logger.error("...", ex)`. + +### B6 (инфо). Опечатки в лог-сообщениях +Напр. «Утверждение поана» (план), «Иммитация» (имитация) в FlowService. + +--- + +## Фронтенд (pcp-tgu-ops-ui) + +### F1 (средний). Дублирование HTTP-/error-обвязки в API-клиентах +`extractErrorText` скопирован в 5 файлах: `tguApi.ts`, `ballisticsApi.ts`, `missionPlanningApi.ts`, +`complexPlanApi.ts`, `tguPlanApi.ts`. Шаблон `try { fetch } catch { TguApiError("network") }` + +разбор `!response.ok` повторяется ~22 раза (7 в missionPlanningApi, 5 в tguPlanApi и т.д.). +В `tguApi.ts` уже есть аккуратный `fetchJson()` — остальные клиенты переписывают то же руками. + +Рекомендация: вынести единый `src/api/http.ts` (обёртка `fetchJson` + `extractErrorText` + +маппинг ошибок) и переиспользовать. Сократит ~150 строк и устранит расхождения (см. F5). + +### F2 (низкий). Островок нетипизированного JS во вкладке «Группировка» +`features/constellation/*` — это `.jsx`/`.js` (`ConstellationDashboard.jsx`, `sim.js`, `data.js`, +`MapView.jsx`, `Panels.jsx`, `Timeline.jsx`, `util.js`), включён через `allowJs`. Остальное +приложение — строгий TS. Прототипный код подключён в прод через `ConstellationTab.tsx` (обёртка с +`as ComponentType`). Типы в нём фактически `any` — провал строгой типизации в этой ветке UI. +Долг: постепенно перевести на `.tsx` или хотя бы добавить декларации типов на границе. + +### F3 (низкий). Мёртвый barrel `constellation/index.js` +`features/constellation/index.js` (реэкспорт `SIM`, `SATELLITES`, `STATIONS`, ...) нигде не +импортируется — можно удалить. + +### F4 (низкий). God-компонент `TguEditorTab.tsx` (1727 строк) +Самый крупный файл фронта; держит десятки `useState`/`useRef`, режимы правой панели, построение +маршрутов, валидацию, undo/redo. Хорошо закомментирован, но монолитен. Кандидат на декомпозицию +(вынос хуков `useDraftHistory`, `useRouteBuilder`, разнесение панелей) для снижения связности. + +### F5 (инфо). Расхождение приоритета полей ошибки +В `tguApi.extractErrorText` порядок полей `reason, message, error, detail`, а в +`missionPlanningApi.extractErrorText` — `message, error, reason, detail`. При едином ответе бэка +пользователь увидит разный текст из разных клиентов. Устранится при выносе общей обвязки (F1). + +--- + +## Гигиена репозитория + +### R1 (средний). Крупные дампы данных в корне репозитория (в git) +Отслеживаются git и лежат в корне: +- `pcp_requests_converted_create_requests.json` (~228 КБ) +- `response_заданияКубернетс.json` (~157 КБ) +- `response_заданияЕмельянов.json` (~66 КБ) + +Это тестовые выгрузки, засоряющие корень и историю. Перенести в `docs/`/`*/src/test/resources` +или убрать из git (оставив в `.gitignore`). + +### R2 (низкий). Посторонние артефакты в корне (не в git) +- `node_modules/` в корне + пустой `package-lock.json` (`"packages": {}`, 0 зависимостей) — + корневого Node-проекта нет, фронт живёт в `services/pcp-tgu-ops-ui`. Мусор от случайного + `npm install` в корне; удалить, при желании добавить `/package-lock.json` в `.gitignore`. +- `obj_terra.zip` (~1.5 МБ) в корне — не в git, но захламляет дерево. + +### R3 (низкий). 26 устаревших каталогов `bin/` на диске +Скомпилированные `bin/main/...` (gitignored) задваивают исходники: любой `grep` по `services` +выдаёт каждый файл дважды (src + bin) — шум при навигации. Прогнать `./gradlew clean` / +удалить `bin/` каталоги. + +--- + +## Конфигурация и безопасность + +### C1 (средний). Несогласованное задание паролей БД в config-repo +В большинстве сервисов пароль зашит как `password: password`, и только +[pcp-tgu-service.yaml](../config-repo/pcp-tgu-service.yaml) использует переопределяемый паттерн +`password: ${DB_PASSWORD:password}`. Для локалки риск низкий, но шаблон важен: привести все +сервисы к `${DB_PASSWORD:...}` (и прочие креды — к env-переменным), чтобы прод/стенды +переопределялись без правки config-repo. + +### C2 (низкий). CORS: широкий в ui-service, отсутствует в остальных +[WebConfig.kt](../services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/configuration/WebConfig.kt) +задаёт `allowedOriginPatterns("*")` + `allowCredentials=true` — широко; для прода имеет смысл +сузить origin до конкретных хостов. Остальные сервисы, которые фронт зовёт напрямую (tgu, +mission, request, ballistics, complex, stations), CORS-конфига не имеют — сейчас работает только +потому, что обращение идёт через Vite-прокси/nginx (same-origin). Прямые кросс-доменные вызовы к +ним из браузера упадут — это ограничение стоит держать в курсе при изменении схемы доступа. + +--- + +## Что в порядке (намеренно не правим) +- Заглушки/тест-контроллеры/пустой контракт валидации миссии — осознанные MVP-срезы, не дефекты. +- Прямые вызовы микросервисов из фронта через прокси (без BFF) — принятое решение этапа. +- Naive `LocalDateTime` как локальное настенное время — намеренная договорённость проекта. + +## Рекомендованный порядок работ +1. **B1** — починить обработку ошибок в Camunda-воркерах (надёжность процессов). +2. **R1, C1** — убрать дампы из корня; унифицировать env-пароли. +3. **F1** — общий `http.ts`, заодно закрыть F5. +4. **B2** — снять `Thread.sleep`-имитацию при появлении реального канала подтверждений. +5. Фоном: **R2/R3** (чистка), **F2/F4** (типизация constellation, декомпозиция редактора), + **B3/B4** (пакеты/опечатки) — отдельными задачами из-за широкого охвата. diff --git a/docs/бэклог-pcp-tgu-ops-ui.md b/docs/бэклог-pcp-tgu-ops-ui.md index acb036f..5d65995 100644 --- a/docs/бэклог-pcp-tgu-ops-ui.md +++ b/docs/бэклог-pcp-tgu-ops-ui.md @@ -38,6 +38,11 @@ _Обновлено: 2026-06-11_ ### Бэкенд / инфраструктура - [ ] **#7** — Перевести сервис станций на ordinis (по примеру получения аппаратов в сервисе tgu). +- [ ] **#47** — Перевести сервисы с Jackson 2 на Jackson 3 и убрать двойной стек. Под Spring Boot 4.0.2 MVC уже десериализует входящий JSON через **Jackson 3** (`tools.jackson.*`), а весь код монорепы написан под **Jackson 2** (`com.fasterxml.jackson`, `versions.jackson_databind=2.18.2`); в `pcp-request-service` для внутреннего парсинга держится отдельный Jackson 2-бин (`JacksonConfig`). Из-за этой рассинхронизации словили баг: Jackson 3 по умолчанию включает `FAIL_ON_NULL_FOR_PRIMITIVES`, и явный `null` на примитивном `Double coverageRequiredPercent` ронял создание заявки (контракт-тесты не ловили — они на Jackson 2). Срочности/EOL-риска нет, это техдолг и консистентность. + - **Сейчас, дёшево (часы):** системно снять класс багов «null на примитиве» — выставить `spring.jackson.deserialization.fail-on-null-for-primitives=false` в config-repo request-service (проверить, что свойство применяется к Jackson 3-бину; если нет — через `Jackson3ObjectMapperBuilderCustomizer`). Не латать каждое поле по очереди. + - **Среднесрок, по сервису:** мигрировать по одному, пилот — `pcp-request-service` (~7 файлов main, ~0.5–1 день). Аннотации **не трогаем** — `jackson-annotations` в Jackson 3 остались в пакете `com.fasterxml.jackson.annotation` (у нас это основная масса: `@JsonIgnoreProperties` ×36, `@JsonProperty` ×16, `@JsonInclude` ×15…). Менять только databind-поверхность: `ObjectMapper` ×23, `TypeReference` ×8, `JsonNode` ×5, `jacksonObjectMapper()` ×12 → пакеты `tools.jackson.databind/core/module.kotlin`; в Jackson 3 `JsonProcessingException` стала unchecked. Дропнуть Jackson 2-зависимости и Jackson 2-бин `JacksonConfig`. + - **Главный риск — Kafka:** `KafkaConsumerConfig`/сериализаторы завязаны на `com.fasterxml` ObjectMapper. Перед стартом — спайк (~полдня): умеет ли `spring-kafka` в нашем Boot 4 Jackson 3-сериализаторы. Если нет — Kafka-слой мигрируем отдельно, HTTP/внутренний JSON переводим независимо. + - **Масштаб (src/main):** 67 файлов в 10 сервисах — pcp-dynamic-plan-service 22, pcp-ui-service 16 (выводится из эксплуатации, ср. #40 — можно не мигрировать), pcp-request-service 7, slots-service/pcp-tgu-service по 5, остальные 1–4. **Грубая оценка всей репы — ~6–10 человеко-дней**, основной риск — Kafka. Big-bang не нужен: шаг 1 закрывает причину бага, полную миграцию вести фоново сервис за сервисом. - [ ] **#45** — Собрать native-образ сервиса (Kotlin/Native, `kompile`) и контейнер на его основе. Цель — быстрый старт и меньший footprint в сравнении с JVM-образом. Уточнить: какой сервис(ы) переводим первым, совместимость с рефлексией/AOT-конфигом (reachability metadata), сборку в CI и базовый образ контейнера. - [ ] **#37** — Рассинхрон номеров витков между баллистическими источниками. Для одного и того же КА/времени `flight-line` (трасса, `/api/tgu-planning/spacecraft/{norad}/flight-line`) и `survey-route/build` (контур, pcp-mission-planing) отдают **разный номер витка**: на Emissio 1 (norad 2) 2026-06-06 12:50–12:55 трасса помечена витком 142, а контур/сохранённые режимы — витком 144 (по широте ~74° с.ш. согласованы, расходится только нумерация). В редакторе карта фильтрует трассу/полосу обзора по видимому витку (`buildEditorMapScene` → `visibleRevolutions`), поэтому при сужении селектора до «виток N» точки трассы с другим номером выпадают и пропадает орбитальный контекст. Свести нумерацию витков к единому источнику (или маппинг на границе), чтобы трасса и маршрут согласовывались по витку. diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt index efdddf1..16b5a5a 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt @@ -67,7 +67,10 @@ data class CreateRequestDto( */ @field:DecimalMin(value = "0", inclusive = false) @field:DecimalMax("100") - val coverageRequiredPercent: Double = 100.0, + // Nullable с дефолтом: под Jackson 3 (MVC) FAIL_ON_NULL_FOR_PRIMITIVES включён по умолчанию, + // и явный null от клиента падал бы на примитиве Double. Опциональное поле с дефолтом трактуем + // null как «взять дефолт» — при маппинге в сущность коалесим к 100.0. + val coverageRequiredPercent: Double? = 100.0, /** Параметры оптической съемки. */ @field:Valid diff --git a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt index e420f55..cee9cbc 100644 --- a/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt +++ b/services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt @@ -81,7 +81,7 @@ class RequestService( remainingArea = geometryArea, // Порог завершения по покрытию приходит из API (по умолчанию 100%); это прогресс съёмки, // в отличие от importance — важности заявки для карты приоритетов ячеек. - coverageRequiredPercent = request.coverageRequiredPercent, + coverageRequiredPercent = request.coverageRequiredPercent ?: 100.0, importance = importance, beginDateTime = request.beginDateTime, endDateTime = request.endDateTime, diff --git a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt index 4b95e86..4c330ac 100644 --- a/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt +++ b/services/pcp-request-service/src/test/kotlin/org/nstart/dep265/requestservice/service/RequestServiceTest.kt @@ -112,6 +112,23 @@ class RequestServiceTest { assertEquals(80.0, persistedRequests.single().coverageRequiredPercent) } + @Test + fun `create request coalesces null coverage required percent to default`() { + val persistedRequests = mutableListOf() + doReturn(false) + .`when`(requestRepository) + .existsById(REQUEST_ID) + doAnswer { invocation -> + persistedRequests += invocation.getArgument(0) + null + }.`when`(entityManager).persist(any(RequestEntity::class.java)) + + // Явный null (клиент прислал coverageRequiredPercent: null) трактуем как «взять дефолт». + service.createRequest(createRequestDto(coverageRequiredPercent = null)) + + assertEquals(100.0, persistedRequests.single().coverageRequiredPercent) + } + @Test fun `get request by id returns name and importance`() { val request = existingRequest() @@ -296,7 +313,7 @@ class RequestServiceTest { verify(requestCellRepository).deleteByRequestId(REQUEST_ID) } - private fun createRequestDto(coverageRequiredPercent: Double = 100.0): CreateRequestDto { + private fun createRequestDto(coverageRequiredPercent: Double? = 100.0): CreateRequestDto { return CreateRequestDto( id = REQUEST_ID, name = REQUEST_NAME, diff --git a/services/pcp-route-processing-service/Dockerfile b/services/pcp-route-processing-service/Dockerfile new file mode 100644 index 0000000..6fecd3c --- /dev/null +++ b/services/pcp-route-processing-service/Dockerfile @@ -0,0 +1,9 @@ +ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5 +FROM ${RUNTIME_IMAGE} + +ENV JAVA_OPTS="" + +ADD ./build/libs/*.jar /app.jar + +EXPOSE 8080 +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"] diff --git a/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapView.tsx b/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapView.tsx index b0c30e5..0cfbf2d 100644 --- a/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapView.tsx +++ b/services/pcp-tgu-ui-service/src/features/tgu-map-2d/Tgu2DMapView.tsx @@ -24,6 +24,8 @@ type Tgu2DMapViewProps = { selectedRequestPolygonId?: string; /** Внешняя подсветка объекта (контур съёмки или линия сброса) по его id — «как будто кликнули на карте». */ selectedObjectId?: string; + /** Отмеченные объекты (постоянная подсветка независимо от выбора) — напр. слоты, отобранные под бронь. */ + markedObjectIds?: ReadonlySet; /** Точка, на которую центрировать карту; перецентрирование срабатывает при смене focusToken. */ focusPoint?: GeoPoint; focusToken?: number; @@ -84,6 +86,7 @@ export function Tgu2DMapView({ hoveredRequestPolygonId, selectedRequestPolygonId, selectedObjectId, + markedObjectIds, focusPoint, focusToken, enableObjectChooser = false, @@ -529,6 +532,14 @@ export function Tgu2DMapView({ [hoveredRequestPolygonId, activePolygons] ); const overlayPolygons = uniquePolygons([externalSelectedPolygon, selectedPolygon, externalHoveredPolygon, hoverRef.current]); + // Отмеченные объекты (напр. отобранные под бронь слоты) — постоянная подсветка под обычным выбором. + const markedPolygons = useMemo( + () => + markedObjectIds && markedObjectIds.size > 0 + ? activePolygons.filter((p) => markedObjectIds.has(p.id)) + : [], + [markedObjectIds, activePolygons] + ); const overlayMarkers = uniqueMarkers([selectedMarker, hoverMarkerRef.current]); // Накопленное расстояние (км) от первой вершины линейки до каждой — по дуге большого круга. @@ -563,6 +574,24 @@ export function Tgu2DMapView({ >