Compare commits
229 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe54c095ab | |||
| d0ec0ef754 | |||
| 1e8b9f6998 | |||
| f0f39e784e | |||
| d38822db3e | |||
| e015b8aa75 | |||
| 354730fb6c | |||
| c402f376c5 | |||
| aabb550a0a | |||
| 653c930dd6 | |||
| 61a2c92d28 | |||
| b092e50dd8 | |||
| 8faaa03af3 | |||
| 7c10f9a10d | |||
| 1f44c1a5bb | |||
| 20d9eb9579 | |||
| bd35280691 | |||
| 6938700218 | |||
| 5869994690 | |||
| 3c94815ffc | |||
| b38a97b9a5 | |||
| 052ed5f04b | |||
| 1790192e06 | |||
| ee68da30f2 | |||
| f49709d544 | |||
| 88fb380be4 | |||
| 60ddec0928 | |||
| bffb8e47b7 | |||
| 33b56ba951 | |||
| 29b8206867 | |||
| 7fdac8c71e | |||
| 07ba650e03 | |||
| b9adbce207 | |||
| 1c9cdf4ad2 | |||
| 6db196066d | |||
| 7f9cd34ae9 | |||
| a5f3f0e375 | |||
| 3b1a13e532 | |||
| 11416de7aa | |||
| 3ab5b8860f | |||
| ce32a03072 | |||
| 8c18959c6b | |||
| a28dd62506 | |||
| bf2e88431e | |||
| c64222a90d | |||
| 345529cde0 | |||
| 6689351ef0 | |||
| 802c966347 | |||
| 7202f7cb02 | |||
| bdbc874f41 | |||
| 11380fced5 | |||
| 3765efb4fe | |||
| 3c4ebcd7d3 | |||
| ab3a787da5 | |||
| 8044815e97 | |||
| 6ddafa3dd2 | |||
| 6c2491cbb9 | |||
| cf14880309 | |||
| 9206ef6c49 | |||
| 35b8afc3fe | |||
| 632ef7f141 | |||
| c07fce093e | |||
| ccbb77787f | |||
| c2a847fc2e | |||
| f11e143a9c | |||
| e51c7965ec | |||
| cb439319c5 | |||
| f292291e04 | |||
| 505bf1f31c | |||
| 0b617594fd | |||
| e6822b6caf | |||
| a5c6b66826 | |||
| 060bcce9f2 | |||
| faf88eaf94 | |||
| e1e96b1ff0 | |||
| 69f2d6ad4f | |||
| 2a04a81bc0 | |||
| 8ac4afa195 | |||
| 1a0348cdeb | |||
| 5fa3ceaac9 | |||
| 2c8b317c42 | |||
| ff0b2639c9 | |||
| c74327011a | |||
| c1259c3105 | |||
| 2499aebf2d | |||
| 316e0ee552 | |||
| a06dfcac4e | |||
| 45fd6e8da9 | |||
| 3239e7d26c | |||
| 1d07588690 | |||
| b454c01c09 | |||
| 26bd1f111d | |||
| 821de2b0c0 | |||
| ea207088bc | |||
| 995396da3b | |||
| 69dfd8025d | |||
| bd2bc9d583 | |||
| 9f1756a9f9 | |||
| e6f1219c9c | |||
| fae57e957e | |||
| faadc63028 | |||
| 87905311b1 | |||
| 5235142fa6 | |||
| 9e7cae4a56 | |||
| f0e56b6e2a | |||
| b62a7cf8ef | |||
| 9e5460d646 | |||
| d9f8260120 | |||
| fd3603a086 | |||
| a5a1cdfa64 | |||
| 294b17757d | |||
| faedd21f43 | |||
| 5049395911 | |||
| 3638afdb85 | |||
| 0e534c5bd5 | |||
| 9c2d8516bc | |||
| 057452f19d | |||
| 2a841fc7b3 | |||
| ec67d9d83d | |||
| ae7effe1fa | |||
| 8133a5144f | |||
| f5211fa5a0 | |||
| 85ce238f52 | |||
| 0d69617420 | |||
| 80e281dd6d | |||
| e060e13b85 | |||
| c32e13e09c | |||
| 9a2ba655e1 | |||
| 938248d150 | |||
| 17b8679e44 | |||
| b61be0afb8 | |||
| 51fd8a446b | |||
| f0e16aa4e8 | |||
| e68c17e792 | |||
| cde4ffa9b6 | |||
| 84e70350b9 | |||
| 405f02aa86 | |||
| 240e12f442 | |||
| c2e2b2922f | |||
| 260583b134 | |||
| ac2220d063 | |||
| fb0332a030 | |||
| faf264e416 | |||
| a61e23c095 | |||
| 061ac1390c | |||
| e738c236a3 | |||
| 86215b300a | |||
| c76a823dbc | |||
| d630a891f6 | |||
| e0aa1ccf0e | |||
| e8f253ac6a | |||
| bd59204fcf | |||
| 96f698112b | |||
| edd24d93ba | |||
| 0128c5569c | |||
| 715b5108d6 | |||
| ef97ebbcf7 | |||
| fafaeed418 | |||
| 78887dd63a | |||
| 2bdc9ad64f | |||
| e5fd2a4586 | |||
| 02e172479f | |||
| 34c988c2d0 | |||
| bc3dd8f0b8 | |||
| ed33c3c834 | |||
| 0ec8c304b4 | |||
| 58d067c36f | |||
| da753b4ca9 | |||
| d7dda1e2f5 | |||
| 6d1b8e7cde | |||
| 4ab765b0ee | |||
| 637a5e8848 | |||
| 42708450bb | |||
| bb84844674 | |||
| 3d508b6be9 | |||
| 39f108e496 | |||
| 82eddedc92 | |||
| f084775cbb | |||
| c3a1a8b4a1 | |||
| 9fca4d4051 | |||
| 24af140213 | |||
| 651c85d64c | |||
| 723ceead32 | |||
| 913b3d7c9b | |||
| 4556592e63 | |||
| eff24a254d | |||
| 366ed8f451 | |||
| 2d100fdd54 | |||
| 41c3539b17 | |||
| a6f179550c | |||
| e7c2060e2e | |||
| 3fd702ca47 | |||
| d693bf799d | |||
| 7dfa1163e9 | |||
| 2c65f412cf | |||
| 6e62a0bd19 | |||
| 7dc786362c | |||
| 5c3f145267 | |||
| b4dda51460 | |||
| c7f3a77ff1 | |||
| c95d5239ec | |||
| bfb9d5b01f | |||
| a7e950a131 | |||
| da1217cbe8 | |||
| db9e995d46 | |||
| b846317f47 | |||
| d07b17e03e | |||
| f4014a08a1 | |||
| 29c06a4c9c | |||
| 3dce0aec0d | |||
| 1a6e51bf25 | |||
| 2aafa561ed | |||
| cf3bb63bc1 | |||
| 12d8b01bae | |||
| 4d2a5952c9 | |||
| 23c540ead0 | |||
| 467f91a374 | |||
| 4edf9d7aa8 | |||
| 38c1449e97 | |||
| cbbafebf79 | |||
| 1229995f90 | |||
| d4682fdeb7 | |||
| f549e15c63 | |||
| 1ef912be66 | |||
| fa357a78b4 | |||
| 4073897efc | |||
| b5990a55ac | |||
| 9d6f838692 | |||
| d48ddd2657 |
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gradle
|
||||
.idea
|
||||
**/build
|
||||
out
|
||||
*.iml
|
||||
@@ -0,0 +1,3 @@
|
||||
/gradlew text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.jar binary
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
*.tmp
|
||||
*.log
|
||||
*.gz
|
||||
*.zip
|
||||
*.patch
|
||||
.gradle-local/
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
|
||||
### Local env ###
|
||||
.env
|
||||
/.env.example
|
||||
/pcp.zip
|
||||
/promt/Промт1.txt
|
||||
/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 ###
|
||||
/package-lock.json
|
||||
/node_modules/
|
||||
/services/pcp-tgu-ops-ui/node_modules/
|
||||
/services/pcp-tgu-ops-ui/dist/
|
||||
/services/pcp-tgu-ops-ui/.cache/
|
||||
/services/pcp-tgu-ops-ui/.vite/
|
||||
/services/pcp-tgu-ops-ui/.vitest/
|
||||
/services/pcp-tgu-ops-ui/coverage/
|
||||
/services/pcp-tgu-ops-ui/.nyc_output/
|
||||
/services/pcp-tgu-ops-ui/.eslintcache
|
||||
/services/pcp-tgu-ops-ui/npm-debug.log*
|
||||
/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/
|
||||
|
||||
# Локальная раскладка собранных bootJar всех сервисов (артефакты, ~1.7G) — не версионируем.
|
||||
/serviceAllBuild/
|
||||
# Машинно-зависимый npm prefix фронта (локальный путь разработчика).
|
||||
/services/pcp-tgu-ui-service/.npmrc
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
workflow:
|
||||
rules:
|
||||
# Аварийный рубильник: «Run pipeline» c переменной RUN_ALL=true прогоняет
|
||||
# все gradle-сервисы (кроме pcp-tgu-ui-service) независимо от изменений.
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes:
|
||||
- services/pcp-ballistics-service/**/*
|
||||
- services/pcp-srpring-boot-admin-server/**/*
|
||||
- services/spring-cloud-config-server/**/*
|
||||
- services/pcp-complex-mission-service/**/*
|
||||
- services/tle-monitoring-service/**/*
|
||||
- services/pcp-mission-planing-service/**/*
|
||||
- services/pcp-request-service/**/*
|
||||
- services/pcp-stations-service/**/*
|
||||
- services/pcp-satellite-catalog-service/**/*
|
||||
- services/slots-service/**/*
|
||||
- services/pcp-ui-service/**/*
|
||||
- services/pcp-coverage-scheme-service/**/*
|
||||
- services/pcp-tgu-service/**/*
|
||||
- services/pcp-tgu-ui-service/**/*
|
||||
- services/pcp-dynamic-plan-service/**/*
|
||||
- services/pcp-gateway-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- gradle.properties
|
||||
- gradle/**/*
|
||||
- .gitlab-ci.yml
|
||||
- .gitlab/ci/**/*
|
||||
- helm/pcp-ballistics-service/**/*
|
||||
- helm/pcp-spring-boot-admin-server/**/*
|
||||
- helm/spring-cloud-config-server/**/*
|
||||
- helm/pcp-complex-mission-service/**/*
|
||||
- helm/tle-monitoring-service/**/*
|
||||
- helm/pcp-mission-planing-service/**/*
|
||||
- helm/pcp-request-service/**/*
|
||||
- helm/pcp-stations-service/**/*
|
||||
- helm/pcp-satellite-catalog-service/**/*
|
||||
- helm/slots-service/**/*
|
||||
- helm/pcp-ui-service/**/*
|
||||
- helm/pcp-coverage-scheme-service/**/*
|
||||
- helm/pcp-tgu-service/**/*
|
||||
- helm/pcp-tgu-ui-service/**/*
|
||||
- helm/pcp-dynamic-plan-service/**/*
|
||||
- helm/pcp-gateway-service/**/*
|
||||
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||
changes:
|
||||
- .gitlab-ci.yml
|
||||
- .gitlab/ci/**/*
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
|
||||
changes:
|
||||
- services/pcp-ballistics-service/**/*
|
||||
- services/pcp-srpring-boot-admin-server/**/*
|
||||
- services/spring-cloud-config-server/**/*
|
||||
- services/pcp-complex-mission-service/**/*
|
||||
- services/tle-monitoring-service/**/*
|
||||
- services/pcp-mission-planing-service/**/*
|
||||
- services/pcp-request-service/**/*
|
||||
- services/pcp-stations-service/**/*
|
||||
- services/pcp-satellite-catalog-service/**/*
|
||||
- services/slots-service/**/*
|
||||
- services/pcp-ui-service/**/*
|
||||
- services/pcp-coverage-scheme-service/**/*
|
||||
- services/pcp-tgu-service/**/*
|
||||
- services/pcp-tgu-ui-service/**/*
|
||||
- services/pcp-dynamic-plan-service/**/*
|
||||
- services/pcp-gateway-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- gradle.properties
|
||||
- gradle/**/*
|
||||
- .gitlab-ci.yml
|
||||
- .gitlab/ci/**/*
|
||||
- helm/pcp-ballistics-service/**/*
|
||||
- helm/pcp-spring-boot-admin-server/**/*
|
||||
- helm/spring-cloud-config-server/**/*
|
||||
- helm/pcp-complex-mission-service/**/*
|
||||
- helm/tle-monitoring-service/**/*
|
||||
- helm/pcp-mission-planing-service/**/*
|
||||
- helm/pcp-request-service/**/*
|
||||
- helm/pcp-stations-service/**/*
|
||||
- helm/pcp-satellite-catalog-service/**/*
|
||||
- helm/slots-service/**/*
|
||||
- helm/pcp-ui-service/**/*
|
||||
- helm/pcp-coverage-scheme-service/**/*
|
||||
- helm/pcp-tgu-service/**/*
|
||||
- helm/pcp-tgu-ui-service/**/*
|
||||
- helm/pcp-dynamic-plan-service/**/*
|
||||
- helm/pcp-gateway-service/**/*
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
|
||||
changes:
|
||||
- services/pcp-ballistics-service/**/*
|
||||
- services/pcp-srpring-boot-admin-server/**/*
|
||||
- services/spring-cloud-config-server/**/*
|
||||
- services/pcp-complex-mission-service/**/*
|
||||
- services/tle-monitoring-service/**/*
|
||||
- services/pcp-mission-planing-service/**/*
|
||||
- services/pcp-request-service/**/*
|
||||
- services/pcp-stations-service/**/*
|
||||
- services/pcp-satellite-catalog-service/**/*
|
||||
- services/slots-service/**/*
|
||||
- services/pcp-ui-service/**/*
|
||||
- services/pcp-coverage-scheme-service/**/*
|
||||
- services/pcp-tgu-service/**/*
|
||||
- services/pcp-tgu-ui-service/**/*
|
||||
- services/pcp-dynamic-plan-service/**/*
|
||||
- services/pcp-gateway-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- gradle.properties
|
||||
- gradle/**/*
|
||||
- .gitlab-ci.yml
|
||||
- .gitlab/ci/**/*
|
||||
- helm/pcp-ballistics-service/**/*
|
||||
- helm/pcp-spring-boot-admin-server/**/*
|
||||
- helm/spring-cloud-config-server/**/*
|
||||
- helm/pcp-complex-mission-service/**/*
|
||||
- helm/tle-monitoring-service/**/*
|
||||
- helm/pcp-mission-planing-service/**/*
|
||||
- helm/pcp-request-service/**/*
|
||||
- helm/pcp-stations-service/**/*
|
||||
- helm/pcp-satellite-catalog-service/**/*
|
||||
- helm/slots-service/**/*
|
||||
- helm/pcp-ui-service/**/*
|
||||
- helm/pcp-coverage-scheme-service/**/*
|
||||
- helm/pcp-tgu-service/**/*
|
||||
- helm/pcp-tgu-ui-service/**/*
|
||||
- helm/pcp-dynamic-plan-service/**/*
|
||||
- helm/pcp-gateway-service/**/*
|
||||
- when: never
|
||||
|
||||
default:
|
||||
tags:
|
||||
- nstart
|
||||
|
||||
variables:
|
||||
INTERNAL_REGISTRY: "$CI_REGISTRY_IMAGE"
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- package
|
||||
- deploy
|
||||
|
||||
ci:config:
|
||||
stage: test
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
- changes:
|
||||
- .gitlab-ci.yml
|
||||
- .gitlab/ci/**/*
|
||||
- when: never
|
||||
script:
|
||||
- echo "GitLab CI configuration changed"
|
||||
|
||||
include:
|
||||
- local: .gitlab/ci/pcp-ballistics-service.yml
|
||||
- local: .gitlab/ci/pcp-spring-boot-admin-server.yml
|
||||
- local: .gitlab/ci/spring-cloud-config-server.yml
|
||||
- local: .gitlab/ci/pcp-complex-mission-service-tle.yml
|
||||
- local: .gitlab/ci/pcp-mission-planing-service.yml
|
||||
- local: .gitlab/ci/pcp-request-service.yml
|
||||
- local: .gitlab/ci/pcp-stations-service.yml
|
||||
- local: .gitlab/ci/pcp-satellite-catalog-service.yml
|
||||
- local: .gitlab/ci/slots-service.yml
|
||||
- local: .gitlab/ci/pcp-ui-service.yml
|
||||
- local: .gitlab/ci/pcp-coverage-scheme-service.yml
|
||||
- local: .gitlab/ci/pcp-tgu-service.yml
|
||||
- local: .gitlab/ci/pcp-tgu-ui-service.yml
|
||||
- local: .gitlab/ci/pcp-dynamic-plan-service.yml
|
||||
- local: .gitlab/ci/pcp-gateway-service.yml
|
||||
@@ -0,0 +1,267 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
BALLISTICS_SERVICE_DIR: "services/pcp-ballistics-service"
|
||||
|
||||
# При необходимости переопредели путь к chart через CI/CD variable
|
||||
BALLISTICS_HELM_CHART_DIR: "helm/pcp-ballistics-service"
|
||||
|
||||
# Имя релиза в k8s
|
||||
BALLISTICS_HELM_RELEASE: "pcp-ballistics-service"
|
||||
|
||||
# Полный путь до image во внутреннем registry
|
||||
BALLISTICS_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-ballistics-service"
|
||||
|
||||
# Окружения / namespace
|
||||
BALLISTICS_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
BALLISTICS_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
|
||||
BALLISTICS_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
.ballistics:changes: &ballistics_changes
|
||||
- services/pcp-ballistics-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-ballistics-service.*
|
||||
- helm/pcp-ballistics-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.ballistics:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *ballistics_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *ballistics_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *ballistics_changes
|
||||
- when: never
|
||||
|
||||
.ballistics:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *ballistics_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *ballistics_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *ballistics_changes
|
||||
- when: never
|
||||
|
||||
.ballistics:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *ballistics_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.ballistics:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *ballistics_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.ballistics:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-ballistics-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.ballistics:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.ballistics:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
ballistics:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
services:
|
||||
- name: repo.nstart.cloud/postgis/postgis:16-3.4
|
||||
alias: postgres
|
||||
variables:
|
||||
POSTGRES_DB: "pcp_ballistics"
|
||||
POSTGRES_USER: "postgres"
|
||||
POSTGRES_PASSWORD: "password"
|
||||
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/pcp_ballistics"
|
||||
SPRING_DATASOURCE_USERNAME: "postgres"
|
||||
SPRING_DATASOURCE_PASSWORD: "password"
|
||||
extends:
|
||||
- .ballistics:rules:test
|
||||
- .ballistics:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-ballistics-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-ballistics-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-ballistics-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
ballistics:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .ballistics:rules:build-and-image
|
||||
- .ballistics:gradle-cache
|
||||
needs:
|
||||
- job: ballistics:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-ballistics-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-ballistics-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
ballistics:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .ballistics:rules:build-and-image
|
||||
- .ballistics:docker-build
|
||||
needs:
|
||||
- job: ballistics:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "BALLISTICS_IMAGE_REPOSITORY=$BALLISTICS_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$BALLISTICS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$BALLISTICS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$BALLISTICS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$BALLISTICS_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$BALLISTICS_IMAGE_REPOSITORY:$BALLISTICS_IMAGE_TAG" docker-context
|
||||
- docker push "$BALLISTICS_IMAGE_REPOSITORY:$BALLISTICS_IMAGE_TAG"
|
||||
- printf "BALLISTICS_IMAGE=%s\n" "$BALLISTICS_IMAGE_REPOSITORY:$BALLISTICS_IMAGE_TAG" > ballistics-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: ballistics-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
ballistics:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .ballistics:rules:deploy-dev
|
||||
- .ballistics:helm-deploy
|
||||
needs:
|
||||
- job: ballistics:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-ballistics-service
|
||||
resource_group: pcp-ballistics-service-dev
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${BALLISTICS_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${BALLISTICS_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$BALLISTICS_HELM_RELEASE"
|
||||
"$BALLISTICS_HELM_CHART_DIR"
|
||||
--namespace "$BALLISTICS_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$BALLISTICS_HELM_CHART_DIR/values.yaml"
|
||||
-f "$BALLISTICS_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
ballistics:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .ballistics:rules:deploy-master
|
||||
- .ballistics:helm-deploy
|
||||
needs:
|
||||
- job: ballistics:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-ballistics-service
|
||||
resource_group: pcp-ballistics-service-master
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${BALLISTICS_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${BALLISTICS_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$BALLISTICS_HELM_RELEASE"
|
||||
"$BALLISTICS_HELM_CHART_DIR"
|
||||
--namespace "$BALLISTICS_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$BALLISTICS_HELM_CHART_DIR/values.yaml"
|
||||
-f "$BALLISTICS_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,471 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
COMPLEX_MISSION_SERVICE_DIR: "services/pcp-complex-mission-service"
|
||||
COMPLEX_MISSION_SERVICE_HELM_CHART_DIR: "helm/pcp-complex-mission-service"
|
||||
COMPLEX_MISSION_SERVICE_HELM_RELEASE: "pcp-complex-mission-service"
|
||||
COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-complex-mission-service"
|
||||
COMPLEX_MISSION_SERVICE_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
TLE_MONITORING_SERVICE_DIR: "services/tle-monitoring-service"
|
||||
TLE_MONITORING_HELM_CHART_DIR: "helm/tle-monitoring-service"
|
||||
TLE_MONITORING_HELM_RELEASE: "tle-monitoring-service"
|
||||
TLE_MONITORING_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/tle-monitoring-service"
|
||||
TLE_MONITORING_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
TLE_MONITORING_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
TLE_MONITORING_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.complex_mission_service_changes: &complex_mission_service_changes
|
||||
- services/pcp-complex-mission-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-complex-mission-service-tle.yml
|
||||
- helm/pcp-complex-mission-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.tle_monitoring_changes: &tle_monitoring_changes
|
||||
- services/tle-monitoring-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-complex-mission-service-tle.yml
|
||||
- helm/tle-monitoring-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.complex-mission-service:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *complex_mission_service_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *complex_mission_service_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *complex_mission_service_changes
|
||||
- when: never
|
||||
|
||||
.complex-mission-service:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *complex_mission_service_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *complex_mission_service_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *complex_mission_service_changes
|
||||
- when: never
|
||||
|
||||
.complex-mission-service:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *complex_mission_service_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.complex-mission-service:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *complex_mission_service_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.tle-monitoring:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *tle_monitoring_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tle_monitoring_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tle_monitoring_changes
|
||||
- when: never
|
||||
|
||||
.tle-monitoring:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *tle_monitoring_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tle_monitoring_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tle_monitoring_changes
|
||||
- when: never
|
||||
|
||||
.tle-monitoring:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tle_monitoring_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.tle-monitoring:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tle_monitoring_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.pcp-gradle-cache:
|
||||
cache:
|
||||
key: "gradle-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.pcp-docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.pcp-helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube ~/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config ~/.kube/config
|
||||
|
||||
complex-mission-service:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .complex-mission-service:rules:test
|
||||
- .pcp-gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-complex-mission-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-complex-mission-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-complex-mission-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
complex-mission-service:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .complex-mission-service:rules:build-and-image
|
||||
- .pcp-gradle-cache
|
||||
needs:
|
||||
- job: complex-mission-service:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-complex-mission-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-complex-mission-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
complex-mission-service:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .complex-mission-service:rules:build-and-image
|
||||
- .pcp-docker-build
|
||||
needs:
|
||||
- job: complex-mission-service:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY=$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$COMPLEX_MISSION_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$COMPLEX_MISSION_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$COMPLEX_MISSION_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$COMPLEX_MISSION_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY:$COMPLEX_MISSION_SERVICE_IMAGE_TAG" docker-context
|
||||
- docker push "$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY:$COMPLEX_MISSION_SERVICE_IMAGE_TAG"
|
||||
- printf "COMPLEX_MISSION_SERVICE_IMAGE=%s\n" "$COMPLEX_MISSION_SERVICE_IMAGE_REPOSITORY:$COMPLEX_MISSION_SERVICE_IMAGE_TAG" > complex-mission-service-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: complex-mission-service-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
complex-mission-service:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .complex-mission-service:rules:deploy-dev
|
||||
- .pcp-helm-deploy
|
||||
needs:
|
||||
- job: complex-mission-service:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-complex-mission-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${COMPLEX_MISSION_SERVICE_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${COMPLEX_MISSION_SERVICE_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$COMPLEX_MISSION_SERVICE_HELM_RELEASE"
|
||||
"$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR"
|
||||
--namespace "$COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values.yaml"
|
||||
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
complex-mission-service:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .complex-mission-service:rules:deploy-master
|
||||
- .pcp-helm-deploy
|
||||
needs:
|
||||
- job: complex-mission-service:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-complex-mission-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${COMPLEX_MISSION_SERVICE_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${COMPLEX_MISSION_SERVICE_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$COMPLEX_MISSION_SERVICE_HELM_RELEASE"
|
||||
"$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR"
|
||||
--namespace "$COMPLEX_MISSION_SERVICE_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values.yaml"
|
||||
-f "$COMPLEX_MISSION_SERVICE_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
|
||||
tle-monitoring:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
services:
|
||||
- name: repo.nstart.cloud/postgis/postgis:16-3.4
|
||||
alias: postgres
|
||||
variables:
|
||||
POSTGRES_DB: "pcp_tle"
|
||||
POSTGRES_USER: "postgres"
|
||||
POSTGRES_PASSWORD: "password"
|
||||
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/pcp_tle"
|
||||
SPRING_DATASOURCE_USERNAME: "postgres"
|
||||
SPRING_DATASOURCE_PASSWORD: "password"
|
||||
SPRING_TASK_SCHEDULING_ENABLED: "false"
|
||||
extends:
|
||||
- .tle-monitoring:rules:test
|
||||
- .pcp-gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:tle-monitoring-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/tle-monitoring-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/tle-monitoring-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
tle-monitoring:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .tle-monitoring:rules:build-and-image
|
||||
- .pcp-gradle-cache
|
||||
needs:
|
||||
- job: tle-monitoring:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:tle-monitoring-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/tle-monitoring-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
tle-monitoring:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .tle-monitoring:rules:build-and-image
|
||||
- .pcp-docker-build
|
||||
needs:
|
||||
- job: tle-monitoring:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "TLE_MONITORING_IMAGE_REPOSITORY=$TLE_MONITORING_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$TLE_MONITORING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$TLE_MONITORING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$TLE_MONITORING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$TLE_MONITORING_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$TLE_MONITORING_IMAGE_REPOSITORY:$TLE_MONITORING_IMAGE_TAG" docker-context
|
||||
- docker push "$TLE_MONITORING_IMAGE_REPOSITORY:$TLE_MONITORING_IMAGE_TAG"
|
||||
- printf "TLE_MONITORING_IMAGE=%s\n" "$TLE_MONITORING_IMAGE_REPOSITORY:$TLE_MONITORING_IMAGE_TAG" > tle-monitoring-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: tle-monitoring-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
tle-monitoring:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .tle-monitoring:rules:deploy-dev
|
||||
- .pcp-helm-deploy
|
||||
needs:
|
||||
- job: tle-monitoring:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/tle-monitoring-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${TLE_MONITORING_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${TLE_MONITORING_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$TLE_MONITORING_HELM_RELEASE"
|
||||
"$TLE_MONITORING_HELM_CHART_DIR"
|
||||
--namespace "$TLE_MONITORING_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 15m
|
||||
-f "$TLE_MONITORING_HELM_CHART_DIR/values.yaml"
|
||||
-f "$TLE_MONITORING_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
tle-monitoring:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .tle-monitoring:rules:deploy-master
|
||||
- .pcp-helm-deploy
|
||||
needs:
|
||||
- job: tle-monitoring:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/tle-monitoring-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${TLE_MONITORING_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${TLE_MONITORING_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$TLE_MONITORING_HELM_RELEASE"
|
||||
"$TLE_MONITORING_HELM_CHART_DIR"
|
||||
--namespace "$TLE_MONITORING_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 15m
|
||||
-f "$TLE_MONITORING_HELM_CHART_DIR/values.yaml"
|
||||
-f "$TLE_MONITORING_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,244 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
COVERAGE_SCHEME_SERVICE_DIR: "services/pcp-coverage-scheme-service"
|
||||
COVERAGE_SCHEME_HELM_CHART_DIR: "helm/pcp-coverage-scheme-service"
|
||||
COVERAGE_SCHEME_HELM_RELEASE: "pcp-coverage-scheme-service"
|
||||
COVERAGE_SCHEME_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-coverage-scheme-service"
|
||||
COVERAGE_SCHEME_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
COVERAGE_SCHEME_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
COVERAGE_SCHEME_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.coverage_scheme_changes: &coverage_scheme_changes
|
||||
- services/pcp-coverage-scheme-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- gradle.properties
|
||||
- .gitlab/ci/pcp-coverage-scheme-service.yml
|
||||
- helm/pcp-coverage-scheme-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.coverage_scheme:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *coverage_scheme_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *coverage_scheme_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *coverage_scheme_changes
|
||||
- when: never
|
||||
|
||||
.coverage_scheme:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *coverage_scheme_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *coverage_scheme_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *coverage_scheme_changes
|
||||
- when: never
|
||||
|
||||
.coverage_scheme:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *coverage_scheme_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.coverage_scheme:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *coverage_scheme_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.coverage_scheme:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-coverage-scheme-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.coverage_scheme:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.coverage_scheme:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
coverage-scheme:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .coverage_scheme:rules:test
|
||||
- .coverage_scheme:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-coverage-scheme-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-coverage-scheme-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-coverage-scheme-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
coverage-scheme:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .coverage_scheme:rules:build-and-image
|
||||
- .coverage_scheme:gradle-cache
|
||||
needs:
|
||||
- job: coverage-scheme:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-coverage-scheme-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-coverage-scheme-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
coverage-scheme:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .coverage_scheme:rules:build-and-image
|
||||
- .coverage_scheme:docker-build
|
||||
needs:
|
||||
- job: coverage-scheme:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "COVERAGE_SCHEME_IMAGE_REPOSITORY=$COVERAGE_SCHEME_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$COVERAGE_SCHEME_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$COVERAGE_SCHEME_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$COVERAGE_SCHEME_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$COVERAGE_SCHEME_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$COVERAGE_SCHEME_IMAGE_REPOSITORY:$COVERAGE_SCHEME_IMAGE_TAG" docker-context
|
||||
- docker push "$COVERAGE_SCHEME_IMAGE_REPOSITORY:$COVERAGE_SCHEME_IMAGE_TAG"
|
||||
- printf "COVERAGE_SCHEME_IMAGE=%s\n" "$COVERAGE_SCHEME_IMAGE_REPOSITORY:$COVERAGE_SCHEME_IMAGE_TAG" > coverage-scheme-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: coverage-scheme-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
coverage-scheme:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .coverage_scheme:rules:deploy-dev
|
||||
- .coverage_scheme:helm-deploy
|
||||
needs:
|
||||
- job: coverage-scheme:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-coverage-scheme-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${COVERAGE_SCHEME_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${COVERAGE_SCHEME_IMAGE##*:}"'
|
||||
- >
|
||||
helm upgrade --install "$COVERAGE_SCHEME_HELM_RELEASE"
|
||||
"$COVERAGE_SCHEME_HELM_CHART_DIR"
|
||||
--namespace "$COVERAGE_SCHEME_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values.yaml"
|
||||
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
coverage-scheme:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .coverage_scheme:rules:deploy-master
|
||||
- .coverage_scheme:helm-deploy
|
||||
needs:
|
||||
- job: coverage-scheme:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-coverage-scheme-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${COVERAGE_SCHEME_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${COVERAGE_SCHEME_IMAGE##*:}"'
|
||||
- >
|
||||
helm upgrade --install "$COVERAGE_SCHEME_HELM_RELEASE"
|
||||
"$COVERAGE_SCHEME_HELM_CHART_DIR"
|
||||
--namespace "$COVERAGE_SCHEME_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values.yaml"
|
||||
-f "$COVERAGE_SCHEME_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,247 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
DYNAMIC_PLAN_SERVICE_DIR: "services/pcp-dynamic-plan-service"
|
||||
DYNAMIC_PLAN_HELM_CHART_DIR: "helm/pcp-dynamic-plan-service"
|
||||
DYNAMIC_PLAN_HELM_RELEASE: "pcp-dynamic-plan-service"
|
||||
DYNAMIC_PLAN_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-dynamic-plan-service"
|
||||
DYNAMIC_PLAN_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
DYNAMIC_PLAN_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
DYNAMIC_PLAN_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.dynamic_plan_changes: &dynamic_plan_changes
|
||||
- services/pcp-dynamic-plan-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- gradle.properties
|
||||
- .gitlab/ci/pcp-dynamic-plan-service.yml
|
||||
- helm/pcp-dynamic-plan-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.dynamic_plan:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *dynamic_plan_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *dynamic_plan_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *dynamic_plan_changes
|
||||
- when: never
|
||||
|
||||
.dynamic_plan:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *dynamic_plan_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *dynamic_plan_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *dynamic_plan_changes
|
||||
- when: never
|
||||
|
||||
.dynamic_plan:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *dynamic_plan_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.dynamic_plan:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *dynamic_plan_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.dynamic_plan:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-dynamic-plan-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.dynamic_plan:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.dynamic_plan:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
dynamic-plan:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .dynamic_plan:rules:test
|
||||
- .dynamic_plan:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-dynamic-plan-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-dynamic-plan-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-dynamic-plan-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
dynamic-plan:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .dynamic_plan:rules:build-and-image
|
||||
- .dynamic_plan:gradle-cache
|
||||
needs:
|
||||
- job: dynamic-plan:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-dynamic-plan-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-dynamic-plan-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
dynamic-plan:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .dynamic_plan:rules:build-and-image
|
||||
- .dynamic_plan:docker-build
|
||||
needs:
|
||||
- job: dynamic-plan:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "DYNAMIC_PLAN_IMAGE_REPOSITORY=$DYNAMIC_PLAN_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$DYNAMIC_PLAN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$DYNAMIC_PLAN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$DYNAMIC_PLAN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$DYNAMIC_PLAN_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$DYNAMIC_PLAN_IMAGE_REPOSITORY:$DYNAMIC_PLAN_IMAGE_TAG" docker-context
|
||||
- docker push "$DYNAMIC_PLAN_IMAGE_REPOSITORY:$DYNAMIC_PLAN_IMAGE_TAG"
|
||||
- printf "DYNAMIC_PLAN_IMAGE=%s\n" "$DYNAMIC_PLAN_IMAGE_REPOSITORY:$DYNAMIC_PLAN_IMAGE_TAG" > dynamic-plan-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: dynamic-plan-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
dynamic-plan:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .dynamic_plan:rules:deploy-dev
|
||||
- .dynamic_plan:helm-deploy
|
||||
needs:
|
||||
- job: dynamic-plan:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-dynamic-plan-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${DYNAMIC_PLAN_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${DYNAMIC_PLAN_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$DYNAMIC_PLAN_HELM_RELEASE"
|
||||
"$DYNAMIC_PLAN_HELM_CHART_DIR"
|
||||
--namespace "$DYNAMIC_PLAN_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values.yaml"
|
||||
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
dynamic-plan:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .dynamic_plan:rules:deploy-master
|
||||
- .dynamic_plan:helm-deploy
|
||||
needs:
|
||||
- job: dynamic-plan:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-dynamic-plan-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${DYNAMIC_PLAN_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${DYNAMIC_PLAN_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$DYNAMIC_PLAN_HELM_RELEASE"
|
||||
"$DYNAMIC_PLAN_HELM_CHART_DIR"
|
||||
--namespace "$DYNAMIC_PLAN_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values.yaml"
|
||||
-f "$DYNAMIC_PLAN_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,191 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
GATEWAY_SERVICE_DIR: "services/pcp-gateway-service"
|
||||
GATEWAY_HELM_CHART_DIR: "helm/pcp-gateway-service"
|
||||
GATEWAY_HELM_RELEASE: "pcp-gateway-service"
|
||||
GATEWAY_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-gateway-service"
|
||||
GATEWAY_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
GATEWAY_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
|
||||
.gateway_changes: &gateway_changes
|
||||
- services/pcp-gateway-service/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-gateway-service.yml
|
||||
- helm/pcp-gateway-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.gateway:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *gateway_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *gateway_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *gateway_changes
|
||||
- when: never
|
||||
|
||||
.gateway:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *gateway_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *gateway_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *gateway_changes
|
||||
- when: never
|
||||
|
||||
.gateway:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *gateway_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.gateway:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-gateway-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.gateway:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.gateway:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
gateway:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .gateway:rules:test
|
||||
- .gateway:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-gateway-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-gateway-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-gateway-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
gateway:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .gateway:rules:build-and-image
|
||||
- .gateway:gradle-cache
|
||||
needs:
|
||||
- job: gateway:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-gateway-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-gateway-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
gateway:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .gateway:rules:build-and-image
|
||||
- .gateway:docker-build
|
||||
needs:
|
||||
- job: gateway:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "GATEWAY_IMAGE_REPOSITORY=$GATEWAY_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$GATEWAY_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$GATEWAY_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$GATEWAY_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$GATEWAY_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$GATEWAY_IMAGE_REPOSITORY:$GATEWAY_IMAGE_TAG" docker-context
|
||||
- docker push "$GATEWAY_IMAGE_REPOSITORY:$GATEWAY_IMAGE_TAG"
|
||||
- printf "GATEWAY_IMAGE=%s\n" "$GATEWAY_IMAGE_REPOSITORY:$GATEWAY_IMAGE_TAG" > gateway-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: gateway-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
gateway:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .gateway:rules:deploy-dev
|
||||
- .gateway:helm-deploy
|
||||
needs:
|
||||
- job: gateway:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-gateway-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${GATEWAY_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${GATEWAY_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$GATEWAY_HELM_RELEASE"
|
||||
"$GATEWAY_HELM_CHART_DIR"
|
||||
--namespace "$GATEWAY_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--values "$GATEWAY_HELM_CHART_DIR/values.yaml"
|
||||
--values "$GATEWAY_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set-string image.repository="$IMAGE_REPOSITORY"
|
||||
--set-string image.tag="$IMAGE_TAG"
|
||||
--set-string app.profile=dev
|
||||
--set-string app.configLabel=dev
|
||||
--atomic
|
||||
--wait
|
||||
--timeout 15m
|
||||
@@ -0,0 +1,246 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
MISSION_PLANING_SERVICE_DIR: "services/pcp-mission-planing-service"
|
||||
MISSION_PLANING_HELM_CHART_DIR: "helm/pcp-mission-planing-service"
|
||||
MISSION_PLANING_HELM_RELEASE: "pcp-mission-planing-service"
|
||||
MISSION_PLANING_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-mission-planing-service"
|
||||
MISSION_PLANING_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
MISSION_PLANING_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
MISSION_PLANING_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.mission_planing_changes: &mission_planing_changes
|
||||
- services/pcp-mission-planing-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-mission-planing-service.yml
|
||||
- helm/pcp-mission-planing-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.mission-planing:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *mission_planing_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *mission_planing_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *mission_planing_changes
|
||||
- when: never
|
||||
|
||||
.mission-planing:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *mission_planing_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *mission_planing_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *mission_planing_changes
|
||||
- when: never
|
||||
|
||||
.mission-planing:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *mission_planing_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.mission-planing:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *mission_planing_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.mission-planing:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-mission-planing-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.mission-planing:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.mission-planing:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
mission-planing:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .mission-planing:rules:test
|
||||
- .mission-planing:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-mission-planing-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-mission-planing-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-mission-planing-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
mission-planing:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .mission-planing:rules:build-and-image
|
||||
- .mission-planing:gradle-cache
|
||||
needs:
|
||||
- job: mission-planing:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-mission-planing-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-mission-planing-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
mission-planing:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .mission-planing:rules:build-and-image
|
||||
- .mission-planing:docker-build
|
||||
needs:
|
||||
- job: mission-planing:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "MISSION_PLANING_IMAGE_REPOSITORY=$MISSION_PLANING_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$MISSION_PLANING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$MISSION_PLANING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$MISSION_PLANING_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$MISSION_PLANING_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$MISSION_PLANING_IMAGE_REPOSITORY:$MISSION_PLANING_IMAGE_TAG" docker-context
|
||||
- docker push "$MISSION_PLANING_IMAGE_REPOSITORY:$MISSION_PLANING_IMAGE_TAG"
|
||||
- printf "MISSION_PLANING_IMAGE=%s\n" "$MISSION_PLANING_IMAGE_REPOSITORY:$MISSION_PLANING_IMAGE_TAG" > mission-planing-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: mission-planing-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
mission-planing:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .mission-planing:rules:deploy-dev
|
||||
- .mission-planing:helm-deploy
|
||||
needs:
|
||||
- job: mission-planing:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-mission-planing-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${MISSION_PLANING_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${MISSION_PLANING_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$MISSION_PLANING_HELM_RELEASE"
|
||||
"$MISSION_PLANING_HELM_CHART_DIR"
|
||||
--namespace "$MISSION_PLANING_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$MISSION_PLANING_HELM_CHART_DIR/values.yaml"
|
||||
-f "$MISSION_PLANING_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
mission-planing:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .mission-planing:rules:deploy-master
|
||||
- .mission-planing:helm-deploy
|
||||
needs:
|
||||
- job: mission-planing:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-mission-planing-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${MISSION_PLANING_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${MISSION_PLANING_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$MISSION_PLANING_HELM_RELEASE"
|
||||
"$MISSION_PLANING_HELM_CHART_DIR"
|
||||
--namespace "$MISSION_PLANING_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$MISSION_PLANING_HELM_CHART_DIR/values.yaml"
|
||||
-f "$MISSION_PLANING_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,247 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
REQUEST_SERVICE_DIR: "services/pcp-request-service"
|
||||
REQUEST_HELM_CHART_DIR: "helm/pcp-request-service"
|
||||
REQUEST_HELM_RELEASE: "pcp-request-service"
|
||||
REQUEST_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-request-service"
|
||||
REQUEST_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
REQUEST_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
REQUEST_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.request_changes: &request_changes
|
||||
- services/pcp-request-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- gradle.properties
|
||||
- gradle/**/*
|
||||
- .gitlab/ci/pcp-request-service.yml
|
||||
- helm/pcp-request-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.request:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *request_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *request_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *request_changes
|
||||
- when: never
|
||||
|
||||
.request:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *request_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *request_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *request_changes
|
||||
- when: never
|
||||
|
||||
.request:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *request_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.request:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *request_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.request:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-request-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.request:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.request:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
request:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .request:rules:test
|
||||
- .request:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-request-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-request-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-request-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
request:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .request:rules:build-and-image
|
||||
- .request:gradle-cache
|
||||
needs:
|
||||
- job: request:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-request-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-request-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
request:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .request:rules:build-and-image
|
||||
- .request:docker-build
|
||||
needs:
|
||||
- job: request:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "REQUEST_IMAGE_REPOSITORY=$REQUEST_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$REQUEST_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$REQUEST_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$REQUEST_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$REQUEST_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$REQUEST_IMAGE_REPOSITORY:$REQUEST_IMAGE_TAG" docker-context
|
||||
- docker push "$REQUEST_IMAGE_REPOSITORY:$REQUEST_IMAGE_TAG"
|
||||
- printf "REQUEST_IMAGE=%s\n" "$REQUEST_IMAGE_REPOSITORY:$REQUEST_IMAGE_TAG" > request-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: request-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
request:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .request:rules:deploy-dev
|
||||
- .request:helm-deploy
|
||||
needs:
|
||||
- job: request:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-request-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${REQUEST_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${REQUEST_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$REQUEST_HELM_RELEASE"
|
||||
"$REQUEST_HELM_CHART_DIR"
|
||||
--namespace "$REQUEST_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 15m
|
||||
-f "$REQUEST_HELM_CHART_DIR/values.yaml"
|
||||
-f "$REQUEST_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
request:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .request:rules:deploy-master
|
||||
- .request:helm-deploy
|
||||
needs:
|
||||
- job: request:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-request-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${REQUEST_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${REQUEST_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$REQUEST_HELM_RELEASE"
|
||||
"$REQUEST_HELM_CHART_DIR"
|
||||
--namespace "$REQUEST_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 15m
|
||||
-f "$REQUEST_HELM_CHART_DIR/values.yaml"
|
||||
-f "$REQUEST_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,245 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
SATELLITE_CATALOG_SERVICE_DIR: "services/pcp-satellite-catalog-service"
|
||||
SATELLITE_CATALOG_HELM_CHART_DIR: "helm/pcp-satellite-catalog-service"
|
||||
SATELLITE_CATALOG_HELM_RELEASE: "pcp-satellite-catalog-service"
|
||||
SATELLITE_CATALOG_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-satellite-catalog-service"
|
||||
SATELLITE_CATALOG_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
SATELLITE_CATALOG_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
SATELLITE_CATALOG_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.satellite_catalog_changes: &satellite_catalog_changes
|
||||
- services/pcp-satellite-catalog-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-satellite-catalog-service.*
|
||||
- helm/pcp-satellite-catalog-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.satellite-catalog:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *satellite_catalog_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *satellite_catalog_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *satellite_catalog_changes
|
||||
- when: never
|
||||
|
||||
.satellite-catalog:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *satellite_catalog_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *satellite_catalog_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *satellite_catalog_changes
|
||||
- when: never
|
||||
|
||||
.satellite-catalog:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *satellite_catalog_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.satellite-catalog:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *satellite_catalog_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.satellite-catalog:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-satellite-catalog-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.satellite-catalog:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.satellite-catalog:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
satellite-catalog:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .satellite-catalog:rules:test
|
||||
- .satellite-catalog:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-satellite-catalog-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-satellite-catalog-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-satellite-catalog-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
satellite-catalog:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .satellite-catalog:rules:build-and-image
|
||||
- .satellite-catalog:gradle-cache
|
||||
needs:
|
||||
- job: satellite-catalog:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-satellite-catalog-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-satellite-catalog-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
satellite-catalog:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .satellite-catalog:rules:build-and-image
|
||||
- .satellite-catalog:docker-build
|
||||
needs:
|
||||
- job: satellite-catalog:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "SATELLITE_CATALOG_IMAGE_REPOSITORY=$SATELLITE_CATALOG_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$SATELLITE_CATALOG_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$SATELLITE_CATALOG_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$SATELLITE_CATALOG_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$SATELLITE_CATALOG_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$SATELLITE_CATALOG_IMAGE_REPOSITORY:$SATELLITE_CATALOG_IMAGE_TAG" docker-context
|
||||
- docker push "$SATELLITE_CATALOG_IMAGE_REPOSITORY:$SATELLITE_CATALOG_IMAGE_TAG"
|
||||
- printf "SATELLITE_CATALOG_IMAGE=%s\n" "$SATELLITE_CATALOG_IMAGE_REPOSITORY:$SATELLITE_CATALOG_IMAGE_TAG" > satellite-catalog-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: satellite-catalog-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
satellite-catalog:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .satellite-catalog:rules:deploy-dev
|
||||
- .satellite-catalog:helm-deploy
|
||||
needs:
|
||||
- job: satellite-catalog:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-satellite-catalog-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${SATELLITE_CATALOG_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${SATELLITE_CATALOG_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$SATELLITE_CATALOG_HELM_RELEASE"
|
||||
"$SATELLITE_CATALOG_HELM_CHART_DIR"
|
||||
--namespace "$SATELLITE_CATALOG_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values.yaml"
|
||||
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
satellite-catalog:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .satellite-catalog:rules:deploy-master
|
||||
- .satellite-catalog:helm-deploy
|
||||
needs:
|
||||
- job: satellite-catalog:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-satellite-catalog-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${SATELLITE_CATALOG_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${SATELLITE_CATALOG_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$SATELLITE_CATALOG_HELM_RELEASE"
|
||||
"$SATELLITE_CATALOG_HELM_CHART_DIR"
|
||||
--namespace "$SATELLITE_CATALOG_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values.yaml"
|
||||
-f "$SATELLITE_CATALOG_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,191 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
SPRING_BOOT_ADMIN_SERVICE_DIR: "services/pcp-srpring-boot-admin-server"
|
||||
SPRING_BOOT_ADMIN_HELM_CHART_DIR: "helm/pcp-spring-boot-admin-server"
|
||||
SPRING_BOOT_ADMIN_HELM_RELEASE: "pcp-spring-boot-admin-server"
|
||||
SPRING_BOOT_ADMIN_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-spring-boot-admin-server"
|
||||
SPRING_BOOT_ADMIN_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
SPRING_BOOT_ADMIN_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
|
||||
.spring_boot_admin_changes: &spring_boot_admin_changes
|
||||
- services/pcp-srpring-boot-admin-server/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-spring-boot-admin-server.yml
|
||||
- helm/pcp-spring-boot-admin-server/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.spring-boot-admin:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *spring_boot_admin_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *spring_boot_admin_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *spring_boot_admin_changes
|
||||
- when: never
|
||||
|
||||
.spring-boot-admin:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *spring_boot_admin_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *spring_boot_admin_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *spring_boot_admin_changes
|
||||
- when: never
|
||||
|
||||
.spring-boot-admin:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *spring_boot_admin_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.spring-boot-admin:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-spring-boot-admin-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.spring-boot-admin:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.spring-boot-admin:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
spring-boot-admin:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .spring-boot-admin:rules:test
|
||||
- .spring-boot-admin:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-srpring-boot-admin-server:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-srpring-boot-admin-server/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-srpring-boot-admin-server/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
spring-boot-admin:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .spring-boot-admin:rules:build-and-image
|
||||
- .spring-boot-admin:gradle-cache
|
||||
needs:
|
||||
- job: spring-boot-admin:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-srpring-boot-admin-server:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-srpring-boot-admin-server/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
spring-boot-admin:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .spring-boot-admin:rules:build-and-image
|
||||
- .spring-boot-admin:docker-build
|
||||
needs:
|
||||
- job: spring-boot-admin:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "SPRING_BOOT_ADMIN_IMAGE_REPOSITORY=$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$SPRING_BOOT_ADMIN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$SPRING_BOOT_ADMIN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$SPRING_BOOT_ADMIN_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$SPRING_BOOT_ADMIN_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY:$SPRING_BOOT_ADMIN_IMAGE_TAG" docker-context
|
||||
- docker push "$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY:$SPRING_BOOT_ADMIN_IMAGE_TAG"
|
||||
- printf "SPRING_BOOT_ADMIN_IMAGE=%s\n" "$SPRING_BOOT_ADMIN_IMAGE_REPOSITORY:$SPRING_BOOT_ADMIN_IMAGE_TAG" > spring-boot-admin-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: spring-boot-admin-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
spring-boot-admin:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .spring-boot-admin:rules:deploy-dev
|
||||
- .spring-boot-admin:helm-deploy
|
||||
needs:
|
||||
- job: spring-boot-admin:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-spring-boot-admin-server
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${SPRING_BOOT_ADMIN_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${SPRING_BOOT_ADMIN_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$SPRING_BOOT_ADMIN_HELM_RELEASE"
|
||||
"$SPRING_BOOT_ADMIN_HELM_CHART_DIR"
|
||||
--namespace "$SPRING_BOOT_ADMIN_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--values "$SPRING_BOOT_ADMIN_HELM_CHART_DIR/values.yaml"
|
||||
--values "$SPRING_BOOT_ADMIN_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set-string image.repository="$IMAGE_REPOSITORY"
|
||||
--set-string image.tag="$IMAGE_TAG"
|
||||
--set-string app.profile=dev
|
||||
--set-string app.configLabel=dev
|
||||
--atomic
|
||||
--wait
|
||||
--timeout 15m
|
||||
@@ -0,0 +1,245 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
STATIONS_SERVICE_DIR: "services/pcp-stations-service"
|
||||
STATIONS_HELM_CHART_DIR: "helm/pcp-stations-service"
|
||||
STATIONS_HELM_RELEASE: "pcp-stations-service"
|
||||
STATIONS_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-stations-service"
|
||||
STATIONS_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
STATIONS_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
STATIONS_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.stations_changes: &stations_changes
|
||||
- services/pcp-stations-service/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-stations-service.*
|
||||
- helm/pcp-stations-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.stations:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *stations_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *stations_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *stations_changes
|
||||
- when: never
|
||||
|
||||
.stations:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *stations_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *stations_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *stations_changes
|
||||
- when: never
|
||||
|
||||
.stations:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *stations_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.stations:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *stations_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.stations:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-stations-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.stations:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.stations:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
stations:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .stations:rules:test
|
||||
- .stations:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-stations-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-stations-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-stations-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
stations:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .stations:rules:build-and-image
|
||||
- .stations:gradle-cache
|
||||
needs:
|
||||
- job: stations:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-stations-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-stations-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
stations:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .stations:rules:build-and-image
|
||||
- .stations:docker-build
|
||||
needs:
|
||||
- job: stations:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "STATIONS_IMAGE_REPOSITORY=$STATIONS_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$STATIONS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$STATIONS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$STATIONS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$STATIONS_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$STATIONS_IMAGE_REPOSITORY:$STATIONS_IMAGE_TAG" docker-context
|
||||
- docker push "$STATIONS_IMAGE_REPOSITORY:$STATIONS_IMAGE_TAG"
|
||||
- printf "STATIONS_IMAGE=%s\n" "$STATIONS_IMAGE_REPOSITORY:$STATIONS_IMAGE_TAG" > stations-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: stations-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
stations:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .stations:rules:deploy-dev
|
||||
- .stations:helm-deploy
|
||||
needs:
|
||||
- job: stations:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-stations-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${STATIONS_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${STATIONS_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$STATIONS_HELM_RELEASE"
|
||||
"$STATIONS_HELM_CHART_DIR"
|
||||
--namespace "$STATIONS_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$STATIONS_HELM_CHART_DIR/values.yaml"
|
||||
-f "$STATIONS_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
stations:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .stations:rules:deploy-master
|
||||
- .stations:helm-deploy
|
||||
needs:
|
||||
- job: stations:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-stations-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${STATIONS_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${STATIONS_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$STATIONS_HELM_RELEASE"
|
||||
"$STATIONS_HELM_CHART_DIR"
|
||||
--namespace "$STATIONS_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$STATIONS_HELM_CHART_DIR/values.yaml"
|
||||
-f "$STATIONS_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,203 @@
|
||||
variables:
|
||||
TGU_SERVICE_DIR: "services/pcp-tgu-service"
|
||||
TGU_HELM_CHART_DIR: "helm/pcp-tgu-service"
|
||||
TGU_HELM_RELEASE: "pcp-tgu-service"
|
||||
TGU_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-tgu-service"
|
||||
TGU_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
TGU_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
TGU_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.tgu_changes: &tgu_changes
|
||||
- services/pcp-tgu-service/**/*
|
||||
# - libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-tgu-service.yml
|
||||
- helm/pcp-tgu-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.tgu:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *tgu_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tgu_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tgu_changes
|
||||
- when: never
|
||||
|
||||
.tgu:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *tgu_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tgu_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tgu_changes
|
||||
- when: never
|
||||
|
||||
.tgu:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tgu_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.tgu:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tgu_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.tgu:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-tgu-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.tgu:docker-build:
|
||||
image: docker:26
|
||||
services:
|
||||
- name: docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.tgu:helm-deploy:
|
||||
image:
|
||||
name: alpine/helm:3.16.4
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- mkdir -p ~/.kube
|
||||
- helm version
|
||||
|
||||
tgu:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .tgu:rules:test
|
||||
- .tgu:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-tgu-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-tgu-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-tgu-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
tgu:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .tgu:rules:build-and-image
|
||||
- .tgu:gradle-cache
|
||||
needs:
|
||||
- job: tgu:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-tgu-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-tgu-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
tgu:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .tgu:rules:build-and-image
|
||||
- .tgu:docker-build
|
||||
needs:
|
||||
- job: tgu:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "TGU_IMAGE_REPOSITORY=$TGU_IMAGE_REPOSITORY"'
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$TGU_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$TGU_SERVICE_DIR"/build/libs/*.jar docker-context/build/libs/
|
||||
- docker build --file docker-context/Dockerfile --tag "$TGU_IMAGE_REPOSITORY:$TGU_IMAGE_TAG" docker-context
|
||||
- docker push "$TGU_IMAGE_REPOSITORY:$TGU_IMAGE_TAG"
|
||||
- printf "TGU_IMAGE=%s\n" "$TGU_IMAGE_REPOSITORY:$TGU_IMAGE_TAG" > tgu-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: tgu-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
tgu:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .tgu:rules:deploy-dev
|
||||
- .tgu:helm-deploy
|
||||
needs:
|
||||
- job: tgu:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-tgu-service
|
||||
script:
|
||||
- 'echo "$KUBE_CONFIG_DEV_B64" | base64 -d > ~/.kube/config'
|
||||
- chmod 600 ~/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${TGU_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${TGU_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$TGU_HELM_RELEASE"
|
||||
"$TGU_HELM_CHART_DIR"
|
||||
--namespace "$TGU_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$TGU_HELM_CHART_DIR/values.yaml"
|
||||
-f "$TGU_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
tgu:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .tgu:rules:deploy-master
|
||||
- .tgu:helm-deploy
|
||||
needs:
|
||||
- job: tgu:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-tgu-service
|
||||
script:
|
||||
- 'echo "$KUBE_CONFIG_MASTER_B64" | base64 -d > ~/.kube/config'
|
||||
- chmod 600 ~/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${TGU_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${TGU_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$TGU_HELM_RELEASE"
|
||||
"$TGU_HELM_CHART_DIR"
|
||||
--namespace "$TGU_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$TGU_HELM_CHART_DIR/values.yaml"
|
||||
-f "$TGU_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,187 @@
|
||||
# pcp-tgu-ui-service — статический фронт ТГУ (Node/Vite → nginx). В отличие от
|
||||
# остальных сервисов группы (gradle/Spring), собирается на Node: test = vitest,
|
||||
# build = npm run build (tsc+vite), package = docker по своему Dockerfile.
|
||||
variables:
|
||||
TGU_UI_SERVICE_DIR: "services/pcp-tgu-ui-service"
|
||||
TGU_UI_HELM_CHART_DIR: "helm/pcp-tgu-ui-service"
|
||||
TGU_UI_HELM_RELEASE: "pcp-tgu-ui-service"
|
||||
TGU_UI_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-tgu-ui-service"
|
||||
TGU_UI_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
TGU_UI_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
TGU_UI_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
TGU_UI_NODE_IMAGE: "repo.nstart.cloud/nstart/node:24-trixie-slim"
|
||||
|
||||
.tgu_ui_changes: &tgu_ui_changes
|
||||
- services/pcp-tgu-ui-service/**/*
|
||||
- .gitlab/ci/pcp-tgu-ui-service.yml
|
||||
- helm/pcp-tgu-ui-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.tgu-ui:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *tgu_ui_changes
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tgu_ui_changes
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tgu_ui_changes
|
||||
- when: never
|
||||
|
||||
.tgu-ui:rules:test:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *tgu_ui_changes
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tgu_ui_changes
|
||||
when: manual
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tgu_ui_changes
|
||||
- when: never
|
||||
|
||||
.tgu-ui:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *tgu_ui_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.tgu-ui:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"'
|
||||
changes: *tgu_ui_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.tgu-ui:npm-cache:
|
||||
cache:
|
||||
key: "npm-tgu-ui-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- services/pcp-tgu-ui-service/.npm
|
||||
policy: pull-push
|
||||
|
||||
.tgu-ui:docker-build:
|
||||
image: docker:26
|
||||
services:
|
||||
- name: docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.tgu-ui:helm-deploy:
|
||||
image:
|
||||
name: alpine/helm:3.16.4
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- mkdir -p ~/.kube
|
||||
- helm version
|
||||
|
||||
tgu-ui:test:
|
||||
stage: test
|
||||
image: $TGU_UI_NODE_IMAGE
|
||||
extends:
|
||||
- .tgu-ui:rules:test
|
||||
- .tgu-ui:npm-cache
|
||||
script:
|
||||
- cd "$TGU_UI_SERVICE_DIR"
|
||||
- npm ci --cache .npm --prefer-offline
|
||||
- npm run test
|
||||
|
||||
tgu-ui:build:
|
||||
stage: build
|
||||
image: $TGU_UI_NODE_IMAGE
|
||||
extends:
|
||||
- .tgu-ui:rules:build-and-image
|
||||
- .tgu-ui:npm-cache
|
||||
script:
|
||||
- cd "$TGU_UI_SERVICE_DIR"
|
||||
- npm ci --cache .npm --prefer-offline
|
||||
- npm run build
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-tgu-ui-service/dist/
|
||||
expire_in: 7 days
|
||||
|
||||
# Образ собирает сам Dockerfile (multi-stage: npm ci + build внутри). Артефакт
|
||||
# tgu-ui:build — быстрый gate на tsc+vite перед более тяжёлым docker build.
|
||||
tgu-ui:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .tgu-ui:rules:build-and-image
|
||||
- .tgu-ui:docker-build
|
||||
needs:
|
||||
- job: tgu-ui:build
|
||||
artifacts: false
|
||||
script:
|
||||
- 'echo "TGU_UI_IMAGE_REPOSITORY=$TGU_UI_IMAGE_REPOSITORY"'
|
||||
- docker build --file "$TGU_UI_SERVICE_DIR/Dockerfile" --tag "$TGU_UI_IMAGE_REPOSITORY:$TGU_UI_IMAGE_TAG" "$TGU_UI_SERVICE_DIR"
|
||||
- docker push "$TGU_UI_IMAGE_REPOSITORY:$TGU_UI_IMAGE_TAG"
|
||||
- printf "TGU_UI_IMAGE=%s\n" "$TGU_UI_IMAGE_REPOSITORY:$TGU_UI_IMAGE_TAG" > tgu-ui-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: tgu-ui-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
tgu-ui:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .tgu-ui:rules:deploy-dev
|
||||
- .tgu-ui:helm-deploy
|
||||
needs:
|
||||
- job: tgu-ui:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-tgu-ui-service
|
||||
script:
|
||||
- 'echo "$KUBE_CONFIG_DEV_B64" | base64 -d > ~/.kube/config'
|
||||
- chmod 600 ~/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${TGU_UI_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${TGU_UI_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$TGU_UI_HELM_RELEASE"
|
||||
"$TGU_UI_HELM_CHART_DIR"
|
||||
--namespace "$TGU_UI_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$TGU_UI_HELM_CHART_DIR/values.yaml"
|
||||
-f "$TGU_UI_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
|
||||
tgu-ui:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .tgu-ui:rules:deploy-master
|
||||
- .tgu-ui:helm-deploy
|
||||
needs:
|
||||
- job: tgu-ui:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-tgu-ui-service
|
||||
script:
|
||||
- 'echo "$KUBE_CONFIG_MASTER_B64" | base64 -d > ~/.kube/config'
|
||||
- chmod 600 ~/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${TGU_UI_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${TGU_UI_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$TGU_UI_HELM_RELEASE"
|
||||
"$TGU_UI_HELM_CHART_DIR"
|
||||
--namespace "$TGU_UI_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$TGU_UI_HELM_CHART_DIR/values.yaml"
|
||||
-f "$TGU_UI_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
@@ -0,0 +1,246 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
UI_SERVICE_DIR: "services/pcp-ui-service"
|
||||
UI_HELM_CHART_DIR: "helm/pcp-ui-service"
|
||||
UI_HELM_RELEASE: "pcp-ui-service"
|
||||
UI_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-ui-service"
|
||||
UI_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
UI_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
UI_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.ui_changes: &ui_changes
|
||||
- services/pcp-ui-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/pcp-ui-service.yml
|
||||
- helm/pcp-ui-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.ui:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *ui_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *ui_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *ui_changes
|
||||
- when: never
|
||||
|
||||
.ui:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *ui_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *ui_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *ui_changes
|
||||
- when: never
|
||||
|
||||
.ui:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *ui_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.ui:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *ui_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.ui:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-ui-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.ui:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.ui:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
ui:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .ui:rules:test
|
||||
- .ui:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-ui-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/pcp-ui-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/pcp-ui-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
ui:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .ui:rules:build-and-image
|
||||
- .ui:gradle-cache
|
||||
needs:
|
||||
- job: ui:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:pcp-ui-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/pcp-ui-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
ui:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .ui:rules:build-and-image
|
||||
- .ui:docker-build
|
||||
needs:
|
||||
- job: ui:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "UI_IMAGE_REPOSITORY=$UI_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$UI_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$UI_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$UI_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$UI_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$UI_IMAGE_REPOSITORY:$UI_IMAGE_TAG" docker-context
|
||||
- docker push "$UI_IMAGE_REPOSITORY:$UI_IMAGE_TAG"
|
||||
- printf "UI_IMAGE=%s\n" "$UI_IMAGE_REPOSITORY:$UI_IMAGE_TAG" > ui-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: ui-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
ui:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .ui:rules:deploy-dev
|
||||
- .ui:helm-deploy
|
||||
needs:
|
||||
- job: ui:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-ui-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${UI_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${UI_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$UI_HELM_RELEASE"
|
||||
"$UI_HELM_CHART_DIR"
|
||||
--namespace "$UI_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$UI_HELM_CHART_DIR/values.yaml"
|
||||
-f "$UI_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
ui:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .ui:rules:deploy-master
|
||||
- .ui:helm-deploy
|
||||
needs:
|
||||
- job: ui:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-ui-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${UI_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${UI_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$UI_HELM_RELEASE"
|
||||
"$UI_HELM_CHART_DIR"
|
||||
--namespace "$UI_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 10m
|
||||
-f "$UI_HELM_CHART_DIR/values.yaml"
|
||||
-f "$UI_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,256 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
SLOTS_SERVICE_DIR: "services/slots-service"
|
||||
SLOTS_HELM_CHART_DIR: "helm/slots-service"
|
||||
SLOTS_HELM_RELEASE: "slots-service"
|
||||
SLOTS_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/slots-service"
|
||||
SLOTS_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
SLOTS_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
SLOTS_K8S_NAMESPACE_MASTER: "pcp-master"
|
||||
|
||||
.slots_changes: &slots_changes
|
||||
- services/slots-service/**/*
|
||||
- libs/ballistics-lib/**/*
|
||||
- libs/pcp-types-lib/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/slots-service.yml
|
||||
- helm/slots-service/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.slots:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *slots_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *slots_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *slots_changes
|
||||
- when: never
|
||||
|
||||
.slots:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *slots_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *slots_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *slots_changes
|
||||
- when: never
|
||||
|
||||
.slots:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *slots_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.slots:rules:deploy-master:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *slots_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.slots:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-slots-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.slots:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.slots:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
slots:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
services:
|
||||
- name: repo.nstart.cloud/postgis/postgis:16-3.4
|
||||
alias: postgres
|
||||
variables:
|
||||
POSTGRES_DB: "pcp_slots"
|
||||
POSTGRES_USER: "postgres"
|
||||
POSTGRES_PASSWORD: "password"
|
||||
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/pcp_slots"
|
||||
SPRING_DATASOURCE_USERNAME: "postgres"
|
||||
SPRING_DATASOURCE_PASSWORD: "password"
|
||||
extends:
|
||||
- .slots:rules:test
|
||||
- .slots:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:slots-service:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/slots-service/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/slots-service/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
slots:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .slots:rules:build-and-image
|
||||
- .slots:gradle-cache
|
||||
needs:
|
||||
- job: slots:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:slots-service:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/slots-service/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
slots:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .slots:rules:build-and-image
|
||||
- .slots:docker-build
|
||||
needs:
|
||||
- job: slots:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "SLOTS_IMAGE_REPOSITORY=$SLOTS_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$SLOTS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$SLOTS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$SLOTS_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$SLOTS_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$SLOTS_IMAGE_REPOSITORY:$SLOTS_IMAGE_TAG" docker-context
|
||||
- docker push "$SLOTS_IMAGE_REPOSITORY:$SLOTS_IMAGE_TAG"
|
||||
- printf "SLOTS_IMAGE=%s\n" "$SLOTS_IMAGE_REPOSITORY:$SLOTS_IMAGE_TAG" > slots-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: slots-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
slots:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .slots:rules:deploy-dev
|
||||
- .slots:helm-deploy
|
||||
needs:
|
||||
- job: slots:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/pcp-slots-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${SLOTS_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${SLOTS_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$SLOTS_HELM_RELEASE"
|
||||
"$SLOTS_HELM_CHART_DIR"
|
||||
--namespace "$SLOTS_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 30m
|
||||
-f "$SLOTS_HELM_CHART_DIR/values.yaml"
|
||||
-f "$SLOTS_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=dev
|
||||
--set app.configLabel=dev
|
||||
|
||||
slots:deploy:master:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .slots:rules:deploy-master
|
||||
- .slots:helm-deploy
|
||||
needs:
|
||||
- job: slots:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: master/pcp-slots-service
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_MASTER:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_MASTER_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_MASTER_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_MASTER_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_MASTER_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_MASTER_B64 is not valid base64. Set KUBE_CONFIG_MASTER to raw kubeconfig or KUBE_CONFIG_MASTER_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_MASTER or KUBE_CONFIG_MASTER_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- 'export IMAGE_REPOSITORY="${SLOTS_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${SLOTS_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$SLOTS_HELM_RELEASE"
|
||||
"$SLOTS_HELM_CHART_DIR"
|
||||
--namespace "$SLOTS_K8S_NAMESPACE_MASTER"
|
||||
--create-namespace
|
||||
--wait
|
||||
--atomic
|
||||
--timeout 30m
|
||||
-f "$SLOTS_HELM_CHART_DIR/values.yaml"
|
||||
-f "$SLOTS_HELM_CHART_DIR/values-master.yaml"
|
||||
--set image.repository="$IMAGE_REPOSITORY"
|
||||
--set image.tag="$IMAGE_TAG"
|
||||
--set app.profile=master
|
||||
--set app.configLabel=master
|
||||
@@ -0,0 +1,214 @@
|
||||
variables:
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
|
||||
CONFIG_SERVER_SERVICE_DIR: "services/spring-cloud-config-server"
|
||||
CONFIG_SERVER_HELM_CHART_DIR: "helm/spring-cloud-config-server"
|
||||
CONFIG_SERVER_HELM_RELEASE: "spring-cloud-config-server"
|
||||
CONFIG_SERVER_IMAGE_REPOSITORY: "$INTERNAL_REGISTRY/pcp-cloud-config-server"
|
||||
CONFIG_SERVER_IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"
|
||||
CONFIG_SERVER_K8S_NAMESPACE_DEV: "pcp-dev"
|
||||
CONFIG_SERVER_GIT_SECRET_NAME: "spring-cloud-config-server-git"
|
||||
CONFIG_GIT_URI: "https://git.nstart.cloud/2-6/2-6-4/missionis/observatio-terrae.git"
|
||||
CONFIG_GIT_USERNAME: "gitlab+deploy-token-5"
|
||||
CONFIG_GIT_DEFAULT_LABEL: "dev"
|
||||
CONFIG_GIT_SEARCH_PATHS: "config-repo"
|
||||
CONFIG_GIT_SKIP_SSL_VALIDATION: "true"
|
||||
|
||||
.config_server_changes: &config_server_changes
|
||||
- services/spring-cloud-config-server/**/*
|
||||
- build.gradle.kts
|
||||
- settings.gradle.kts
|
||||
- .gitlab/ci/spring-cloud-config-server.yml
|
||||
- helm/spring-cloud-config-server/**/*
|
||||
- deploy/**/*
|
||||
|
||||
.config-server:rules:build-and-image:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *config_server_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *config_server_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *config_server_changes
|
||||
- when: never
|
||||
|
||||
.config-server:rules:test:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
changes: *config_server_changes
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *config_server_changes
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
changes: *config_server_changes
|
||||
- when: never
|
||||
|
||||
.config-server:rules:deploy-dev:
|
||||
rules:
|
||||
- if: '$RUN_ALL == "true"'
|
||||
when: manual
|
||||
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||
changes: *config_server_changes
|
||||
when: manual
|
||||
- when: never
|
||||
|
||||
.config-server:gradle-cache:
|
||||
cache:
|
||||
key: "gradle-config-server-$CI_DEFAULT_BRANCH"
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
policy: pull-push
|
||||
|
||||
.config-server:docker-build:
|
||||
image: repo.nstart.cloud/library/docker:26
|
||||
services:
|
||||
- name: repo.nstart.cloud/library/docker:26-dind
|
||||
alias: docker
|
||||
command: ["--tls=false", "--insecure-registry=registry.k8s.265.nstart.local"]
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_BUILDKIT: "1"
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
|
||||
.config-server:helm-deploy:
|
||||
image: repo.nstart.cloud/dtzar/helm-kubectl:3.14.0
|
||||
before_script:
|
||||
- mkdir -p /root/.kube
|
||||
- helm version
|
||||
after_script:
|
||||
- rm -f /root/.kube/config
|
||||
|
||||
config-server:test:
|
||||
stage: test
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .config-server:rules:test
|
||||
- .config-server:gradle-cache
|
||||
script:
|
||||
- gradle --no-daemon :services:spring-cloud-config-server:test
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit:
|
||||
- services/spring-cloud-config-server/build/test-results/test/*.xml
|
||||
paths:
|
||||
- services/spring-cloud-config-server/build/reports/tests/
|
||||
expire_in: 7 days
|
||||
|
||||
config-server:build:
|
||||
stage: build
|
||||
image: repo.nstart.cloud/library/gradle:jdk21-ubi10
|
||||
extends:
|
||||
- .config-server:rules:build-and-image
|
||||
- .config-server:gradle-cache
|
||||
needs:
|
||||
- job: config-server:test
|
||||
artifacts: true
|
||||
script:
|
||||
- gradle --no-daemon :services:spring-cloud-config-server:bootJar
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- services/spring-cloud-config-server/build/libs/*.jar
|
||||
expire_in: 7 days
|
||||
|
||||
config-server:package:
|
||||
stage: package
|
||||
extends:
|
||||
- .config-server:rules:build-and-image
|
||||
- .config-server:docker-build
|
||||
needs:
|
||||
- job: config-server:build
|
||||
artifacts: true
|
||||
script:
|
||||
- 'echo "CONFIG_SERVER_IMAGE_REPOSITORY=$CONFIG_SERVER_IMAGE_REPOSITORY"'
|
||||
- |
|
||||
JAR_COUNT="$(find "$CONFIG_SERVER_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | wc -l)"
|
||||
|
||||
if [ "$JAR_COUNT" -ne 1 ]; then
|
||||
echo "Expected exactly one executable jar, found: $JAR_COUNT"
|
||||
find "$CONFIG_SERVER_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' -print || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAR_FILE="$(find "$CONFIG_SERVER_SERVICE_DIR/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"
|
||||
- rm -rf docker-context
|
||||
- mkdir -p docker-context/build/libs
|
||||
- cp "$CONFIG_SERVER_SERVICE_DIR/Dockerfile" docker-context/Dockerfile
|
||||
- cp "$JAR_FILE" docker-context/build/libs/app.jar
|
||||
- find docker-context -maxdepth 4 -type f -print -exec ls -la {} \;
|
||||
- docker build --file docker-context/Dockerfile --tag "$CONFIG_SERVER_IMAGE_REPOSITORY:$CONFIG_SERVER_IMAGE_TAG" docker-context
|
||||
- docker push "$CONFIG_SERVER_IMAGE_REPOSITORY:$CONFIG_SERVER_IMAGE_TAG"
|
||||
- printf "CONFIG_SERVER_IMAGE=%s\n" "$CONFIG_SERVER_IMAGE_REPOSITORY:$CONFIG_SERVER_IMAGE_TAG" > config-server-image.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: config-server-image.env
|
||||
expire_in: 7 days
|
||||
|
||||
config-server:deploy:dev:
|
||||
stage: deploy
|
||||
extends:
|
||||
- .config-server:rules:deploy-dev
|
||||
- .config-server:helm-deploy
|
||||
needs:
|
||||
- job: config-server:package
|
||||
artifacts: true
|
||||
environment:
|
||||
name: dev/spring-cloud-config-server
|
||||
script:
|
||||
- |
|
||||
if [ -n "${KUBE_CONFIG_DEV:-}" ]; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV" > /root/.kube/config
|
||||
elif [ -n "${KUBE_CONFIG_DEV_B64:-}" ]; then
|
||||
if printf '%s' "$KUBE_CONFIG_DEV_B64" | grep -qE '(^apiVersion:|^kind: Config|^clusters:)'; then
|
||||
printf '%s' "$KUBE_CONFIG_DEV_B64" > /root/.kube/config
|
||||
elif ! printf '%s' "$KUBE_CONFIG_DEV_B64" | base64 -d > /root/.kube/config; then
|
||||
echo "KUBE_CONFIG_DEV_B64 is not valid base64. Set KUBE_CONFIG_DEV to raw kubeconfig or KUBE_CONFIG_DEV_B64 to base64 kubeconfig."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "KUBE_CONFIG_DEV or KUBE_CONFIG_DEV_B64 must be set."
|
||||
exit 1
|
||||
fi
|
||||
- chmod 600 /root/.kube/config
|
||||
- export KUBECONFIG=/root/.kube/config
|
||||
- |
|
||||
if [ -z "${CONFIG_GIT_TOKEN:-}" ]; then
|
||||
echo "CONFIG_GIT_TOKEN must be set as a GitLab CI/CD variable."
|
||||
exit 1
|
||||
fi
|
||||
- kubectl create namespace "$CONFIG_SERVER_K8S_NAMESPACE_DEV" --dry-run=client -o yaml | kubectl apply -f -
|
||||
- >
|
||||
kubectl create secret generic "$CONFIG_SERVER_GIT_SECRET_NAME"
|
||||
--namespace "$CONFIG_SERVER_K8S_NAMESPACE_DEV"
|
||||
--from-literal=token="$CONFIG_GIT_TOKEN"
|
||||
--dry-run=client
|
||||
-o yaml
|
||||
| kubectl apply -f -
|
||||
- 'export IMAGE_REPOSITORY="${CONFIG_SERVER_IMAGE%:*}"'
|
||||
- 'export IMAGE_TAG="${CONFIG_SERVER_IMAGE##*:}"'
|
||||
- 'echo "Deploy image: $IMAGE_REPOSITORY:$IMAGE_TAG"'
|
||||
- >
|
||||
helm upgrade --install "$CONFIG_SERVER_HELM_RELEASE"
|
||||
"$CONFIG_SERVER_HELM_CHART_DIR"
|
||||
--namespace "$CONFIG_SERVER_K8S_NAMESPACE_DEV"
|
||||
--create-namespace
|
||||
--values "$CONFIG_SERVER_HELM_CHART_DIR/values.yaml"
|
||||
--values "$CONFIG_SERVER_HELM_CHART_DIR/values-dev.yaml"
|
||||
--set-string image.repository="$IMAGE_REPOSITORY"
|
||||
--set-string image.tag="$IMAGE_TAG"
|
||||
--set-string app.configGitUri="$CONFIG_GIT_URI"
|
||||
--set-string app.configGitUsername="$CONFIG_GIT_USERNAME"
|
||||
--set-string app.configGitDefaultLabel="$CONFIG_GIT_DEFAULT_LABEL"
|
||||
--set-string app.configGitSearchPaths="$CONFIG_GIT_SEARCH_PATHS"
|
||||
--set-string app.configGitSkipSslValidation="$CONFIG_GIT_SKIP_SSL_VALIDATION"
|
||||
--set-string gitTokenSecret.name="$CONFIG_SERVER_GIT_SECRET_NAME"
|
||||
--atomic
|
||||
--wait
|
||||
--timeout 15m
|
||||
@@ -0,0 +1,273 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ задаёт правила работы агента и базовые инженерные правила для изменений в репозитории.
|
||||
|
||||
Цель агента:
|
||||
- вносить минимальные, безопасные и ревьюопригодные изменения;
|
||||
- сохранять архитектурные границы;
|
||||
- не ломать существующие контракты без явного запроса;
|
||||
- следовать уже используемым в репозитории паттернам и стилю.
|
||||
|
||||
Этот документ **не** является полным платформенным стандартом для BPM/Kafka. Такие правила вынесены в `docs/standards/PLATFORM_STANDARD.md`.
|
||||
|
||||
---
|
||||
|
||||
## Дополнительные обязательные документы
|
||||
|
||||
При работе в этом репозитории:
|
||||
|
||||
- Для изменений, связанных с BPM / Zeebe / Camunda / Kafka / retries / DLQ / compensation / schema evolution, перед изменением кода прочитай `docs/standards/PLATFORM_STANDARD.md`.
|
||||
- Перед завершением нетривиальных изменений проверь `docs/standards/CHECKLIST.md`.
|
||||
- Если задача затрагивает только обычный Kotlin/Spring-код без BPM/Kafka-аспектов, достаточно следовать правилам этого `AGENTS.md`.
|
||||
|
||||
---
|
||||
|
||||
## Обязательный стиль работы
|
||||
|
||||
Перед внесением изменений:
|
||||
|
||||
1. Найди похожую реализацию в том же модуле, сервисе или соседнем слое.
|
||||
2. Следуй уже принятому стилю проекта и существующей структуре.
|
||||
3. Прочитай полный execution path до редактирования кода.
|
||||
4. Держи решение в правильном архитектурном слое.
|
||||
5. Сделай минимальное изменение, которое полностью решает задачу.
|
||||
6. Обнови или добавь тесты для изменённого поведения.
|
||||
|
||||
Для нетривиальных задач:
|
||||
|
||||
1. Кратко определи затронутые модули и файлы.
|
||||
2. Явно зафиксируй ключевые допущения, если есть неоднозначность.
|
||||
3. Предпочитай самый безопасный и наименее сложный вариант вместо редизайна.
|
||||
|
||||
Если требования неоднозначны:
|
||||
- не придумывай редизайн;
|
||||
- сохраняй текущее поведение, если нет явного запроса на его изменение;
|
||||
- явно фиксируй допущения;
|
||||
- предпочитай production safety «красивым» решениям.
|
||||
|
||||
---
|
||||
|
||||
## Предпочтение в дизайне решения
|
||||
|
||||
По умолчанию предпочитай самое простое и понятное решение, которое корректно удовлетворяет текущим требованиям.
|
||||
|
||||
Более сложная архитектура, абстракции, дополнительные слои или более общие конструкции допустимы только тогда, когда более простое решение заметно ухудшит хотя бы одно из следующего:
|
||||
- корректность;
|
||||
- понятность и поддерживаемость;
|
||||
- границы ответственности;
|
||||
- тестируемость;
|
||||
- надёжность;
|
||||
- интеграцию с существующей архитектурой.
|
||||
|
||||
Не усложняй решение как подготовку к возможным будущим требованиям.
|
||||
Если есть несколько рабочих вариантов, выбирай наименее сложный, который не ухудшает качество решения.
|
||||
|
||||
---
|
||||
|
||||
## Базовые предположения по стеку
|
||||
|
||||
- Язык: Kotlin
|
||||
- Framework: Spring Boot
|
||||
- Возможные интеграции:
|
||||
- REST
|
||||
- Kafka
|
||||
- BPM / Zeebe / Camunda 8
|
||||
- database persistence
|
||||
- external HTTP/gRPC APIs
|
||||
|
||||
Следуй реальной структуре и библиотекам, уже используемым в репозитории.
|
||||
Не вводи новый стиль, если в проекте уже есть устоявшийся.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные границы
|
||||
|
||||
### Ключевой принцип
|
||||
|
||||
Сервис владеет доменным состоянием и бизнес-правилами.
|
||||
|
||||
### Владение по слоям
|
||||
|
||||
- `controller/api` -> только transport
|
||||
- `application/service/use case` -> orchestration внутри границы сервиса
|
||||
- `domain` -> бизнес-правила, инварианты, решения
|
||||
- `repository/adapter/client` -> только persistence и внешняя коммуникация
|
||||
|
||||
### Где нельзя размещать бизнес-логику
|
||||
|
||||
Нельзя размещать доменную бизнес-логику в:
|
||||
- controllers
|
||||
- repository implementations
|
||||
- Kafka listeners / consumers
|
||||
- Zeebe workers
|
||||
- external client adapters
|
||||
- BPMN process definitions
|
||||
|
||||
---
|
||||
|
||||
## Правила Kotlin
|
||||
|
||||
- Предпочитай явный и читаемый код.
|
||||
- Делай функции небольшими и отражающими намерение.
|
||||
- Предпочитай `val` вместо `var`, если мутация не нужна.
|
||||
- Избегай чрезмерно «умных» chain/scope-function конструкций, если они ухудшают читаемость.
|
||||
- Используй понятные имена; не вводи аббревиатуры без необходимости.
|
||||
- Не вводи DSL-подобный или чрезмерно функциональный стиль, если он не доминирует в проекте.
|
||||
- Сохраняй текущий подход проекта к null-safety.
|
||||
- Не заменяй явные ошибки на `null`-driven control flow.
|
||||
- Предпочитай прямой и понятный код вместо «умных», слишком универсальных или академичных решений.
|
||||
|
||||
### Правило комментариев для Kotlin/Spring кода
|
||||
|
||||
Комментарии обязательны там, где они реально помогают сопровождению кода:
|
||||
|
||||
- для публичных функций и публичных контрактов, если их назначение неочевидно из сигнатуры;
|
||||
- для сложных бизнес-правил, инвариантов и ограничений;
|
||||
- для retry / compensation / idempotency / transactional логики;
|
||||
- для неочевидных решений, важных допущений и edge cases;
|
||||
- для переменных и локальных вычислений только тогда, когда без комментария намерение неясно.
|
||||
|
||||
Не нужно комментировать каждую переменную или каждую функцию механически.
|
||||
Комментарии должны объяснять цель и reasoning, а не пересказывать синтаксис.
|
||||
|
||||
Плохой комментарий:
|
||||
- `// increment counter`
|
||||
|
||||
Хороший комментарий:
|
||||
- `// Хранит количество уже выполненных попыток доставки для этой операции, чтобы retry-логика могла определить, нужно ли повторить обработку или отправить сообщение в DLQ.`
|
||||
|
||||
---
|
||||
|
||||
## Правила Spring Boot
|
||||
|
||||
- Контроллеры должны оставаться тонкими.
|
||||
- Контроллеры должны только:
|
||||
- принимать и валидировать вход;
|
||||
- маппить transport-модели;
|
||||
- вызывать application/service layer;
|
||||
- возвращать response.
|
||||
- Бизнес-правила не должны жить в контроллерах.
|
||||
- Репозитории не должны содержать бизнес-решения.
|
||||
- Transaction boundaries должны находиться в service/application layer, если модуль уже не использует другой устоявшийся паттерн.
|
||||
- Не выполняй долгий remote I/O внутри DB-транзакций без явной необходимости текущего дизайна.
|
||||
- Переиспользуй существующий стиль Spring-конфигурации, properties, beans, mappers и utilities.
|
||||
|
||||
---
|
||||
|
||||
## Правила API и error handling
|
||||
|
||||
- Сохраняй публичные REST/gRPC контракты, если явно не запрошено иное.
|
||||
- Переиспользуй существующий response/error format.
|
||||
- Переиспользуй текущий validation style.
|
||||
- Держи backward compatibility в фокусе.
|
||||
- Если контракт меняется, обнови тесты и nearby examples/docs.
|
||||
|
||||
- Не используй broad `try/catch`, скрывающий ошибки.
|
||||
- Не проглатывай исключения.
|
||||
- Не делай silent downgrade ошибок в дефолтные значения.
|
||||
- Следуй существующему exception mapping pattern.
|
||||
- Держи error handling явным и удобным для дебага.
|
||||
- Избегай дублирующих шумных логов для одного и того же failure path.
|
||||
|
||||
---
|
||||
|
||||
## Правила observability
|
||||
|
||||
Для важных бизнес-флоу сохраняй или улучшай:
|
||||
- structured logging;
|
||||
- metrics;
|
||||
- tracing/correlation;
|
||||
- operation identifiers.
|
||||
|
||||
Если меняется Kafka/BPM/external integration logic:
|
||||
- сохрани или добавь correlation identifiers;
|
||||
- сохрани или добавь `operation_id`, где это релевантно;
|
||||
- логируй достаточно для дебага retries, duplicates и compensations;
|
||||
- никогда не логируй секреты или sensitive payloads.
|
||||
|
||||
---
|
||||
|
||||
## Правила тестирования
|
||||
|
||||
Всегда обновляй тесты, если меняется поведение.
|
||||
|
||||
Добавляй или обновляй unit tests, если меняются:
|
||||
- business rules;
|
||||
- branching logic;
|
||||
- validation;
|
||||
- idempotency behavior;
|
||||
- retry/compensation behavior.
|
||||
|
||||
Добавляй или обновляй integration tests, если меняются:
|
||||
- Spring wiring;
|
||||
- controller behavior;
|
||||
- persistence behavior;
|
||||
- Kafka behavior;
|
||||
- Zeebe worker behavior;
|
||||
- transaction behavior;
|
||||
- external integration behavior.
|
||||
|
||||
Каждое bug fix-изменение по возможности должно сопровождаться regression test.
|
||||
|
||||
---
|
||||
|
||||
## Validation commands
|
||||
|
||||
Сначала предпочитай самый маленький релевантный прогон, потом более широкий.
|
||||
|
||||
Типичные команды:
|
||||
- `./gradlew test`
|
||||
- `./gradlew ktlintCheck`
|
||||
- `./gradlew build`
|
||||
|
||||
Если в репозитории есть module-specific команды, используй их, когда это уместно.
|
||||
|
||||
Если команда не была запущена:
|
||||
- точно укажи, какая именно команда не запускалась;
|
||||
- объясни почему;
|
||||
- опиши остаточный риск.
|
||||
|
||||
---
|
||||
|
||||
## Правила контроля scope
|
||||
|
||||
Держи изменения узко ограниченными.
|
||||
|
||||
Не делай:
|
||||
- переименование файлов / классов / пакетов без явной необходимости;
|
||||
- перенос кода между модулями без сильной архитектурной причины;
|
||||
- переформатирование несвязанных файлов;
|
||||
- «попутную уборку» соседнего кода без необходимости для задачи;
|
||||
- введение абстракций «на будущее».
|
||||
|
||||
Предпочитай:
|
||||
- локальные изменения;
|
||||
- существующие абстракции;
|
||||
- явные, простые, сопровождаемые решения.
|
||||
|
||||
---
|
||||
|
||||
## Обязательный формат финального ответа
|
||||
|
||||
В конце задачи всегда предоставляй:
|
||||
|
||||
1. `Changed files`
|
||||
- список изменённых файлов
|
||||
|
||||
2. `What changed`
|
||||
- краткое summary реализации
|
||||
|
||||
3. `Architecture compliance`
|
||||
- объясни, почему решение соблюдает layer boundaries
|
||||
- если участвует BPM или messaging, кратко укажи, как соблюдены платформенные требования из `docs/standards/PLATFORM_STANDARD.md`
|
||||
|
||||
4. `Validation`
|
||||
- какие команды запускались
|
||||
- результаты
|
||||
|
||||
5. `Risks / assumptions`
|
||||
- что не было полностью проверено
|
||||
- compatibility considerations
|
||||
- edge cases
|
||||
@@ -1,4 +1,4 @@
|
||||
# observatio-terrae
|
||||
# pcp
|
||||
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ Already a pro? Just edit this README.md and make it your own. Want to make it ea
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://git.nstart.cloud/2-6/2-6-4/missionis/observatio-terrae.git
|
||||
git remote add origin https://git.nstart.local/emelianov.an/pcp.git
|
||||
git branch -M master
|
||||
git push -uf origin master
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
* [Set up project integrations](https://git.nstart.cloud/2-6/2-6-4/missionis/observatio-terrae/-/settings/integrations)
|
||||
* [Set up project integrations](https://git.nstart.local/emelianov.an/pcp/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") apply false
|
||||
kotlin("plugin.spring") apply false
|
||||
kotlin("plugin.jpa") apply false
|
||||
kotlin("plugin.lombok") apply false
|
||||
id("org.springframework.boot") apply false
|
||||
id("io.spring.dependency-management") apply false
|
||||
//id("org.sonarqube") apply false
|
||||
}
|
||||
|
||||
|
||||
allprojects {
|
||||
group = "space.nstart.pcp"
|
||||
ext.set("kotlinVersion", "2.2.20")
|
||||
repositories {
|
||||
// maven {
|
||||
// url = uri("https://repo.nstart.cloud/repository/maven-proxy/")
|
||||
// }
|
||||
mavenCentral {
|
||||
content {
|
||||
includeGroup("org.jacoco")
|
||||
includeGroup("org.ow2.asm")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
gradlePluginPortal()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
maven("https://repo.osgeo.org/repository/release/")
|
||||
}
|
||||
filter {
|
||||
includeGroup("org.geotools")
|
||||
includeGroup("org.geotools.ogc")
|
||||
includeGroup("javax.media")
|
||||
includeGroup("it.geosolutions.jgridshift")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
plugins.withId("java") {
|
||||
extensions.configure<JavaPluginExtension> {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of((property("versions.java") as String).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins.withId("org.springframework.boot") {
|
||||
if (
|
||||
path != ":services:pcp-srpring-boot-admin-server" &&
|
||||
path != ":services:spring-cloud-config-server"
|
||||
) {
|
||||
dependencies.add(
|
||||
"implementation",
|
||||
"de.codecentric:spring-boot-admin-starter-client:${property("versions.spring.boot.admin")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Сборка bootJar всех Spring-сервисов.
|
||||
#
|
||||
# ./buildAll.sh # собрать все jar (без тестов; bootJar тесты не гоняет)
|
||||
# ./buildAll.sh --clean # clean + сборка с нуля
|
||||
#
|
||||
# Фронт pcp-tgu-ui-service здесь не собирается (это npm-проект, не gradle).
|
||||
# Чтобы после сборки разложить jar в serviceAllBuild/ — запусти ./serviceAllBuild.sh
|
||||
# (или сразу ./serviceAllBuild.sh --build, который делает сборку и раскладку вместе).
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
ARGS=()
|
||||
[[ "${1:-}" == "--clean" ]] && ARGS+=(clean)
|
||||
ARGS+=(bootJar)
|
||||
|
||||
echo "==> ./gradlew ${ARGS[*]}"
|
||||
"$ROOT/gradlew" "${ARGS[@]}"
|
||||
|
||||
echo
|
||||
echo "Готово. Разложить в одну папку: ./serviceAllBuild.sh"
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <old-ip> <new-ip>"
|
||||
echo "Example: $0 192.168.60.146 192.168.60.146"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OLD_IP="$1"
|
||||
NEW_IP="$2"
|
||||
|
||||
is_valid_ip() {
|
||||
local ip="$1"
|
||||
|
||||
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
|
||||
|
||||
IFS='.' read -r a b c d <<< "$ip"
|
||||
|
||||
for part in "$a" "$b" "$c" "$d"; do
|
||||
(( part >= 0 && part <= 255 )) || return 1
|
||||
done
|
||||
}
|
||||
|
||||
if ! is_valid_ip "$OLD_IP"; then
|
||||
echo "Invalid old IP: $OLD_IP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! is_valid_ip "$NEW_IP"; then
|
||||
echo "Invalid new IP: $NEW_IP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export OLD_IP
|
||||
export NEW_IP
|
||||
|
||||
changed_files=0
|
||||
skipped_files=0
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
# Пропускаем бинарные файлы
|
||||
if ! grep -Iq . "$file"; then
|
||||
skipped_files=$((skipped_files + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if perl -0ne '
|
||||
BEGIN { $old = quotemeta($ENV{"OLD_IP"}); }
|
||||
exit 0 if /(?<![0-9.])$old(?![0-9.])/;
|
||||
exit 1;
|
||||
' "$file"; then
|
||||
perl -0pi -e '
|
||||
BEGIN {
|
||||
$old = quotemeta($ENV{"OLD_IP"});
|
||||
$new = $ENV{"NEW_IP"};
|
||||
}
|
||||
s/(?<![0-9.])$old(?![0-9.])/$new/g;
|
||||
' "$file"
|
||||
|
||||
echo "changed: $file"
|
||||
changed_files=$((changed_files + 1))
|
||||
fi
|
||||
done < <(
|
||||
find . \
|
||||
-path './.git' -prune -o \
|
||||
-type f \
|
||||
-print0
|
||||
)
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
echo "IP replaced: $OLD_IP -> $NEW_IP"
|
||||
echo "Files updated: $changed_files"
|
||||
echo "Files skipped: $skipped_files"
|
||||
@@ -0,0 +1,37 @@
|
||||
# Dynamic Slots Change Log
|
||||
|
||||
1. pcp-ballistics-service
|
||||
- Ужесточены уровни логирования для production/runtime окружения.
|
||||
- Отключены подробные SQL/JDBC/Flyway/Spring Web логи, чтобы уменьшить шум в логах сервиса.
|
||||
|
||||
2. pcp-dynamic-plan-service
|
||||
- Добавлена история запусков расчета динамического плана с возможностью получать список запусков и отдельный запуск.
|
||||
- Добавлено восстановление незавершенных расчетов после рестарта сервиса: зависшие `PENDING` и `RUNNING` запуски переводятся в `FAILED`.
|
||||
- Добавлены режимы расчета `FULL` и `GREEDY`.
|
||||
- Добавлен выбор направления витка для расчета: оба, восходящий или нисходящий.
|
||||
- Добавлен флаг фильтрации маршрутов по уже покрытой территории.
|
||||
- Добавлено сохранение построенных интервалов расчета и API для их получения.
|
||||
- Расширен результат расчета метаданными: режим расчета, режим витка, флаг фильтрации, время последнего маршрута и процент покрытия территории.
|
||||
- Добавлена greedy-логика выбора маршрутов с учетом ограничений КА, суточной загрузки, загрузки по виткам и конфликтов по времени.
|
||||
- Добавлена диагностика причин, по которым интервалы не удалось закрыть маршрутом.
|
||||
- Добавлен параллельный расчет параметров наблюдения по чанкам с настройками размера чанка и parallelism.
|
||||
- Добавлены debug API для расчета параметров наблюдения, проверки greedy-математики и построения маршрутов по сохраненным параметрам наблюдения.
|
||||
- Добавлена возможность сохранять debug-результаты параметров наблюдения во временную директорию.
|
||||
- Generated debug-артефакты `obsParams` исключены из git.
|
||||
- Добавлены миграции БД для новых параметров запуска и хранения интервалов.
|
||||
- Обновлены unit/controller tests для новых сценариев расчета, истории запусков, интервалов и debug-функций.
|
||||
|
||||
3. pcp-ui-service
|
||||
- Добавлен просмотр истории запусков динамического плана.
|
||||
- Добавлено открытие ранее выполненного запуска и просмотр его результата.
|
||||
- Добавлены UI-контролы для выбора режима расчета, направления витка и фильтрации по покрытой территории.
|
||||
- Интервал расчета в UI переведен на выбор дат с автоматическим построением границ дня.
|
||||
- В результат расчета добавлено отображение режима, направления витка, фильтра покрытия, последнего маршрута и процента покрытия территории.
|
||||
- Добавлены действия для отображения и скрытия результата расчета на карте.
|
||||
- Добавлен слой карты для отображения интервалов и маршрутов dynamic plan в Cesium.
|
||||
- Обновлен экран карты, чтобы он подключал и инициализировал слой dynamic plan.
|
||||
- Обновлены стили страницы dynamic plan под историю запусков, новые параметры расчета и map controls.
|
||||
- Обновлены тесты UI-контроллера под новые endpoints и поведение.
|
||||
|
||||
4. spring-cloud-config-server
|
||||
- Отключена регистрация config-server в Spring Boot Admin.
|
||||
@@ -0,0 +1,25 @@
|
||||
# config-repo
|
||||
|
||||
Файлы в этом подпроекте предназначены для `spring-cloud-config-server`.
|
||||
|
||||
Ожидаемый сценарий:
|
||||
|
||||
- этот каталог хранится в GitLab-репозитории с конфигами
|
||||
- `spring-cloud-config-server` читает его через `CONFIG_GIT_URI`
|
||||
- сервисы запрашивают конфиги по имени `spring.application.name`
|
||||
|
||||
Для `tle-monitoring-service` используются файлы:
|
||||
|
||||
- `tle-monitoring-service.yaml`
|
||||
|
||||
Пример запроса к Config Server:
|
||||
|
||||
`GET /tle-monitoring-service/local`
|
||||
|
||||
Архитектурные договоренности:
|
||||
|
||||
- `pcp-request-service` является единственным owner request/grid-домена.
|
||||
- `pcp.services.request` должен использоваться как основной runtime endpoint для requests/cells/grid.
|
||||
- `pcp.services.earth-grid` сохранен только как legacy alias и должен резолвиться в `pcp.services.request`.
|
||||
- lifecycle booked slots должен идти Kafka-first; legacy HTTP `/api/slots/booking/process` не считается рабочим основным путем.
|
||||
- `pcp.services.admin` указывает на `pcp-srpring-boot-admin-server`; сервисы PCP должны регистрироваться в нем через Spring Boot Admin client.
|
||||
@@ -0,0 +1,62 @@
|
||||
# TODO before merge request: вернуть старые адреса окружения.
|
||||
# pcp.infra.postgres.host: localhost
|
||||
pcp:
|
||||
infra:
|
||||
postgres:
|
||||
host: localhost
|
||||
port: 35400
|
||||
kafka:
|
||||
host: localhost
|
||||
port: 19092
|
||||
|
||||
network:
|
||||
host: localhost
|
||||
|
||||
ports:
|
||||
config: 8888
|
||||
admin: 38889
|
||||
route-processing: 8080
|
||||
request: 8080
|
||||
ballistics: 8080
|
||||
complex-mission-service: 8080
|
||||
stations: 8080
|
||||
slots: 8080
|
||||
mission: 8080
|
||||
ui: 8080
|
||||
tgu: 8080
|
||||
tle-monitoring: 8080
|
||||
complex-mission-flow: 8080
|
||||
coverage-scheme: 8080
|
||||
satellite-catalog: 8080
|
||||
dynamic-plan: 8080
|
||||
gateway: 8080
|
||||
|
||||
services:
|
||||
config: http://${pcp.network.host}:${pcp.ports.config}
|
||||
admin: http://${pcp.network.host}:${pcp.ports.admin}
|
||||
route-processing: http://${pcp.network.host}:${pcp.ports.route-processing}
|
||||
request: http://pcp-request-service:${pcp.ports.request}
|
||||
earth-grid: ${pcp.services.request}
|
||||
ballistics: http://pcp-ballistics-service:${pcp.ports.ballistics}
|
||||
complex-mission-service: http://pcp-complex-mission-service:${pcp.ports.complex-mission-service}
|
||||
stations: http://pcp-stations-service:${pcp.ports.stations}
|
||||
slots: http://pcp-slots-service:${pcp.ports.slots}
|
||||
mission: http://pcp-mission-planing-service:${pcp.ports.mission}
|
||||
ui: http://${pcp.network.host}:${pcp.ports.ui}
|
||||
tgu: http://${pcp.network.host}:${pcp.ports.tgu}
|
||||
tle-monitoring: http://tle-monitoring-service:${pcp.ports.tle-monitoring}
|
||||
coverage-scheme: http://pcp-coverage-scheme-service:${pcp.ports.coverage-scheme}
|
||||
satellite-catalog: http://pcp-satellite-catalog-service:${pcp.ports.satellite-catalog}
|
||||
dynamic-plan: http://pcp-dynamic-plan-service:${pcp.ports.dynamic-plan}
|
||||
gateway: http://pcp-gateway-service:${pcp.ports.gateway}
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
enabled: true
|
||||
url:
|
||||
- ${pcp.services.admin}
|
||||
auto-registration: true
|
||||
auto-deregistration: true
|
||||
register-once: true
|
||||
@@ -0,0 +1,80 @@
|
||||
pcp:
|
||||
infra:
|
||||
postgres:
|
||||
host: postgres
|
||||
port: 5432
|
||||
kafka:
|
||||
host: kafka
|
||||
port: 9092
|
||||
|
||||
network:
|
||||
host: localhost
|
||||
|
||||
ports:
|
||||
config: 8888
|
||||
admin: 7000
|
||||
route-processing: 7004
|
||||
request: 7005
|
||||
ballistics: 7003
|
||||
complex-mission-service: 7002
|
||||
stations: 7009
|
||||
slots: 7006
|
||||
mission: 7010
|
||||
ui: 7008
|
||||
tgu: 7011
|
||||
tle-monitoring: 7001
|
||||
complex-mission-flow: 7007
|
||||
coverage-scheme: 7012
|
||||
satellite-catalog: 7013
|
||||
dynamic-plan: 7014
|
||||
gateway: 7080
|
||||
|
||||
services:
|
||||
config: http://spring-cloud-config-server:${pcp.ports.config}
|
||||
admin: http://pcp-srpring-boot-admin-server:${pcp.ports.admin}
|
||||
route-processing: http://pcp-route-processing-service:${pcp.ports.route-processing}
|
||||
request: http://pcp-request-service:${pcp.ports.request}
|
||||
earth-grid: ${pcp.services.request}
|
||||
ballistics: http://pcp-ballistics-service:${pcp.ports.ballistics}
|
||||
complex-mission-service: http://pcp-complex-mission-service:${pcp.ports.complex-mission-service}
|
||||
stations: http://pcp-stations-service:${pcp.ports.stations}
|
||||
slots: http://slots-service:${pcp.ports.slots}
|
||||
mission: http://pcp-mission-planing-service:${pcp.ports.mission}
|
||||
ui: http://pcp-ui-service:${pcp.ports.ui}
|
||||
tgu: http://pcp-tgu-service:${pcp.ports.tgu}
|
||||
tle-monitoring: http://tle-monitoring-service:${pcp.ports.tle-monitoring}
|
||||
coverage-scheme: http://pcp-coverage-scheme-service:${pcp.ports.coverage-scheme}
|
||||
satellite-catalog: http://pcp-satellite-catalog-service:${pcp.ports.satellite-catalog}
|
||||
dynamic-plan: http://pcp-dynamic-plan-service:${pcp.ports.dynamic-plan}
|
||||
gateway: http://pcp-gateway-service:${pcp.ports.gateway}
|
||||
|
||||
spring:
|
||||
kafka:
|
||||
admin:
|
||||
auto-create: true
|
||||
listener:
|
||||
missing-topics-fatal: false
|
||||
|
||||
camunda:
|
||||
client:
|
||||
enabled: ${CAMUNDA_CLIENT_ENABLED:false}
|
||||
grpc-address: ${CAMUNDA_GRPC_ADDRESS:http://192.168.60.201:26500}
|
||||
auth:
|
||||
method: none
|
||||
prefer-rest-over-grpc: false
|
||||
|
||||
planning:
|
||||
external-update-enabled: false
|
||||
|
||||
classifier:
|
||||
platforms-url: https://ordinis.nstart.cloud/api/v1/spacecraft/records
|
||||
platforms-cache-ttl: 15m
|
||||
allowed-platform-statuses:
|
||||
- OPERATIONAL
|
||||
|
||||
tle:
|
||||
polling:
|
||||
enabled: ${TLE_POLLING_ENABLED:false}
|
||||
|
||||
settings:
|
||||
ic-service: http://pcp-satellite-catalog-service:${pcp.ports.satellite-catalog}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Local profile defaults are intentionally overridable because developer machines
|
||||
# and local Docker deployments use different network names.
|
||||
pcp:
|
||||
infra:
|
||||
postgres:
|
||||
host: ${PCP_POSTGRES_HOST:localhost}
|
||||
port: ${PCP_POSTGRES_PORT:5432}
|
||||
kafka:
|
||||
host: ${PCP_KAFKA_HOST:localhost}
|
||||
port: ${PCP_KAFKA_PORT:29092}
|
||||
|
||||
|
||||
|
||||
network:
|
||||
host: ${PCP_NETWORK_HOST:localhost}
|
||||
|
||||
ports:
|
||||
config: 38888
|
||||
admin: 7000
|
||||
route-processing: 7004
|
||||
request: 7005
|
||||
ballistics: 7003
|
||||
complex-mission-service: 7002
|
||||
stations: 7009
|
||||
slots: 7006
|
||||
mission: 7010
|
||||
ui: 7008
|
||||
tgu: 7011
|
||||
tle-monitoring: 7001
|
||||
complex-mission-flow: 7007
|
||||
coverage-scheme: 7012
|
||||
satellite-catalog: 7013
|
||||
dynamic-plan: 7014
|
||||
gateway: 7080
|
||||
|
||||
services:
|
||||
config: http://${pcp.network.host}:${pcp.ports.config}
|
||||
admin: http://${pcp.network.host}:${pcp.ports.admin}
|
||||
route-processing: http://${pcp.network.host}:${pcp.ports.route-processing}
|
||||
request: http://${pcp.network.host}:${pcp.ports.request}
|
||||
earth-grid: ${pcp.services.request}
|
||||
ballistics: http://${pcp.network.host}:${pcp.ports.ballistics}
|
||||
complex-mission-service: http://${pcp.network.host}:${pcp.ports.complex-mission-service}
|
||||
stations: http://${pcp.network.host}:${pcp.ports.stations}
|
||||
slots: http://${pcp.network.host}:${pcp.ports.slots}
|
||||
mission: http://${pcp.network.host}:${pcp.ports.mission}
|
||||
ui: http://${pcp.network.host}:${pcp.ports.ui}
|
||||
tgu: http://${pcp.network.host}:${pcp.ports.tgu}
|
||||
tle-monitoring: http://${pcp.network.host}:${pcp.ports.tle-monitoring}
|
||||
coverage-scheme: http://${pcp.network.host}:${pcp.ports.coverage-scheme}
|
||||
satellite-catalog: http://${pcp.network.host}:${pcp.ports.satellite-catalog}
|
||||
dynamic-plan: http://${pcp.network.host}:${pcp.ports.dynamic-plan}
|
||||
gateway: http://${pcp.network.host}:${pcp.ports.gateway}
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
enabled: true
|
||||
url:
|
||||
- ${pcp.services.admin}
|
||||
auto-registration: true
|
||||
auto-deregistration: true
|
||||
register-once: true
|
||||
@@ -0,0 +1 @@
|
||||
description = "Configuration repository for Spring Cloud Config Server"
|
||||
@@ -0,0 +1,57 @@
|
||||
server:
|
||||
port: 38080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
probes:
|
||||
enabled: true
|
||||
show-details: always
|
||||
|
||||
meteo:
|
||||
s3:
|
||||
endpoint: http://minio:9999
|
||||
region: us-east-1
|
||||
access-key: ${METEO_S3_ACCESS_KEY}
|
||||
secret-key: ${METEO_S3_SECRET_KEY}
|
||||
bucket: meteo
|
||||
object-prefix: gfs
|
||||
path-style-access-enabled: true
|
||||
|
||||
cloud:
|
||||
layer:
|
||||
layer-id: tcdc
|
||||
title: TCDC — Total Cloud Cover
|
||||
description: Отображение общей облачности из GFS, загруженной сервисом meteodownloader
|
||||
source-parameter: TCDC
|
||||
unit: "%"
|
||||
min-value: 0.0
|
||||
max-value: 100.0
|
||||
|
||||
render:
|
||||
derived-prefix: gfs-derived/tcdc
|
||||
catalog-prefix: gfs-derived/tcdc-catalog
|
||||
frame-file-name: frame.json
|
||||
pmtiles-file-name: frame.pmtiles
|
||||
pmtiles-content-type: application/vnd.pmtiles
|
||||
tile-format: png
|
||||
tile-content-type: image/png
|
||||
tile-size: 256
|
||||
min-zoom: 0
|
||||
native-max-zoom: 6
|
||||
display-max-zoom: 12
|
||||
chunk-render-parallelism: 8
|
||||
render-chunk-tile-count: 32
|
||||
max-in-flight-render-chunks: 20
|
||||
frame-render-parallelism: 2
|
||||
render-missing-on-startup: false
|
||||
startup-render-limit: 4
|
||||
catalog-cache-seconds: 30
|
||||
temp-dir: ./meteo-render-temp
|
||||
render-version: v5-pmtiles-direct-leaflet
|
||||
single-instance-mode: true
|
||||
keep-latest-runs: 1
|
||||
@@ -0,0 +1,133 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.ballistics}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.ballistics}
|
||||
health-url: ${pcp.services.ballistics}/actuator/health
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp-ballistics-service
|
||||
auto-offset-reset: earliest
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
settings:
|
||||
calculation-interval: 7
|
||||
stations-service: ${pcp.services.stations}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
admin:
|
||||
auto-create: ${SPRING_KAFKA_ADMIN_AUTO_CREATE:false}
|
||||
consumer:
|
||||
group-id: pcp-ballistics-service
|
||||
auto-offset-reset: earliest
|
||||
listener:
|
||||
auto-startup: ${SPRING_KAFKA_LISTENER_AUTO_STARTUP:false}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_ballistics
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
settings:
|
||||
calculation-interval: 28
|
||||
ic-service: http://${pcp.network.host}:9080
|
||||
stations-service: ${pcp.services.stations}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
|
||||
server:
|
||||
port: 7003
|
||||
@@ -0,0 +1,141 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.complex-mission-service}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.complex-mission-service}
|
||||
health-url: ${pcp.services.complex-mission-service}/actuator/health
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
complex-plan:
|
||||
batch-chunk-size: 100
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_satellites
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
consumer:
|
||||
group-id: pcp-complex-mission-service
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.complex-mission-service:7002}
|
||||
|
||||
settings:
|
||||
calculation-interval: 3
|
||||
ballistics-service: ${pcp.services.ballistics}
|
||||
request-service: ${pcp.services.request}
|
||||
earth-grid-service: ${pcp.services.request}
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
stations-service: ${pcp.services.stations}
|
||||
slots-service: ${pcp.services.slots}
|
||||
coverage-scheme-service: ${pcp.services.coverage-scheme}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
consumer:
|
||||
group-id: pcp-complex-mission-service
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
settings:
|
||||
calculation-interval: 3
|
||||
ballistics-service: ${pcp.services.ballistics}
|
||||
request-service: ${pcp.services.request}
|
||||
earth-grid-service: ${pcp.services.request}
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
stations-service: ${pcp.services.stations}
|
||||
slots-service: ${pcp.services.slots}
|
||||
coverage-scheme-service: ${pcp.services.coverage-scheme}
|
||||
@@ -0,0 +1,72 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.coverage-scheme}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.coverage-scheme}
|
||||
health-url: ${pcp.services.coverage-scheme}/actuator/health
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
coverage-scheme:
|
||||
ballistics-service-url: ${pcp.services.ballistics}
|
||||
complex-mission-service-url: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service-url: ${pcp.services.satellite-catalog}
|
||||
default-roll-step-degrees: 5.0
|
||||
default-minimum-technology-overlap: 0.05
|
||||
default-allow-partial-coverage: true
|
||||
boundary-touch-buffer: 0.05
|
||||
continuity-gap-tolerance: 0.000001
|
||||
default-orientation-tolerance-degrees: 15.0
|
||||
max-roll-samples-per-window: 7
|
||||
flight-line-padding-seconds: 120
|
||||
web-client-response-timeout-seconds: 30
|
||||
web-client-in-memory-size-mb: 20
|
||||
fallback-swath-width-degrees: 1.0
|
||||
fallback-shift-per-roll-degree: 0.02
|
||||
default-minimum-time-between-modes-seconds: 0
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.coverage-scheme:7012}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
@@ -0,0 +1,117 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.dynamic-plan}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.dynamic-plan}
|
||||
health-url: ${pcp.services.dynamic-plan}/actuator/health
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
complex-plan:
|
||||
request-service-url: ${pcp.services.request}
|
||||
request-selection-path: /v1/requests
|
||||
ballistics-service-url: ${pcp.services.ballistics}
|
||||
satellite-catalog-service-url: ${pcp.services.satellite-catalog}
|
||||
report-dir: ${java.io.tmpdir}/pcp-dynamic-plan-service/reports
|
||||
interval-coverage-check-enabled: false
|
||||
geo-layers:
|
||||
enabled: false
|
||||
output-dir: ${java.io.tmpdir}/pcp-dynamic-plan-service/geo-layers
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_complex_plan
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration/pcp-complex-plan
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
consumer:
|
||||
group-id: pcp-dynamic-plan-service
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.dynamic-plan:7014}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration/pcp-complex-plan
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
consumer:
|
||||
group-id: pcp-dynamic-plan-service
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
@@ -0,0 +1,114 @@
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.gateway}
|
||||
management-base-url: ${pcp.services.gateway}
|
||||
cloud:
|
||||
gateway:
|
||||
server:
|
||||
# WebFlux-вариант Spring Cloud Gateway (Boot 4 / Spring Cloud 2025.1).
|
||||
webflux:
|
||||
# CORS теперь живёт ТОЛЬКО здесь — единая точка на edge. Раньше дублировался
|
||||
# per-service (pcp-ui-service/WebConfig.kt), но после сворачивания фронта на
|
||||
# gateway браузер ходит к бэкендам строго через него. Страховка от cross-origin
|
||||
# к самому gateway (dev напрямую, LAN-адрес рабочей станции): allowedOriginPatterns
|
||||
# "*" совместим с allowCredentials=true (в отличие от allowedOrigins "*").
|
||||
globalcors:
|
||||
cors-configurations:
|
||||
'[/**]':
|
||||
allowedOriginPatterns: "*"
|
||||
allowedMethods: "*"
|
||||
allowedHeaders: "*"
|
||||
allowCredentials: true
|
||||
maxAge: 3600
|
||||
routes:
|
||||
# --- Доменные сервисы. Префикс /api/pcp-<x> СРЕЗАЕТСЯ (StripPrefix=2 убирает
|
||||
# 2 сегмента: api + pcp-<x>), т.е. /api/pcp-tgu/api/plans -> бэкенд /api/plans.
|
||||
# Это ровно семантика прежнего vite-rewrite. Цели берём из реестра pcp.services.*.
|
||||
- id: tgu
|
||||
uri: ${pcp.services.tgu}
|
||||
predicates:
|
||||
- Path=/api/pcp-tgu/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: ballistics
|
||||
uri: ${pcp.services.ballistics}
|
||||
predicates:
|
||||
- Path=/api/pcp-ballistics/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: mission
|
||||
uri: ${pcp.services.mission}
|
||||
predicates:
|
||||
- Path=/api/pcp-mission/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: request
|
||||
uri: ${pcp.services.request}
|
||||
predicates:
|
||||
- Path=/api/pcp-request/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: complex
|
||||
uri: ${pcp.services.complex-mission-service}
|
||||
predicates:
|
||||
- Path=/api/pcp-complex/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
- id: slots
|
||||
uri: ${pcp.services.slots}
|
||||
predicates:
|
||||
- Path=/api/pcp-slots/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
# Каталог КА: фронт сюда не ходит, но slots-service обращается к нему строго
|
||||
# через gateway (settings.satellite-catalog-service -> /api/pcp-satellite-catalog).
|
||||
# StripPrefix=2: /api/pcp-satellite-catalog/api/satellites -> бэкенд /api/satellites.
|
||||
- id: satellite-catalog
|
||||
uri: ${pcp.services.satellite-catalog}
|
||||
predicates:
|
||||
- Path=/api/pcp-satellite-catalog/**
|
||||
filters:
|
||||
- StripPrefix=2
|
||||
# --- Станции: путь СОХРАНЯЕМ (в vite было без rewrite) — без StripPrefix.
|
||||
- id: stations
|
||||
uri: ${pcp.services.stations}
|
||||
predicates:
|
||||
- Path=/api/stations/**
|
||||
# Catch-all /api/** -> pcp-ui-service СНЯТ: фронт переведён на источники напрямую
|
||||
# (tgu/баллистика/mission), ui-service выводится из эксплуатации. См.
|
||||
# docs/план-перевода-с-pcp-ui-service.md и задачу #40 бэклога. Любой не-матчнутый
|
||||
# /api/** теперь честно отдаёт 404 на gateway, а не молча уходит в умирающий сервис.
|
||||
|
||||
# Валидация JWT на edge. По умолчанию ВЫКЛЮЧЕНА (anonymous, как сейчас работает фронт).
|
||||
# Для закрытого контура: GATEWAY_JWT_ENABLED=true И задать issuer через env
|
||||
# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=<realm new-start>.
|
||||
# Issuer намеренно НЕ объявлен здесь: пустое значение заставило бы автоконфиг поднимать
|
||||
# JwtDecoder и падать на старте.
|
||||
gateway:
|
||||
security:
|
||||
jwt-enabled: ${GATEWAY_JWT_ENABLED:false}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
server:
|
||||
port: ${pcp.ports.gateway:7080}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
server:
|
||||
port: ${pcp.ports.gateway:8080}
|
||||
@@ -0,0 +1,163 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.mission}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.mission}
|
||||
health-url: ${pcp.services.mission}/actuator/health
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
# Возвращаем текст причины ошибки клиенту (reason/exception message в поле `message` тела ответа),
|
||||
# иначе UI видит только HTTP-фразу и не понимает, почему сохранение съёмки/сброса отклонено.
|
||||
server:
|
||||
error:
|
||||
include-message: always
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
consumer:
|
||||
group-id: pcp-mission-planing-service
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
enable-auto-commit: false
|
||||
listener:
|
||||
missing-topics-fatal: false
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_missions
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
camunda:
|
||||
client:
|
||||
mode: self-managed
|
||||
grpc-address: ${CAMUNDA_GRPC_ADDRESS:http://192.168.60.201:26500}
|
||||
auth:
|
||||
method: none
|
||||
prefer-rest-over-grpc: false
|
||||
server:
|
||||
port: ${pcp.ports.mission:7010}
|
||||
|
||||
settings:
|
||||
ballistics-service: ${pcp.services.ballistics}
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
stations-service: ${pcp.services.stations}
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
booked-slots: pcp.slots.status.v1
|
||||
mode-status-changed: pcp.route.in.v1
|
||||
satellites: pcp.satellites
|
||||
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
consumer:
|
||||
group-id: pcp-mission-planing-service
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
enable-auto-commit: false
|
||||
listener:
|
||||
missing-topics-fatal: false
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
camunda:
|
||||
client:
|
||||
mode: self-managed
|
||||
grpc-address: http://camunda.k8s.265.nstart.cloud:30901
|
||||
auth:
|
||||
method: none
|
||||
prefer-rest-over-grpc: false
|
||||
server:
|
||||
port: ${pcp.ports.mission:8080}
|
||||
|
||||
settings:
|
||||
ballistics-service: ${pcp.services.ballistics}
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
stations-service: ${pcp.services.stations}
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
booked-slots: pcp.slots.status.v1
|
||||
mode-status-changed: pcp.route.in.v1
|
||||
satellites: pcp.satellites
|
||||
@@ -0,0 +1,91 @@
|
||||
server:
|
||||
port: ${pcp.ports.request:7005}
|
||||
|
||||
pcp:
|
||||
outbox:
|
||||
request-status-topic: pcp.request.status.v1
|
||||
publish-batch-size: 50
|
||||
publish-fixed-delay-ms: 5000
|
||||
max-attempts: 10
|
||||
initial-backoff-ms: 1000
|
||||
max-backoff-ms: 60000
|
||||
request:
|
||||
expiry:
|
||||
enabled: true
|
||||
fixed-delay-ms: 60000
|
||||
batch-size: 200
|
||||
max-batches-per-run: 50
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.request}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.request}
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp-request-service
|
||||
auto-offset-reset: latest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger/ui
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
org.hibernate.SQL: WARN
|
||||
org.springframework.kafka: INFO
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
route: pcp.route.georeference.v1
|
||||
consumer:
|
||||
route:
|
||||
dlq-topic: pcp.route.georeference-dlq.v1
|
||||
max-attempts: 5
|
||||
backoff-ms: 1000
|
||||
max-backoff-ms: 30000
|
||||
grid:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_requests
|
||||
username: postgres
|
||||
password: password
|
||||
@@ -0,0 +1,70 @@
|
||||
server:
|
||||
port: ${pcp.ports.route-processing:7004}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger/ui
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.route-processing}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.route-processing}
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_route_processing
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp-route-processing-service
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
enable-auto-commit: false
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
acks: all
|
||||
retries: 10
|
||||
properties:
|
||||
enable.idempotence: true
|
||||
max.in.flight.requests.per.connection: 5
|
||||
listener:
|
||||
missing-topics-fatal: false
|
||||
ack-mode: manual_immediate
|
||||
|
||||
app:
|
||||
route-passport:
|
||||
consumer:
|
||||
retry:
|
||||
max-attempts: 3
|
||||
backoff-ms: 1000
|
||||
max-backoff-ms: 30000
|
||||
outbox:
|
||||
enabled: true
|
||||
batch-size: 50
|
||||
fixed-delay-ms: 1000
|
||||
lock-timeout-ms: 60000
|
||||
initial-backoff-ms: 1000
|
||||
max-backoff-ms: 60000
|
||||
kafka:
|
||||
topics:
|
||||
input: pcp.request.survey-georeference.v1
|
||||
input-dlq: pcp.request.survey-georeference-dlq.v1
|
||||
processed-route: pcp.route.in.v1
|
||||
route-georeference: pcp.route.georeference.v1
|
||||
@@ -0,0 +1,106 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.satellite-catalog}
|
||||
management-base-url: ${pcp.services.satellite-catalog}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_satellite_catalog
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.satellite-catalog:7013}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
@@ -0,0 +1,21 @@
|
||||
server:
|
||||
port: ${pcp.ports.admin:38889}
|
||||
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
enabled: false
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,env,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
logging:
|
||||
level:
|
||||
de.codecentric.boot.admin: INFO
|
||||
@@ -0,0 +1,90 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.stations}
|
||||
management-base-url: ${pcp.services.stations}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_stations
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
server:
|
||||
port: ${pcp.ports.stations:7009}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
server:
|
||||
port: ${pcp.ports.stations:8080}
|
||||
@@ -0,0 +1,156 @@
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.tgu}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.tgu}
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
camunda:
|
||||
client:
|
||||
mode: self-managed
|
||||
grpc-address: http://camunda.k8s.265.nstart.local:30901
|
||||
auth:
|
||||
method: none
|
||||
prefer-rest-over-grpc: false
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
|
||||
# Возвращаем текст причины ошибки клиенту (reason из ResponseStatusException попадает в поле
|
||||
# `message` тела ответа). Без этого UI видит только HTTP-фразу «Unprocessable Content» и не
|
||||
# понимает, почему действие отклонено (напр. «план заложен — используйте fork-forward»).
|
||||
server:
|
||||
error:
|
||||
include-message: always
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger/ui
|
||||
|
||||
planning:
|
||||
notification-before-start-minutes: 263
|
||||
target-plan-duration-minutes: 1440
|
||||
target-plan-duration-delta-minutes: 240
|
||||
external-update-enabled: true
|
||||
external-points-lookahead-days: 10
|
||||
in-memory-history-days: 14
|
||||
external-update-fixed-delay-ms: 600000
|
||||
notification-check-fixed-delay-ms: 30000
|
||||
min-reissue-lead-minutes: 5
|
||||
reissue-prep-minutes: 30
|
||||
worker-lock-ttl-seconds: 300
|
||||
waiting-decision-grace-minutes: 30
|
||||
# Заморозка перед стартом: ручная replace-перевыдача плана в свой слот запрещена,
|
||||
# если до старта осталось меньше этого числа минут (часть «точки невозврата»).
|
||||
layin-freeze-minutes: 10
|
||||
|
||||
topics:
|
||||
visibility-windows-changed: pcp.tgu.visibility-windows-changed.v1
|
||||
satellite-plan-decision: pcp.tgu.satellite-plan-decision.v1
|
||||
layin-failed: pcp.tgu.layin-failed.v1
|
||||
layin-confirmed: pcp.tgu.layin-confirmed.v1
|
||||
plan-not-accepted: pcp.tgu.plan-not-accepted.v1
|
||||
station-params-changed: pcp.tgu.station-params-changed.v1
|
||||
|
||||
tgu:
|
||||
test-controller:
|
||||
enabled: true
|
||||
|
||||
classifier:
|
||||
platforms-url: https://ordinis.nstart.cloud/api/v1/spacecraft/records
|
||||
platforms-cache-ttl: 15m
|
||||
allowed-platform-statuses:
|
||||
- OPERATIONAL
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: ${DB_URL:jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_tgu}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:password}
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp-tgu-service
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
spring.json.trusted.packages: "*"
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
|
||||
|
||||
external-api:
|
||||
base-url: ${pcp.services.ballistics}
|
||||
|
||||
mission-planing:
|
||||
base-url: ${pcp.services.mission}
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.tgu:7011}
|
||||
|
||||
camunda:
|
||||
client:
|
||||
grpc-address: ${CAMUNDA_GRPC_ADDRESS:http://192.168.60.201:26500}
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp-tgu-service
|
||||
auto-offset-reset: earliest
|
||||
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
spring.json.trusted.packages: "*"
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
|
||||
admin:
|
||||
auto-create: true
|
||||
|
||||
external-api:
|
||||
base-url: ${pcp.services.ballistics}
|
||||
server:
|
||||
port: ${pcp.ports.tgu:8080}
|
||||
@@ -0,0 +1,47 @@
|
||||
server:
|
||||
port: ${pcp.ports.ui:7008}
|
||||
shutdown: graceful
|
||||
ssl:
|
||||
enabled: false
|
||||
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
thymeleaf.reactive.max-chunk-size: 8192
|
||||
jackson:
|
||||
serialization-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.ui}
|
||||
management-base-url: ${pcp.services.ui}
|
||||
health-url: ${pcp.services.ui}/actuator/health
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
web:
|
||||
resources:
|
||||
static-locations: classpath:/static/
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
settings:
|
||||
calculation-interval: 3
|
||||
request-service: ${pcp.services.request}
|
||||
earth-grid-service: ${pcp.services.request}
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
dynamic-plan-service: ${pcp.services.dynamic-plan}
|
||||
tgu-service: ${pcp.services.tgu}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
ballistics-service: ${pcp.services.ballistics}
|
||||
tle-monitoring-service: ${pcp.services.tle-monitoring}
|
||||
stations-service: ${pcp.services.stations}
|
||||
slots-service: ${pcp.services.slots}
|
||||
mission-service: ${pcp.services.mission}
|
||||
coverage-scheme-service: ${pcp.services.coverage-scheme}
|
||||
@@ -0,0 +1,161 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.slots}
|
||||
service-path: /swagger/ui
|
||||
management-base-url: ${pcp.services.slots}
|
||||
health-url: ${pcp.services.slots}/actuator/health
|
||||
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_slots
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
|
||||
# Все обращения к другим сервисам — строго через gateway (StripPrefix снимает /api/pcp-<x>).
|
||||
# Напрямую по адресам сервисов slots-service не ходит.
|
||||
settings:
|
||||
request-service: ${pcp.services.gateway}/api/pcp-request
|
||||
earth-grid-service: ${pcp.services.gateway}/api/pcp-request
|
||||
satellite-catalog-service: ${pcp.services.gateway}/api/pcp-satellite-catalog
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
booked-slots: pcp.slots.status.v1
|
||||
satellites: pcp.satellites
|
||||
|
||||
pcp:
|
||||
booked-slots:
|
||||
timeout-check-interval: 1h
|
||||
timeout-threshold: 1h
|
||||
slot-service:
|
||||
calc:
|
||||
parallelism: 8
|
||||
chunk-duration-seconds: 86400
|
||||
flush-threshold: 40000
|
||||
stream-batch-size: 2000
|
||||
source-margin-seconds: 120
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.slots:7006}
|
||||
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
|
||||
# Все обращения к другим сервисам — строго через gateway (StripPrefix снимает /api/pcp-<x>).
|
||||
# Напрямую по адресам сервисов slots-service не ходит.
|
||||
settings:
|
||||
request-service: ${pcp.services.gateway}/api/pcp-request
|
||||
earth-grid-service: ${pcp.services.gateway}/api/pcp-request
|
||||
satellite-catalog-service: ${pcp.services.gateway}/api/pcp-satellite-catalog
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
booked-slots: pcp.slots.status.v1
|
||||
satellites: pcp.satellites
|
||||
|
||||
pcp:
|
||||
booked-slots:
|
||||
timeout-check-interval: 1h
|
||||
timeout-threshold: 1h
|
||||
slot-service:
|
||||
calc:
|
||||
parallelism: 4
|
||||
chunk-duration-seconds: 86400
|
||||
flush-threshold: 20000
|
||||
stream-batch-size: 2000
|
||||
source-margin-seconds: 120
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.slots:8080}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
enabled: true
|
||||
url:
|
||||
- ${pcp.services.admin}
|
||||
auto-registration: true
|
||||
auto-deregistration: true
|
||||
register-once: true
|
||||
instance:
|
||||
service-base-url: ${pcp.services.config}
|
||||
management-base-url: ${pcp.services.config}
|
||||
cloud:
|
||||
config:
|
||||
server:
|
||||
git:
|
||||
uri: ${CONFIG_GIT_URI:}
|
||||
default-label: ${CONFIG_GIT_DEFAULT_LABEL:main}
|
||||
search-paths: ${CONFIG_GIT_SEARCH_PATHS:config-repo}
|
||||
clone-on-start: ${CONFIG_GIT_CLONE_ON_START:false}
|
||||
force-pull: ${CONFIG_GIT_FORCE_PULL:true}
|
||||
timeout: ${CONFIG_GIT_TIMEOUT:10}
|
||||
username: ${CONFIG_GIT_USERNAME:oauth2}
|
||||
password: ${CONFIG_GIT_TOKEN:}
|
||||
skipSslValidation: ${CONFIG_GIT_SKIP_SSL_VALIDATION:false}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8888}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,env,refresh
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.config.server: INFO
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: test
|
||||
cloud:
|
||||
config:
|
||||
server:
|
||||
git:
|
||||
uri: https://gitlab.example.invalid/group/config-repo.git
|
||||
clone-on-start: false
|
||||
@@ -0,0 +1,127 @@
|
||||
spring:
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
instance:
|
||||
service-base-url: ${pcp.services.tle-monitoring}
|
||||
management-base-url: ${pcp.services.tle-monitoring}
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
|
||||
tle:
|
||||
polling:
|
||||
enabled: false
|
||||
timeout-seconds: 10800
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp
|
||||
auto-offset-reset: earliest
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: "${SPRING_DATASOURCE_URL}"
|
||||
username: "${SPRING_DATASOURCE_USERNAME}"
|
||||
password: "${SPRING_DATASOURCE_PASSWORD}"
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
settings:
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
kafka:
|
||||
bootstrap-servers: ${pcp.infra.kafka.host}:${pcp.infra.kafka.port}
|
||||
consumer:
|
||||
group-id: pcp
|
||||
auto-offset-reset: earliest
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://${pcp.infra.postgres.host}:${pcp.infra.postgres.port}/pcp_tle
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
app:
|
||||
kafka:
|
||||
topics:
|
||||
satellites: pcp.satellites
|
||||
|
||||
settings:
|
||||
complex-mission-service: ${pcp.services.complex-mission-service}
|
||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||
|
||||
server:
|
||||
port: ${pcp.ports.tle-monitoring:7001}
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
./create-chatgpt-archive.sh [archive.zip]
|
||||
./create-chatgpt-archive.sh --list
|
||||
|
||||
Creates a zip archive with project files useful for code analysis.
|
||||
|
||||
Environment:
|
||||
MAX_FILE_SIZE_BYTES Per-file size limit. Default: 1048576
|
||||
|
||||
The script includes source, configuration, build metadata, CI, Helm and docs.
|
||||
It excludes build outputs, logs, local caches, IDE files, archives, binaries and env files.
|
||||
USAGE
|
||||
}
|
||||
|
||||
mode="create"
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "--list" ]]; then
|
||||
mode="list"
|
||||
shift
|
||||
fi
|
||||
|
||||
if (( $# > 1 )); then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
archive_arg="${1:-pcp-chatgpt-context.zip}"
|
||||
max_file_size_bytes="${MAX_FILE_SIZE_BYTES:-1048576}"
|
||||
|
||||
if [[ "$archive_arg" = /* ]]; then
|
||||
archive_path="$archive_arg"
|
||||
else
|
||||
archive_path="$project_root/$archive_arg"
|
||||
fi
|
||||
|
||||
include_paths=(
|
||||
"AGENTS.md"
|
||||
"README.md"
|
||||
"HELP.md"
|
||||
".dockerignore"
|
||||
".gitattributes"
|
||||
".gitignore"
|
||||
".gitlab-ci.yml"
|
||||
"build.gradle.kts"
|
||||
"settings.gradle.kts"
|
||||
"gradle.properties"
|
||||
"gradlew"
|
||||
"gradlew.bat"
|
||||
"create-chatgpt-archive.sh"
|
||||
"changeLogsDynamicSlots.md"
|
||||
".gitlab"
|
||||
"config-repo"
|
||||
"docs"
|
||||
"gradle"
|
||||
"helm"
|
||||
"libs"
|
||||
"schemes"
|
||||
"services"
|
||||
)
|
||||
|
||||
should_skip_path() {
|
||||
local path="$1"
|
||||
|
||||
case "$path" in
|
||||
.git|.git/*|.gradle|.gradle/*|.gradle-local|.gradle-local/*|.idea|.idea/*|.kotlin|.kotlin/*|.vscode|.vscode/*)
|
||||
return 0
|
||||
;;
|
||||
build|build/*|logs|logs/*|out|out/*|bin|bin/*|target|target/*|node_modules|node_modules/*)
|
||||
return 0
|
||||
;;
|
||||
*/build|*/build/*|*/logs|*/logs/*|*/out|*/out/*|*/bin|*/bin/*|*/target|*/target/*|*/node_modules|*/node_modules/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$path" in
|
||||
.env|.env.*|*/.env|*/.env.*)
|
||||
return 0
|
||||
;;
|
||||
*.log|*.tmp|*.bak|*.orig|*.class|*.jar|*.war|*.ear|*.zip|*.tar|*.tgz|*.tar.gz|*.gz|*.7z|*.rar)
|
||||
return 0
|
||||
;;
|
||||
*.png|*.jpg|*.jpeg|*.gif|*.webp|*.ico|*.pdf|*.doc|*.docx|*.xls|*.xlsx|*.ppt|*.pptx)
|
||||
return 0
|
||||
;;
|
||||
*.keystore|*.jks|*.p12|*.pem|*.key|*.crt)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_allowed_file() {
|
||||
local path="$1"
|
||||
|
||||
case "$path" in
|
||||
AGENTS.md|README.md|HELP.md|.dockerignore|.gitattributes|.gitignore|.gitlab-ci.yml)
|
||||
return 0
|
||||
;;
|
||||
build.gradle.kts|settings.gradle.kts|gradle.properties|gradlew|gradlew.bat|create-chatgpt-archive.sh|changeLogsDynamicSlots.md)
|
||||
return 0
|
||||
;;
|
||||
Dockerfile|Makefile|Jenkinsfile)
|
||||
return 0
|
||||
;;
|
||||
*.kt|*.kts|*.java|*.groovy|*.xml|*.yaml|*.yml|*.properties|*.json|*.toml|*.md|*.txt)
|
||||
return 0
|
||||
;;
|
||||
*.sql|*.graphql|*.graphqls|*.proto|*.bpmn|*.dmn|*.tpl|*.sh)
|
||||
return 0
|
||||
;;
|
||||
*.html|*.css|*.scss|*.js|*.jsx|*.ts|*.tsx|*.vue)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
manifest_file="$(mktemp)"
|
||||
trap 'rm -f "$manifest_file"' EXIT
|
||||
|
||||
collect_files() {
|
||||
local root_path
|
||||
local relative_path
|
||||
local file_size
|
||||
|
||||
for root_path in "${include_paths[@]}"; do
|
||||
if [[ ! -e "$project_root/$root_path" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -f "$project_root/$root_path" ]]; then
|
||||
printf '%s\n' "$root_path"
|
||||
else
|
||||
(cd "$project_root" && find "$root_path" -type f -print)
|
||||
fi
|
||||
done | while IFS= read -r relative_path; do
|
||||
relative_path="${relative_path#./}"
|
||||
|
||||
if should_skip_path "$relative_path"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_allowed_file "$relative_path"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
file_size="$(stat -c '%s' "$project_root/$relative_path")"
|
||||
if (( file_size > max_file_size_bytes )); then
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '%s\n' "$relative_path"
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
collect_files > "$manifest_file"
|
||||
|
||||
file_count="$(wc -l < "$manifest_file" | tr -d ' ')"
|
||||
if (( file_count == 0 )); then
|
||||
echo "No files selected for archive." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "list" ]]; then
|
||||
cat "$manifest_file"
|
||||
echo
|
||||
echo "Selected files: $file_count" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v zip >/dev/null 2>&1; then
|
||||
echo "zip command is required but was not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$archive_path")"
|
||||
rm -f "$archive_path"
|
||||
|
||||
(cd "$project_root" && zip -q -X "$archive_path" -@ < "$manifest_file")
|
||||
|
||||
archive_size="$(du -h "$archive_path" | awk '{print $1}')"
|
||||
echo "Archive created: $archive_path"
|
||||
echo "Selected files: $file_count"
|
||||
echo "Archive size: $archive_size"
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="pcp-dev"
|
||||
POSTGRES_POD="pcp-postgres-0"
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="$1"
|
||||
|
||||
if [[ ! "$db_name" =~ ^[a-z][a-z0-9_]*$ ]]; then
|
||||
echo "ERROR: invalid database name: $db_name" >&2
|
||||
echo "Allowed: lowercase letters, digits, underscore; must start with a letter." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
secret_name_for_db() {
|
||||
local db_name="$1"
|
||||
echo "${db_name//_/-}-db"
|
||||
}
|
||||
|
||||
user_name_for_db() {
|
||||
local db_name="$1"
|
||||
echo "${db_name}_user"
|
||||
}
|
||||
|
||||
get_or_create_password() {
|
||||
local secret_name="$1"
|
||||
|
||||
if kubectl -n "$NAMESPACE" get secret "$secret_name" >/dev/null 2>&1; then
|
||||
kubectl -n "$NAMESPACE" get secret "$secret_name" \
|
||||
-o jsonpath='{.data.SPRING_DATASOURCE_PASSWORD}' | base64 -d
|
||||
else
|
||||
openssl rand -base64 32 | tr -d '/+=' | head -c 32
|
||||
fi
|
||||
}
|
||||
|
||||
create_db_user_and_secret() {
|
||||
local db_name="$1"
|
||||
|
||||
validate_db_name "$db_name"
|
||||
|
||||
local db_user
|
||||
local secret_name
|
||||
local db_password
|
||||
|
||||
db_user="$(user_name_for_db "$db_name")"
|
||||
secret_name="$(secret_name_for_db "$db_name")"
|
||||
db_password="$(get_or_create_password "$secret_name")"
|
||||
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo "Creating database package"
|
||||
echo " database: $db_name"
|
||||
echo " user: $db_user"
|
||||
echo " secret: $secret_name"
|
||||
echo "============================================================"
|
||||
|
||||
echo "1/4 Create or update PostgreSQL user and database"
|
||||
|
||||
kubectl -n "$NAMESPACE" exec -i "$POSTGRES_POD" -- psql \
|
||||
-U postgres \
|
||||
-d postgres <<SQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_roles WHERE rolname = '$db_user'
|
||||
) THEN
|
||||
CREATE ROLE "$db_user" LOGIN PASSWORD '$db_password';
|
||||
ELSE
|
||||
ALTER ROLE "$db_user" WITH LOGIN PASSWORD '$db_password';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
|
||||
SELECT 'CREATE DATABASE "$db_name" OWNER "$db_user"'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT FROM pg_database WHERE datname = '$db_name'
|
||||
) \gexec
|
||||
|
||||
ALTER DATABASE "$db_name" OWNER TO "$db_user";
|
||||
GRANT CONNECT ON DATABASE "$db_name" TO "$db_user";
|
||||
SQL
|
||||
|
||||
echo "2/4 Enable PostGIS"
|
||||
|
||||
kubectl -n "$NAMESPACE" exec -i "$POSTGRES_POD" -- psql \
|
||||
-U postgres \
|
||||
-d "$db_name" <<SQL
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
ALTER SCHEMA public OWNER TO "$db_user";
|
||||
GRANT USAGE, CREATE ON SCHEMA public TO "$db_user";
|
||||
SQL
|
||||
|
||||
echo "3/4 Create or update Kubernetes Secret"
|
||||
|
||||
kubectl -n "$NAMESPACE" create secret generic "$secret_name" \
|
||||
--from-literal=SPRING_DATASOURCE_USERNAME="$db_user" \
|
||||
--from-literal=SPRING_DATASOURCE_PASSWORD="$db_password" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "4/4 Check connection and PostGIS"
|
||||
|
||||
kubectl -n "$NAMESPACE" exec -i "$POSTGRES_POD" -- env PGPASSWORD="$db_password" psql \
|
||||
-h localhost \
|
||||
-U "$db_user" \
|
||||
-d "$db_name" \
|
||||
-c 'select current_database(), current_user, postgis_full_version();'
|
||||
|
||||
echo "Done: $db_name"
|
||||
}
|
||||
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
echo "Usage: $0 <db_name> [db_name...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for db_name in "$@"; do
|
||||
create_db_user_and_secret "$db_name"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "All requested databases processed."
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="${NAMESPACE:-pcp-dev}"
|
||||
POSTGRES_SERVICE="${POSTGRES_SERVICE:-pcp-postgres}"
|
||||
CLUSTER_DOMAIN="${CLUSTER_DOMAIN:-altum.local}"
|
||||
POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgis/postgis:15-3.3}"
|
||||
ADMIN_SECRET="${ADMIN_SECRET:-pcp-postgres-admin}"
|
||||
ADMIN_PASSWORD_KEY="${ADMIN_PASSWORD_KEY:-POSTGRES_PASSWORD}"
|
||||
|
||||
POSTGRES_HOST="${POSTGRES_SERVICE}.${NAMESPACE}.svc.${CLUSTER_DOMAIN}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
$0 <db_name> [db_name...]
|
||||
|
||||
$0 -f databases.txt
|
||||
|
||||
Examples:
|
||||
$0 pcp_satellite_catalog pcp_ballistics
|
||||
|
||||
cat > databases.txt <<LIST
|
||||
pcp_satellite_catalog
|
||||
pcp_ballistics
|
||||
pcp_requests
|
||||
LIST
|
||||
|
||||
$0 -f databases.txt
|
||||
|
||||
Environment overrides:
|
||||
NAMESPACE=pcp-dev
|
||||
POSTGRES_SERVICE=pcp-postgres
|
||||
CLUSTER_DOMAIN=altum.local
|
||||
POSTGRES_IMAGE=postgis/postgis:15-3.3
|
||||
EOF
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "ERROR: required command not found: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_db_name() {
|
||||
local db_name="$1"
|
||||
|
||||
if [[ ! "$db_name" =~ ^[a-z][a-z0-9_]*$ ]]; then
|
||||
echo "ERROR: invalid database name: $db_name" >&2
|
||||
echo "Allowed format: lowercase letters, digits, underscore; must start with a letter." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
secret_name_for_db() {
|
||||
local db_name="$1"
|
||||
local dashed="${db_name//_/-}"
|
||||
echo "${dashed}-db"
|
||||
}
|
||||
|
||||
user_name_for_db() {
|
||||
local db_name="$1"
|
||||
echo "${db_name}_user"
|
||||
}
|
||||
|
||||
make_pod_name() {
|
||||
local prefix="$1"
|
||||
local db_name="$2"
|
||||
local safe="${db_name//_/-}"
|
||||
safe="${safe:0:35}"
|
||||
echo "${prefix}-${safe}-${RANDOM}${RANDOM}"
|
||||
}
|
||||
|
||||
get_admin_password() {
|
||||
kubectl -n "$NAMESPACE" get secret "$ADMIN_SECRET" \
|
||||
-o jsonpath="{.data.${ADMIN_PASSWORD_KEY}}" | base64 -d
|
||||
}
|
||||
|
||||
get_or_create_service_password() {
|
||||
local secret_name="$1"
|
||||
|
||||
if kubectl -n "$NAMESPACE" get secret "$secret_name" >/dev/null 2>&1; then
|
||||
kubectl -n "$NAMESPACE" get secret "$secret_name" \
|
||||
-o jsonpath='{.data.SPRING_DATASOURCE_PASSWORD}' | base64 -d
|
||||
else
|
||||
openssl rand -base64 32 | tr -d '/+=' | head -c 32
|
||||
fi
|
||||
}
|
||||
|
||||
create_database_and_user() {
|
||||
local db_name="$1"
|
||||
local db_user="$2"
|
||||
local db_password="$3"
|
||||
local admin_password="$4"
|
||||
|
||||
local pod_name
|
||||
pod_name="$(make_pod_name "psql-create" "$db_name")"
|
||||
|
||||
echo "Creating/updating database and user:"
|
||||
echo " database: $db_name"
|
||||
echo " user: $db_user"
|
||||
|
||||
kubectl -n "$NAMESPACE" run --rm -i "$pod_name" \
|
||||
--image="$POSTGRES_IMAGE" \
|
||||
--restart=Never \
|
||||
--env="PGPASSWORD=$admin_password" \
|
||||
--command -- psql \
|
||||
-h "$POSTGRES_HOST" \
|
||||
-U postgres \
|
||||
-d postgres <<SQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_roles WHERE rolname = '$db_user'
|
||||
) THEN
|
||||
CREATE ROLE "$db_user" LOGIN PASSWORD '$db_password';
|
||||
ELSE
|
||||
ALTER ROLE "$db_user" WITH LOGIN PASSWORD '$db_password';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
|
||||
SELECT 'CREATE DATABASE "$db_name" OWNER "$db_user"'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT FROM pg_database WHERE datname = '$db_name'
|
||||
) \gexec
|
||||
|
||||
ALTER DATABASE "$db_name" OWNER TO "$db_user";
|
||||
GRANT CONNECT ON DATABASE "$db_name" TO "$db_user";
|
||||
SQL
|
||||
}
|
||||
|
||||
enable_postgis() {
|
||||
local db_name="$1"
|
||||
local db_user="$2"
|
||||
local admin_password="$3"
|
||||
|
||||
local pod_name
|
||||
pod_name="$(make_pod_name "psql-postgis" "$db_name")"
|
||||
|
||||
echo "Enabling PostGIS:"
|
||||
echo " database: $db_name"
|
||||
|
||||
kubectl -n "$NAMESPACE" run --rm -i "$pod_name" \
|
||||
--image="$POSTGRES_IMAGE" \
|
||||
--restart=Never \
|
||||
--env="PGPASSWORD=$admin_password" \
|
||||
--command -- psql \
|
||||
-h "$POSTGRES_HOST" \
|
||||
-U postgres \
|
||||
-d "$db_name" <<SQL
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
ALTER SCHEMA public OWNER TO "$db_user";
|
||||
GRANT USAGE, CREATE ON SCHEMA public TO "$db_user";
|
||||
SQL
|
||||
}
|
||||
|
||||
create_or_update_secret() {
|
||||
local db_name="$1"
|
||||
local db_user="$2"
|
||||
local db_password="$3"
|
||||
local secret_name="$4"
|
||||
|
||||
echo "Creating/updating Kubernetes Secret:"
|
||||
echo " secret: $secret_name"
|
||||
|
||||
kubectl -n "$NAMESPACE" create secret generic "$secret_name" \
|
||||
--from-literal=SPRING_DATASOURCE_USERNAME="$db_user" \
|
||||
--from-literal=SPRING_DATASOURCE_PASSWORD="$db_password" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
}
|
||||
|
||||
check_database() {
|
||||
local db_name="$1"
|
||||
local db_user="$2"
|
||||
local db_password="$3"
|
||||
|
||||
local pod_name
|
||||
pod_name="$(make_pod_name "psql-check" "$db_name")"
|
||||
|
||||
echo "Checking database:"
|
||||
echo " database: $db_name"
|
||||
echo " user: $db_user"
|
||||
|
||||
kubectl -n "$NAMESPACE" run --rm -i "$pod_name" \
|
||||
--image="$POSTGRES_IMAGE" \
|
||||
--restart=Never \
|
||||
--env="PGPASSWORD=$db_password" \
|
||||
--command -- psql \
|
||||
-h "$POSTGRES_HOST" \
|
||||
-U "$db_user" \
|
||||
-d "$db_name" \
|
||||
-c 'select current_database(), current_user, postgis_full_version();'
|
||||
}
|
||||
|
||||
read_db_list() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == "-f" || "$1" == "--file" ]]; then
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "ERROR: file path is required after $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local file="$2"
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "ERROR: file not found: $file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -vE '^\s*($|#)' "$file"
|
||||
else
|
||||
printf '%s\n' "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_command kubectl
|
||||
require_command openssl
|
||||
require_command base64
|
||||
|
||||
echo "Namespace: $NAMESPACE"
|
||||
echo "Postgres host: $POSTGRES_HOST"
|
||||
echo "Postgres image: $POSTGRES_IMAGE"
|
||||
echo "Admin secret: $ADMIN_SECRET"
|
||||
echo
|
||||
|
||||
local admin_password
|
||||
admin_password="$(get_admin_password)"
|
||||
|
||||
mapfile -t databases < <(read_db_list "$@")
|
||||
|
||||
if [[ "${#databases[@]}" -eq 0 ]]; then
|
||||
echo "ERROR: database list is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for db_name in "${databases[@]}"; do
|
||||
validate_db_name "$db_name"
|
||||
|
||||
local db_user
|
||||
local secret_name
|
||||
local db_password
|
||||
local jdbc_url
|
||||
|
||||
db_user="$(user_name_for_db "$db_name")"
|
||||
secret_name="$(secret_name_for_db "$db_name")"
|
||||
db_password="$(get_or_create_service_password "$secret_name")"
|
||||
jdbc_url="jdbc:postgresql://${POSTGRES_HOST}:5432/${db_name}"
|
||||
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo "Processing database: $db_name"
|
||||
echo "============================================================"
|
||||
|
||||
create_database_and_user "$db_name" "$db_user" "$db_password" "$admin_password"
|
||||
enable_postgis "$db_name" "$db_user" "$admin_password"
|
||||
create_or_update_secret "$db_name" "$db_user" "$db_password" "$secret_name"
|
||||
check_database "$db_name" "$db_user" "$db_password"
|
||||
|
||||
echo
|
||||
echo "Done:"
|
||||
echo " database: $db_name"
|
||||
echo " user: $db_user"
|
||||
echo " secret: $secret_name"
|
||||
echo " JDBC URL: $jdbc_url"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "All databases processed successfully."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
pcp_ballistics
|
||||
|
||||
pcp_satellites
|
||||
|
||||
pcp_complex_plan
|
||||
|
||||
pcp_missions
|
||||
|
||||
pcp_requests
|
||||
|
||||
pcp_route_processing
|
||||
|
||||
pcp_satellite_catalog +
|
||||
|
||||
pcp_slots
|
||||
|
||||
pcp_stations
|
||||
|
||||
pcp_tgu
|
||||
|
||||
pcp_tle
|
||||
|
||||
|
||||
|
||||
pcp_ballistics pcp_satellites pcp_complex_plan pcp_missions pcp_requests pcp_route_processing pcp_satellite_catalog pcp_slots pcp_stations pcp_tgu pcp_tle
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# PostgreSQL connection settings. Override via env if needed:
|
||||
# PGHOST=... PGPORT=... PGUSER=... PGPASSWORD=... ./create_pcp_databases.sh
|
||||
PGHOST="${PGHOST:-localhost}"
|
||||
PGPORT="${PGPORT:-5432}"
|
||||
PGUSER="${PGUSER:-postgres}"
|
||||
PGPASSWORD="${PGPASSWORD:-password}"
|
||||
export PGPASSWORD
|
||||
|
||||
DATABASES=(
|
||||
pcp_ballistics
|
||||
pcp_complex_plan
|
||||
pcp_missions
|
||||
pcp_requests
|
||||
pcp_satellite_catalog
|
||||
pcp_satellites
|
||||
pcp_slots
|
||||
pcp_stations
|
||||
pcp_tgu
|
||||
pcp_tle
|
||||
)
|
||||
|
||||
# Services whose migrations use PostGIS geometry/extension.
|
||||
POSTGIS_DATABASES=(
|
||||
pcp_ballistics
|
||||
pcp_missions
|
||||
pcp_requests
|
||||
pcp_slots
|
||||
pcp_stations
|
||||
)
|
||||
|
||||
psql_admin() {
|
||||
psql -v ON_ERROR_STOP=1 \
|
||||
--host="$PGHOST" \
|
||||
--port="$PGPORT" \
|
||||
--username="$PGUSER" \
|
||||
--dbname=postgres \
|
||||
"$@"
|
||||
}
|
||||
|
||||
psql_db() {
|
||||
local db="$1"
|
||||
shift
|
||||
psql -v ON_ERROR_STOP=1 \
|
||||
--host="$PGHOST" \
|
||||
--port="$PGPORT" \
|
||||
--username="$PGUSER" \
|
||||
--dbname="$db" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
create_database_if_missing() {
|
||||
local db="$1"
|
||||
local exists
|
||||
exists="$(psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname = '$db'")"
|
||||
if [[ "$exists" == "1" ]]; then
|
||||
echo "Database already exists: $db"
|
||||
else
|
||||
echo "Creating database: $db"
|
||||
psql_admin -c "CREATE DATABASE \"$db\" OWNER \"$PGUSER\" ENCODING 'UTF8';"
|
||||
fi
|
||||
}
|
||||
|
||||
enable_postgis_if_possible() {
|
||||
local db="$1"
|
||||
echo "Enabling PostGIS in database: $db"
|
||||
psql_db "$db" -c "CREATE EXTENSION IF NOT EXISTS postgis;"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "Target PostgreSQL: ${PGHOST}:${PGPORT}, user=${PGUSER}"
|
||||
|
||||
for db in "${DATABASES[@]}"; do
|
||||
create_database_if_missing "$db"
|
||||
done
|
||||
|
||||
for db in "${POSTGIS_DATABASES[@]}"; do
|
||||
enable_postgis_if_possible "$db"
|
||||
done
|
||||
|
||||
echo "Done. Databases are ready. Services will create their schemas via Flyway on startup."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,52 @@
|
||||
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
|
||||
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_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
|
||||
PCP_STATIONS_PORT=7009
|
||||
PCP_MISSION_PORT=7010
|
||||
PCP_TGU_PORT=7011
|
||||
PCP_COVERAGE_SCHEME_PORT=7012
|
||||
PCP_SATELLITE_CATALOG_PORT=7013
|
||||
PCP_DYNAMIC_PLAN_PORT=7014
|
||||
|
||||
CONFIG_REPO_DIR=./config-repo
|
||||
@@ -0,0 +1,317 @@
|
||||
# PCP local Docker bundle
|
||||
|
||||
Локальный bundle предназначен для запуска PCP на машине с Docker без GitLab Config Repo, Kubernetes и Helm.
|
||||
|
||||
В bundle входят backend-сервисы PCP, кроме `tle-monitoring-service`; отдельный frontend
|
||||
`pcp-tgu-ops-ui` также не включается.
|
||||
|
||||
## Состав
|
||||
|
||||
- `docker-compose.yml` - локальная схема запуска.
|
||||
- `.env.example` - пример параметров запуска.
|
||||
- `.env` - создаётся сборочным скриптом в итоговом архиве.
|
||||
- `config-repo/` - локальный Spring Cloud Config repository.
|
||||
- `images/` - Docker images, сохранённые через `docker save`.
|
||||
- `scripts/import-images.sh` - импорт Docker images и запуск базовых сервисов.
|
||||
- `scripts/init-db.sh` - создание баз данных в одном Postgres контейнере.
|
||||
- `scripts/start.sh` - запуск инфраструктуры, config-server и приложений.
|
||||
- `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/<service>:<PCP_IMAGE_TAG>`;
|
||||
- запускает `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:<PCP_IMAGE_TAG>`;
|
||||
- `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
|
||||
```
|
||||
|
||||
## Сборка архива
|
||||
|
||||
Из корня репозитория:
|
||||
|
||||
```bash
|
||||
./deploy/local-bundle/build-local-bundle.sh
|
||||
```
|
||||
|
||||
Можно задать версию:
|
||||
|
||||
```bash
|
||||
LOCAL_BUNDLE_VERSION=1.0.0-local ./deploy/local-bundle/build-local-bundle.sh
|
||||
```
|
||||
|
||||
Результат будет создан в:
|
||||
|
||||
```bash
|
||||
deploy/local-bundle/build/pcp-local-bundle-<version>.tar.gz
|
||||
```
|
||||
|
||||
Скрипт сборки:
|
||||
|
||||
- собирает `bootJar` для разворачиваемых сервисов;
|
||||
- запускает Gradle в offline mode по умолчанию;
|
||||
- собирает Docker images сервисов;
|
||||
- сохраняет PCP service images в один `images/pcp-services.tar`;
|
||||
- по умолчанию не сохраняет публичные infra images, чтобы не раздувать архив;
|
||||
- копирует `docker-compose.yml`, `config-repo`, scripts и документацию;
|
||||
- формирует итоговый `tar.gz`.
|
||||
|
||||
Публичные images `postgis/postgis` и `apache/kafka` будут скачаны на целевой машине командой `docker compose up`, если их нет локально.
|
||||
|
||||
Если нужен полностью offline bundle, включите сохранение infra images:
|
||||
|
||||
```bash
|
||||
INCLUDE_INFRA_IMAGES=true ./deploy/local-bundle/build-local-bundle.sh
|
||||
```
|
||||
|
||||
По умолчанию сборка `bootJar` выполняется последовательно, чтобы не упираться в heap Kotlin compiler daemon на локальной машине. Параметры можно переопределить:
|
||||
|
||||
```bash
|
||||
GRADLE_MAX_WORKERS=1 \
|
||||
GRADLE_JVMARGS="-Xmx2g -Dfile.encoding=UTF-8" \
|
||||
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
|
||||
```
|
||||
|
||||
## Запуск на локальной машине
|
||||
|
||||
Распаковать архив:
|
||||
|
||||
```bash
|
||||
tar -xzf pcp-local-bundle-<version>.tar.gz
|
||||
cd pcp-local-bundle-<version>
|
||||
```
|
||||
|
||||
Проверить `.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
|
||||
./scripts/import-images.sh
|
||||
```
|
||||
|
||||
Создать базы данных:
|
||||
|
||||
```bash
|
||||
./scripts/init-db.sh
|
||||
```
|
||||
|
||||
Запустить приложение:
|
||||
|
||||
```bash
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
`start.sh` также запускает `init-db.sh`, если не задано `SKIP_DB_INIT=true`.
|
||||
|
||||
## Остановка и статус
|
||||
|
||||
```bash
|
||||
./scripts/status.sh
|
||||
./scripts/stop.sh
|
||||
```
|
||||
|
||||
Чтобы удалить volumes с данными Postgres/Kafka:
|
||||
|
||||
```bash
|
||||
./scripts/stop.sh -v
|
||||
```
|
||||
|
||||
## Открытые порты
|
||||
|
||||
- Config Server: `http://localhost:38888`
|
||||
- Spring Boot Admin: `http://localhost:7000`
|
||||
- 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`
|
||||
- Stations: `http://localhost:7009`
|
||||
- Mission Planning: `http://localhost:7010`
|
||||
- TGU: `http://localhost:7011`
|
||||
- Coverage Scheme: `http://localhost:7012`
|
||||
- Satellite Catalog: `http://localhost:7013`
|
||||
- Dynamic Plan: `http://localhost:7014`
|
||||
- Postgres: `localhost:5432`
|
||||
- Kafka external listener: `localhost:29092`
|
||||
|
||||
## Проверка готовности
|
||||
|
||||
Config Server:
|
||||
|
||||
```bash
|
||||
curl http://localhost:38888/actuator/health
|
||||
curl http://localhost:38888/pcp-ui-service/local,docker-local
|
||||
```
|
||||
|
||||
UI:
|
||||
|
||||
```bash
|
||||
curl http://localhost:7008/actuator/health
|
||||
```
|
||||
|
||||
Статус контейнеров:
|
||||
|
||||
```bash
|
||||
./scripts/status.sh
|
||||
```
|
||||
|
||||
## Config Server
|
||||
|
||||
В локальном сценарии `spring-cloud-config-server` запускается с:
|
||||
|
||||
```text
|
||||
SPRING_PROFILES_ACTIVE=local
|
||||
CONFIG_NATIVE_SEARCH_LOCATIONS=file:/config-repo
|
||||
```
|
||||
|
||||
`CONFIG_GIT_URI`, GitLab token и `git clone` не используются.
|
||||
|
||||
Сервисы запускаются с профилями:
|
||||
|
||||
```text
|
||||
SPRING_PROFILES_ACTIVE=local,docker-local
|
||||
SPRING_CLOUD_CONFIG_PROFILE=local,docker-local
|
||||
SPRING_CLOUD_CONFIG_LABEL=
|
||||
```
|
||||
|
||||
Профиль `local` сохраняет существующие service-specific настройки, а `docker-local` переопределяет IP-адреса на Docker Compose DNS names:
|
||||
|
||||
- `postgres`
|
||||
- `kafka`
|
||||
- `pcp-ballistics-service`
|
||||
- `slots-service`
|
||||
- остальные service names из `docker-compose.yml`
|
||||
|
||||
## Базы данных
|
||||
|
||||
`scripts/init-db.sh` создаёт только пустые базы данных:
|
||||
|
||||
- `pcp_ballistics`
|
||||
- `pcp_requests`
|
||||
- `pcp_route_processing`
|
||||
- `pcp_missions`
|
||||
- `pcp_slots`
|
||||
- `pcp_complex_plan`
|
||||
- `pcp_satellite_catalog`
|
||||
- `pcp_stations`
|
||||
- `pcp_tgu`
|
||||
- `pcp_satellites`
|
||||
|
||||
Таблицы, схемы и seed data вручную не создаются. Схемы создаются существующими Flyway migrations внутри сервисов при старте приложений.
|
||||
|
||||
`pcp_satellites` добавлена дополнительно, потому что текущий `config-repo/pcp-complex-mission-service.yaml` использует эту БД для `pcp-complex-mission-service`.
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/../.." && pwd)"
|
||||
|
||||
version="${LOCAL_BUNDLE_VERSION:-${1:-}}"
|
||||
if [[ -z "$version" ]]; then
|
||||
if git -C "$repo_root" rev-parse --short HEAD >/dev/null 2>&1; then
|
||||
version="$(git -C "$repo_root" rev-parse --short HEAD)"
|
||||
else
|
||||
version="$(date +%Y%m%d%H%M%S)"
|
||||
fi
|
||||
fi
|
||||
|
||||
bundle_name="pcp-local-bundle-$version"
|
||||
build_dir="$script_dir/build"
|
||||
dist_dir="$build_dir/$bundle_name"
|
||||
docker_contexts_dir="$build_dir/docker-contexts"
|
||||
archive_path="$build_dir/$bundle_name.tar.gz"
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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=(
|
||||
"--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
|
||||
|
||||
infra_images=(
|
||||
"${POSTGRES_IMAGE:-postgis/postgis:16-3.4}"
|
||||
"${KAFKA_IMAGE:-apache/kafka:3.7.2}"
|
||||
)
|
||||
|
||||
sanitize_image_name() {
|
||||
printf '%s' "$1" | sed 's#[/:@]#-#g'
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
copy_config_repo() {
|
||||
local target_dir="$1"
|
||||
local source_file
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
(
|
||||
cd "$repo_root/config-repo"
|
||||
find . -type f \
|
||||
! -path './build/*' \
|
||||
! -path './.gradle/*' \
|
||||
! -name '*.log' \
|
||||
! -name '*.tmp' \
|
||||
-print0
|
||||
) | while IFS= read -r -d '' source_file; do
|
||||
mkdir -p "$target_dir/$(dirname "$source_file")"
|
||||
cp "$repo_root/config-repo/$source_file" "$target_dir/$source_file"
|
||||
done
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
require_command docker
|
||||
require_command tar
|
||||
|
||||
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" --project-dir "$repo_root" "${gradle_common_args[@]}" ":services:$service_name:bootJar"
|
||||
done
|
||||
|
||||
rm -rf "$dist_dir" "$docker_contexts_dir" "$archive_path"
|
||||
mkdir -p "$dist_dir/images" "$docker_contexts_dir"
|
||||
|
||||
echo "Building service Docker images..."
|
||||
service_images=()
|
||||
for service_name in "${service_names[@]}"; do
|
||||
image_name="pcp/$service_name:$version"
|
||||
context_dir="$docker_contexts_dir/$service_name"
|
||||
|
||||
copy_single_boot_jar "$service_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
|
||||
|
||||
echo "Saving service Docker images into one archive..."
|
||||
docker save --output "$dist_dir/images/pcp-services.tar" "${service_images[@]}"
|
||||
|
||||
if [[ "$include_infra_images" == "true" ]]; then
|
||||
echo "Pulling and saving infrastructure images..."
|
||||
for image_name in "${infra_images[@]}"; do
|
||||
image_file="$(sanitize_image_name "$image_name").tar"
|
||||
docker pull "$image_name"
|
||||
docker save --output "$dist_dir/images/$image_file" "$image_name"
|
||||
done
|
||||
else
|
||||
echo "Skipping public infrastructure images. They will be pulled by docker compose on the target machine."
|
||||
printf '%s\n' "${infra_images[@]}" > "$dist_dir/images/public-images.txt"
|
||||
fi
|
||||
|
||||
echo "Preparing bundle files..."
|
||||
cp "$script_dir/docker-compose.yml" "$dist_dir/docker-compose.yml"
|
||||
cp "$script_dir/.env.example" "$dist_dir/.env.example"
|
||||
cp "$script_dir/.env.example" "$dist_dir/.env"
|
||||
sed -i "s/^PCP_IMAGE_TAG=.*/PCP_IMAGE_TAG=$version/" "$dist_dir/.env"
|
||||
cp "$script_dir/README-local.md" "$dist_dir/README-local.md"
|
||||
cp -R "$script_dir/scripts" "$dist_dir/scripts"
|
||||
copy_config_repo "$dist_dir/config-repo"
|
||||
|
||||
find "$dist_dir/scripts" -type f -name '*.sh' -exec chmod +x {} +
|
||||
|
||||
echo "Creating archive..."
|
||||
tar -C "$build_dir" -czf "$archive_path" "$bundle_name"
|
||||
|
||||
echo "Local bundle created: $archive_path"
|
||||
echo "Bundle version: $version"
|
||||
echo "To use: tar -xzf $archive_path && cd $bundle_name"
|
||||
@@ -0,0 +1,205 @@
|
||||
name: ${COMPOSE_PROJECT_NAME:-pcp-local}
|
||||
|
||||
x-pcp-service-env: &pcp-service-env
|
||||
CONFIG_SERVER_URI: http://spring-cloud-config-server:8888
|
||||
CONFIG_SERVER_FAIL_FAST: "true"
|
||||
SPRING_PROFILES_ACTIVE: local,docker-local
|
||||
SPRING_CLOUD_CONFIG_PROFILE: local,docker-local
|
||||
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
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: ${POSTGRES_IMAGE:-postgis/postgis:16-3.4}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
kafka:
|
||||
image: ${KAFKA_IMAGE:-apache/kafka:3.7.2}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
KAFKA_NODE_ID: "1"
|
||||
KAFKA_PROCESS_ROLES: broker,controller
|
||||
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
|
||||
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:29092
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:${KAFKA_PORT:-29092}
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT
|
||||
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1"
|
||||
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: "1"
|
||||
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: "1"
|
||||
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: "0"
|
||||
KAFKA_LOG_DIRS: /var/lib/kafka/data
|
||||
ports:
|
||||
- "${KAFKA_PORT:-29092}:29092"
|
||||
volumes:
|
||||
- kafka-data:/var/lib/kafka/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
spring-cloud-config-server:
|
||||
image: pcp/spring-cloud-config-server:${PCP_IMAGE_TAG:-local}
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: local
|
||||
CONFIG_NATIVE_SEARCH_LOCATIONS: file:/config-repo
|
||||
SERVER_PORT: 8888
|
||||
ports:
|
||||
- "${CONFIG_SERVER_PORT:-38888}:8888"
|
||||
volumes:
|
||||
- ${CONFIG_REPO_DIR:-./config-repo}:/config-repo:ro
|
||||
|
||||
pcp-srpring-boot-admin-server:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-srpring-boot-admin-server:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_ADMIN_PORT:-7000}:7000"
|
||||
|
||||
pcp-complex-mission-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-complex-mission-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_COMPLEX_MISSION_PORT:-7002}:7002"
|
||||
|
||||
pcp-ballistics-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-ballistics-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_BALLISTICS_PORT:-7003}:7003"
|
||||
|
||||
pcp-request-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-request-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
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}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_SLOTS_PORT:-7006}:7006"
|
||||
|
||||
pcp-ui-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-ui-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_UI_PORT:-7008}:7008"
|
||||
|
||||
pcp-stations-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-stations-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_STATIONS_PORT:-7009}:7009"
|
||||
|
||||
pcp-mission-planing-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-mission-planing-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_MISSION_PORT:-7010}:7010"
|
||||
|
||||
pcp-tgu-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-tgu-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_TGU_PORT:-7011}:7011"
|
||||
|
||||
pcp-coverage-scheme-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-coverage-scheme-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_COVERAGE_SCHEME_PORT:-7012}:7012"
|
||||
|
||||
pcp-satellite-catalog-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-satellite-catalog-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_SATELLITE_CATALOG_PORT:-7013}:7013"
|
||||
|
||||
pcp-dynamic-plan-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-dynamic-plan-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_DYNAMIC_PLAN_PORT:-7014}:7014"
|
||||
|
||||
pcp-gateway-service:
|
||||
<<: *pcp-service-common
|
||||
image: pcp/pcp-gateway-service:${PCP_IMAGE_TAG:-local}
|
||||
environment:
|
||||
<<: *pcp-service-env
|
||||
ports:
|
||||
- "${PCP_GATEWAY_PORT:-7080}:7080"
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
kafka-data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ${PCP_DOCKER_NETWORK:-pcp-local_default}
|
||||
external: ${PCP_DOCKER_NETWORK_EXTERNAL:-false}
|
||||
Executable
+199
@@ -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
|
||||
Executable
+242
@@ -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
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
bundle_dir="$(cd "$script_dir/.." && pwd)"
|
||||
images_dir="$bundle_dir/images"
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
echo "docker compose or docker-compose is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ ! -f "$bundle_dir/.env" && -f "$bundle_dir/.env.example" ]]; then
|
||||
cp "$bundle_dir/.env.example" "$bundle_dir/.env"
|
||||
echo "Created .env from .env.example"
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
image_archives=("$images_dir"/*.tar)
|
||||
if (( ${#image_archives[@]} == 0 )); then
|
||||
echo "No Docker image archives found in $images_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for image_archive in "${image_archives[@]}"; do
|
||||
echo "Loading image: $(basename "$image_archive")"
|
||||
docker load -i "$image_archive"
|
||||
done
|
||||
|
||||
if [[ -f "$images_dir/public-images.txt" ]]; then
|
||||
echo "Public images are not bundled and will be pulled by docker compose if missing:"
|
||||
sed 's/^/ - /' "$images_dir/public-images.txt"
|
||||
fi
|
||||
|
||||
cd "$bundle_dir"
|
||||
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."
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
bundle_dir="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
echo "docker compose or docker-compose is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ ! -f "$bundle_dir/.env" && -f "$bundle_dir/.env.example" ]]; then
|
||||
cp "$bundle_dir/.env.example" "$bundle_dir/.env"
|
||||
echo "Created .env from .env.example"
|
||||
fi
|
||||
}
|
||||
|
||||
load_env() {
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$bundle_dir/.env"
|
||||
set +a
|
||||
}
|
||||
|
||||
wait_for_postgres() {
|
||||
local attempt
|
||||
for attempt in {1..60}; do
|
||||
if compose exec -T postgres pg_isready -U "${POSTGRES_USER:-postgres}" -d postgres >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Postgres is not ready after 120 seconds." >&2
|
||||
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
|
||||
|
||||
exists="$(
|
||||
compose exec -T postgres 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"
|
||||
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_satellites
|
||||
)
|
||||
|
||||
ensure_env_file
|
||||
load_env
|
||||
|
||||
cd "$bundle_dir"
|
||||
if [[ "${USE_EXTERNAL_INFRA:-false}" == "true" ]]; then
|
||||
wait_for_external_postgres
|
||||
|
||||
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."
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
bundle_dir="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
echo "docker compose or docker-compose is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ ! -f "$bundle_dir/.env" && -f "$bundle_dir/.env.example" ]]; then
|
||||
cp "$bundle_dir/.env.example" "$bundle_dir/.env"
|
||||
echo "Created .env from .env.example"
|
||||
fi
|
||||
}
|
||||
|
||||
load_env() {
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$bundle_dir/.env"
|
||||
set +a
|
||||
}
|
||||
|
||||
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 attempt
|
||||
|
||||
for attempt in {1..90}; do
|
||||
if http_get "$url"; then
|
||||
echo "$name is ready: $url"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "$name is not ready after 180 seconds: $url" >&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
|
||||
}
|
||||
|
||||
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
|
||||
pcp-coverage-scheme-service
|
||||
pcp-dynamic-plan-service
|
||||
pcp-stations-service
|
||||
pcp-ui-service
|
||||
pcp-tgu-service
|
||||
)
|
||||
|
||||
ensure_env_file
|
||||
load_env
|
||||
|
||||
cd "$bundle_dir"
|
||||
|
||||
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"
|
||||
fi
|
||||
|
||||
echo "Starting spring-cloud-config-server..."
|
||||
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..."
|
||||
for service_name in "${app_services[@]}"; do
|
||||
echo "Starting $service_name..."
|
||||
compose up -d "$service_name"
|
||||
done
|
||||
|
||||
echo "Application startup requested."
|
||||
compose ps
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
bundle_dir="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
echo "docker compose or docker-compose is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cd "$bundle_dir"
|
||||
compose ps
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
bundle_dir="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
else
|
||||
echo "docker compose or docker-compose is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cd "$bundle_dir"
|
||||
compose down "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
+5
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
+5
@@ -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 "$@"
|
||||
+5
@@ -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 "$@"
|
||||
+5
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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:<repo>/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
|
||||
```
|
||||
Executable
+538
@@ -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
|
||||
@@ -0,0 +1,266 @@
|
||||
# Observatio Terrae (pcp) — карта сервисов, взаимодействие и контракты
|
||||
|
||||
> Базовый справочный документ для доработки системы планирования ДЗЗ.
|
||||
> Составлен из исходного кода (контроллеры, Kafka listeners/publishers, HTTP-клиенты) и `config-repo`.
|
||||
> Помечено явно, что **проверено по коду**, а что **требует уточнения** (`⚠`).
|
||||
|
||||
Стек: Kotlin + Spring Boot, Spring Cloud Config, Spring Boot Admin, Camunda 8 / Zeebe (оркестрация), Kafka (события), PostgreSQL (БД на сервис, Flyway), Helm + GitLab CI.
|
||||
|
||||
---
|
||||
|
||||
## 1. Карта сервисов — кто за что отвечает
|
||||
|
||||
### Доменные сервисы
|
||||
|
||||
| Сервис | Зона ответственности | REST base | Хранит | Kafka (in / out) | Зрелость* |
|
||||
|---|---|---|---|---|---|
|
||||
| **pcp-request-service** | Приём и жизненный цикл заявок на съёмку; сетка Земли (earth-grid), геопривязка ячеек, сопоставление маршрутов с заявками | `/v1/requests`, `/v1/cells`, `/v1/grid` | заявки, ячейки сетки, outbox | in: `pcp.route.georeference.v1` · out: `pcp.request.status.v1` (outbox → geoportal) | 🟢 высокая |
|
||||
| **pcp-route-processing-service** | Приём «паспортов маршрутов», нормализация/геопривязка, ретрансляция дальше; собственный outbox + DLQ | `/api/route-processing` (test) | outbox маршрутов | in: `pcp.request.survey-georeference.v1` · out: `pcp.route.georeference.v1`, `pcp.route.in.v1`, DLQ | 🟢 высокая |
|
||||
| **pcp-satellite-catalog-service** | Каталог КА (источник истины): спутники, группы, observation/slot-профили | `/api/satellites`, `/api/satellite-groups` | каталог КА, профили | out: `pcp.satellites` (`SatelliteDeletedEvent`) | 🟡 средняя (мало тестов) |
|
||||
| **pcp-ballistics-service** | Баллистика: орбиты, ЗРВ/RVA, узлы, трассы, time-extract по TLE | `/api/satellites`, `/api/obj-view`, `/api/tle`, `/api/rva` | орбитальные данные | in: `pcp.tle` (IC/ICRV/TLE фильтры), `pcp.satellites` | 🟡 средняя |
|
||||
| **tle-monitoring-service** | Подтяжка TLE (CelesTrak), мониторинг каталога | — | TLE-история | in: `pcp.satellites` · out: `pcp.tle` ⚠ | 🟡 средняя |
|
||||
| **pcp-coverage-scheme-service** | Расчёт схемы покрытия (coverage scheme) | `/api/coverage-schemes` | результаты схем | in: `pcp.satellites` | 🟡 средняя |
|
||||
| **slots-service** | Слоты сброса/ресурсы: расчёт, бронирование, статусы слотов | (см. контроллеры) | слоты, брони | in/out: `pcp.slots.status.v1` (`BookedSlotsStatusChangedEvent`); in: `pcp.satellites` | 🟢 высокая (лучшее покрытие тестами) |
|
||||
| **pcp-complex-mission-service** | Комплексный план по тепловой карте методом слотов; снапшоты планов | `/api/satellites`, `/api/com-plan` | комплексные планы, снапшоты | in: `pcp.satellites` | 🟢 высокая |
|
||||
| **pcp-mission-planing-service** | План по конкретному КА: съёмка / сброс / стирание; **хост Zeebe-воркеров** оркестрации | `/api/missions` | миссии, режимы | in: `pcp.slots.status.v1`, `pcp.route.in.v1`, `pcp.satellites` | 🔴 низкая (есть заглушки в `FlowService`) |
|
||||
| **pcp-dynamic-plan-service** | Динамический/текущий план (greedy-математика, расчёт по интервалам) | `/v1/main/calcPlan` | прогоны (runs), результаты | in: `pcp.satellites` | 🟡 средняя |
|
||||
| **pcp-tgu-service** | Операторская оркестрация (ТГУ/Mission Ops): окна видимости, выдача планов, решения по планам; **деплой и запуск BPMN** | `/api/plans`, `/api/platforms`, `/test/tgu` | планы КА, dedup-таблицы | in: `pcp.tgu.satellite-plan-decision.v1` · out: `pcp.tgu.visibility-windows-changed.v1` | 🟢 высокая |
|
||||
| **pcp-stations-service** | Каталог наземных станций приёма | `/api/stations` | станции | — | 🟡 средняя |
|
||||
| **pcp-ui-service** | BFF/агрегатор для веб-UI и 3D-глобуса (CZML/Cesium); проксирует во все сервисы | `/requests`, `/map`, `/api/*` | — (stateless) | — | 🟡 средняя |
|
||||
|
||||
\* Зрелость = оценка по соотношению тестов, наличию idempotency/outbox, чистоте слоёв и отсутствию заглушек (см. предыдущий разбор).
|
||||
|
||||
### Инфраструктурные модули
|
||||
|
||||
| Модуль | Роль |
|
||||
|---|---|
|
||||
| **spring-cloud-config-server** | Раздаёт конфигурацию из `config-repo` всем сервисам |
|
||||
| **pcp-srpring-boot-admin-server** | Spring Boot Admin — мониторинг/health всех сервисов (опечатка в имени: `srpring`) |
|
||||
| **pcp-tgu-ops-ui** | Отдельная SPA Mission Ops/TGU; карта возможностей и источников данных: [`PCP_TGU_OPS_UI_CAPABILITY_MAP.md`](PCP_TGU_OPS_UI_CAPABILITY_MAP.md) |
|
||||
| **libs/pcp-types-lib** | Общие DTO и конверт `KafkaMessage<T>`, перечисление `PcpKafkaEvent` |
|
||||
| **libs/ballistics-lib** | Общие баллистические расчёты |
|
||||
|
||||
---
|
||||
|
||||
## 2. Диаграммы взаимодействия
|
||||
|
||||
### 2.1. Синхронные зависимости (REST) — кто кого вызывает
|
||||
|
||||
Рёбра восстановлены из ключей `*-service*` base-url в `config-repo` и имён HTTP-клиентов в коде.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
UI[pcp-ui-service<br/>BFF / агрегатор]
|
||||
REQ[pcp-request-service<br/>заявки + earth-grid]
|
||||
CAT[pcp-satellite-catalog-service<br/>каталог КА]
|
||||
BAL[pcp-ballistics-service]
|
||||
CM[pcp-complex-mission-service]
|
||||
MP[pcp-mission-planing-service]
|
||||
SL[slots-service]
|
||||
CS[pcp-coverage-scheme-service]
|
||||
DP[pcp-dynamic-plan-service]
|
||||
TGU[pcp-tgu-service]
|
||||
ST[pcp-stations-service]
|
||||
TLE[tle-monitoring-service]
|
||||
|
||||
UI --> REQ & CM & DP & TGU & CAT & BAL & ST & SL & MP & CS
|
||||
|
||||
CM --> BAL & REQ & CAT & ST & SL & CS
|
||||
MP --> BAL & CM & CAT & ST
|
||||
SL --> REQ & CAT
|
||||
DP --> REQ & BAL & CAT
|
||||
CS --> BAL & CM & CAT
|
||||
TLE --> CM & CAT
|
||||
TGU --> BAL
|
||||
|
||||
TLE -.внешний.-> CTRAK[(CelesTrak)]
|
||||
TGU -.внешний.-> ORD[(ordinis.nstart.cloud<br/>классификатор платформ + external points)]
|
||||
```
|
||||
|
||||
Наблюдения по графу:
|
||||
- `pcp-satellite-catalog-service` и `pcp-ballistics-service` — самые востребованные (почти все зовут их). Кандидаты на повышенные требования к SLA/надёжности.
|
||||
- `pcp-ui-service` — точка агрегации (BFF), завязан на всё.
|
||||
- ⚠ Есть взаимный вызов `complex-mission ⇄ coverage-scheme` (CM зовёт CS на расчёт, CS в конфиге знает про CM) — проверить, нет ли циклической зависимости в рантайме.
|
||||
|
||||
### 2.2. Событийный слой (Kafka) — потоки данных
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
EXT[Внешний источник / UI ⚠] -->|pcp.request.survey-georeference.v1| RP[route-processing]
|
||||
RP -->|pcp.route.georeference.v1| REQ[request-service]
|
||||
RP -->|pcp.route.in.v1| MP[mission-planing]
|
||||
REQ -->|pcp.request.status.v1 · COMPLETED/EXPIRED| GEO[geoportal · внешний]
|
||||
|
||||
CAT[satellite-catalog] -->|pcp.satellites / SatelliteDeletedEvent| BAL[ballistics]
|
||||
CAT --> TLEM[tle-monitoring]
|
||||
CAT --> SL[slots]
|
||||
CAT --> MP
|
||||
CAT --> CM[complex-mission]
|
||||
CAT --> DP[dynamic-plan]
|
||||
|
||||
TLEM -->|pcp.tle| BAL
|
||||
|
||||
SL -->|pcp.slots.status.v1 / BookedSlotsStatusChanged| MP
|
||||
SL -->|pcp.slots.status.v1| SL
|
||||
|
||||
MissionOps[Mission Ops / КА ⚠] -->|pcp.tgu.satellite-plan-decision.v1| TGU[tgu-service]
|
||||
TGU -->|pcp.tgu.visibility-windows-changed.v1| TGU
|
||||
```
|
||||
|
||||
### 2.3. Сквозной сценарий планирования (sequence)
|
||||
|
||||
Упрощённый happy-path от заявки до выдачи задания на КА.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as UI
|
||||
participant RQ as request-service
|
||||
participant RP as route-processing
|
||||
participant CM as complex-mission
|
||||
participant SL as slots-service
|
||||
participant TGU as tgu-service (Camunda)
|
||||
participant MP as mission-planing (Zeebe workers)
|
||||
|
||||
U->>RQ: POST заявка на съёмку
|
||||
RP-->>RQ: pcp.route.georeference.v1 (геопривязанные маршруты)
|
||||
RQ->>RQ: сопоставление маршрут↔заявка, RequestCompleted (outbox)
|
||||
U->>CM: POST /api/com-plan/process (комплексный план)
|
||||
CM->>SL: расчёт/бронирование слотов
|
||||
CM->>CM: снапшот комплексного плана
|
||||
U->>TGU: POST /test/tgu/plans/{id}/issue
|
||||
TGU->>TGU: start BPMN createSatelliteMission
|
||||
Note over TGU,MP: Zeebe job dispatch по jobType
|
||||
TGU-->>MP: calculateSatelliteSurveyMissions
|
||||
MP->>MP: план съёмки
|
||||
TGU-->>MP: calculateSatelliteDropMissions
|
||||
MP->>MP: план сброса
|
||||
TGU-->>MP: sendSatellitePlan
|
||||
MP-->>TGU: Message_SatellitePlanAccepted ⚠ (сейчас имитация)
|
||||
MissionOps-->>TGU: pcp.tgu.satellite-plan-decision.v1 (ACCEPTED/REJECTED)
|
||||
TGU->>TGU: updateAcceptedPlanSlots BPMN → updateSlotsStatus
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Контракты между сервисами
|
||||
|
||||
### 3.1. Оркестрация Camunda 8 / Zeebe
|
||||
|
||||
**Деплой BPMN:** `pcp-tgu-service` через `@Deployment` при старте грузит:
|
||||
`classpath:BPMN/createSatelliteMission.bpmn`, `classpath:BPMN/updateAcceptedPlanSlots.bpmn`.
|
||||
|
||||
**Zeebe job workers** (реализованы в `pcp-mission-planing-service/FlowService`):
|
||||
|
||||
| jobType | Назначение | Вход (process variables) | Выход | Примечание |
|
||||
|---|---|---|---|---|
|
||||
| `calculateSatelliteSurveyMissions` | План съёмки | `satellitePlanId`, `spacecraftId`, `planStartTime`, `planEndTime` | `modes: Int` | `autoComplete=false` |
|
||||
| `calculateSatelliteDropMissions` | План сброса | `satellitePlanId` | `modes: Int` | — |
|
||||
| `sendSatellitePlan` | Отправка плана на КА | `satellitePlanId` | — | ⚠ сейчас `Thread.sleep` + имитация подтверждения |
|
||||
| `updateSlotsStatus` | Подтверждение, обновление слотов | `satellitePlanId` | `slots: List` | используется в `updateAcceptedPlanSlots` |
|
||||
|
||||
**Camunda message:** `Message_SatellitePlanAccepted`, `correlationKey = satellitePlanId`.
|
||||
|
||||
> ⚠ `schemes/pcp_v1.bpmn` — большой концептуальный процесс (шлюзы, параллельные ветки, подпроцессы), **не задеплоен** и без jobType. Реально работает только минимальный `createSatelliteMission.bpmn` (start → 3 service task → end, без обработки ошибок).
|
||||
|
||||
### 3.2. Kafka-топики (реестр)
|
||||
|
||||
Конверт по умолчанию — `KafkaMessage<T>` из `pcp-types-lib`: поля `type`, `data`, `traceId`, `correlationId`, `id`, `source`, `time`. Тип события — enum `PcpKafkaEvent`.
|
||||
|
||||
| Топик | Producer | Consumer(s) | Тип события / payload | Ключ |
|
||||
|---|---|---|---|---|
|
||||
| `pcp.satellites` | satellite-catalog | ballistics, tle-monitoring, slots, mission-planing, complex-mission, dynamic-plan | `SatelliteDeletedEvent` (`satelliteId`, `noradId`) | `satelliteId` |
|
||||
| `pcp.tle` | tle-monitoring / catalog ⚠ | ballistics (фильтры IC/ICRV/TLE) | TLE / `ICPlacedEvent` / `ICRVPlacedEvent` / `ICUpdatedEvent` | ⚠ |
|
||||
| `pcp.request.survey-georeference.v1` | внешний / UI ⚠ | route-processing | RoutePassport (сырой) | ⚠ |
|
||||
| `pcp.request.survey-georeference-dlq.v1` | route-processing | (DLQ) | — | — |
|
||||
| `pcp.route.georeference.v1` | route-processing | request-service | `RoutePassportDto` (геопривязанный) | ⚠ |
|
||||
| `pcp.route.in.v1` | route-processing | mission-planing (`ModeStatusChangedEvent`) | processed route / mode-status | ⚠ |
|
||||
| `pcp.request.in.v1` | request-service / UI ⚠ | ⚠ требует уточнения | `RequestPlacedEvent` | ⚠ |
|
||||
| `pcp.request.status.v1` | request-service (outbox) | **geoportal** (внешний) | `REQUEST_TERMINAL_STATUS` (COMPLETED/EXPIRED) | `aggregateId` (UUID заявки) |
|
||||
| `pcp.slots.status.v1` | slots-service | mission-planing, slots-service (self) | `BookedSlotsStatusChangedEvent` | ⚠ |
|
||||
| `pcp.tgu.satellite-plan-decision.v1` | Mission Ops / КА ⚠ | tgu-service | `PlanDecisionMessage` (ACCEPTED/REJECTED, `attemptId`) | ⚠ |
|
||||
| `pcp.tgu.visibility-windows-changed.v1` | tgu-service | tgu-service | `VisibilityWindowsChangedMessage` | ⚠ |
|
||||
|
||||
> ⚠ Реализация консьюмеров неоднородна: одни читают типизированный `KafkaMessage<T>`, другие парсят сырое дерево и ждут поле `data`; используются обе версии Jackson (2 и 3). Это первый кандидат на унификацию контракта.
|
||||
|
||||
### 3.3. REST-эндпоинты (контракты API по сервисам)
|
||||
|
||||
**request-service** (`/v1`)
|
||||
```
|
||||
GET /v1/requests/{id}
|
||||
GET /v1/requests/{requestId}/with-cells
|
||||
DELETE /v1/requests/{id}
|
||||
GET /v1/cells/with-requests
|
||||
GET /v1/grid/settings PUT /v1/grid/settings POST /v1/grid/rebuild
|
||||
```
|
||||
|
||||
**satellite-catalog-service** (`/api/satellites`, `/api/satellite-groups`)
|
||||
```
|
||||
GET/PUT/DELETE /api/satellites/{satellite_id}
|
||||
GET /api/satellites/by-norad/{norad_id}
|
||||
GET/POST/PUT/DELETE /api/satellites/{satellite_id}/observation-profile
|
||||
GET/POST/PUT/DELETE /api/satellites/{satellite_id}/slot-profile
|
||||
POST /api/satellites/batch
|
||||
GET/PUT/DELETE /api/satellite-groups/{group_id}
|
||||
```
|
||||
|
||||
**ballistics-service** (`/api`)
|
||||
```
|
||||
POST /api/satellites/{satellite_id}/extract-time POST /api/satellites/{satellite_id}/clear
|
||||
GET /api/satellites/{norad_id}/orbit | /asc-node | /flight-line | /rva
|
||||
GET /api/satellites/orbit/availability[/{id}]
|
||||
POST /api/obj-view/mpl-square | /mpl-point GET /api/obj-view/cell-coverage/{number}
|
||||
POST /api/tle/parse | /api/tle/rva
|
||||
```
|
||||
|
||||
**complex-mission-service** (`/api`)
|
||||
```
|
||||
POST /api/com-plan/process | /clear GET/DELETE /api/com-plan/runs/{id}
|
||||
GET /api/com-plan/runs | /runs/{id}/modes | /cell-with-mars/{cell_number}
|
||||
GET /api/satellites/{satellite_id}[/mission|/modes|/snapshots|/mission/statistics|/mission-for-paint]
|
||||
GET /api/satellites/snapshots[/{snapshot_id}[/modes]] | /modes POST /api/satellites/prepare
|
||||
```
|
||||
|
||||
**mission-planing-service** (`/api/missions`)
|
||||
```
|
||||
GET /api/missions/{mission_id}[/modes] GET /api/missions/modes/satellite
|
||||
POST /api/missions/{mission_id}/drops/calculate POST /api/missions/{mission_id}/surveys/calculate
|
||||
POST /api/missions/{mission_id}/confirm PUT /api/missions/survey-modes/{mode_id}/status
|
||||
PUT/DELETE /api/missions/{mission_id}
|
||||
```
|
||||
|
||||
**slots-service / coverage-scheme / dynamic-plan / stations / tgu**
|
||||
```
|
||||
coverage-scheme : POST /api/coverage-schemes/calculate
|
||||
dynamic-plan : POST /v1/main/calcPlan ; GET /v1/main/calcPlan/runs|/{runId}[/result|/routes|/intervals]
|
||||
stations : GET /api/stations[/{station_id}] ; POST /api/stations ; DELETE /api/stations/{station_id}
|
||||
tgu : GET /api/plans/{spacecraftId} ; /api/platforms ;
|
||||
POST /test/tgu/plans/{planId}/issue|/decision ; /spacecraft/{id}/refresh-visibility|/rebuild-plans
|
||||
```
|
||||
|
||||
**ui-service** — BFF, агрегирует вызовы: `/requests/*`, `/map`, `/api/requests`, `/api/slots/booking/request`,
|
||||
`/api/com-plan/*`, `/api/tgu-planning/*`, `/api/current-plans/*`, `/api/dynamic-plan/*`, `/api/stations/*`.
|
||||
|
||||
### 3.4. Внешние интеграции
|
||||
|
||||
| Откуда | Куда | Назначение |
|
||||
|---|---|---|
|
||||
| tle-monitoring-service | CelesTrak | загрузка TLE |
|
||||
| tgu-service | `https://ordinis.nstart.cloud/api/v1/spacecraft/records` | классификатор платформ (фильтр `OPERATIONAL`), кэш 15m |
|
||||
| tgu-service | external points API (`pcp.services.ballistics`) | внешние точки (lookahead 3 дня) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Открытые вопросы для уточнения перед доработкой
|
||||
|
||||
Эти пункты помечены `⚠` выше — их стоит зафиксировать до изменений:
|
||||
|
||||
1. Producer топиков `pcp.request.survey-georeference.v1` и `pcp.request.in.v1` (внешняя система или UI?).
|
||||
2. ~~Consumer `pcp.request.completed.v1`~~ — уточнено: потребитель терминальных статусов заявки = внешний **geoportal**, топик переименован в `pcp.request.status.v1` (COMPLETED/EXPIRED, #35).
|
||||
3. Producer `pcp.tgu.satellite-plan-decision.v1` — реальный Mission Ops/КА vs текущая имитация в `FlowService`.
|
||||
4. Точные DTO payload для `pcp.tle`, `pcp.route.in.v1`, `pcp.slots.status.v1` — зафиксировать схемы.
|
||||
5. Возможная циклическая зависимость `complex-mission ⇄ coverage-scheme` в рантайме.
|
||||
6. Расхождение задеплоенного `createSatelliteMission.bpmn` и концептуального `schemes/pcp_v1.bpmn` — какой считать целевым.
|
||||
|
||||
---
|
||||
|
||||
*Документ описывает текущее состояние «как есть». При доработке обновлять разделы 2–3 синхронно с изменениями контроллеров, listener'ов и `config-repo`.*
|
||||
@@ -0,0 +1,224 @@
|
||||
# Карта взаимодействий сервисов PCP
|
||||
|
||||
> Составлено по исходникам (`services/*/src/main`) и конфигам (`config-repo/`).
|
||||
> Три канала интеграции: **Kafka** (события), **HTTP/REST** (синхронные запросы), **Camunda 8 / Zeebe** (оркестрация процессов).
|
||||
> Имена топиков и адреса сервисов берутся из Spring Cloud Config (`config-repo/*.yaml`,
|
||||
> общая карта адресов — `application-dev.yaml` / `application-local.yaml`, ключи `pcp.services.*`).
|
||||
|
||||
## Содержание
|
||||
1. [Перечень сервисов и порты](#1-перечень-сервисов-и-порты)
|
||||
2. [Kafka — карта топиков и событий](#2-kafka--карта-топиков-и-событий)
|
||||
3. [HTTP/REST — карта вызовов между сервисами](#3-httprest--карта-вызовов-между-сервисами)
|
||||
4. [Camunda — процессы и воркеры](#4-camunda--процессы-и-воркеры)
|
||||
|
||||
---
|
||||
|
||||
## 1. Перечень сервисов и порты
|
||||
|
||||
Локальные порты — из `config-repo/application-local.yaml` (`pcp.ports.*`). В dev/docker все слушают `8080`.
|
||||
|
||||
| Сервис | Локальный порт | Роль |
|
||||
|---|---|---|
|
||||
| `spring-cloud-config-server` | 8888 | раздача конфигов |
|
||||
| `pcp-srpring-boot-admin-server` | (admin) | мониторинг Spring Boot Admin |
|
||||
| `tle-monitoring-service` | 7001 | загрузка/раздача TLE (CelesTrak) |
|
||||
| `pcp-complex-mission-service` | 7002 | комплексные планы/миссии, режимы съёмки КА |
|
||||
| `pcp-ballistics-service` | 7003 | баллистика, расчёт НУ/витков/трасс (он же `ic-service`) |
|
||||
| `pcp-request-service` | 7005 | заявки, маршруты-привязки, outbox |
|
||||
| `slots-service` | 7006 | слоты, бронирования, расчёт покрытия |
|
||||
| `pcp-ui-service` | 7008 | BFF/агрегатор для старого UI |
|
||||
| `pcp-stations-service` | 7009 | каталог наземных станций |
|
||||
| `pcp-mission-planing-service` | 7010 | планирование миссий + **Camunda job-воркеры** |
|
||||
| `pcp-tgu-service` | 7011 | ТГУ: выдача планов, **запуск Camunda-процессов** |
|
||||
| `pcp-coverage-scheme-service` | 7012 | расчёт схем покрытия |
|
||||
| `pcp-satellite-catalog-service` | 7013 | каталог КА |
|
||||
| `pcp-dynamic-plan-service` | 7014 | динамические/комплексные планы (calcPlan) |
|
||||
| `pcp-route-processing-service` | — | обработка маршрутных паспортов |
|
||||
|
||||
`pcp-tgu-ops-ui` — фронтенд (Vite), ходит в микросервисы напрямую через прокси, в этой карте не участвует как backend.
|
||||
|
||||
---
|
||||
|
||||
## 2. Kafka — карта топиков и событий
|
||||
|
||||
Событие — JSON-конверт `KafkaMessage` с заголовком `type` (значения из `PcpKafkaEvent`).
|
||||
Консьюмеры фильтруют по этому заголовку.
|
||||
|
||||
### Топики
|
||||
|
||||
| Топик | Продюсер(ы) | Консьюмер(ы) | Событие / назначение |
|
||||
|---|---|---|---|
|
||||
| `pcp.tle` *(default-topic)* | `tle-monitoring-service` (TLE); `slots-service` (`ICRVPlacedEvent`) | `pcp-ballistics-service` (фильтры `placedIcFilter`, `updatedIcFilter`, `placedIcRvFilter`) | общий «шина TLE / НУ-РВ»: новые TLE и заложенные начальные условия |
|
||||
| `pcp.satellites` | `pcp-satellite-catalog-service` (`SatelliteDeletedEvent`) | `tle-monitoring`, `slots-service`, `pcp-complex-mission-service`, `pcp-dynamic-plan-service`, `pcp-ballistics-service`, `pcp-mission-planing-service` | удаление КА (fan-out для очистки производных данных) |
|
||||
| `pcp.slots.status.v1` | `pcp-mission-planing-service` (`BookedSlotsStatusChangedEvent`) | `slots-service` (`BookedSlotsKafkaListener`) | смена статуса забронированных слотов (lifecycle Kafka-first) |
|
||||
| `pcp.route.in.v1` | `pcp-route-processing-service` (`processed-route`) | `pcp-mission-planing-service` (`ModeStatusChangedKafkaListener`, `ModeStatusChangedEvent`) | обработанный маршрут → смена статуса режима съёмки |
|
||||
| `pcp.route.georeference.v1` | `pcp-route-processing-service` (`route-georeference`) | `pcp-request-service` (`RequestKafkaListener.consumeRoute`) | геопривязка маршрута к заявке |
|
||||
| `pcp.request.survey-georeference.v1` | (инжект `RouteProcessingTestController` / внешний источник) | `pcp-route-processing-service` (`RoutePassportMessageListener`, вход) | входной маршрутный паспорт на обработку |
|
||||
| `pcp.request.survey-georeference-dlq.v1` | `pcp-route-processing-service` (DLQ) | — | dead-letter для входных паспортов |
|
||||
| `pcp.request.status.v1` | `pcp-request-service` (`OutboxPublisherService`, outbox) | **geoportal** (внешний, в репо нет) | терминальный статус заявки `COMPLETED`/`EXPIRED` (`REQUEST_TERMINAL_STATUS`) |
|
||||
| `pcp.request.in.v1` | — | *(объявлен в конфиге `pcp-request-service`, активного слушателя нет)* | входящие заявки (legacy/зарезервировано) |
|
||||
|
||||
### Поток данных (упрощённо)
|
||||
|
||||
```
|
||||
CelesTrak ──HTTP──> tle-monitoring ──pcp.tle(TLE)──────────────> ballistics
|
||||
slots-service ─────pcp.tle(ICRVPlacedEvent)───────────────────> ballistics
|
||||
|
||||
satellite-catalog ──pcp.satellites(SatelliteDeletedEvent)──┬──> tle-monitoring
|
||||
├──> slots-service
|
||||
├──> complex-mission
|
||||
├──> dynamic-plan
|
||||
├──> ballistics
|
||||
└──> mission-planing
|
||||
|
||||
route-processing ──pcp.route.in.v1──────> mission-planing (ModeStatusChanged)
|
||||
route-processing ──pcp.route.georeference.v1──> request-service
|
||||
(вход) pcp.request.survey-georeference.v1 ──> route-processing
|
||||
|
||||
mission-planing ──pcp.slots.status.v1(BookedSlots)──────> slots-service
|
||||
request-service ──pcp.request.status.v1 (outbox)────────> geoportal (внешний)
|
||||
```
|
||||
|
||||
### ТГУ-топики (`pcp.tgu.*`)
|
||||
|
||||
`pcp-tgu-service` объявляет консьюмеры на свои топики (`config-repo/pcp-tgu-service.yaml`, блок `topics:`).
|
||||
Продюсеры этих событий — внешняя система СПО УОГ / тестовые эндпоинты; внутри репозитория стабильных продюсеров нет.
|
||||
|
||||
| Топик | Консьюмер в tgu | Событие |
|
||||
|---|---|---|
|
||||
| `pcp.tgu.visibility-windows-changed.v1` | `VisibilityWindowsChangedConsumer` | изменение окон видимости |
|
||||
| `pcp.tgu.satellite-plan-decision.v1` | `PlanDecisionConsumer` | решение по плану (принят/отклонён) |
|
||||
| `pcp.tgu.layin-confirmed.v1` | `LayinConfirmedConsumer` | подтверждение закладки |
|
||||
| `pcp.tgu.layin-failed.v1` | `LayinFailedConsumer` | ошибка закладки |
|
||||
| `pcp.tgu.plan-not-accepted.v1` | `PlanNotAcceptedConsumer` | план не принят |
|
||||
| `pcp.tgu.station-params-changed.v1` | `StationParamsChangedConsumer` | смена параметров станции |
|
||||
|
||||
---
|
||||
|
||||
## 3. HTTP/REST — карта вызовов между сервисами
|
||||
|
||||
Адреса берутся из `settings.*` → `pcp.services.*` (Spring Cloud Config). Клиенты — `WebClient`.
|
||||
|
||||
| Вызывающий сервис | Целевой сервис | Эндпоинты (примеры) | Назначение |
|
||||
|---|---|---|---|
|
||||
| `pcp-ui-service` (BFF) | `tle-monitoring` | `/v1/api/tle`, `/v1/api/tle/satellite/{id}` | TLE |
|
||||
| | `pcp-satellite-catalog` | `/api/satellites`, `/api/satellites/{id}/observation-profile`, `/slot-profile`, `/api/satellite-groups` | каталог КА |
|
||||
| | `pcp-complex-mission` | `/api/missions`, `/api/missions/{id}/drops/calculate`, `/api/com-plan/runs`, `/api/com-plan/process` | миссии и комплексные планы |
|
||||
| | `pcp-dynamic-plan` | `/v1/main/calcPlan`, `/{runId}/result`, `/routes`, `/intervals` | динамические планы |
|
||||
| | `pcp-request` (request / earth-grid) | `/v1/requests`, `/v1/requests/{id}/cells` | заявки и ячейки |
|
||||
| | `pcp-stations` | `/api/stations`, `/api/stations/{id}` | станции |
|
||||
| | `slots-service` | `/api/slots/...`, `/api/slots/booking[...]` | слоты и бронирования |
|
||||
| | `pcp-ballistics` | `/api/satellites/{id}/asc-node`, `/flight-line`, `/orbit`, `/extract-time`, `/initial-conditions/current`, `/receive-rv`, `/api/tle/parse`, `/api/tle/point` | баллистика |
|
||||
| | `pcp-coverage-scheme` | `/api/coverage-schemes/calculate` | схемы покрытия |
|
||||
| | `pcp-tgu` | `/api/platforms`, `/api/plans[...]`, `/api/plans/{id}/dismiss`, `/restore`, `/test/tgu/plans/{id}/decision`, `/confirm-layin` | планы ТГУ |
|
||||
| `pcp-tgu-service` | `pcp-mission-planing` (`mission-planing.base-url`) | `/api/missions/{id}/clone`, `/api/missions/blank`, `/api/missions/{id}` | управление миссиями плана |
|
||||
| | `pcp-ballistics` (`ballistics.base-url`) | расчёт точек/баллистики (`ExternalPointsClient`) | внешние точки |
|
||||
| | `ordinis.nstart.cloud` (внешний) | `/api/v1/spacecraft/records` (`platforms-url`) | классификатор платформ КА |
|
||||
| `pcp-mission-planing` | `pcp-ballistics` | `/api/obj-view/mpl-square`, `/mpl-point`, `/cell-coverage/{cell}`, `/api/satellites/{id}/rva`, `/extract-time` | баллистика для планирования |
|
||||
| | `pcp-complex-mission` | `/api/satellites/snapshots/{id}`, режимы КА | данные комплексной миссии |
|
||||
| | `pcp-satellite-catalog` | `/api/satellites/{id}` | каталог КА |
|
||||
| `pcp-complex-mission` | `pcp-ballistics` (`ic-service`) | расчёты НУ/баллистики | баллистика |
|
||||
| | `pcp-satellite-catalog` | `/api/satellites`, `/api/satellites/batch` | каталог КА |
|
||||
| | `slots-service` | покрытие/слоты (`CoverageBatchClient`, `SlotService`) | слоты |
|
||||
| | `pcp-request` (`earth-grid`) | сетка Земли/ячейки (`EarthGridService`) | заявки/ячейки |
|
||||
| | `pcp-coverage-scheme` | расчёт схемы покрытия | покрытие |
|
||||
| `pcp-dynamic-plan` | `pcp-request` | `/v1/requests` (выборка заявок) | заявки |
|
||||
| | `pcp-ballistics` | `/api/satellites/orbit/availability/{norad}`, `/extract-time`, `/api/obj-view/mpl-square` | баллистика |
|
||||
| | `pcp-satellite-catalog` | `/api/satellites` | каталог КА |
|
||||
| `pcp-coverage-scheme` | `pcp-ballistics` | `/api/obj-view/mpl-square`, `/mpl-point`, `/extract-time` | баллистика |
|
||||
| | `pcp-complex-mission` | `/api/satellites/{id}` | данные КА/миссии |
|
||||
| `pcp-ballistics` | `pcp-stations` | `/api/stations` | станции (для расчётов видимости) |
|
||||
| `slots-service` | `pcp-satellite-catalog` | `/api/satellites`, `/batch`, `/slot-profile` | каталог КА |
|
||||
| | `pcp-request` (`earth-grid`) | `/v1/requests`, `/cells` | заявки/ячейки |
|
||||
| `tle-monitoring` | `pcp-satellite-catalog` | `/api/satellites` | каталог КА |
|
||||
|
||||
Все сервисы дополнительно ходят в `spring-cloud-config-server` (8888) за конфигом и регистрируются в `spring-boot-admin-server`.
|
||||
|
||||
### Граф зависимостей (кто кого зовёт по HTTP)
|
||||
|
||||
```
|
||||
ui-service ──> почти все (BFF-агрегатор)
|
||||
tgu ─────────> mission-planing, ballistics, (ordinis ext)
|
||||
mission-planing ─> ballistics, complex-mission, satellite-catalog
|
||||
complex-mission ─> ballistics, satellite-catalog, slots, request, coverage-scheme
|
||||
dynamic-plan ───> request, ballistics, satellite-catalog
|
||||
coverage-scheme ─> ballistics, complex-mission
|
||||
ballistics ─────> stations
|
||||
slots ──────────> satellite-catalog, request
|
||||
tle-monitoring ─> satellite-catalog (+ CelesTrak внешний)
|
||||
```
|
||||
|
||||
`pcp-ballistics` и `pcp-satellite-catalog` — наиболее востребованные «нижние» сервисы.
|
||||
|
||||
---
|
||||
|
||||
## 4. Camunda — процессы и воркеры
|
||||
|
||||
Используется **Camunda 8 (Zeebe)**, `io.camunda:camunda-spring-boot-4-starter:8.8.10`.
|
||||
Брокер внешний: gRPC `camunda.k8s.265.nstart.cloud:30901` (dev) / `localhost:26500` (локально),
|
||||
ключ `camunda.client.grpc-address`.
|
||||
|
||||
- **`pcp-tgu-service`** — *оркестратор*: содержит BPMN-определения, разворачивает их и запускает экземпляры процессов (`CamundaClient.newCreateInstanceCommand()`).
|
||||
- **`pcp-mission-planing-service`** — *исполнитель*: реализует все job-воркеры (`@JobWorker` в `FlowService`).
|
||||
|
||||
### Процесс `createSatelliteMission`
|
||||
|
||||
BPMN: [createSatelliteMission.bpmn](../../services/pcp-tgu-service/src/main/resources/BPMN/createSatelliteMission.bpmn).
|
||||
Запуск: `BpmnPlanIssueProcessStarter` ← `IssueDuePlanWorker.issuePlanNow()` ← `PlanController` `POST /api/plans/{planId}/issue`.
|
||||
|
||||
```
|
||||
startSatelliteMission
|
||||
→ [calculateSatelliteSurveyMissions] «Сформировать план съёмки»
|
||||
→ [calculateSatelliteDropMissions] «Сформировать план сбросов»
|
||||
→ [sendSatellitePlan] «Отправка плана в СПО УОГ»
|
||||
→ endSatelliteMission
|
||||
```
|
||||
|
||||
### Процесс `updateAcceptedPlanSlots`
|
||||
|
||||
BPMN: [updateAcceptedPlanSlots.bpmn](../../services/pcp-tgu-service/src/main/resources/BPMN/updateAcceptedPlanSlots.bpmn).
|
||||
Запуск: `AcceptedPlanSlotsUpdateProcessStarter` ← `AcceptedPlanSlotsUpdateWorker`.
|
||||
|
||||
```
|
||||
start → [updateSlotsStatus] «Обновление статуса слотов» → end
|
||||
```
|
||||
|
||||
### Соответствие job-type → воркер
|
||||
|
||||
Все воркеры — в `pcp-mission-planing-service`, [FlowService.kt](../../services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/FlowService.kt), `autoComplete = false` (ручной `newCompleteCommand`).
|
||||
|
||||
| Job type (BPMN) | Метод воркера | Процесс |
|
||||
|---|---|---|
|
||||
| `calculateSatelliteSurveyMissions` | `FlowService.surveyPlan` | createSatelliteMission |
|
||||
| `calculateSatelliteDropMissions` | `FlowService.surveyDrop` | createSatelliteMission |
|
||||
| `sendSatellitePlan` | `FlowService.sendPlan` | createSatelliteMission |
|
||||
| `updateSlotsStatus` | `FlowService.confirmPlan` | updateAcceptedPlanSlots |
|
||||
|
||||
### Схема
|
||||
|
||||
```
|
||||
pcp-tgu-service pcp-mission-planing-service
|
||||
(оркестратор) (job-воркеры, FlowService)
|
||||
PlanController POST /api/plans/{id}/issue
|
||||
│
|
||||
▼
|
||||
BpmnPlanIssueProcessStarter
|
||||
│ newCreateInstanceCommand("createSatelliteMission")
|
||||
▼
|
||||
┌─────────────── Zeebe / Camunda 8 broker (gRPC) ───────────────┐
|
||||
│ calculateSatelliteSurveyMissions ─────────────────────────► surveyPlan
|
||||
│ calculateSatelliteDropMissions ─────────────────────────► surveyDrop
|
||||
│ sendSatellitePlan ─────────────────────────► sendPlan
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
|
||||
AcceptedPlanSlotsUpdateProcessStarter
|
||||
│ newCreateInstanceCommand("updateAcceptedPlanSlots")
|
||||
▼
|
||||
┌─────────────── Zeebe broker ───────────────┐
|
||||
│ updateSlotsStatus ───────────────────────► confirmPlan
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> Связь между HTTP и Camunda: воркеры `pcp-mission-planing-service` в ходе выполнения задач
|
||||
> сами ходят по HTTP в `pcp-ballistics`, `pcp-complex-mission`, `pcp-satellite-catalog`
|
||||
> и публикуют Kafka-событие `pcp.slots.status.v1` (см. разделы 2–3).
|
||||
@@ -0,0 +1,493 @@
|
||||
# pcp-tgu-ops-ui — карта возможностей и источников данных
|
||||
|
||||
Дата анализа: 2026-05-31.
|
||||
|
||||
Документ фиксирует, что сейчас умеет `pcp-tgu-ops-ui`: что отображает, чем управляет, какие backend endpoints вызывает и откуда реально приходят данные. Анализ выполнен по исходному коду frontend и по execution path backend endpoints до service/repository/external source.
|
||||
|
||||
## Кратко
|
||||
|
||||
`pcp-tgu-ops-ui` — отдельная Vite/React SPA для Mission Ops/TGU. Приложение рендерит `TguPlanningPage` и имеет три вкладки:
|
||||
|
||||
- `Планы` — timeline persisted TGU-планов по КА.
|
||||
- `Карта 2D` — карта с полигонами заявок на съемку.
|
||||
- `Редактор` — локальный редактор включений КА с черновиком, проверками и заглушкой сохранения.
|
||||
|
||||
Реально изменять состояние backend UI умеет только через отправку решения по плану: `ACCEPTED` или `REJECTED`. Редакторские правки пока не сохраняются во внешний сервис.
|
||||
|
||||
## Общая схема
|
||||
|
||||
```text
|
||||
Browser
|
||||
|
|
||||
v
|
||||
pcp-tgu-ops-ui SPA
|
||||
|
|
||||
|-- /api/tgu-planning/*
|
||||
| |
|
||||
| v
|
||||
| pcp-ui-service :7008
|
||||
| |
|
||||
| v
|
||||
| pcp-tgu-service :7011
|
||||
| |
|
||||
| |-- pcp_tgu DB
|
||||
| |
|
||||
| `-- Ordinis NSI spacecraft classifier
|
||||
|
|
||||
|-- /api/current-plans/*
|
||||
| |
|
||||
| v
|
||||
| pcp-ui-service :7008
|
||||
| |
|
||||
| v
|
||||
| pcp-mission-planing-service :7010
|
||||
| |
|
||||
| `-- mission planning DB
|
||||
|
|
||||
|-- /api/pcp-request/v1/*
|
||||
| |
|
||||
| v
|
||||
| pcp-request-service :7005
|
||||
| |
|
||||
| `-- request DB
|
||||
|
|
||||
`-- https://*.basemaps.cartocdn.com
|
||||
|
|
||||
`-- CARTO basemap tiles
|
||||
```
|
||||
|
||||
Dev proxy:
|
||||
|
||||
- `/api/pcp-request` -> `http://localhost:7005`, prefix `/api/pcp-request` stripped.
|
||||
- `/api` -> `http://localhost:7008`.
|
||||
|
||||
Production nginx:
|
||||
|
||||
- `/api/pcp-request/` -> `http://host.docker.internal:7005/`.
|
||||
- `/api/` -> `http://host.docker.internal:7008/api/`.
|
||||
|
||||
## Endpoint inventory
|
||||
|
||||
| UI endpoint | Метод | Использование в UI | Backend execution path | Источник |
|
||||
|---|---:|---|---|---|
|
||||
| `/api/tgu-planning/platforms` | GET | список КА, подписи, поиск, редактор | `pcp-ui-service` `TguPlanningRestController.platforms()` -> `TguPlanningService.platforms()` -> `pcp-tgu-service /api/platforms` -> `PlatformService.loadPlatforms()` | in-memory cache, Ordinis NSI |
|
||||
| `/api/tgu-planning/plans?historyDays=N` | GET | загрузка всех TGU-планов для timeline/map/editor context | `pcp-ui-service` -> `pcp-tgu-service /api/plans` -> `PlanQueryService.getAllPlans()` -> `PlannedPlanRepository.findAllByOrderByStartTime()` | `planned_plan` |
|
||||
| `/api/tgu-planning/spacecraft/{spacecraftId}/plans?historyDays=N` | GET | есть в API-клиенте, текущая страница в основном грузит все планы | `pcp-ui-service` -> `pcp-tgu-service /api/plans/{spacecraftId}` -> `PlanQueryService.getPlansBySpacecraftId()` | `planned_plan` |
|
||||
| `/api/tgu-planning/plans/{planId}/decision?decision=...&reason=...` | POST | кнопки принять/отклонить для `WAITING_DECISION` | `pcp-ui-service` -> `pcp-tgu-service /test/tgu/plans/{planId}/decision` -> `TestPlanDecisionService.generateDecision()` -> `HandlePlanDecisionUseCase.handle()` | `planned_plan`, `plan_issue_attempt`, `spacecraft_planning_state`, `processed_plan_decision_event` |
|
||||
| `/api/pcp-request/v1/requests/map?...` | GET | полигоны заявок на карте 2D и в редакторе | напрямую в `pcp-request-service /v1/requests/map` -> `RequestController.listRequestsForMap()` -> `RequestService.listRequestsForMap()` -> `RequestRepository.findRequests()` | `requests` |
|
||||
| `/api/current-plans/missions/{missionId}/modes` | GET | контуры существующих включений в редакторе | `pcp-ui-service` -> `pcp-mission-planing-service /api/missions/{missionId}/modes` -> `MissionPlaningService.modesByMission()` | `missions`, `plans`, `modes`, `survey_modes`, `drop_modes`, `drop_mode_survey`, `mode_booking` |
|
||||
|
||||
## Вкладка "Планы"
|
||||
|
||||
### Что отображает
|
||||
|
||||
- Список КА: `name`, `NORAD`, `mission`, `status`.
|
||||
- Количество планов по КА в выбранном интервале.
|
||||
- Индикатор проблемных статусов: `REJECTED`, `EXPIRED`, `START_AMBIGUOUS`.
|
||||
- Timeline persisted TGU-планов по КА.
|
||||
- Стрелки между последовательными планами одного КА.
|
||||
- Линию текущего времени, если `now` попадает в выбранный диапазон.
|
||||
- Детали выбранного плана:
|
||||
- `planId`;
|
||||
- `spacecraftId`;
|
||||
- КА;
|
||||
- `startTime`;
|
||||
- `endTime`;
|
||||
- duration;
|
||||
- `kppId`;
|
||||
- `status`.
|
||||
|
||||
### Чем управляет
|
||||
|
||||
- Фильтр диапазона дат.
|
||||
- Быстрые диапазоны: 24 часа, 3 суток, 7 суток.
|
||||
- Refresh данных.
|
||||
- Поиск по КА.
|
||||
- Выбор конкретного КА.
|
||||
- Выбор плана.
|
||||
- Отправка решения `ACCEPTED`/`REJECTED`, но только для плана в статусе `WAITING_DECISION`.
|
||||
|
||||
### Execution path решения по плану
|
||||
|
||||
```text
|
||||
User
|
||||
|
|
||||
v
|
||||
TguPlanDetails
|
||||
|
|
||||
|-- Принять -> decision=ACCEPTED, reason=UI_TEST_ACCEPTED
|
||||
|
|
||||
`-- Отклонить -> decision=REJECTED, reason from prompt
|
||||
|
|
||||
v
|
||||
sendPlanDecision()
|
||||
|
|
||||
v
|
||||
POST /api/tgu-planning/plans/{planId}/decision
|
||||
|
|
||||
v
|
||||
pcp-ui-service
|
||||
|
|
||||
v
|
||||
TguPlanningService.sendDecision()
|
||||
|
|
||||
v
|
||||
POST pcp-tgu-service /test/tgu/plans/{planId}/decision
|
||||
|
|
||||
v
|
||||
TestPlanDecisionService.generateDecision()
|
||||
|
|
||||
|-- checks plan exists
|
||||
|-- checks spacecraft_planning_state exists
|
||||
|-- checks activePlanId == planId
|
||||
|-- checks activeAttemptId exists and belongs to plan
|
||||
|-- checks attempt status STARTING/STARTED
|
||||
|
|
||||
v
|
||||
HandlePlanDecisionUseCase.handle()
|
||||
|
|
||||
|-- insert processed_plan_decision_event, dedup by eventId
|
||||
|-- lock plan_issue_attempt by attemptId
|
||||
|-- lock planned_plan by planId
|
||||
|-- lock/create spacecraft_planning_state
|
||||
|
|
||||
|-- ACCEPTED:
|
||||
| |-- attempt=ACCEPTED
|
||||
| |-- plan=ACCEPTED
|
||||
| |-- currentBasePlanId=planId
|
||||
| |-- activePlanId/activeAttemptId cleared
|
||||
| |-- enqueue plan_recalculation_job
|
||||
| `-- enqueue accepted_plan_slots_update_job
|
||||
|
|
||||
`-- REJECTED:
|
||||
|-- attempt=REJECTED
|
||||
|-- activePlanId/activeAttemptId cleared
|
||||
|-- if enough lead time: plan=PLANNED, issueNotBefore=now
|
||||
`-- else: plan=EXPIRED, promote next plan or enqueue rebuild
|
||||
```
|
||||
|
||||
Важный caveat: UI отправляет решение через test-only controller `pcp-tgu-service /test/tgu/...`. Если `tgu.test-controller.enabled=false`, эта команда перестанет работать.
|
||||
|
||||
## Вкладка "Карта 2D"
|
||||
|
||||
### Что отображает
|
||||
|
||||
- CARTO dark basemap tiles.
|
||||
- Lat/lon grid поверх тайлов.
|
||||
- Полигоны заявок на съемку из `pcp-request-service`.
|
||||
- Цвета заявок по `surveyType`:
|
||||
- `OPTICS`;
|
||||
- `RSA`;
|
||||
- `COMBINED`.
|
||||
- Hover tooltip по полигону.
|
||||
- Выбор полигона заявки на карте.
|
||||
- Детальную панель с выбранным планом и выбранным объектом карты.
|
||||
|
||||
### Чем управляет
|
||||
|
||||
- Панорамирование карты drag-ом.
|
||||
- Zoom колесом и кнопками `+`/`-`.
|
||||
- Reset view.
|
||||
- Включение/выключение слоев.
|
||||
- Загрузка заявок за отдельный интервал.
|
||||
- Быстрые интервалы заявок: сегодня, 7 дней, 30 дней.
|
||||
- Скрытие/показ КА в sidebar.
|
||||
|
||||
### Что сейчас заявлено в UI, но не имеет данных
|
||||
|
||||
В главной карте 2D есть переключатели слоев:
|
||||
|
||||
- трасса КА;
|
||||
- полоса обзора;
|
||||
- работы плана;
|
||||
- станции;
|
||||
- маркеры КА;
|
||||
- заявки на съемку.
|
||||
|
||||
Но текущий `buildTgu2DMapScene()` наполняет только `request` polygons. Для остальных слоев рисовалки существуют, но данные в scene не передаются. UI показывает предупреждение: `Данные слоя недоступны в текущем API.`
|
||||
|
||||
### Execution path карты 2D
|
||||
|
||||
```text
|
||||
User
|
||||
|
|
||||
v
|
||||
Tgu2DMapTab
|
||||
|
|
||||
|-- selectedPlan / range / layers / hiddenSpacecraftIds
|
||||
|
|
||||
|-- load requests
|
||||
| |
|
||||
| v
|
||||
| fetchRequestsForMap()
|
||||
| |
|
||||
| |-- page size = 500
|
||||
| |-- max pages = 10
|
||||
| |-- max loaded requests = 5000
|
||||
| |-- sort = beginDateTime,asc
|
||||
| |
|
||||
| v
|
||||
| GET /api/pcp-request/v1/requests/map
|
||||
| |
|
||||
| v
|
||||
| pcp-request-service
|
||||
| |
|
||||
| v
|
||||
| RequestController.listRequestsForMap()
|
||||
| |
|
||||
| v
|
||||
| RequestService.listRequestsForMap()
|
||||
| |
|
||||
| v
|
||||
| RequestRepository.findRequests()
|
||||
| |
|
||||
| |-- status filter, optional
|
||||
| |-- surveyType filter, optional
|
||||
| |-- kpp filter, supported by backend but not passed by current UI
|
||||
| |-- highPriorityTransmit filter, supported by backend but not passed by current UI
|
||||
| |-- beginFrom/beginTo/endFrom/endTo
|
||||
| |-- exclude DELETED by default
|
||||
| |
|
||||
| v
|
||||
| requests table
|
||||
|
|
||||
v
|
||||
buildTgu2DMapScene()
|
||||
|
|
||||
|-- parse WKT from request.geometry
|
||||
|-- supports POLYGON and MULTIPOLYGON
|
||||
|-- build MapPolygon objects with layer=requests
|
||||
|
|
||||
v
|
||||
Tgu2DMapView
|
||||
|
|
||||
|-- drawTiles()
|
||||
| |
|
||||
| `-- https://{a|b|c|d}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png
|
||||
|
|
||||
|-- drawBaseMap()
|
||||
| |
|
||||
| `-- 30 degree lat/lon grid
|
||||
|
|
||||
|-- drawRequests()
|
||||
| |
|
||||
| `-- request polygons on canvas
|
||||
|
|
||||
|-- createPolygonSpatialIndex()
|
||||
|-- hitTestPolygons()
|
||||
|
|
||||
v
|
||||
Canvas + hover tooltip + selected object details
|
||||
```
|
||||
|
||||
### Фильтр заявок на карте
|
||||
|
||||
Для выбранного интервала `[from, to]` UI передает:
|
||||
|
||||
```text
|
||||
endFrom = from
|
||||
beginTo = to
|
||||
```
|
||||
|
||||
То есть выбираются заявки, которые пересекают интервал карты.
|
||||
|
||||
## Вкладка "Редактор"
|
||||
|
||||
### Что отображает
|
||||
|
||||
- Выбранный КА и выбранный TGU-план.
|
||||
- Черновик включений:
|
||||
- `shoot` — съемка;
|
||||
- `downlink` — сброс.
|
||||
- Timeline черновика.
|
||||
- Окна видимости цели/станции.
|
||||
- Синтетическую трассу КА на карте редактора.
|
||||
- Полигоны заявок из `pcp-request-service`.
|
||||
- Контуры `SURVEY` из `pcp-mission-planing-service`, если endpoint `/current-plans/missions/{id}/modes` вернул режимы.
|
||||
- Счетчики:
|
||||
- всего включений;
|
||||
- съемок;
|
||||
- сбросов;
|
||||
- конфликтов.
|
||||
|
||||
### Что умеет делать локально
|
||||
|
||||
- Добавить съемку через клик точки на карте.
|
||||
- Добавить сброс по станции и окну связи.
|
||||
- Перетаскивать включения по timeline.
|
||||
- Менять начало/конец включения drag resize.
|
||||
- Менять время через stepper.
|
||||
- Дублировать включение.
|
||||
- Удалять включение.
|
||||
- Undo/redo.
|
||||
- Проверять пересечения включений по времени.
|
||||
- Проверять видимость цели/станции по синтетической орбитальной модели `passes.ts`.
|
||||
- Показывать контекст других КА в rail, но сейчас `contextLanes.works` пустой.
|
||||
|
||||
### Что не умеет сохранять
|
||||
|
||||
Кнопка `Применить` вызывает `saveEditedPlan()`, но функция является заглушкой:
|
||||
|
||||
```text
|
||||
Сохранение правок плана во внешний сервис еще не реализовано в backend.
|
||||
```
|
||||
|
||||
Backend endpoint для сохранения отредактированного набора включений отсутствует.
|
||||
|
||||
### Execution path редактора
|
||||
|
||||
```text
|
||||
User
|
||||
|
|
||||
v
|
||||
TguEditorTab
|
||||
|
|
||||
|-- selectedSpacecraftId / selectedPlan from TguPlanningPage
|
||||
|
|
||||
|-- initialize draft
|
||||
| |
|
||||
| v
|
||||
| seedDraft(selectedPlan)
|
||||
| |
|
||||
| |-- if selectedPlan has embedded works:
|
||||
| | `-- use embedded works
|
||||
| |
|
||||
| `-- else:
|
||||
| `-- create fallback downlink work:
|
||||
| id = {planId}:plan-window
|
||||
| kind = downlink
|
||||
| start/end = selected plan start/end
|
||||
| stationId = selectedPlan.kppId
|
||||
|
|
||||
|-- load request context
|
||||
| |
|
||||
| v
|
||||
| fetchRequestsForMap()
|
||||
| |
|
||||
| v
|
||||
| GET /api/pcp-request/v1/requests/map
|
||||
| |
|
||||
| v
|
||||
| pcp-request-service
|
||||
| |
|
||||
| v
|
||||
| requests table
|
||||
|
|
||||
|-- load plan modes
|
||||
| |
|
||||
| v
|
||||
| fetchPlanModes(selectedPlan.planId)
|
||||
| |
|
||||
| v
|
||||
| GET /api/current-plans/missions/{selectedPlan.planId}/modes
|
||||
| |
|
||||
| v
|
||||
| pcp-ui-service
|
||||
| |
|
||||
| v
|
||||
| pcp-mission-planing-service
|
||||
| |
|
||||
| v
|
||||
| MissionPlaningService.modesByMission()
|
||||
| |
|
||||
| |-- check mission exists
|
||||
| |-- find modes by mission id
|
||||
| |-- map SurveyModeEntity -> SurveyModeResponseDTO
|
||||
| |-- map DropModeEntity -> DropModeResponseDTO
|
||||
| |
|
||||
| v
|
||||
| mission planning DB
|
||||
|
|
||||
|-- editor map scene
|
||||
| |
|
||||
| |-- groundTrack(spacecraftId) from frontend synthetic orbit
|
||||
| |-- station markers only if station lat/lon exists
|
||||
| |-- plan mode SURVEY contourWkt -> planWorks polygons
|
||||
| `-- request.geometry WKT -> request polygons
|
||||
|
|
||||
|-- local draft operations
|
||||
| |
|
||||
| |-- addShoot()
|
||||
| |-- addDownlink()
|
||||
| |-- updateWork()
|
||||
| |-- deleteWork()
|
||||
| |-- duplicateWork()
|
||||
| |-- undo()
|
||||
| |-- redo()
|
||||
| |-- overlaps()
|
||||
| `-- checkVisibility()
|
||||
|
|
||||
`-- apply draft
|
||||
|
|
||||
v
|
||||
saveEditedPlan()
|
||||
|
|
||||
v
|
||||
stub result only, no backend mutation
|
||||
```
|
||||
|
||||
### Важные caveats редактора
|
||||
|
||||
- `fetchPlanModes()` использует `selectedPlan.planId` как `missionId` для `/api/current-plans/missions/{missionId}/modes`.
|
||||
- Если UUID TGU-плана не совпадает с UUID mission в `pcp-mission-planing-service`, endpoint вернет 404, а редактор просто останется без режимов плана.
|
||||
- Станции строятся из `selectedPlan.kppId`, но координаты станции из backend не загружаются. Поэтому проверка видимости downlink часто возвращает конфликт `Нет координат станции для проверки видимости`.
|
||||
- Орбитальная модель в редакторе frontend-only и синтетическая: параметры выводятся из hash `spacecraftId`, а не из реальной баллистики/TLE.
|
||||
|
||||
## Capability matrix
|
||||
|
||||
| Область | Сейчас умеет | Ограничение |
|
||||
|---|---|---|
|
||||
| Платформы КА | читать из TGU/NSI, показывать name/NORAD/mission/status | при падении NSI `pcp-tgu-service` может использовать stale cache, если есть |
|
||||
| TGU-планы | читать persisted plans, строить timeline | UI грузит все планы и фильтрует на клиенте |
|
||||
| Решение по плану | отправить `ACCEPTED`/`REJECTED` для `WAITING_DECISION` | через test-only controller; нужен active plan + active attempt |
|
||||
| Карта заявок | загрузить и показать WKT-полигоны заявок | до 5000 заявок, без server-side фильтров kpp/highPriority в UI |
|
||||
| Слои карты | UI-переключатели есть | кроме заявок, основные слои главной карты не получают данных |
|
||||
| Редактор включений | локальный draft, drag/resize, add/delete/duplicate, undo/redo, checks | сохранения в backend нет |
|
||||
| Режимы плана в редакторе | пытается читать `/current-plans/missions/{id}/modes` | возможен mismatch `planId` vs `missionId` |
|
||||
| Станции | может показывать station markers только при наличии lat/lon | lat/lon станций сейчас не загружаются |
|
||||
| Тайлы карты | CARTO CDN | внешний network dependency браузера |
|
||||
|
||||
## Проверенные исходники
|
||||
|
||||
Frontend:
|
||||
|
||||
- `services/pcp-tgu-ops-ui/src/App.tsx`
|
||||
- `services/pcp-tgu-ops-ui/src/api/tguApi.ts`
|
||||
- `services/pcp-tgu-ops-ui/src/api/requestApi.ts`
|
||||
- `services/pcp-tgu-ops-ui/src/features/tgu-planning/*`
|
||||
- `services/pcp-tgu-ops-ui/src/features/tgu-map-2d/*`
|
||||
- `services/pcp-tgu-ops-ui/src/features/tgu-editor/*`
|
||||
- `services/pcp-tgu-ops-ui/vite.config.ts`
|
||||
- `services/pcp-tgu-ops-ui/nginx.conf`
|
||||
|
||||
Backend/BFF:
|
||||
|
||||
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TguPlanningController.kt`
|
||||
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TguPlanningService.kt`
|
||||
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/CurrentPlansController.kt`
|
||||
- `services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/MissionService.kt`
|
||||
|
||||
TGU:
|
||||
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlatformService.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/PlanQueryService.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/TestPlanDecisionService.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/service/HandlePlanDecisionUseCase.kt`
|
||||
- `services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/repository/*`
|
||||
|
||||
Requests:
|
||||
|
||||
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/controller/RequestController.kt`
|
||||
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/service/RequestService.kt`
|
||||
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/repository/RequestRepository.kt`
|
||||
- `services/pcp-request-service/src/main/kotlin/org/nstart/dep265/requestservice/dto/RequestApiDtos.kt`
|
||||
|
||||
Mission planning:
|
||||
|
||||
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/controller/MissionController.kt`
|
||||
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/MissionPlaningService.kt`
|
||||
- `services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/repository/*`
|
||||
- `libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/*`
|
||||
@@ -0,0 +1,153 @@
|
||||
# Жизненный цикл статусов плана ТГУ
|
||||
|
||||
Справочник по статусам business-plan в расписании ТГУ: когда наступает каждый статус, что он
|
||||
означает и какой переход его порождает. Источник правды — enum
|
||||
[`PlannedPlanStatus`](../../services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/entity/PlannedPlanStatus.kt);
|
||||
переходы — в `service/IssueDuePlanWorker`, `service/HandlePlanDecisionUseCase`,
|
||||
`service/CreateDraftUseCase`, `service/CreateChainPlanUseCase`, `dto/PlanResponse`.
|
||||
|
||||
Эти же статусы (кроме «зарезервированных», см. ниже) показаны в легенде вкладки «Планы»
|
||||
([`tguStatus.ts`](../../services/pcp-tgu-ops-ui/src/features/tgu-planning/tguStatus.ts)) и задают
|
||||
цвет плашек на таймлайне.
|
||||
|
||||
## Диаграмма переходов (упрощённо)
|
||||
|
||||
```
|
||||
создать черновик
|
||||
(нет плана) ───────────────────────────────► DRAFT
|
||||
│ promote («Применить») / авто-выдача
|
||||
▼
|
||||
проекция будущего ····(виртуально)····► PLANNED ──(подошёл срок, авто-выдача)──► ISSUING
|
||||
│ BPMN-процесс стартовал
|
||||
▼
|
||||
WAITING_DECISION
|
||||
ACCEPTED-решение │ │ REJECTED-решение
|
||||
▼ ▼
|
||||
AWAITING_LAYIN REJECTED (цепочка на паузе)
|
||||
│
|
||||
┌────────────────────────────────┼────────────────────────────────┐
|
||||
событие «заложен» событие «не заложен» событие «не принят»
|
||||
▼ ▼ ▼
|
||||
LAID_IN LAYIN_FAILED NOT_ACCEPTED
|
||||
(база цепочки; конкуренты (срочная перевыдача) (срочная перевыдача)
|
||||
того же окна → SUPERSEDED)
|
||||
|
||||
при сбое старта BPMN из ISSUING ─────────────────────────────────────────► START_AMBIGUOUS
|
||||
```
|
||||
|
||||
## Статусы
|
||||
|
||||
### DRAFT — черновик
|
||||
- **Когда.** Оператор создаёт план через «Создать план» (`CreateChainPlanUseCase`) или «Добавить»/
|
||||
редактирование (`CreateDraftUseCase`).
|
||||
- **Что значит.** План в стадии правки. Персистится, но **не** является головой цепочки, **не**
|
||||
попадает в проекцию/read-путь как запланированный слот и **не** выдаётся авто-воркером.
|
||||
- **Дальше.** Становится активным только после promote («Применить») — переходит в `ISSUING`.
|
||||
Удаление черновика (abandon) убирает его и клон-миссию.
|
||||
- В UI помечен как «ЧЕРНОВИК», виден только при включённом режиме «Черновики».
|
||||
|
||||
### PLANNED — проекция будущего (виртуальный)
|
||||
- **Когда.** Вычисляется на лету `PlanProjectionService`/`PlanResponse.projected()` при чтении
|
||||
расписания. **Никогда не пишется в БД.**
|
||||
- **Что значит.** Предполагаемый будущий слот цепочки (следующие ЗРВ) — «как пойдёт цепочка, если
|
||||
ничего не менять». Не несёт `basePlanId`, стартом цепочки не считается.
|
||||
- **Дальше.** Когда подходит срок выдачи, по этому слоту авто-воркер материализует реальный план
|
||||
сразу в `ISSUING` (`materializeIssuingPlan` → `insertIssuingPlan`).
|
||||
- В UI виден только при включённом тумблере «Возможные будущие планы».
|
||||
|
||||
### ISSUING — идёт выдача
|
||||
- **Когда.** `IssueDuePlanWorker` начинает выдачу: promote черновика (`prepareDraftPromotion`),
|
||||
ручная (пере)выдача конкретного плана (`prepareSpecificPlan`) или авто-материализация головы
|
||||
проекции (`materializeIssuingPlan`).
|
||||
- **Что значит.** Запущен BPMN-процесс выдачи плана на борт; создан attempt со статусом `STARTING`.
|
||||
- **Дальше.** Успешный старт процесса → `WAITING_DECISION` (`markStarted`). Сбой старта BPMN →
|
||||
`START_AMBIGUOUS` (`markAmbiguous`).
|
||||
|
||||
### WAITING_DECISION — ожидает решения
|
||||
- **Когда.** BPMN-процесс выдачи успешно стартовал, attempt → `STARTED` (`markStarted`).
|
||||
- **Что значит.** План выдан, ждём решения оператора/борта: ACCEPTED либо REJECTED.
|
||||
- **Дальше.**
|
||||
- ACCEPTED → `AWAITING_LAYIN` (`acceptPlan`).
|
||||
- REJECTED → `REJECTED` (`rejectPlan`).
|
||||
- **Grace-окно.** Если решения нет дольше `waitingDecisionGraceMinutes` после `startTime`, цепочка
|
||||
«молчаливо» едет дальше: якорь сдвигается на этот план, блокировка снимается, но **статус
|
||||
остаётся `WAITING_DECISION`** (`tryAdvancePastUnconfirmedPlan`). Запоздалое решение запишется на
|
||||
attempt и ушедшую вперёд цепочку не тронет. Поэтому в UI кнопки решения гасятся после grace.
|
||||
|
||||
### AWAITING_LAYIN — принят, ждёт подтверждения закладки
|
||||
- **Когда.** Пришло решение ACCEPTED (`acceptPlan`).
|
||||
- **Что значит.** План принят и ждёт закладки. **ACCEPT больше НЕ делает план базой и не двигает
|
||||
`chainVersion`** — это разъединено намеренно: после приёмки можно отправить и принять коррекцию
|
||||
того же окна, поэтому по одному окну может одновременно «лететь» несколько AWAITING_LAYIN/
|
||||
WAITING_DECISION. Базой и версией цепочки управляет только закладка (`LAID_IN`). На ACCEPT
|
||||
запускается джоб обновления слотов принятого плана (`acceptedPlanSlotsUpdateJobService.enqueue`).
|
||||
- **Дальше.** При подтверждении закладки → `LAID_IN` (становится базой). При негативе —
|
||||
`LAYIN_FAILED`/`NOT_ACCEPTED`. Если базой стала **другая** коррекция того же окна — этот план
|
||||
гасится в `SUPERSEDED`.
|
||||
- В легенде это «принятый, ждёт закладки» статус (зелёный). Перевыдача/коррекция в свой слот ещё
|
||||
возможна, пока до старта больше `layinFreezeMinutes` (после — «точка невозврата», только
|
||||
fork-forward).
|
||||
|
||||
### REJECTED — отклонён
|
||||
- **Когда.** Пришло решение REJECTED (`rejectPlan`).
|
||||
- **Что значит.** План признан неправильным. Авто-перевыдача **не** взводится (повторять тот же план
|
||||
бессмысленно). Цепочка ставится **на паузу** (`pausedAfterReject = true`) — **только если по КА не
|
||||
осталось других живых кандидатов** (в полёте/приняты, но не заложены): при нескольких выдачах
|
||||
отказ одной не глушит цепочку, пока жив другой кандидат. На паузе проекция пуста, воркер ничего не
|
||||
выдаёт. Базовый план не трогается.
|
||||
- **Дальше.** Оператор вручную создаёт исправленный план; его promote/ручная выдача снимают паузу.
|
||||
- В UI у КА с паузой рисуется пометка «⏸ на паузе».
|
||||
|
||||
### NOT_ACCEPTED — не принят (негативный фидбек)
|
||||
- **Когда.** Пришло внешнее событие «не принят» по текущему базовому плану в `AWAITING_LAYIN`
|
||||
(`handleNotAccepted` → `applyNegativeFeedback`).
|
||||
- **Что значит.** Терминальный негатив по базовому плану. В отличие от REJECTED — это **технический**
|
||||
негатив, поэтому запускается **срочная перевыдача** на ближайшую ЗРВ (`reissueOnNearestWindow`).
|
||||
- Событие, пришедшее не по текущему базовому плану (устаревшее/дубль), игнорируется.
|
||||
|
||||
### LAYIN_FAILED — не заложен (негативный фидбек)
|
||||
- **Когда.** Пришло внешнее событие «не заложен» по текущему базовому плану в `AWAITING_LAYIN`
|
||||
(`handleLayinFailed` → `applyNegativeFeedback`).
|
||||
- **Что значит.** Терминальный негатив: закладка не удалась. Поведение сейчас идентично
|
||||
`NOT_ACCEPTED` — **срочная перевыдача** на ближайшую ЗРВ.
|
||||
|
||||
### START_AMBIGUOUS — неопределённый старт
|
||||
- **Когда.** Из `ISSUING` не удалось достоверно стартовать BPMN-процесс выдачи (исключение в
|
||||
`startPreparedAttempt` → `markAmbiguous`); attempt → `AMBIGUOUS`.
|
||||
- **Что значит.** Неизвестно, ушёл план на борт или нет — требуется ручной разбор оператором.
|
||||
Опасный статус, в UI выделен пунктиром.
|
||||
|
||||
### LAID_IN — заложен (терминальный успех, база цепочки)
|
||||
- **Когда.** Пришло внешнее событие «план заложен» по плану в `AWAITING_LAYIN`
|
||||
(`LayinConfirmedConsumer` → `handleLayinConfirmed`, топик `layin-confirmed`).
|
||||
- **Что значит.** Подтверждённая закладка — **именно она делает план базой цепочки**
|
||||
(`state.currentBasePlanId = plan.planId`, `chainVersion += 1`, снимается пауза/срочная
|
||||
перевыдача). «Точка невозврата»: дальше план меняется только через fork-forward (гарды
|
||||
`CreateDraftUseCase`, `assertReissuableInSlot`).
|
||||
- **Supersede конкурентов.** Все ещё не заложенные планы КА (ISSUING/WAITING_DECISION/AWAITING_LAYIN)
|
||||
с окном, пересекающим окно заложенного, переводятся в `SUPERSEDED` (`supersedeCompetingPlans`).
|
||||
- Событие по плану не в `AWAITING_LAYIN` (устаревшее/дубль/уже вытеснён) — игнорируется.
|
||||
- В легенде — зелёный, темнее `AWAITING_LAYIN`.
|
||||
|
||||
### SUPERSEDED — вытеснён заложенной коррекцией (терминал)
|
||||
- **Когда.** По тому же окну КА другой план ушёл в `LAID_IN`, а этот был конкурирующей выдачей/
|
||||
коррекцией (`supersedeCompetingPlans`).
|
||||
- **Что значит.** Терминал, цепочку не двигает. Запоздавшее решение по его attempt записывается, но
|
||||
игнорируется (план уже не in-flight). Базовый план не трогается.
|
||||
|
||||
## Зарезервированные / не возникающие сейчас статусы
|
||||
|
||||
- **ACCEPTED** — в enum присутствует, но приёмка (`acceptPlan`) переводит план **сразу** в
|
||||
`AWAITING_LAYIN`, минуя `ACCEPTED`. Фактически это легаси-значение, нигде не присваивается; в
|
||||
легенду вкладки «Планы» намеренно не вынесено.
|
||||
|
||||
## Связанные конфиги (`PlanningProperties`)
|
||||
- `waitingDecisionGraceMinutes` — сколько ждём решение после старта, прежде чем «молчаливо» ехать
|
||||
дальше (см. `WAITING_DECISION`).
|
||||
- `layinFreezeMinutes` — за сколько минут до старта окно выдачи замораживается («точка невозврата»):
|
||||
перевыдача в свой слот запрещена, только fork-forward.
|
||||
- `notificationBeforeStartMinutes` — за сколько до старта авто-воркер начинает выдачу головы цепочки.
|
||||
|
||||
## См. также
|
||||
- [PCP_ARCHITECTURE.md](PCP_ARCHITECTURE.md)
|
||||
- [PCP_TGU_OPS_UI_CAPABILITY_MAP.md](PCP_TGU_OPS_UI_CAPABILITY_MAP.md)
|
||||
@@ -0,0 +1,136 @@
|
||||
# GitLab CI/CD for `pcp-ballistics-service`
|
||||
|
||||
This document describes the pilot GitLab CI/CD setup for `pcp-ballistics-service`.
|
||||
It is intentionally scoped to one service so the same structure can be reused later for other services in this monorepo.
|
||||
|
||||
## Pipeline behavior
|
||||
|
||||
The root pipeline is defined in [.gitlab-ci.yml](/home/emelianovan/WORK/Kotlin/pcp/.gitlab-ci.yml) and includes the service-specific jobs from [.gitlab/ci/pcp-ballistics-service.yml](/home/emelianovan/WORK/Kotlin/pcp/.gitlab/ci/pcp-ballistics-service.yml).
|
||||
|
||||
Pipeline creation is limited by `workflow: rules`:
|
||||
|
||||
- merge request: create a pipeline only when `pcp-ballistics-service` or its dependencies changed
|
||||
- push to `dev`: create a pipeline only when `pcp-ballistics-service` or its dependencies changed
|
||||
- push to `master`: create a pipeline only when `pcp-ballistics-service` or its dependencies changed
|
||||
|
||||
No deploy jobs are created for merge requests.
|
||||
|
||||
## Jobs
|
||||
|
||||
The pilot flow has four stages:
|
||||
|
||||
1. `ballistics:test`
|
||||
Runs `:services:pcp-ballistics-service:test` outside Docker.
|
||||
Publishes JUnit and HTML/JaCoCo reports.
|
||||
|
||||
2. `ballistics:build`
|
||||
Runs `:services:pcp-ballistics-service:bootJar` outside Docker.
|
||||
Publishes `build/libs/*.jar` as an artifact.
|
||||
|
||||
3. `ballistics:image`
|
||||
Builds and pushes the Docker image with Kaniko using the jar artifact from the previous stage.
|
||||
Tags:
|
||||
- `${CI_COMMIT_SHORT_SHA}`
|
||||
- `${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}`
|
||||
|
||||
4. deploy jobs
|
||||
- `ballistics:deploy:dev`
|
||||
- `ballistics:deploy:master`
|
||||
|
||||
Both are manual and use Helm:
|
||||
`helm upgrade --install --wait --atomic --timeout`.
|
||||
|
||||
## Branch behavior
|
||||
|
||||
### `dev`
|
||||
|
||||
Push to `dev`:
|
||||
|
||||
- runs test, build and image jobs
|
||||
- exposes a manual deploy button to the `dev` environment
|
||||
|
||||
### `master`
|
||||
|
||||
Push to `master`:
|
||||
|
||||
- runs test, build and image jobs
|
||||
- exposes a manual deploy button to the `master` environment
|
||||
|
||||
### merge request
|
||||
|
||||
Merge request pipeline:
|
||||
|
||||
- runs test and build only
|
||||
- does not create deploy jobs
|
||||
|
||||
## `rules:changes` scope
|
||||
|
||||
The pilot dependency map for `pcp-ballistics-service` includes:
|
||||
|
||||
- `services/pcp-ballistics-service/**/*`
|
||||
- `libs/pcp-types-lib/**/*`
|
||||
- `libs/ballistics-lib/**/*`
|
||||
- `config-repo/pcp-ballistics-service.yaml`
|
||||
- `config-repo/application*.yaml`
|
||||
- Gradle root files and wrapper
|
||||
- CI files
|
||||
- `charts/pcp-ballistics-service/**/*`
|
||||
|
||||
This is the pattern to reuse later for other services: service directory + direct shared libraries + service config + CI/Helm files.
|
||||
|
||||
## Helm deploy model
|
||||
|
||||
The chart lives in [charts/pcp-ballistics-service](/home/emelianovan/WORK/Kotlin/pcp/charts/pcp-ballistics-service).
|
||||
|
||||
The deployment model is kubeconfig-based, not GitLab Agent-based.
|
||||
This keeps the pilot setup simple and practical. A later migration to GitLab Agent is still possible because deploy logic is already isolated in dedicated jobs.
|
||||
|
||||
## Required GitLab CI/CD variables
|
||||
|
||||
Registry auth uses standard GitLab registry variables:
|
||||
|
||||
- `CI_REGISTRY`
|
||||
- `CI_REGISTRY_USER`
|
||||
- `CI_REGISTRY_PASSWORD`
|
||||
|
||||
Deploy jobs require:
|
||||
|
||||
- `BALLISTICS_KUBE_CONFIG_DEV`
|
||||
- `BALLISTICS_KUBE_NAMESPACE_DEV`
|
||||
- `BALLISTICS_CONFIG_SERVER_URI_DEV`
|
||||
- `BALLISTICS_KUBE_CONTEXT_DEV` optional
|
||||
- `BALLISTICS_CONFIG_LABEL_DEV` optional, defaults to `master`
|
||||
- `BALLISTICS_SPRING_PROFILE_DEV` optional, defaults to `dev`
|
||||
|
||||
- `BALLISTICS_KUBE_CONFIG_MASTER`
|
||||
- `BALLISTICS_KUBE_NAMESPACE_MASTER`
|
||||
- `BALLISTICS_CONFIG_SERVER_URI_MASTER`
|
||||
- `BALLISTICS_KUBE_CONTEXT_MASTER` optional
|
||||
- `BALLISTICS_CONFIG_LABEL_MASTER` optional, defaults to `master`
|
||||
- `BALLISTICS_SPRING_PROFILE_MASTER` optional, defaults to `dev`
|
||||
|
||||
`BALLISTICS_KUBE_CONFIG_*` must contain base64-encoded kubeconfig content.
|
||||
|
||||
## Config server and Vault
|
||||
|
||||
Runtime secrets are not duplicated in GitLab CI.
|
||||
|
||||
The Helm chart only passes bootstrap parameters needed by Spring Cloud Config:
|
||||
|
||||
- `CONFIG_SERVER_URI`
|
||||
- `SPRING_PROFILES_ACTIVE`
|
||||
- `SPRING_CLOUD_CONFIG_LABEL`
|
||||
- `CONFIG_SERVER_FAIL_FAST`
|
||||
|
||||
`pcp-ballistics-service` continues to obtain runtime configuration and secrets through `spring-cloud-config-server` and Vault at runtime.
|
||||
|
||||
## How to scale this to other services
|
||||
|
||||
To onboard another service later:
|
||||
|
||||
1. create a dedicated `.gitlab/ci/<service>.yml`
|
||||
2. define that service's `rules:changes`
|
||||
3. add a dedicated Helm chart
|
||||
4. keep the same stage structure:
|
||||
`test -> build -> image -> manual deploy`
|
||||
5. keep config-service/Vault integration at runtime, without copying secrets into GitLab
|
||||
@@ -0,0 +1,105 @@
|
||||
# CHECKLIST.md
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ содержит краткие checklists для review/merge, Definition of Done, обязательных тестов и validation.
|
||||
|
||||
Пункты ниже применяются по принципу **where relevant**:
|
||||
- для обычных Kotlin/Spring-изменений достаточно общего checklist;
|
||||
- для изменений в BPM / Zeebe / Camunda / Kafka / retries / DLQ / compensation / schema evolution дополнительно применяются профильные пункты;
|
||||
- для критичных процессов глубина проверок и тестов должна быть пропорциональна риску изменения.
|
||||
|
||||
---
|
||||
|
||||
## Review checklist
|
||||
|
||||
### Для всех нетривиальных изменений
|
||||
|
||||
Перед merge необходимо подтвердить:
|
||||
- запрошенное поведение реализовано;
|
||||
- решение осталось в корректной архитектурной границе;
|
||||
- существующие публичные контракты не сломаны без явного запроса;
|
||||
- добавлены или обновлены релевантные тесты;
|
||||
- не внесено несвязанных правок;
|
||||
- время на API — naive `LocalDateTime` без зоны; backend штампует `now` через `LocalDateTime.now()`, фронт парсит/форматирует через `utils/utcTime` без `new Date("<строка>")`;
|
||||
- egress фронта к бэку — naive ISO (`YYYY-MM-DDTHH:mm:ss`, без Z);
|
||||
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
|
||||
|
||||
### Дополнительно для BPM / Kafka / Zeebe / external integration изменений
|
||||
|
||||
Перед merge необходимо подтвердить, где это релевантно:
|
||||
- business key определён и используется консистентно;
|
||||
- operation id принимается от вызывающей стороны и используется для dedup;
|
||||
- ошибки классифицированы семантически;
|
||||
- side effects идемпотентны или явно компенсируются;
|
||||
- доменные события публикуются через Outbox;
|
||||
- logs / metrics / traces содержат обязательные поля корреляции;
|
||||
- тесты покрывают duplicate delivery и retry behavior;
|
||||
- изменения деплоя и конфигурации трассируемы в VCS.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Задача считается завершённой только если одновременно выполнено всё ниже, где это релевантно:
|
||||
|
||||
### Общее
|
||||
1. Запрошенное поведение реализовано.
|
||||
2. Решение осталось в корректной архитектурной границе.
|
||||
3. Нужные тесты добавлены или обновлены.
|
||||
4. Нужный validation выполнен, либо невозможность запуска объяснена.
|
||||
5. Не внесено несвязанных правок.
|
||||
|
||||
### Для BPM / Kafka / Zeebe / external integration
|
||||
6. BPM остаётся только оркестратором, если BPM участвует.
|
||||
7. Доменные правила остаются внутри сервисной границы.
|
||||
8. Flows остаются безопасными при at-least-once delivery.
|
||||
9. Идемпотентность обеспечена.
|
||||
10. `operation_id` сохранён или добавлен, где это требуется контрактом.
|
||||
11. Outbox сохранён или использован, где операция меняет состояние и публикует события.
|
||||
12. Retries ограничены и управляемы.
|
||||
13. DLQ handling сохранён для критичных consumers.
|
||||
14. Versioning / backward compatibility сохранены.
|
||||
15. Observability сохранена или улучшена.
|
||||
|
||||
---
|
||||
|
||||
## Обязательные тесты для значимых изменений
|
||||
|
||||
### Базовый минимум
|
||||
Как минимум покрывать, где применимо:
|
||||
- happy path;
|
||||
- изменённые ветки branching / validation logic;
|
||||
- regression scenario для bug fix.
|
||||
|
||||
### Дополнительно для BPM / Kafka / Zeebe / external integration
|
||||
Покрывать, где применимо:
|
||||
- retryable failure с последующим успехом;
|
||||
- исчерпание retries;
|
||||
- ветку business error;
|
||||
- обработку timeout;
|
||||
- сценарий compensation;
|
||||
- duplicate delivery / duplicate event handling;
|
||||
- contract compatibility;
|
||||
- schema compatibility.
|
||||
|
||||
### Для критичных процессов и high-risk изменений
|
||||
Покрывать пропорционально риску изменения через automated tests и/или явный resilience test plan:
|
||||
- перезапуск воркера во время выполнения;
|
||||
- временную недоступность БД;
|
||||
- временную недоступность внешней зависимости;
|
||||
- разрыв связности Kafka / BPM;
|
||||
- сценарий load / lag recovery.
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
Следуй разделу `Validation commands` из `AGENTS.md`.
|
||||
|
||||
Если для изменения есть module-specific команды, используй минимальный релевантный прогон, а затем более широкий, когда это необходимо.
|
||||
|
||||
Если какая-либо команда не была запущена:
|
||||
- точно укажи, какая именно команда не запускалась;
|
||||
- объясни почему;
|
||||
- опиши остаточный риск.
|
||||
@@ -0,0 +1,303 @@
|
||||
# PLATFORM_STANDARD.md
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ задаёт платформенные требования для сервисов, работающих в BPM / Kafka / Zeebe-среде.
|
||||
|
||||
Документ определяет:
|
||||
- архитектурные инварианты;
|
||||
- требования к идемпотентности и надёжности;
|
||||
- правила BPM / Kafka интеграции;
|
||||
- требования к observability, compatibility, retry / DLQ, compensation и rollout safety.
|
||||
|
||||
Это **внутренний платформенный стандарт**. Некоторые правила ниже являются осознанной инженерной политикой команды и не должны трактоваться как универсальная истина для всей отрасли.
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные инварианты
|
||||
|
||||
- BPM — только оркестратор процесса.
|
||||
- Доменное состояние и бизнес-правила принадлежат сервису, а не BPM.
|
||||
- BPM не должен использоваться как master data store для доменных данных.
|
||||
- BPM может хранить только идентификаторы, ссылки и технические метаданные, необходимые для оркестрации.
|
||||
- Везде предполагается at-least-once доставка. Повторная доставка — норма, а не исключение.
|
||||
- Exactly-once не предполагается; корректность достигается через идемпотентность, dedup, Outbox и управляемые retries.
|
||||
- Каждый обработчик обязан быть идемпотентным.
|
||||
|
||||
---
|
||||
|
||||
## Правила использования BPM
|
||||
|
||||
- BPM использовать только для операторских / регламентных процессов и long-running orchestration.
|
||||
- Не использовать BPM для высокочастотных near-real-time streaming-потоков.
|
||||
- Каждый BPM service task должен представлять ровно одну атомарную доменную операцию.
|
||||
- Логика ожидания, таймаутов, ретраев, эскалаций и компенсаций должна моделироваться явно в BPM, а не быть скрыта внутри сервиса.
|
||||
- BPMN flow не должен становиться местом владения бизнес-логикой.
|
||||
|
||||
---
|
||||
|
||||
## Camunda 8 / Zeebe
|
||||
|
||||
### Граница интеграции
|
||||
|
||||
- BPM-to-service-task интеграция должна идти через Zeebe gRPC Job Worker.
|
||||
- Для нашей платформы использование REST connector для service task считается **не рекомендуемым и по умолчанию запрещённым**, если нет отдельного архитектурного согласования.
|
||||
- Нельзя изменять `jobType`, пока существуют активные инстансы процесса.
|
||||
|
||||
### Обязательные поля корреляции
|
||||
|
||||
Каждый service task обязан принимать / обрабатывать:
|
||||
- `businessKey`
|
||||
- `operationId`
|
||||
- `correlationId`
|
||||
- `processInstanceKey`
|
||||
- `traceId` или `traceparent`
|
||||
|
||||
### Контракт воркера
|
||||
|
||||
Для каждого Zeebe worker:
|
||||
- читать входные данные только из process / job variables;
|
||||
- валидировать обязательные поля корреляции до начала обработки;
|
||||
- проверять dedup store по `operationId` до выполнения side effects;
|
||||
- возвращать семантические результаты:
|
||||
- `SUCCESS` -> `CompleteJob`
|
||||
- `RETRYABLE_ERROR` -> `FailJob` с retry / backoff
|
||||
- `BUSINESS_ERROR` -> `ThrowError`
|
||||
- `FATAL_ERROR` -> ветка инцидента или обработка через исчерпание retries
|
||||
- никогда не генерировать `operationId` внутри сервиса для входящих оркестрируемых вызовов;
|
||||
- вызывать `CompleteJob` только после commit доменной транзакции.
|
||||
|
||||
---
|
||||
|
||||
## Идемпотентность
|
||||
|
||||
Все операции, вызываемые из BPM, и все Kafka consumers обязаны быть идемпотентными.
|
||||
|
||||
Обязательные правила:
|
||||
- `operationId` должен приходить от вызывающей стороны / оркестратора;
|
||||
- dedup storage должен обеспечивать atomic check-and-write semantics;
|
||||
- dedup storage должен защищать от race conditions;
|
||||
- повторный вызов с тем же `operationId` должен возвращать тот же семантический результат и не должен повторять side effects;
|
||||
- для записей dedup должен быть документирован TTL в соответствии с бизнес-окном возможного повтора;
|
||||
- non-atomic реализация идемпотентности запрещена.
|
||||
|
||||
Дополнительное обязательное правило:
|
||||
- повторный вызов с тем же `operationId`, но с другим `businessKey` или семантически иным payload / параметрами, считается конфликтом контракта, а не валидным retry; такой случай должен приводить к явной ошибке / инциденту и быть достаточно залогирован для расследования.
|
||||
|
||||
Рекомендуемая реализация:
|
||||
- таблица в доменной БД, например `idempotency_keys(operation_id, business_key, status, result_hash, created_at)`.
|
||||
|
||||
---
|
||||
|
||||
## Порядок транзакции и публикации событий
|
||||
|
||||
Когда операция изменяет доменное состояние и публикует события:
|
||||
|
||||
1. Выполнить доменную операцию.
|
||||
2. Записать событие(я) в outbox в рамках той же транзакции.
|
||||
3. Сделать commit транзакции.
|
||||
4. Только после этого подтверждать BPM (`CompleteJob`) или внешний success.
|
||||
|
||||
Публикация событий вне Outbox запрещена.
|
||||
|
||||
Это правило одинаково применяется к:
|
||||
- синхронным сервисным операциям;
|
||||
- Zeebe workers;
|
||||
- Kafka consumers, которые меняют состояние и публикуют новые события.
|
||||
|
||||
---
|
||||
|
||||
## Process variables
|
||||
|
||||
- Process variables должны быть минимальными.
|
||||
- Хранить только ID, ссылки, routing data и небольшие технические метаданные.
|
||||
- Не хранить файлы, большие JSON payload, большие массивы в process variables.
|
||||
- Большие payload должны храниться в доменной БД или object storage; в process variables должны храниться только ссылки.
|
||||
- Process variables должны быть версионированы и документированы.
|
||||
- Для long-running процессов изменения набора или формы variables должны оставаться совместимыми с уже запущенными инстансами, либо сопровождаться явной migration strategy.
|
||||
- Для нашей платформы безопасным внутренним ориентиром считается бюджет **50–100 KB на один process instance**. Это не технический максимум платформы, а engineering guardrail для управляемости и надёжности процессов.
|
||||
|
||||
---
|
||||
|
||||
## Семантика результатов
|
||||
|
||||
Каждая операция должна возвращать семантическую классификацию результата, а не только transport status codes.
|
||||
|
||||
Допустимые классы результатов:
|
||||
- `SUCCESS`
|
||||
- `RETRYABLE_ERROR`
|
||||
- `BUSINESS_ERROR`
|
||||
- `FATAL_ERROR`
|
||||
|
||||
Каждая long-running операция должна иметь:
|
||||
- явно заданный SLA / timeout;
|
||||
- async processing model, где это нужно;
|
||||
- status lookup по `operationId`;
|
||||
- определённое поведение cancel, если отмена поддерживается.
|
||||
|
||||
---
|
||||
|
||||
## Kafka / event-driven
|
||||
|
||||
### Базовые правила
|
||||
|
||||
- Базовое предположение по доставке — at-least-once.
|
||||
- Consumers должны быть идемпотентными по `event_id` и / или `operation_id`.
|
||||
- Нельзя размещать бизнес-логику напрямую в consumer / listener классах.
|
||||
- Consumers / listeners должны делегировать в application / domain services.
|
||||
- Сохранять текущую retry / DLQ-стратегию, если явно не запрошено её изменение.
|
||||
|
||||
### Семантика `event_id` и `operation_id`
|
||||
|
||||
- `event_id` используется для dedup повторной доставки **одного и того же опубликованного события**.
|
||||
- `operation_id` используется для dedup повторного выполнения **одной и той же бизнес-операции**, если этот идентификатор протягивается через события и обработчики.
|
||||
- Если в сообщении присутствуют оба идентификатора, обработчик обязан явно понимать и документировать, какой из них используется для какой семантики dedup.
|
||||
|
||||
### Контракты событий
|
||||
|
||||
Требования ниже относятся прежде всего к **domain events**. Для технических, retry, DLQ, compacted state и других служебных топиков может использоваться отдельная документированная конвенция.
|
||||
|
||||
Для domain events:
|
||||
- именование топиков должно следовать шаблону `<domain>.<entity>.<event>.v<major>`;
|
||||
- partition key должен сохранять порядок событий по одной сущности через `business_key`;
|
||||
- каждое событие должно содержать как минимум:
|
||||
- `event_id`
|
||||
- `event_type`
|
||||
- `event_version`
|
||||
- `occurred_at`
|
||||
- `producer`
|
||||
- `business_key`
|
||||
- `correlation_id`
|
||||
- `trace_id`, если доступен
|
||||
- `process_instance_id`, если применимо
|
||||
- предпочтительны Avro / Protobuf со Schema Registry;
|
||||
- строго валидируемая JSON Schema допустима только если это согласовано платформой.
|
||||
|
||||
### Что нельзя менять без явного запроса
|
||||
|
||||
- topic names
|
||||
- event schema
|
||||
- key semantics
|
||||
- partition assumptions
|
||||
- serialization format
|
||||
- retry topology
|
||||
- DLQ routing rules
|
||||
|
||||
---
|
||||
|
||||
## Schema evolution и совместимость
|
||||
|
||||
- Breaking changes требуют новой major-версии.
|
||||
- Добавление optional fields с default значениями допустимо.
|
||||
- Удаление или переименование полей требует новой major-версии.
|
||||
- По умолчанию consumers должны оставаться совместимыми как минимум с текущей схемой и согласованной политикой совместимости платформы. Если для конкретного домена зафиксировано правило поддержки текущей и предыдущей minor-версии, оно должно быть явно указано в контракте этого домена.
|
||||
|
||||
Для BPM, messaging и public APIs:
|
||||
- сохранять backward compatibility, если явно не запрошен breaking change;
|
||||
- учитывать rolling deployment compatibility;
|
||||
- учитывать coexistence старых и новых consumers;
|
||||
- избегать изменений, требующих lockstep deployment, если это явно не запрошено.
|
||||
|
||||
---
|
||||
|
||||
## Retry / DLQ
|
||||
|
||||
Retries должны быть управляемыми и конечными.
|
||||
|
||||
Рекомендуемая политика:
|
||||
- `RETRYABLE_ERROR`: ограниченные retries с exponential backoff и jitter
|
||||
- `BUSINESS_ERROR`: никаких слепых retries; отправка в operator handling / parking lot / DLQ
|
||||
- `FATAL_ERROR`: никаких слепых retries; создание инцидента и маршрутизация в DLQ, где применимо
|
||||
|
||||
Обязательные правила:
|
||||
- бесконечные retries запрещены;
|
||||
- отключение DLQ для критичных consumer flows запрещено;
|
||||
- replay из DLQ без установленной причины запрещён;
|
||||
- Zeebe retries и Kafka retries должны рассматриваться как два независимых контура управления.
|
||||
|
||||
---
|
||||
|
||||
## Compensation
|
||||
|
||||
Если операция имеет side effects, должно быть явно определено одно из следующего:
|
||||
- compensating operation;
|
||||
- явное указание, что компенсация невозможна;
|
||||
- ветка operator / manual remediation.
|
||||
|
||||
Компенсация должна быть:
|
||||
- идемпотентной;
|
||||
- детерминированной;
|
||||
- семантически классифицированной;
|
||||
- залогированной с `businessKey`, `operationId`, причиной и результатом.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
Структурированные JSON-логи обязательны.
|
||||
|
||||
Каждый log / event / span должен содержать, где применимо:
|
||||
- `correlation_id`
|
||||
- `trace_id`
|
||||
- `process_instance_id`
|
||||
- `business_key`
|
||||
- `operation_id`
|
||||
- `service_name`
|
||||
- `env`
|
||||
|
||||
Обязательная телеметрия:
|
||||
- latency (`p50/p95/p99`)
|
||||
- error rate по семантическим классам
|
||||
- retry rate / attempts histogram
|
||||
- active process instances / stuck instances
|
||||
- consumer lag / DLQ rate
|
||||
- compensation rate
|
||||
|
||||
Трассировка:
|
||||
- OpenTelemetry обязателен
|
||||
- каждый шаг процесса должен создавать span
|
||||
- span attributes должны содержать `process_instance_id`, `business_key`, `operation_id`, `step_name`
|
||||
|
||||
Если меняется Kafka / BPM / external integration logic:
|
||||
- сохранить или добавить correlation identifiers;
|
||||
- сохранить или добавить `operation_id`, где это релевантно;
|
||||
- логировать достаточно для дебага retries, duplicates и compensations;
|
||||
- никогда не логировать секреты или sensitive payloads.
|
||||
|
||||
---
|
||||
|
||||
## Backpressure и runtime load
|
||||
|
||||
- Воркеры должны явно ограничивать `maxJobsActive` и concurrency.
|
||||
- Нельзя использовать рост timeout или retry как механизм масштабирования.
|
||||
- Backpressure должен управляться через масштабирование воркеров и управление пропускной способностью.
|
||||
- Предположения о throughput воркеров должны быть документированы.
|
||||
- CPU / RAM saturation должна наблюдаться и иметь алерты.
|
||||
|
||||
---
|
||||
|
||||
## Безопасность и аудит
|
||||
|
||||
- Сервисы должны поддерживать JWT / OAuth2 там, где это применимо.
|
||||
- Административные операции должны быть защищены role-based или attribute-based access control.
|
||||
- Секреты нельзя хранить в репозитории.
|
||||
- Административные действия, такие как retry / cancel / compensate / migrate, должны быть аудитируемыми.
|
||||
|
||||
---
|
||||
|
||||
## CI / CD и production safety
|
||||
|
||||
Минимально обязательные гейты **для изменений, где это релевантно**:
|
||||
- unit tests
|
||||
- coverage threshold
|
||||
- static analysis
|
||||
- API compatibility checks
|
||||
- schema compatibility checks
|
||||
- BPM + worker integration tests
|
||||
- resilience tests для поведения timeout / retry / DLQ
|
||||
|
||||
Правила деплоя:
|
||||
- деплой BPMN только через pipeline;
|
||||
- деплой воркеров только через pipeline;
|
||||
- ручной деплой BPMN в production запрещён;
|
||||
- критичные изменения retry / timeout / jobType не должны вноситься только через UI без трассируемости в VCS.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Следующая задача: маршруты, сбросы и перенос включений между планами
|
||||
|
||||
> Статус: запланировано (дизайн ещё не начат). Это фиксация намерения, не финальный план.
|
||||
|
||||
Следующий блок работ — три связанные темы вокруг содержимого плана (включений) и
|
||||
их съёмочно-сбросовой обвязки:
|
||||
|
||||
## 1. Построение маршрутов
|
||||
Довести построение маршрутов наблюдения по плану. Сейчас маршруты порождаются в
|
||||
потоке `createSatelliteMission` (BPMN), где `planId == missionId`; результат
|
||||
ложится в БД mission-planing. Уточнить: когда и по какому триггеру строятся
|
||||
маршруты для нового/перевыданного плана, и как они привязаны к включениям.
|
||||
|
||||
## 2. Формирование сбросов
|
||||
Формирование сбросов (`DropMode`) поверх съёмочных включений (`SurveyMode`):
|
||||
расчёт окон сброса, привязка к ЗРВ/КПП, согласование времени сброса со временем
|
||||
съёмки. Внутренние времена — naive `LocalDateTime` (см. сквозную модель без зон).
|
||||
|
||||
## 3. Перенос включений из плана в план
|
||||
При перевыдаче/версионировании плана переносить уже сформированные включения
|
||||
(съёмки + сбросы) из предыдущего плана в новый — чтобы не терять набранное
|
||||
содержимое цепочки. Определить, что переносится как есть, что пересчитывается,
|
||||
и как это стыкуется с двумя осями версионирования (цепочка ТГУ + контент миссии).
|
||||
|
||||
## Контекст / связанные места
|
||||
- Маршруты и их сохранение: `createSatelliteMission`, `planId == missionId`.
|
||||
- Сбросы/съёмки: `pcp-mission-planing-service` — `SurveyMode`/`DropMode`,
|
||||
`MissionPlaningService`.
|
||||
- Версионирование/перевыдача плана — две оси (цепочка ТГУ + контент миссии).
|
||||
- Время — сквозной naive `LocalDateTime` без зоны (после отката UTC-слоя).
|
||||
@@ -0,0 +1,208 @@
|
||||
<!-- /autoplan restore point: /home/dasolovyev/.gstack/projects/missionis-observatio-terrae/feat-tgu-slots-filters-autoplan-restore-20260616-165024.md -->
|
||||
|
||||
# Эргономика выбора слотов: выбор «полосами» + правило выполнимости брони
|
||||
|
||||
## Проблема (как сформулировал оператор)
|
||||
Сделана работа по сортировке, подсветке брони, показу забронированных, расчёту %
|
||||
покрытия. Но **сам выбор слотов под бронь по-прежнему неудобен**: сейчас оператор
|
||||
отмечает слоты галочками поштучно (или Shift+клик диапазоном, или «все видимые /
|
||||
все сутки»). На сотнях-тысячах слотов это утомительно. Плюс выбор «слепой» — он не
|
||||
знает, можно ли вообще физически отснять выбранный набор (манёвренность КА).
|
||||
|
||||
## Решённые premise (premise-гейт, 2026-06-16, скорректировано после CEO-ревью)
|
||||
1. **Ограничение брони через смену крена реально существует** (агилити КА).
|
||||
2. **КОРРЕКЦИЯ:** модель манёвренности УЖЕ есть в slots-service, по-КА из каталога
|
||||
(`mmi`, `maxSurveyDuration`, `maxChainLength`, `ROLL_EPSILON=0.01` — см. ниже).
|
||||
Ручная бронь (`bookReq`) её не проверяет, но coverage-solver и `buildSurveyChains`
|
||||
её соблюдают. Поэтому **не хардкодим физику на фронте** — проверка выполнимости
|
||||
будет **бэкенд validate-эндпоинтом**, переиспользующим эти по-КА параметры.
|
||||
3. **Iteration 1 = только UX выбора полосами** (frontend, без правила). Проверка
|
||||
выполнимости — **iteration 2** (бэкенд validate-эндпоинт + проводка на фронте).
|
||||
|
||||
## Что уже есть в бэкенде (источник правды для it.2)
|
||||
- Параметры по-КА из каталога ([SatelliteCatalogClient.kt:116](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatelliteCatalogClient.kt#L116),
|
||||
[AbstractSatellite.kt:25](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/AbstractSatellite.kt#L25)):
|
||||
`mmi`=10с (мин. интервал маневра, тот же крен), `maxSurveyDuration`=300с,
|
||||
`maxChainLength`=30 слотов, `ROLL_EPSILON`=0.01.
|
||||
- `buildSurveyChains` / `canMergeInTime` / `sameRoll` ([SlotService.kt:2151](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt#L2151))
|
||||
— склейка цепочек по-КА (для отрисовки), `OccupiedIntervalPrefilter` — mmi/maxSurveyDuration.
|
||||
- **Несоответствия исходного черновика** (закрыты решением «правило за бэком»): фронтовые
|
||||
150с ≠ бэкендовые 300с; фронтовый ε=0.1° ≠ бэкендовый 0.01; «30с на смену крена» —
|
||||
в бэке отдельной такой константы НЕТ (склейка только при том же крене). Эти числа
|
||||
сверяются с каталогом/баллистиками в it.2, НЕ хардкодятся.
|
||||
|
||||
## Что уже есть (карта на существующий код)
|
||||
- Выбор: `selectedKeys: Set<slotKey>` в [TguSlotsTab.tsx](../../services/pcp-tgu-ui-service/src/features/tgu-slots/TguSlotsTab.tsx).
|
||||
Поштучный чек, Shift+клик-диапазон ([slotSelection.ts](../../services/pcp-tgu-ui-service/src/features/tgu-slots/slotSelection.ts) `selectionAfterCheck`),
|
||||
«выбрать видимые», «все сутки», «снять выбор».
|
||||
- Слот несёт `roll`, `revolution`, `revolutionSign`, `cycle`, `satelliteId`, `slotNumber`, `tn`, `tk`, `state`.
|
||||
- Чистые модули с юнит-тестами: slotFilters.ts, slotSelection.ts, coverage.ts (+ .test.ts) — образец стиля.
|
||||
- Бронь: `book()` → подтверждение → `performBook` (чанк 200), снятие аналогично.
|
||||
- Бэкенд бронирует без валидации манёвренности ([SlotService.kt:2500](../../services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt#L2500)). В it.1 не трогаем.
|
||||
|
||||
## Решение — Iteration 1 (только UX выбора полосами, frontend) — ФИНАЛ после ревью
|
||||
|
||||
### Модуль 1: `slotStrips.ts` (чистая логика + юнит-тесты)
|
||||
- `groupStrips(slots, keyOf): Map<slotKey, StripId>` — максимальные same-roll цепочки
|
||||
**по-КА**. `keyOf` — аксессор ключа слота (как `keyAt` в `selectionAfterCheck`), чтобы
|
||||
не дублировать формулу `${satelliteId}:${slotNumber}:${cycle}`.
|
||||
- **Детерминизм:** внутри функции тотальный порядок — сортировка по
|
||||
`(satelliteId, parseLocal(tn), parseLocal(tk), slotNumber, cycle, slotKey)`. Результат
|
||||
не зависит от порядка входного массива.
|
||||
- **ε крена = `ROLL_EPSILON = 0.01`** (именованная константа, как в бэкенде; НЕ 0.1).
|
||||
Сравнение — **с якорным креном полосы** (`abs(roll − anchorRoll) ≤ 0.01`), НЕ с
|
||||
предыдущим слотом (иначе дрейф крена 0.00→0.009→0.018 ошибочно склеит). Совпадает с
|
||||
бэкендовым `sameRoll(roll, chainRoll)`.
|
||||
- **StripId стабилен и читаем:** `strip:${slotKey первого члена}` (не счётчик — счётчик
|
||||
зависит от порядка обхода КА).
|
||||
- Краевые: один слот → своя полоса; два КА вперемешку по времени НЕ склеиваются;
|
||||
`tn` не парсится (NaN) → синглтон без падения; ровно на границе ε (`==0.01`) → та же полоса.
|
||||
|
||||
### Модуль 2: выбор «полосами» (UX в TguSlotsTab) — решения ревью
|
||||
- **Жест: чип в строке `полоса ×N`** (N = число видимых членов полосы). НЕ Alt+клик
|
||||
(необнаружим, конфликт с чек/Shift), НЕ клик по карте как основной. Чип появляется
|
||||
только при N≥2 и если есть хоть один выбираемый (не-BUSY) член.
|
||||
- **Identity ≠ selection target:**
|
||||
- `stripIdByKey = useMemo(groupStrips(displaySlots, slotKey), [displaySlots])` — id по
|
||||
СТАБИЛЬНОМУ набору (не filteredSlots/sortedSlots — иначе фильтр-дырка ложно склеит/разорвёт).
|
||||
- Видимые выбираемые члены полосы берём из `filteredSlots` ∩ `!slotIsBusy` (по образцу
|
||||
`selectableVisible`).
|
||||
- **Поведение чипа = `toggleAllVisible`, но в рамках одной полосы:** если ВСЕ видимые
|
||||
выбираемые члены уже выбраны → снять их; иначе добавить. После — `anchorIndexRef.current = null`.
|
||||
Без нового state (`selectedStrips` не заводим — рассинхрон с `selectedKeys`).
|
||||
- **Группировка визуально по StripId, sort-aware:** в сортировке «время» — левый рельс
|
||||
(border-left, цвет по `StripId`, приглушённый) на смежных строках полосы. В «крен»/«вклад»
|
||||
слоты полосы НЕ подряд → рельс НЕ рисуем; подсветка членов полосы по StripId при наведении.
|
||||
Фон строки оставляем за состоянием (active/selected); статус-ячейки не перекрашиваем.
|
||||
- **Карта it.1:** оставляем существующую подсветку выбранных слотов (`markedObjectIds` уже
|
||||
ведётся от `selectedKeys`). Отдельную раскраску полос на карте и «выбрать полосу из
|
||||
`SlotPickMenu`» — НЕ делаем в it.1 (расширяет scope на модель/рисовалку карты).
|
||||
- **CSS/таблица:** обновить `colCount`; выравнивание ячеек — классами, не сдвигом
|
||||
`nth-child`; чип держать в пределах `ROW_HEIGHT = 34`.
|
||||
|
||||
### Матрица состояний строки (зафиксировать перед реализацией)
|
||||
| Состояние | Источник | Визуал |
|
||||
|---|---|---|
|
||||
| active (фокус по карте) | `activeSlotId` | подсветка строки (как есть) |
|
||||
| selected | `selectedKeys.has(key)` | чек отмечен |
|
||||
| strip-selected | все видимые выбираемые члены полосы выбраны | чип в активном виде |
|
||||
| partial | часть членов полосы выбрана | чип в нейтральном виде |
|
||||
| disabled chip | N<2 или 0 выбираемых членов | чипа нет |
|
||||
| booked/occupied | как есть (бейдж) | не меняется; в полосу входит визуально, в выбор — нет |
|
||||
|
||||
## Реализовано (it.1, 2026-06-17 — пересмотрено)
|
||||
|
||||
ВАЖНО: первая версия it.1 (авто-чип «полоса ×N» по равному крену) оказалась не той фичей и
|
||||
переделана. На реальных данных полоса по «подряд по времени + равный крен» не формировалась
|
||||
(в один момент идёт «веер» кренов с шагом ~3°, равный крен повторяется не подряд, а вдоль трассы
|
||||
через каденцию). Оператор хотел не авто-магию, а ручной выбор полосы мышью «от первого к последнему»
|
||||
с запретом выйти за полосу.
|
||||
|
||||
Переопределение полосы (согласовано с оператором):
|
||||
- **полоса = один КА + один крен (допуск `ROLL_EPSILON`) + подряд по времени (без разрыва каденции);**
|
||||
- **виток/цикл в границу НЕ входят** — у экватора номер витка меняется, а слоты остаются в одной полосе.
|
||||
|
||||
Сделано:
|
||||
- `slotStrips.ts`: `groupStrips(slots, keyOf) → { idByKey, membersOf }`. Группировка: по КА →
|
||||
кластеризация по крену (`ROLL_EPSILON=0.5`, разрыв > ε начинает кластер) → внутри кластера деление
|
||||
по непрерывности времени (`CADENCE_GAP_FACTOR=1.5` от медианной каденции). `PolosaId =
|
||||
polosa:${ключ первого по времени члена}`; `membersOf` — члены в порядке времени.
|
||||
- `slotSelection.ts`: вместо `toggleStripSelection` — `selectStripRange(current, members, fromKey,
|
||||
toKey)` (чистая: добавляет членов полосы между двумя кликами включительно, без мутации).
|
||||
- `TguSlotsTab.tsx`: убран чип/hover/`stripInfo`. Жест: обычный клик ставит якорь; Shift+клик по
|
||||
слоту той же полосы выделяет членов между якорем и кликом (BUSY исключаются); Shift+клик по слоту
|
||||
другой полосы/КА — выбор не меняется + подсказка «только в пределах одной полосы». Якорь — состояние
|
||||
(`anchorKey`), строки полосы якоря подсвечены рельсом (оттенок по `PolosaId`); сбрасывается при
|
||||
смене заявки/сортировки/фильтров/расчёта. Подсказка жеста в selbar.
|
||||
- `tgu-planning.css`: удалены стили чипа/hover; рельс `tgu-slots-row--strip` переиспользован под
|
||||
полосу якоря, добавлен `is-strip-anchor`.
|
||||
- Калибровка `ROLL_EPSILON=0.5` / `CADENCE_GAP_FACTOR=1.5` — по скрину данных; при ручном прогоне
|
||||
возможна подстройка по фактическим `roll`/`tn`.
|
||||
- Бэкенд/DTO не тронуты. Правило выполнимости (манёвренность КА) по-прежнему it.2.
|
||||
|
||||
Доработки (2026-06-17):
|
||||
- **Shift+клик ближе к якорю «сжимает» выделение.** `selectStripRange` теперь задаёт выбор полосы
|
||||
РОВНО диапазоном [from..to]: члены полосы вне диапазона снимаются, выбор в других полосах сохраняется.
|
||||
Якорь при Shift+клике не двигается (остаётся первым кликом) — можно тянуть конец туда-обратно.
|
||||
- **Жест выбора полосы мышью на карте (Shift+drag).** Слоты на карте уже кликабельные полигоны
|
||||
(`slot:${key}` в spatial-index). Карта на жесте отдаёт наверх ключи слотов под курсором
|
||||
(`onStripAnchor`/`onStripDrag`/`onStripCommit`), а полосу/диапазон считает `TguSlotsTab`
|
||||
(`pickStripAnchor` — самый ранний по времени слот под курсором = начало; `pickStripEnd` — самый
|
||||
поздний член ТОЙ ЖЕ полосы под курсором = конец; за границу полосы выделение не уходит). Обычный
|
||||
drag = панорама, Shift+drag по слоту = выбор полосы (без отдельной кнопки-режима). Подсветка
|
||||
диапазона живая — через `markedObjectIds` (SVG-оверлей `is-marked`), отдельный слой превью не нужен.
|
||||
|
||||
## Решение — Iteration 2 (проверка выполнимости, отдельно)
|
||||
- Бэкенд: read-only `POST /api/slots/booking/validate` (набор слотов → violations),
|
||||
переиспользует по-КА `mmi`/`maxSurveyDuration`/`maxChainLength`/`ROLL_EPSILON` из каталога.
|
||||
Сверить точную семантику (300 vs «150 на полосу», нужна ли отдельная 30с-граница) с
|
||||
баллистиками. Опционально позже — валидация и в самом `bookReq`.
|
||||
- Фронт: гонять validate при выборе/перед бронью, показывать violations, решить
|
||||
блокировать кнопку или предупреждать.
|
||||
|
||||
## NOT in scope (it.1)
|
||||
- Любая физика выполнимости на фронте (перенесено в it.2, за бэкенд).
|
||||
- Переработка автоподбора покрытия (GREEDY/CONTINUOUS).
|
||||
- Изменение модели хранения броней.
|
||||
|
||||
## Тест-план (it.1) — финал
|
||||
- `slotStrips.test.ts` (groupStrips): мультиполоса в одном КА; два КА вперемешку по времени
|
||||
(не склеиваются); один слот → своя полоса; граница ε (`diff==0.01` склеивает, `0.0101` рвёт);
|
||||
дрейф крена 0.00/0.009/0.018 (с якорным сравнением → две полосы); устойчивость к порядку
|
||||
входа (равенство РАЗБИЕНИЯ при перемешивании); tie-break по равному `tn`; NaN `tn` → синглтон
|
||||
без падения; пустой вход → пустая Map.
|
||||
- **Чистый хелпер выбора полосы** (выделить из компонента, как slotSelection): скрытые члены
|
||||
не выбираются; BUSY исключён; «все видимые выбраны → снять, иначе добавить»; нет мутации
|
||||
входного `selectedKeys`.
|
||||
- Ручная проверка в vite dev: чип `полоса ×N`, рельс в сортировке «время», подсветка по
|
||||
StripId в «крен»/«вклад», согласованность с Shift+клик/чек/«все сутки», счётчик «Выбрано».
|
||||
|
||||
## Смежная находка (вне scope, флаг оператору)
|
||||
`anchorIndexRef` (якорь Shift-выбора) сбрасывается при смене сортировки, но НЕ при смене
|
||||
фильтров (статус/КА/ветвь/сутки), хотя они тоже меняют порядок строк → Shift-диапазон после
|
||||
фильтрации может выбрать не то. Существующий латентный баг, рядом с этой задачей. Дёшево
|
||||
починить (сбрасывать якорь и при смене фильтров) — включить в it.1 или отдельным фиксом.
|
||||
|
||||
---
|
||||
|
||||
## Review artifacts (/autoplan, 2026-06-16)
|
||||
|
||||
### CEO consensus
|
||||
| # | Вопрос | Claude | Codex | Консенсус |
|
||||
|---|--------|--------|-------|-----------|
|
||||
| 1 | Та ли проблема | да (UX выбора) | да | CONFIRMED |
|
||||
| 2 | Фронт-физика — регрет? | да, критично | да, критично | CONFIRMED → правило за бэк |
|
||||
| 3 | Бэкенд уже имеет модель? | да (нашёл chain-логику) | — | подтверждено в коде |
|
||||
| 4 | Калибровка scope | разбить | разбить | CONFIRMED → it.1 только UX |
|
||||
|
||||
### Design consensus
|
||||
| Тема | Claude | Codex | Консенсус |
|
||||
|------|--------|-------|-----------|
|
||||
| Жест | чип в строке | чип в строке | CONFIRMED |
|
||||
| Grouping vs sort | sort-aware по StripId | sort-aware по StripId | CONFIRMED |
|
||||
| Скрытые члены | только видимые | только видимые | CONFIRMED |
|
||||
| Карта | вторично/отложить | вторично/отложить | CONFIRMED |
|
||||
| Состояния | матрица | матрица | CONFIRMED |
|
||||
|
||||
### Eng consensus
|
||||
| Тема | Claude | Codex | Консенсус |
|
||||
|------|--------|-------|-----------|
|
||||
| StripId над displaySlots | да | да | CONFIRMED |
|
||||
| Identity ≠ selection target | да | да | CONFIRMED |
|
||||
| Memo на [displaySlots] | да | да | CONFIRMED |
|
||||
| ε: якорь vs предыдущий | оставил открытым | якорь (анти-дрейф) | DISAGREE → решено: якорь |
|
||||
| Бэкенд-изменения в it.1 | нет | нет | CONFIRMED |
|
||||
| Сброс якоря на фильтрах | — | да (смежный баг) | флаг оператору |
|
||||
|
||||
### Decision Audit Trail
|
||||
| # | Фаза | Решение | Класс | Принцип | Обоснование |
|
||||
|---|------|---------|-------|---------|-------------|
|
||||
| 1 | premise | Правило выполнимости — за бэкенд (не фронт-хардкод) | User Challenge | — | оба ревьюера + бэкенд уже имеет по-КА модель из каталога |
|
||||
| 2 | premise | it.1 = только UX выбора; правило → it.2 | User Challenge | P2/P3 | развязать безопасный win от спорной физики |
|
||||
| 3 | design | Жест = чип `полоса ×N` | Taste | P5 (явное) | обнаружим, не конфликтует с чек/Shift |
|
||||
| 4 | design | Grouping sort-aware (рельс в «время», подсветка иначе) | Mechanical | P1 | зебра по строкам врёт вне time-сортировки |
|
||||
| 5 | design | Чип выбирает только видимые не-BUSY члены | Mechanical | P1 | не выбирать невидимое (доверие счётчику) |
|
||||
| 6 | eng | StripId над `displaySlots`, memo на `[displaySlots]` | Mechanical | P3 | стабильность id + перф на тысячах строк |
|
||||
| 7 | eng | ε-сравнение с якорным креном полосы | Mechanical | P1 | анти-дрейф, совпадает с бэкендовым sameRoll |
|
||||
| 8 | eng | Без бэкенд/DTO-изменений в it.1 | Mechanical | P4 | всё нужное уже в SlotDTO |
|
||||
| 9 | eng | Карта — без раскраски полос в it.1 | Mechanical | P3/P5 | не расширять scope на модель карты |
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Карта работ КА — Mission Ops</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg-0: oklch(0.18 0.012 165);
|
||||
--bg-1: oklch(0.215 0.013 165);
|
||||
--bg-2: oklch(0.255 0.014 165);
|
||||
--bg-3: oklch(0.30 0.015 165);
|
||||
--line: oklch(0.34 0.014 165);
|
||||
--line-soft: oklch(0.28 0.013 165);
|
||||
--grid: oklch(0.26 0.012 165);
|
||||
|
||||
--ink: oklch(0.95 0.01 150);
|
||||
--ink-1: oklch(0.80 0.012 160);
|
||||
--ink-2: oklch(0.64 0.012 165);
|
||||
--ink-3: oklch(0.50 0.012 165);
|
||||
|
||||
--t-optical: oklch(0.80 0.14 158);
|
||||
--t-radar: oklch(0.82 0.13 82);
|
||||
--t-combo: oklch(0.74 0.15 6);
|
||||
--t-optical-dim: oklch(0.80 0.14 158 / 0.16);
|
||||
--t-radar-dim: oklch(0.82 0.13 82 / 0.16);
|
||||
--t-combo-dim: oklch(0.74 0.15 6 / 0.16);
|
||||
|
||||
--accent: oklch(0.82 0.13 200);
|
||||
--warn: oklch(0.80 0.15 55);
|
||||
--now: oklch(0.86 0.16 25);
|
||||
|
||||
--radius: 7px;
|
||||
--mono: "IBM Plex Mono", ui-monospace, monospace;
|
||||
--sans: "IBM Plex Sans", system-ui, sans-serif;
|
||||
--shadow: 0 8px 30px oklch(0.12 0.01 165 / 0.55);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
font-family: var(--sans);
|
||||
background: var(--bg-0);
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root { height: 100vh; }
|
||||
|
||||
::-webkit-scrollbar { width: 11px; height: 11px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg-0); }
|
||||
::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: 6px; border: 2px solid var(--bg-0); }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--line); }
|
||||
::-webkit-scrollbar-corner { background: var(--bg-0); }
|
||||
|
||||
.mono { font-family: var(--mono); }
|
||||
.tnum { font-variant-numeric: tabular-nums; }
|
||||
button { font-family: inherit; color: inherit; cursor: pointer; }
|
||||
a { -webkit-tap-highlight-color: transparent; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="data.jsx"></script>
|
||||
<script src="world.js"></script>
|
||||
<script src="basemap.js"></script>
|
||||
<script type="text/babel" src="util.jsx"></script>
|
||||
<script type="text/babel" src="orbit.jsx"></script>
|
||||
<script type="text/babel" src="tabs.jsx"></script>
|
||||
<script type="text/babel" src="mapview.jsx"></script>
|
||||
<script type="text/babel" src="mapdetails.jsx"></script>
|
||||
<script type="text/babel" src="mapapp.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,227 @@
|
||||
// Корневой компонент: состояние, фильтры, масштаб, тулбар, сборка строк.
|
||||
const { useState, useMemo, useEffect, useRef } = React;
|
||||
|
||||
// Пресеты масштаба: имя → видимое окно в мс (центрируется на «сейчас» при выборе).
|
||||
const HOUR = D.HOUR, DAY = D.DAY;
|
||||
const SCALES = [
|
||||
{ id: "week", label: "Неделя", span: 7 * DAY, minPx: 64 },
|
||||
{ id: "2week", label: "2 недели", span: 14 * DAY, minPx: 70 },
|
||||
{ id: "month", label: "Месяц", span: 30 * DAY, minPx: 80 },
|
||||
{ id: "range", label: "Весь диапазон", span: D.RANGE_END - D.RANGE_START, minPx: 90 },
|
||||
];
|
||||
|
||||
function App() {
|
||||
// Видимость групп / скрытые КА.
|
||||
const [groupShown, setGroupShown] = useState(() => {
|
||||
const o = {}; D.GROUP_DEFS.forEach((g) => (o[g.id] = true)); return o;
|
||||
});
|
||||
const [scHidden, setScHidden] = useState(() => new Set());
|
||||
const [typeFilter, setTypeFilter] = useState({ optical: true, radar: true, combo: true });
|
||||
const [search, setSearch] = useState("");
|
||||
const [scaleId, setScaleId] = useState("week");
|
||||
const [selectedIv, setSelectedIv] = useState(() => {
|
||||
const p = new URLSearchParams(location.search).get("plan");
|
||||
return p ? p : null;
|
||||
});
|
||||
const [selectedSc, setSelectedSc] = useState(null);
|
||||
const [timelineCollapsed, setTimelineCollapsed] = useState(() => new Set());
|
||||
const [scrollSignal, setScrollSignal] = useState(0);
|
||||
const [winCenter, setWinCenter] = useState(D.NOW);
|
||||
|
||||
const byId = useMemo(() => {
|
||||
const m = {}; D.plans.forEach((iv) => (m[iv.id] = iv)); return m;
|
||||
}, []);
|
||||
const scById = useMemo(() => {
|
||||
const m = {}; D.spacecraft.forEach((sc) => (m[sc.id] = sc)); return m;
|
||||
}, []);
|
||||
|
||||
// Масштаб → px/ms (умещаем span в условные 1100px рабочей ширины).
|
||||
const scale = SCALES.find((s) => s.id === scaleId);
|
||||
const VIEW_W = 1180;
|
||||
const pxPerMs = VIEW_W / scale.span;
|
||||
const rangeStart = D.RANGE_START;
|
||||
const rangeEnd = D.RANGE_END;
|
||||
|
||||
// Фильтрация КА.
|
||||
const scByGroup = useMemo(() => {
|
||||
const m = {};
|
||||
D.spacecraft.forEach((sc) => { (m[sc.groupId] = m[sc.groupId] || []).push(sc); });
|
||||
return m;
|
||||
}, []);
|
||||
|
||||
const q = search.trim().toLowerCase();
|
||||
const matchesSearch = (sc) => !q || sc.name.toLowerCase().includes(q) || sc.short.toLowerCase().includes(q);
|
||||
|
||||
// Счётчики для легенды.
|
||||
const counts = useMemo(() => {
|
||||
const byType = {};
|
||||
D.spacecraft.forEach((sc) => { byType[sc.type] = (byType[sc.type] || 0) + 1; });
|
||||
return { byType };
|
||||
}, []);
|
||||
|
||||
// Интервалы по КА (с учётом типового фильтра не трогаем — фильтр по типу скрывает строки целиком).
|
||||
const ivBySc = useMemo(() => {
|
||||
const m = {};
|
||||
D.plans.forEach((iv) => { (m[iv.scId] = m[iv.scId] || []).push(iv); });
|
||||
return m;
|
||||
}, []);
|
||||
|
||||
// Сборка строк таймлайна.
|
||||
const rows = useMemo(() => {
|
||||
const out = [];
|
||||
for (const g of D.GROUP_DEFS) {
|
||||
if (!groupShown[g.id]) continue;
|
||||
const list = (scByGroup[g.id] || [])
|
||||
.filter((sc) => typeFilter[sc.type])
|
||||
.filter((sc) => !scHidden.has(sc.id))
|
||||
.filter(matchesSearch);
|
||||
if (list.length === 0) continue;
|
||||
out.push({ kind: "group", group: g, count: list.length });
|
||||
if (timelineCollapsed.has(g.id)) continue;
|
||||
for (const sc of list) {
|
||||
out.push({ kind: "sc", sc, ivs: ivBySc[sc.id] || [] });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}, [groupShown, scHidden, typeFilter, q, timelineCollapsed]);
|
||||
|
||||
// Цепочка родословной выбранного интервала.
|
||||
const lineageSet = useMemo(() => {
|
||||
if (!selectedIv) return null;
|
||||
const lin = lineageOf(selectedIv, byId);
|
||||
return new Set(lin.chain);
|
||||
}, [selectedIv]);
|
||||
|
||||
const selIv = selectedIv ? byId[selectedIv] : null;
|
||||
const selSc = selIv ? scById[selIv.scId] : null;
|
||||
const selGroup = selSc ? D.GROUP_DEFS.find((g) => g.id === selSc.groupId) : null;
|
||||
|
||||
const toggleGroup = (id) => setGroupShown((s) => ({ ...s, [id]: !s[id] }));
|
||||
const toggleSc = (id) => setScHidden((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
||||
const toggleCollapse = (id) => setTimelineCollapsed((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
||||
|
||||
const onPickSc = (id) => {
|
||||
setSelectedSc(id);
|
||||
// прокрутка к первому интервалу этого КА
|
||||
const ivs = ivBySc[id] || [];
|
||||
if (ivs.length) { setSelectedIv(null); }
|
||||
};
|
||||
|
||||
// Статистика по видимым.
|
||||
const stats = useMemo(() => {
|
||||
let sc = 0, iv = 0, ov = 0;
|
||||
rows.forEach((r) => {
|
||||
if (r.kind === "sc") {
|
||||
sc++;
|
||||
iv += r.ivs.length;
|
||||
ov += overlapSegments(r.ivs).length;
|
||||
}
|
||||
});
|
||||
return { sc, iv, ov };
|
||||
}, [rows]);
|
||||
|
||||
const allOn = D.GROUP_DEFS.every((g) => groupShown[g.id]) && scHidden.size === 0;
|
||||
const showAll = () => { const o = {}; D.GROUP_DEFS.forEach((g) => (o[g.id] = true)); setGroupShown(o); setScHidden(new Set()); };
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
||||
<TopTabs active="plans" mapHref="Map.html" />
|
||||
<div style={{ flex: 1, display: "grid", gridTemplateColumns: `268px 1fr ${selIv ? "340px" : "0px"}`, minHeight: 0, transition: "grid-template-columns .18s" }}>
|
||||
<Sidebar
|
||||
groups={D.GROUP_DEFS} scByGroup={scByGroup}
|
||||
typeFilter={typeFilter} setTypeFilter={setTypeFilter}
|
||||
groupShown={groupShown} toggleGroup={toggleGroup}
|
||||
scHidden={scHidden} toggleSc={toggleSc}
|
||||
search={search} setSearch={setSearch}
|
||||
counts={counts} selectedSc={selectedSc} onPickSc={onPickSc}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
{/* ТУЛБАР */}
|
||||
<div style={{ height: 50, flex: "none", display: "flex", alignItems: "center", gap: 14, padding: "0 16px", borderBottom: "1px solid var(--line)", background: "var(--bg-1)" }}>
|
||||
{/* масштаб */}
|
||||
<div style={{ display: "flex", gap: 2, background: "var(--bg-0)", borderRadius: 7, padding: 3, border: "1px solid var(--line)" }}>
|
||||
{SCALES.map((s) => (
|
||||
<button key={s.id} onClick={() => { setScaleId(s.id); setScrollSignal((x) => x + 1); }}
|
||||
style={{ padding: "5px 11px", borderRadius: 5, border: "none", fontSize: 11.5, fontWeight: 600, background: scaleId === s.id ? "var(--bg-3)" : "transparent", color: scaleId === s.id ? "var(--ink)" : "var(--ink-3)" }}>{s.label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={() => setScrollSignal((x) => x + 1)} style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 11px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg-2)", fontSize: 11.5, fontWeight: 600, color: "var(--now)" }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: 9, background: "var(--now)", boxShadow: "0 0 6px var(--now)" }} />Сейчас
|
||||
</button>
|
||||
|
||||
<div style={{ width: 1, height: 22, background: "var(--line)" }} />
|
||||
<WorksLegend />
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* статистика */}
|
||||
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
|
||||
<Stat label="АППАРАТОВ" v={stats.sc} />
|
||||
<Stat label="ПЛАНОВ" v={stats.iv} />
|
||||
<Stat label="ПЕРЕСЕЧЕНИЙ" v={stats.ov} accent={stats.ov > 0 ? "var(--now)" : null} />
|
||||
</div>
|
||||
|
||||
{!allOn && (
|
||||
<button onClick={showAll} style={{ padding: "6px 11px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg-2)", fontSize: 11.5, color: "var(--ink-1)" }}>Показать все</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div style={{ flex: 1, display: "grid", placeItems: "center", color: "var(--ink-3)", fontSize: 13 }}>
|
||||
Нет аппаратов по текущим фильтрам.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ flex: 1, minHeight: 0 }} onClick={() => {}}>
|
||||
<Timeline
|
||||
rows={rows} pxPerMs={pxPerMs} rangeStart={rangeStart} rangeEnd={rangeEnd}
|
||||
now={D.NOW} selectedIv={selectedIv} onSelectIv={setSelectedIv}
|
||||
lineageSet={lineageSet} byId={byId} scrollSignal={scrollSignal}
|
||||
timelineCollapsed={timelineCollapsed} toggleCollapse={toggleCollapse}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Details iv={selIv} sc={selSc} group={selGroup} byId={byId} onSelectIv={setSelectedIv} onClose={() => setSelectedIv(null)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorksLegend() {
|
||||
const items = [
|
||||
{ k: "shoot", label: "Съёмка", stripes: true },
|
||||
{ k: "downlink", label: "Сброс", fill: "var(--accent)" },
|
||||
{ k: "calib", label: "Калибровка", fill: "oklch(0.78 0.02 200)" },
|
||||
{ k: "service", label: "Служебная", dashed: true },
|
||||
];
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 11 }}>
|
||||
<span className="mono" style={{ fontSize: 8.5, color: "var(--ink-3)", letterSpacing: ".08em" }}>РАБОТЫ:</span>
|
||||
{items.map((it) => (
|
||||
<div key={it.k} style={{ display: "flex", alignItems: "center", gap: 5 }}>
|
||||
<span style={{
|
||||
width: 11, height: 9, borderRadius: 2, flex: "none",
|
||||
background: it.stripes
|
||||
? "linear-gradient(90deg, var(--t-optical) 0 33%, var(--t-radar) 33% 66%, var(--t-combo) 66% 100%)"
|
||||
: it.dashed ? "transparent" : it.fill,
|
||||
border: it.dashed ? "1px dashed var(--ink-3)" : "none",
|
||||
}} />
|
||||
<span style={{ fontSize: 10.5, color: "var(--ink-2)" }}>{it.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({ label, v, accent }) { return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", lineHeight: 1.1 }}>
|
||||
<span className="mono tnum" style={{ fontSize: 15, fontWeight: 600, color: accent || "var(--ink)" }}>{v}</span>
|
||||
<span className="mono" style={{ fontSize: 8, color: "var(--ink-3)", letterSpacing: ".08em" }}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
||||
@@ -0,0 +1,107 @@
|
||||
// Web Mercator проекция + слой растровых тайлов (slippy-map) + кэш загрузки.
|
||||
// Подложки: тёмная (CARTO, OSM-данные), классическая OSM, без тайлов (схема).
|
||||
(function () {
|
||||
const TILE = 256;
|
||||
const DEG = Math.PI / 180, RAD = 180 / Math.PI;
|
||||
const MAX_LAT = 85.0511;
|
||||
|
||||
// lon/lat -> мировые пиксели при размере мира ws (= TILE * 2^z).
|
||||
function lonToWorldX(lon, ws) { return (lon + 180) / 360 * ws; }
|
||||
function latToWorldY(lat, ws) {
|
||||
const l = Math.max(-MAX_LAT, Math.min(MAX_LAT, lat));
|
||||
const s = Math.sin(l * DEG);
|
||||
const y = 0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI);
|
||||
return y * ws;
|
||||
}
|
||||
function worldXToLon(wx, ws) { return wx / ws * 360 - 180; }
|
||||
function worldYToLat(wy, ws) {
|
||||
const n = Math.PI - 2 * Math.PI * (wy / ws);
|
||||
return RAD * Math.atan(Math.sinh(n));
|
||||
}
|
||||
|
||||
// Метры на пиксель в точке широты (для перевода км -> px).
|
||||
function metersPerPixel(lat, ws) {
|
||||
return Math.cos(lat * DEG) * 2 * Math.PI * 6378137 / ws;
|
||||
}
|
||||
|
||||
const PROVIDERS = {
|
||||
dark: {
|
||||
id: "dark", name: "Тёмная",
|
||||
url: (z, x, y) => `https://a.basemaps.cartocdn.com/dark_all/${z}/${x}/${y}.png`,
|
||||
scrim: 0.0, label: "© OpenStreetMap · © CARTO", maxZoom: 18, dark: true,
|
||||
},
|
||||
osm: {
|
||||
id: "osm", name: "Светлая",
|
||||
url: (z, x, y) => `https://a.basemaps.cartocdn.com/rastertiles/voyager/${z}/${x}/${y}.png`,
|
||||
scrim: 0.34, label: "© OpenStreetMap · © CARTO", maxZoom: 19, dark: false,
|
||||
},
|
||||
schema: {
|
||||
id: "schema", name: "Схема", url: null, scrim: 0, label: "Схематичная основа (оффлайн)", dark: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Кэш тайлов: key -> {img, loaded, error}.
|
||||
const cache = new Map();
|
||||
function getTile(provider, z, x, y, onLoad) {
|
||||
const key = provider.id + "/" + z + "/" + x + "/" + y;
|
||||
let rec = cache.get(key);
|
||||
if (rec) return rec;
|
||||
rec = { img: new Image(), loaded: false, error: false };
|
||||
rec.img.crossOrigin = "anonymous";
|
||||
rec.img.onload = () => { rec.loaded = true; onLoad && onLoad(); };
|
||||
rec.img.onerror = () => { rec.error = true; };
|
||||
rec.img.src = provider.url(z, x, y);
|
||||
cache.set(key, rec);
|
||||
return rec;
|
||||
}
|
||||
|
||||
// Рисует слой тайлов на ctx для текущего вида.
|
||||
// view: { centerLon, centerLat, z }, size: {w,h}. onLoad — колбэк перерисовки.
|
||||
function drawTiles(ctx, provider, view, size, onLoad) {
|
||||
if (!provider || !provider.url) return;
|
||||
const { w, h } = size;
|
||||
const z = view.z;
|
||||
const tz = Math.max(1, Math.min(provider.maxZoom || 18, Math.round(z)));
|
||||
const ws = TILE * Math.pow(2, z); // непрерывный размер мира
|
||||
const k = Math.pow(2, z - tz); // масштаб тайла tz -> экран
|
||||
const tileScreen = TILE * k;
|
||||
const n = Math.pow(2, tz);
|
||||
|
||||
const cwx = lonToWorldX(view.centerLon, ws);
|
||||
const cwy = latToWorldY(view.centerLat, ws);
|
||||
const originX = cwx - w / 2; // мировой px (непрерывный) в левом верхнем углу
|
||||
const originY = cwy - h / 2;
|
||||
|
||||
// Видимый диапазон тайлов в tz-пространстве.
|
||||
const leftTz = (originX / k) / TILE;
|
||||
const topTz = (originY / k) / TILE;
|
||||
const rightTz = ((originX + w) / k) / TILE;
|
||||
const botTz = ((originY + h) / k) / TILE;
|
||||
const x0 = Math.floor(leftTz), x1 = Math.floor(rightTz);
|
||||
const y0 = Math.max(0, Math.floor(topTz)), y1 = Math.min(n - 1, Math.floor(botTz));
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
for (let tx = x0; tx <= x1; tx++) {
|
||||
const wrapX = ((tx % n) + n) % n; // горизонтальный повтор мира
|
||||
for (let ty = y0; ty <= y1; ty++) {
|
||||
const rec = getTile(provider, tz, wrapX, ty, onLoad);
|
||||
const sx = tx * tileScreen - originX;
|
||||
const sy = ty * tileScreen - originY;
|
||||
if (rec.loaded) {
|
||||
ctx.drawImage(rec.img, Math.floor(sx), Math.floor(sy), Math.ceil(tileScreen) + 1, Math.ceil(tileScreen) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// затемняющий scrim (для светлой OSM, чтобы overlay читался)
|
||||
if (provider.scrim > 0) {
|
||||
ctx.fillStyle = `rgba(8,16,12,${provider.scrim})`;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
window.Basemap = {
|
||||
TILE, MAX_LAT, PROVIDERS,
|
||||
lonToWorldX, latToWorldY, worldXToLon, worldYToLat,
|
||||
metersPerPixel, drawTiles, getTile,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,195 @@
|
||||
// Тестовые данные планов КА. Детерминированная генерация (seeded RNG),
|
||||
// чтобы при перезагрузке данные были стабильны.
|
||||
(function () {
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
const rnd = mulberry32(20260529);
|
||||
const pick = (arr) => arr[Math.floor(rnd() * arr.length)];
|
||||
const randint = (a, b) => a + Math.floor(rnd() * (b - a + 1));
|
||||
|
||||
const HOUR = 3600 * 1000;
|
||||
const DAY = 24 * HOUR;
|
||||
// "Сейчас" — фиксируем под текущую дату проекта.
|
||||
const NOW = new Date("2026-05-29T11:20:00Z").getTime();
|
||||
|
||||
// Три типа по аппаратуре (все аппараты — ДЗЗ).
|
||||
const TYPES = {
|
||||
optical: { id: "optical", label: "Оптическая", short: "ОПТ" },
|
||||
radar: { id: "radar", label: "Радиолокационная", short: "РЛ" },
|
||||
combo: { id: "combo", label: "Комбинированная", short: "ОПТ+РЛ" },
|
||||
};
|
||||
|
||||
// Программы/созвездия (название группы). Каждая группа тяготеет к своим типам.
|
||||
const GROUP_DEFS = [
|
||||
{ id: "resurs", name: "Ресурс-ОП", code: "RSO", weights: { optical: 0.8, radar: 0.05, combo: 0.15 } },
|
||||
{ id: "kanopus", name: "Канопус-В", code: "KNP", weights: { optical: 0.7, radar: 0.1, combo: 0.2 } },
|
||||
{ id: "kondor", name: "Кондор-ФКА", code: "KND", weights: { optical: 0.05, radar: 0.85, combo: 0.1 } },
|
||||
{ id: "obzor", name: "Обзор-Р", code: "OBZ", weights: { optical: 0.05, radar: 0.8, combo: 0.15 } },
|
||||
{ id: "meteor", name: "Метеор-М", code: "MTR", weights: { optical: 0.55, radar: 0.15, combo: 0.3 } },
|
||||
{ id: "aist", name: "Аист-2", code: "AST", weights: { optical: 0.6, radar: 0.1, combo: 0.3 } },
|
||||
{ id: "smotr", name: "Смотр-Р", code: "SMT", weights: { optical: 0.1, radar: 0.55, combo: 0.35 } },
|
||||
];
|
||||
|
||||
function weightedType(weights) {
|
||||
const r = rnd();
|
||||
let acc = 0;
|
||||
for (const k of Object.keys(weights)) {
|
||||
acc += weights[k];
|
||||
if (r <= acc) return k;
|
||||
}
|
||||
return "optical";
|
||||
}
|
||||
|
||||
// Виды РАБОТ внутри плана. У каждой работы — своя категория (kind).
|
||||
const WORK_KINDS = {
|
||||
shoot: { id: "shoot", label: "Съёмка" },
|
||||
downlink: { id: "downlink", label: "Сброс на НКПОР" },
|
||||
calib: { id: "calib", label: "Калибровка" },
|
||||
service: { id: "service", label: "Служебная операция" },
|
||||
};
|
||||
const WORK_KIND_ORDER = ["shoot", "downlink", "calib", "service"];
|
||||
// Подпись работы-съёмки зависит от аппаратуры КА.
|
||||
const SHOOT_LABEL = {
|
||||
optical: ["Панхром. съёмка", "Мультиспектр. съёмка", "Стереосъёмка", "Площадная съёмка"],
|
||||
radar: ["РСА съёмка", "Интерферометрия", "Прожекторный режим", "Сканирующий режим"],
|
||||
combo: ["Совмещ. съёмка", "РСА + панхром.", "Площадная съёмка", "Мониторинг ЧС"],
|
||||
};
|
||||
// Названия самих ПЛАНОВ (объемлющих интервалов).
|
||||
const PLAN_NAMES = ["Суточная программа", "Целевая программа", "Спецзадание", "Программа мониторинга", "Сеансовый план", "Программа ДЗЗ"];
|
||||
|
||||
function genWorks(planStart, planEnd, scType, makeId) {
|
||||
const works = [];
|
||||
const span = planEnd - planStart;
|
||||
// Плотность работ ~ 1 на 4–9 часов плана.
|
||||
let cursor = planStart + randint(0, 2) * HOUR;
|
||||
let guard = 0;
|
||||
while (cursor < planEnd - 1 * HOUR && guard < 40) {
|
||||
guard++;
|
||||
const kind = pick(WORK_KIND_ORDER);
|
||||
let dur;
|
||||
if (kind === "shoot") dur = randint(20, 140) * 60 * 1000; // 20м–2.3ч
|
||||
else if (kind === "downlink") dur = randint(8, 35) * 60 * 1000; // сброс
|
||||
else if (kind === "calib") dur = randint(30, 90) * 60 * 1000;
|
||||
else dur = randint(10, 40) * 60 * 1000; // служебная
|
||||
const wStart = cursor;
|
||||
const wEnd = Math.min(planEnd, wStart + dur);
|
||||
if (wEnd <= wStart) break;
|
||||
const label = kind === "shoot" ? pick(SHOOT_LABEL[scType]) : WORK_KINDS[kind].label;
|
||||
works.push({ id: makeId(), kind, label, start: wStart, end: wEnd });
|
||||
// пауза между работами
|
||||
cursor = wEnd + randint(20, 180) * 60 * 1000;
|
||||
}
|
||||
return works;
|
||||
}
|
||||
|
||||
// Генерация КА.
|
||||
const spacecraft = [];
|
||||
let scCounter = 0;
|
||||
for (const g of GROUP_DEFS) {
|
||||
const count = randint(8, 16); // суммарно ~80
|
||||
for (let i = 0; i < count; i++) {
|
||||
const type = weightedType(g.weights);
|
||||
scCounter++;
|
||||
const num = i + 1;
|
||||
spacecraft.push({
|
||||
id: `${g.id}-${num}`,
|
||||
name: `${g.name} №${num}`,
|
||||
short: `${g.code}-${String(num).padStart(2, "0")}`,
|
||||
groupId: g.id,
|
||||
type,
|
||||
orbit: pick(["ССО 510 км", "ССО 575 км", "ССО 700 км", "ССО 480 км"]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Видимый горизонт планирования: ~ -16 сут .. +30 сут от "сейчас".
|
||||
const RANGE_START = NOW - 16 * DAY;
|
||||
const RANGE_END = NOW + 30 * DAY;
|
||||
|
||||
function statusFor(start, end) {
|
||||
if (end < NOW) return "executed";
|
||||
if (start <= NOW && NOW <= end) return "active";
|
||||
return "planned";
|
||||
}
|
||||
|
||||
// Генерация ПЛАНОВ (объемлющих интервалов) с родословной (ревизии плана = потомки).
|
||||
// Каждый план содержит набор РАБОТ внутри своего интервала.
|
||||
const plans = [];
|
||||
let ivCounter = 0;
|
||||
let workCounter = 0;
|
||||
const makeWorkId = () => `W-${String(++workCounter).padStart(5, "0")}`;
|
||||
for (const sc of spacecraft) {
|
||||
// стартовая точка цепочки
|
||||
let cursor = RANGE_START + randint(0, 3) * DAY + randint(0, 20) * HOUR;
|
||||
let prevId = null;
|
||||
const nChains = randint(2, 4);
|
||||
for (let c = 0; c < nChains; c++) {
|
||||
// базовый план в цепочке
|
||||
const dur = randint(8, 60) * HOUR;
|
||||
const start = cursor;
|
||||
const end = start + dur;
|
||||
ivCounter++;
|
||||
const baseId = `PL-${String(ivCounter).padStart(4, "0")}`;
|
||||
const planName = pick(PLAN_NAMES);
|
||||
plans.push({
|
||||
id: baseId,
|
||||
scId: sc.id,
|
||||
start, end,
|
||||
title: planName,
|
||||
rev: 1,
|
||||
parentId: prevId,
|
||||
status: statusFor(start, end),
|
||||
author: pick(["ЦУП-1", "ЦУП-2", "ПЦ Москва", "ПЦ Дубна", "Авто-планировщик"]),
|
||||
works: genWorks(start, end, sc.type, makeWorkId),
|
||||
});
|
||||
prevId = baseId;
|
||||
|
||||
// c вероятностью — ревизия, перекрывающая базовый план (создаёт пересечение)
|
||||
if (rnd() < 0.45) {
|
||||
const overlapStart = start + Math.floor(dur * (0.35 + rnd() * 0.4));
|
||||
const revDur = randint(10, 50) * HOUR;
|
||||
const revEnd = overlapStart + revDur;
|
||||
ivCounter++;
|
||||
const revId = `PL-${String(ivCounter).padStart(4, "0")}`;
|
||||
plans.push({
|
||||
id: revId,
|
||||
scId: sc.id,
|
||||
start: overlapStart,
|
||||
end: revEnd,
|
||||
title: planName,
|
||||
rev: 2,
|
||||
parentId: baseId,
|
||||
status: statusFor(overlapStart, revEnd),
|
||||
author: pick(["ЦУП-1", "ЦУП-2", "ПЦ Москва", "Авто-планировщик"]),
|
||||
works: genWorks(overlapStart, revEnd, sc.type, makeWorkId),
|
||||
});
|
||||
prevId = revId;
|
||||
cursor = revEnd + randint(4, 36) * HOUR;
|
||||
} else {
|
||||
cursor = end + randint(6, 48) * HOUR;
|
||||
}
|
||||
if (cursor > RANGE_END) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Индексы родословной: дети по родителю.
|
||||
const childrenOf = {};
|
||||
for (const iv of plans) {
|
||||
if (iv.parentId) {
|
||||
(childrenOf[iv.parentId] = childrenOf[iv.parentId] || []).push(iv.id);
|
||||
}
|
||||
}
|
||||
|
||||
window.MissionData = {
|
||||
HOUR, DAY, NOW, RANGE_START, RANGE_END,
|
||||
TYPES, GROUP_DEFS, WORK_KINDS, WORK_KIND_ORDER,
|
||||
spacecraft, plans, childrenOf,
|
||||
typeOrder: ["optical", "radar", "combo"],
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,159 @@
|
||||
// Правая панель деталей выбранного интервала + лента родословной (предки/потомки).
|
||||
function StatusBadge({ status }) {
|
||||
const map = {
|
||||
executed: { t: "ВЫПОЛНЕН", c: "var(--ink-2)", bg: "var(--bg-3)" },
|
||||
active: { t: "АКТИВЕН", c: "var(--now)", bg: "oklch(0.86 0.16 25 / 0.16)" },
|
||||
planned: { t: "ЗАПЛАНИРОВАН", c: "var(--accent)", bg: "oklch(0.82 0.13 200 / 0.14)" },
|
||||
};
|
||||
const s = map[status] || map.planned;
|
||||
return <span className="mono" style={{ fontSize: 9.5, fontWeight: 600, letterSpacing: ".06em", color: s.c, background: s.bg, padding: "2px 7px", borderRadius: 4 }}>{s.t}</span>;
|
||||
}
|
||||
|
||||
function Field({ label, children, mono }) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>{label}</span>
|
||||
<span className={mono ? "mono tnum" : ""} style={{ fontSize: 12.5, color: "var(--ink)" }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Details({ iv, sc, group, byId, onSelectIv, onClose }) {
|
||||
if (!iv) {
|
||||
return (
|
||||
<div style={{ height: "100%", background: "var(--bg-1)", borderLeft: "1px solid var(--line)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 10, padding: 24, textAlign: "center" }}>
|
||||
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" style={{ opacity: .3 }}><rect x="3" y="6" width="18" height="3.4" rx="1.5" stroke="var(--ink-2)" strokeWidth="1.6" /><rect x="3" y="13" width="11" height="3.4" rx="1.5" stroke="var(--ink-2)" strokeWidth="1.6" /></svg>
|
||||
<div style={{ fontSize: 12, color: "var(--ink-3)", maxWidth: 180 }}>Выберите план на таймлайне, чтобы увидеть его работы и цепочку ревизий.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const lin = lineageOf(iv.id, byId);
|
||||
const dur = iv.end - iv.start;
|
||||
const col = TYPE_COLOR[sc.type];
|
||||
|
||||
const chainRow = (id) => {
|
||||
const it = byId[id];
|
||||
const isSel = id === iv.id;
|
||||
return (
|
||||
<button key={id} onClick={() => onSelectIv(id)} style={{
|
||||
display: "flex", alignItems: "center", gap: 8, width: "100%", textAlign: "left",
|
||||
padding: "7px 9px", borderRadius: 6, border: `1px solid ${isSel ? "var(--accent)" : "var(--line-soft)"}`,
|
||||
background: isSel ? "oklch(0.82 0.13 200 / 0.12)" : "var(--bg-2)",
|
||||
}}>
|
||||
<span className="mono tnum" style={{ fontSize: 9.5, color: "var(--ink-3)", flex: "none" }}>{it.id}</span>
|
||||
<span style={{ flex: 1, fontSize: 11.5, color: isSel ? "var(--ink)" : "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.title}</span>
|
||||
<span className="mono" style={{ fontSize: 8.5, color: "var(--ink-3)", border: "1px solid var(--line)", borderRadius: 3, padding: "0 4px", flex: "none" }}>r{it.rev}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", background: "var(--bg-1)", borderLeft: "1px solid var(--line)", display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ padding: "14px 16px 12px", borderBottom: "1px solid var(--line-soft)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
||||
<span style={{ width: 10, height: 10, borderRadius: 3, background: col, boxShadow: `0 0 8px ${col}`, flex: "none" }} />
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--ink-3)" }}>{iv.id}</span>
|
||||
</div>
|
||||
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--ink-3)", fontSize: 18, lineHeight: 1, padding: 0 }}>×</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 17, fontWeight: 700, marginTop: 8, lineHeight: 1.2 }}>{iv.title}</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 9 }}>
|
||||
<StatusBadge status={iv.status} />
|
||||
<span className="mono" style={{ fontSize: 10.5, color: "var(--ink-2)" }}>{sc.short} · {group.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "14px 16px 20px", display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
{/* Время */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
||||
<Field label="НАЧАЛО" mono>{fmtDateTime(iv.start)}</Field>
|
||||
<Field label="КОНЕЦ" mono>{fmtDateTime(iv.end)}</Field>
|
||||
<Field label="ДЛИТЕЛЬНОСТЬ" mono>{fmtDur(dur)}</Field>
|
||||
<Field label="РАБОТ В ПЛАНЕ" mono>{iv.works.length}</Field>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "var(--line-soft)" }} />
|
||||
|
||||
{/* Аппарат */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>АППАРАТ</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 9 }}>
|
||||
<TypeDot type={sc.type} size={9} />
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>{sc.name}</div>
|
||||
<div style={{ fontSize: 10.5, color: "var(--ink-3)" }}>{D.TYPES[sc.type].label} · {sc.orbit}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Field label="ИСТОЧНИК ПЛАНА">{iv.author}</Field>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "var(--line-soft)" }} />
|
||||
|
||||
{/* Работы внутри плана */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>РАБОТЫ В ПЛАНЕ</span>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)" }}>{iv.works.length}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
{iv.works.length === 0 && <div style={{ fontSize: 11, color: "var(--ink-3)" }}>Работы не заданы.</div>}
|
||||
{iv.works.map((wk) => {
|
||||
const fill = workFill(wk.kind, sc.type);
|
||||
const isService = wk.kind === "service";
|
||||
return (
|
||||
<div key={wk.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: "var(--bg-2)" }}>
|
||||
<span style={{ width: 9, height: 9, borderRadius: 2, flex: "none", background: isService ? "transparent" : fill, border: isService ? `1px dashed ${fill}` : "none" }} />
|
||||
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{wk.label}</span>
|
||||
<span className="mono tnum" style={{ fontSize: 10, color: "var(--ink-3)", flex: "none" }}>{fmtTime(wk.start)}–{fmtTime(wk.end)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "var(--line-soft)" }} />
|
||||
|
||||
{/* Родословная */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>РОДОСЛОВНАЯ ПЛАНА</span>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)" }}>{lin.chain.length} ревизий</span>
|
||||
</div>
|
||||
|
||||
{lin.ancestors.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<span style={{ fontSize: 9.5, color: "var(--ink-3)" }}>◀ Предки</span>
|
||||
{lin.ancestors.map(chainRow)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<span style={{ fontSize: 9.5, color: "var(--accent)" }}>● Текущий</span>
|
||||
{chainRow(iv.id)}
|
||||
</div>
|
||||
|
||||
{lin.descendants.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<span style={{ fontSize: 9.5, color: "var(--ink-3)" }}>▶ Потомки</span>
|
||||
{lin.descendants.map(chainRow)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lin.ancestors.length === 0 && lin.descendants.length === 0 && (
|
||||
<div style={{ fontSize: 11, color: "var(--ink-3)", padding: "4px 0" }}>Это единственная ревизия плана — без предков и потомков.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "12px 16px", borderTop: "1px solid var(--line-soft)" }}>
|
||||
<a href={`Map.html?plan=${iv.id}`} style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 7, border: "1px solid var(--line)", background: "var(--bg-2)", color: "var(--ink-1)", fontSize: 12, fontWeight: 600, textDecoration: "none" }}>
|
||||
Показать на карте
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" /><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" stroke="currentColor" strokeWidth="1.4" fill="none" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { Details });
|
||||
@@ -0,0 +1,162 @@
|
||||
// Страница «Карта»: вкладки, левые контролы (охват/слои/выбор КА), детали, ?plan=.
|
||||
const { useState: useStateMA, useMemo: useMemoMA, useEffect: useEffectMA } = React;
|
||||
|
||||
const HOUR_MS = 3600000, DAY_MS = 86400000;
|
||||
const PLAN_PAGE = "План КА — Mission Ops.html";
|
||||
|
||||
function MapApp() {
|
||||
const byId = useMemoMA(() => { const m = {}; D.plans.forEach(p => m[p.id] = p); return m; }, []);
|
||||
const scById = useMemoMA(() => { const m = {}; D.spacecraft.forEach(s => m[s.id] = s); return m; }, []);
|
||||
const ivBySc = useMemoMA(() => { const m = {}; D.plans.forEach(p => (m[p.scId] = m[p.scId] || []).push(p)); return m; }, []);
|
||||
const paramsBy = useMemoMA(() => { const m = {}; D.spacecraft.forEach(s => m[s.id] = orbitParams(s)); return m; }, []);
|
||||
|
||||
// начальное состояние из URL
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const urlPlan = urlParams.get("plan");
|
||||
const urlSc = urlParams.get("sc");
|
||||
const initPlan = urlPlan && byId[urlPlan] ? urlPlan : null;
|
||||
const initSc = initPlan ? byId[initPlan].scId : (urlSc && scById[urlSc] ? urlSc : D.spacecraft[0].id);
|
||||
|
||||
const [scope, setScope] = useStateMA(initPlan || urlSc ? "one" : "one");
|
||||
const [selSc, setSelSc] = useStateMA(initSc);
|
||||
const [selPlan, setSelPlan] = useStateMA(initPlan);
|
||||
const [search, setSearch] = useStateMA("");
|
||||
const [layers, setLayers] = useStateMA({ tracks: true, swath: true, shoot: true, downlink: true, stations: true, terminator: true });
|
||||
const [basemap, setBasemap] = useStateMA("dark");
|
||||
|
||||
const sc = scById[selSc];
|
||||
const plan = selPlan ? byId[selPlan] : null;
|
||||
|
||||
// окно времени
|
||||
const win = useMemoMA(() => {
|
||||
if (plan) return { t0: plan.start, t1: Math.min(plan.end, plan.start + 3 * DAY_MS) };
|
||||
return { t0: D.NOW - 3 * HOUR_MS, t1: D.NOW + 9 * HOUR_MS };
|
||||
}, [plan]);
|
||||
|
||||
// список КА в охвате
|
||||
const scList = useMemoMA(() => {
|
||||
if (scope === "one") return [sc];
|
||||
if (scope === "group") return D.spacecraft.filter(s => s.groupId === sc.groupId);
|
||||
return D.spacecraft;
|
||||
}, [scope, sc]);
|
||||
|
||||
const toggleLayer = (k) => setLayers(l => ({ ...l, [k]: !l[k] }));
|
||||
|
||||
const onSelectSc = (id) => { setSelSc(id); setSelPlan(null); if (scope === "all") setScope("group"); };
|
||||
const onSelectPlan = (id) => { const p = byId[id]; setSelSc(p.scId); setSelPlan(id); setScope("one"); };
|
||||
|
||||
// фильтр выбора КА
|
||||
const q = search.trim().toLowerCase();
|
||||
const scResults = useMemoMA(() => D.spacecraft.filter(s => !q || s.name.toLowerCase().includes(q) || s.short.toLowerCase().includes(q)), [q]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
||||
<TopTabs active="map" planHref={PLAN_PAGE} />
|
||||
|
||||
<div style={{ flex: 1, display: "grid", gridTemplateColumns: `288px 1fr ${plan ? "330px" : "0px"}`, minHeight: 0, transition: "grid-template-columns .18s" }}>
|
||||
{/* ЛЕВЫЕ КОНТРОЛЫ */}
|
||||
<div style={{ background: "var(--bg-1)", borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||
{/* подложка */}
|
||||
<div style={{ padding: "12px 14px", borderBottom: "1px solid var(--line-soft)" }}>
|
||||
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 7 }}>ПОДЛОЖКА</div>
|
||||
<div style={{ display: "flex", gap: 3, background: "var(--bg-0)", borderRadius: 7, padding: 3, border: "1px solid var(--line)" }}>
|
||||
{[["dark", "Тёмная"], ["osm", "Светлая"], ["schema", "Схема"]].map(([id, lab]) => (
|
||||
<button key={id} onClick={() => setBasemap(id)} style={{ flex: 1, padding: "6px 0", borderRadius: 5, border: "none", fontSize: 11.5, fontWeight: 600, background: basemap === id ? "var(--bg-3)" : "transparent", color: basemap === id ? "var(--ink)" : "var(--ink-3)" }}>{lab}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* охват */}
|
||||
<div style={{ padding: "12px 14px", borderBottom: "1px solid var(--line-soft)" }}>
|
||||
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 7 }}>ОХВАТ</div>
|
||||
<div style={{ display: "flex", gap: 3, background: "var(--bg-0)", borderRadius: 7, padding: 3, border: "1px solid var(--line)" }}>
|
||||
{[["one", "1 КА"], ["group", "Группа"], ["all", "Все"]].map(([id, lab]) => (
|
||||
<button key={id} onClick={() => setScope(id)} style={{ flex: 1, padding: "6px 0", borderRadius: 5, border: "none", fontSize: 11.5, fontWeight: 600, background: scope === id ? "var(--bg-3)" : "transparent", color: scope === id ? "var(--ink)" : "var(--ink-3)" }}>{lab}</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: "var(--ink-2)", display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<TypeDot type={sc.type} size={8} />
|
||||
<span className="mono">{sc.short}</span>
|
||||
<span style={{ color: "var(--ink-3)" }}>· {D.GROUP_DEFS.find(g => g.id === sc.groupId).name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* окно времени */}
|
||||
<div style={{ padding: "10px 14px", borderBottom: "1px solid var(--line-soft)" }}>
|
||||
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 5 }}>ОКНО ВРЕМЕНИ</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--ink-1)" }}>{fmtDateTime(win.t0)} → {fmtDateTime(win.t1)}</div>
|
||||
<div style={{ fontSize: 10, color: "var(--ink-3)", marginTop: 3 }}>{plan ? `план ${plan.id}` : "сутки вокруг «сейчас»"}</div>
|
||||
</div>
|
||||
|
||||
{/* слои */}
|
||||
<div style={{ padding: "10px 14px", borderBottom: "1px solid var(--line-soft)" }}>
|
||||
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 7 }}>СЛОИ</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<LayerRow on={layers.tracks} onClick={() => toggleLayer("tracks")} swatch={<LineSwatch />} label="Трассы КА" />
|
||||
<LayerRow on={layers.swath} onClick={() => toggleLayer("swath")} swatch={<BandSwatch />} label="Полосы обзора (swath)" />
|
||||
<LayerRow on={layers.shoot} onClick={() => toggleLayer("shoot")} swatch={<PolySwatch />} label="Контуры маршрутов съёмки" />
|
||||
<LayerRow on={layers.downlink} onClick={() => toggleLayer("downlink")} swatch={<DownSwatch />} label="Участки сброса" />
|
||||
<LayerRow on={layers.stations} onClick={() => toggleLayer("stations")} swatch={<StationSwatch />} label="Станции приёма (НКПОР)" />
|
||||
<LayerRow on={layers.terminator} onClick={() => toggleLayer("terminator")} swatch={<NightSwatch />} label="Терминатор день/ночь" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* выбор КА */}
|
||||
<div style={{ padding: "10px 14px 6px" }}>
|
||||
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", letterSpacing: ".1em", marginBottom: 6 }}>АППАРАТ В ФОКУСЕ</div>
|
||||
<div style={{ position: "relative" }}>
|
||||
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Поиск КА…"
|
||||
style={{ width: "100%", padding: "7px 8px", background: "var(--bg-0)", border: "1px solid var(--line)", borderRadius: 6, color: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12, outline: "none" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "4px 8px 12px" }}>
|
||||
{scResults.map((s) => (
|
||||
<div key={s.id} onClick={() => onSelectSc(s.id)}
|
||||
style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, cursor: "pointer", background: s.id === selSc ? "var(--bg-3)" : "transparent" }}>
|
||||
<TypeDot type={s.type} size={7} />
|
||||
<span className="mono" style={{ flex: 1, fontSize: 11, color: s.id === selSc ? "var(--ink)" : "var(--ink-1)" }}>{s.short}</span>
|
||||
<span className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)" }}>{(ivBySc[s.id] || []).length} пл.</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КАРТА */}
|
||||
<div style={{ position: "relative", minWidth: 0 }}>
|
||||
<MapView scList={scList} selectedPlan={plan} window={win} now={D.NOW} layers={layers}
|
||||
onSelectSc={onSelectSc} paramsBy={paramsBy} scById={scById} basemap={basemap} />
|
||||
{/* плашка масштаба/охвата */}
|
||||
<div style={{ position: "absolute", left: 12, top: 12, display: "flex", gap: 8, alignItems: "center", background: "var(--bg-1)", border: "1px solid var(--line)", borderRadius: 7, padding: "6px 10px" }}>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--ink-3)" }}>ОХВАТ:</span>
|
||||
<span style={{ fontSize: 11.5, fontWeight: 600 }}>{scope === "one" ? "1 КА" : scope === "group" ? "Группа" : "Все"}</span>
|
||||
<span style={{ width: 1, height: 14, background: "var(--line)" }} />
|
||||
<span className="mono tnum" style={{ fontSize: 11.5, color: "var(--ink-1)" }}>{scList.length} КА</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ДЕТАЛИ ПЛАНА */}
|
||||
{plan && <MapDetails plan={plan} sc={scById[plan.scId]} params={paramsBy[plan.scId]} byId={byId} onSelectPlan={onSelectPlan} onClose={() => setSelPlan(null)} planPage={PLAN_PAGE} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LayerRow({ on, onClick, swatch, label }) {
|
||||
return (
|
||||
<button onClick={onClick} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 6px", borderRadius: 6, border: "1px solid transparent", background: on ? "var(--bg-2)" : "transparent", opacity: on ? 1 : 0.4, textAlign: "left" }}>
|
||||
<span style={{ width: 18, flex: "none", display: "grid", placeItems: "center" }}>{swatch}</span>
|
||||
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)" }}>{label}</span>
|
||||
<span style={{ width: 14, height: 14, borderRadius: 4, flex: "none", border: `1.5px solid ${on ? "var(--accent)" : "var(--line)"}`, background: on ? "var(--accent)" : "transparent", display: "grid", placeItems: "center" }}>
|
||||
{on && <svg width="9" height="9" viewBox="0 0 10 10"><path d="M1.5 5.2 4 7.6 8.6 2.4" stroke="var(--bg-0)" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
const LineSwatch = () => <svg width="18" height="10"><path d="M0 8 Q6 0 12 5 T18 3" stroke="var(--t-optical)" strokeWidth="1.4" fill="none" /></svg>;
|
||||
const BandSwatch = () => <svg width="18" height="10"><path d="M0 8 Q6 0 12 5 T18 3" stroke="var(--t-optical)" strokeWidth="5" fill="none" opacity="0.25" /></svg>;
|
||||
const PolySwatch = () => <svg width="18" height="10"><rect x="2" y="2" width="14" height="6" fill="color-mix(in oklch, var(--t-optical) 30%, transparent)" stroke="var(--t-optical)" strokeWidth="1" /></svg>;
|
||||
const DownSwatch = () => <svg width="18" height="10"><path d="M1 5 H17" stroke="var(--accent)" strokeWidth="3" strokeLinecap="round" /></svg>;
|
||||
const StationSwatch = () => <svg width="18" height="10"><path d="M9 1 L13 8 L5 8 Z" fill="rgba(180,200,210,0.9)" /></svg>;
|
||||
const NightSwatch = () => <svg width="18" height="10"><rect x="0" y="0" width="9" height="10" fill="rgba(4,8,12,0.6)" /><rect x="9" y="0" width="9" height="10" fill="#15291f" /></svg>;
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<MapApp />);
|
||||
@@ -0,0 +1,125 @@
|
||||
// Панель деталей плана на странице «Карта»: сводка, наземные параметры,
|
||||
// работы (со «съёмка/сброс»), ссылка обратно на таймлайн.
|
||||
function MapStatusBadge({ status }) {
|
||||
const map = {
|
||||
executed: { t: "ВЫПОЛНЕН", c: "var(--ink-2)", bg: "var(--bg-3)" },
|
||||
active: { t: "АКТИВЕН", c: "var(--now)", bg: "oklch(0.86 0.16 25 / 0.16)" },
|
||||
planned: { t: "ЗАПЛАНИРОВАН", c: "var(--accent)", bg: "oklch(0.82 0.13 200 / 0.14)" },
|
||||
};
|
||||
const s = map[status] || map.planned;
|
||||
return <span className="mono" style={{ fontSize: 9.5, fontWeight: 600, letterSpacing: ".06em", color: s.c, background: s.bg, padding: "2px 7px", borderRadius: 4 }}>{s.t}</span>;
|
||||
}
|
||||
|
||||
function MapDetails({ plan, sc, params, byId, onSelectPlan, onClose, planPage }) {
|
||||
const col = TYPE_COLOR[sc.type];
|
||||
const shoots = plan.works.filter(w => w.kind === "shoot");
|
||||
const downs = plan.works.filter(w => w.kind === "downlink");
|
||||
const STATIONS = window.WorldGeo.GROUND_STATIONS;
|
||||
|
||||
// станции, задействованные в сбросах
|
||||
const usedStations = [];
|
||||
for (const d of downs) {
|
||||
const pts = trackPoints(params, d.start, d.end, 10);
|
||||
const mid = pts[Math.floor(pts.length / 2)];
|
||||
const ns = nearestStation(mid.lat, mid.lon, STATIONS);
|
||||
if (ns.st && !usedStations.find(x => x.id === ns.st.id)) usedStations.push(ns.st);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ background: "var(--bg-1)", borderLeft: "1px solid var(--line)", display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||
<div style={{ padding: "14px 16px 12px", borderBottom: "1px solid var(--line-soft)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 8 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
|
||||
<span style={{ width: 10, height: 10, borderRadius: 3, background: col, boxShadow: `0 0 8px ${col}`, flex: "none" }} />
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--ink-3)" }}>{plan.id}</span>
|
||||
</div>
|
||||
<button onClick={onClose} style={{ background: "none", border: "none", color: "var(--ink-3)", fontSize: 18, lineHeight: 1, padding: 0 }}>×</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, marginTop: 8, lineHeight: 1.2 }}>{plan.title}</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 9 }}>
|
||||
<MapStatusBadge status={plan.status} />
|
||||
<span className="mono" style={{ fontSize: 10.5, color: "var(--ink-2)" }}>{sc.short} · {sc.orbit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "14px 16px 20px", display: "flex", flexDirection: "column", gap: 15 }}>
|
||||
{/* наземные параметры */}
|
||||
<div>
|
||||
<div className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em", marginBottom: 8 }}>НАЗЕМНЫЕ ПАРАМЕТРЫ</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
||||
<KV label="ВЫСОТА ОРБИТЫ">{params.h} км</KV>
|
||||
<KV label="ПЕРИОД ВИТКА">{(params.T / 60).toFixed(1)} мин</KV>
|
||||
<KV label="ПОЛОСА ОБЗОРА">{Math.round(params.swathKm)} км</KV>
|
||||
<KV label="НАКЛОНЕНИЕ">{(params.incl * RAD).toFixed(1)}°</KV>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "var(--line-soft)" }} />
|
||||
|
||||
{/* съёмка */}
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
|
||||
<span style={{ width: 10, height: 8, background: col, borderRadius: 2, flex: "none" }} />
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>КОНТУРЫ СЪЁМКИ</span>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", marginLeft: "auto" }}>{shoots.length}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
{shoots.length === 0 && <div style={{ fontSize: 11, color: "var(--ink-3)" }}>Съёмочных маршрутов нет.</div>}
|
||||
{shoots.map(w => (
|
||||
<div key={w.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: "var(--bg-2)" }}>
|
||||
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{w.label}</span>
|
||||
<span className="mono tnum" style={{ fontSize: 10, color: "var(--ink-3)" }}>{fmtTime(w.start)}–{fmtTime(w.end)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* сброс */}
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
|
||||
<span style={{ width: 12, height: 3, background: "var(--accent)", borderRadius: 2, flex: "none", boxShadow: "0 0 5px var(--accent)" }} />
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".08em" }}>УЧАСТКИ СБРОСА</span>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", marginLeft: "auto" }}>{downs.length}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
{downs.length === 0 && <div style={{ fontSize: 11, color: "var(--ink-3)" }}>Сбросов нет.</div>}
|
||||
{downs.map(w => {
|
||||
const pts = trackPoints(params, w.start, w.end, 10);
|
||||
const mid = pts[Math.floor(pts.length / 2)];
|
||||
const ns = nearestStation(mid.lat, mid.lon, STATIONS);
|
||||
return (
|
||||
<div key={w.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "5px 8px", borderRadius: 6, background: "var(--bg-2)" }}>
|
||||
<span style={{ flex: 1, fontSize: 11.5, color: "var(--ink-1)" }}>→ {ns.st.name}</span>
|
||||
<span className="mono tnum" style={{ fontSize: 10, color: "var(--ink-3)" }}>{fmtTime(w.start)}–{fmtTime(w.end)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{usedStations.length > 0 && (
|
||||
<div style={{ fontSize: 10.5, color: "var(--ink-3)", lineHeight: 1.5 }}>
|
||||
Задействованы станции: <span style={{ color: "var(--ink-1)" }}>{usedStations.map(s => s.name).join(", ")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "12px 16px", borderTop: "1px solid var(--line-soft)" }}>
|
||||
<a href={`${planPage}?plan=${plan.id}`} style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 7, border: "1px solid var(--line)", background: "var(--bg-2)", color: "var(--ink-1)", fontSize: 12, fontWeight: 600, textDecoration: "none" }}>
|
||||
← Открыть в таймлайне планов
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KV({ label, children }) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--ink-3)", letterSpacing: ".06em" }}>{label}</span>
|
||||
<span className="mono tnum" style={{ fontSize: 12.5, color: "var(--ink)" }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MapDetails });
|
||||
@@ -0,0 +1,300 @@
|
||||
// Карта: Web Mercator. Canvas-основа (тайлы OSM/CARTO или векторная схема,
|
||||
// сетка, трассы, swath, терминатор) + SVG-overlay (станции, «сейчас»,
|
||||
// контуры съёмки, сброс, hover). Зум/панорама как у slippy-карты.
|
||||
const { useRef: useRefMV, useState: useStateMV, useEffect: useEffectMV, useMemo: useMemoMV, useCallback } = React;
|
||||
|
||||
const STATIONS = window.WorldGeo.GROUND_STATIONS;
|
||||
const BM = window.Basemap;
|
||||
const TILE = BM.TILE;
|
||||
|
||||
function MapView(props) {
|
||||
const { scList, selectedPlan, window: win, now, layers, onSelectSc, paramsBy, scById, basemap } = props;
|
||||
const wrapRef = useRefMV(null);
|
||||
const canvasRef = useRefMV(null);
|
||||
const [dims, setDims] = useStateMV({ w: 1000, h: 560 });
|
||||
const [view, setView] = useStateMV({ centerLon: 45, centerLat: 30, z: 2.2, init: false });
|
||||
const [hover, setHover] = useStateMV(null);
|
||||
const [tileTick, setTileTick] = useStateMV(0);
|
||||
const drag = useRefMV(null);
|
||||
const provider = BM.PROVIDERS[basemap] || BM.PROVIDERS.dark;
|
||||
|
||||
const ws = useMemoMV(() => TILE * Math.pow(2, view.z), [view.z]);
|
||||
|
||||
// Инициализация: вписать мир по ширине.
|
||||
useEffectMV(() => {
|
||||
if (view.init || !dims.w) return;
|
||||
const z = Math.max(0.7, Math.log2(dims.w / TILE) - 0.05);
|
||||
setView((v) => ({ ...v, z, centerLon: 45, centerLat: 28, init: true }));
|
||||
}, [dims, view.init]);
|
||||
|
||||
const project = useCallback((lon, lat) => {
|
||||
const cwx = BM.lonToWorldX(view.centerLon, ws);
|
||||
const cwy = BM.latToWorldY(view.centerLat, ws);
|
||||
let wx = BM.lonToWorldX(lon, ws);
|
||||
// выбрать ближайшую копию мира по X (горизонтальный повтор)
|
||||
const half = ws / 2;
|
||||
while (wx - cwx > half) wx -= ws;
|
||||
while (wx - cwx < -half) wx += ws;
|
||||
return {
|
||||
x: dims.w / 2 + (wx - cwx),
|
||||
y: dims.h / 2 + (BM.latToWorldY(lat, ws) - cwy),
|
||||
};
|
||||
}, [view.centerLon, view.centerLat, ws, dims]);
|
||||
|
||||
const unproject = useCallback((sx, sy) => {
|
||||
const cwx = BM.lonToWorldX(view.centerLon, ws);
|
||||
const cwy = BM.latToWorldY(view.centerLat, ws);
|
||||
return {
|
||||
lon: BM.worldXToLon(cwx + (sx - dims.w / 2), ws),
|
||||
lat: BM.worldYToLat(cwy + (sy - dims.h / 2), ws),
|
||||
};
|
||||
}, [view.centerLon, view.centerLat, ws, dims]);
|
||||
|
||||
// ResizeObserver
|
||||
useEffectMV(() => {
|
||||
const el = wrapRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => setDims({ w: el.clientWidth, h: el.clientHeight }));
|
||||
ro.observe(el);
|
||||
setDims({ w: el.clientWidth, h: el.clientHeight });
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
// Отрисовка canvas.
|
||||
useEffectMV(() => {
|
||||
const cv = canvasRef.current;
|
||||
if (!cv) return;
|
||||
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
||||
cv.width = dims.w * dpr; cv.height = dims.h * dpr;
|
||||
cv.style.width = dims.w + "px"; cv.style.height = dims.h + "px";
|
||||
const ctx = cv.getContext("2d");
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, dims.w, dims.h);
|
||||
|
||||
// фон
|
||||
ctx.fillStyle = provider.dark ? "#0a1410" : "#0a1410";
|
||||
ctx.fillRect(0, 0, dims.w, dims.h);
|
||||
|
||||
const P = (lon, lat) => project(lon, lat);
|
||||
|
||||
if (provider.url) {
|
||||
// растровая подложка (тайлы)
|
||||
BM.drawTiles(ctx, provider, view, dims, () => setTileTick((t) => t + 1));
|
||||
} else {
|
||||
// векторная схема (оффлайн)
|
||||
drawVectorWorld(ctx, P, dims);
|
||||
}
|
||||
|
||||
// сетка координат поверх (тонкая)
|
||||
ctx.strokeStyle = provider.dark ? "rgba(120,180,150,0.10)" : "rgba(255,255,255,0.14)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
for (let lon = -180; lon <= 180; lon += 30) { const a = P(lon, 84), b = P(lon, -84); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); }
|
||||
for (let lat = -60; lat <= 60; lat += 30) { const a = P(-180, lat), b = P(180, lat); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); }
|
||||
ctx.stroke();
|
||||
|
||||
// терминатор / ночь
|
||||
if (layers.terminator) {
|
||||
const tc = terminatorCurve(now);
|
||||
ctx.beginPath();
|
||||
tc.curve.forEach((pt, i) => { const q = P(pt[0], pt[1]); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
|
||||
const topLat = tc.nightInNorth ? 84 : -84;
|
||||
const c1 = P(180, topLat), c2 = P(-180, topLat);
|
||||
ctx.lineTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "rgba(4,8,12,0.42)";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "rgba(255,210,120,0.30)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
tc.curve.forEach((pt, i) => { const q = P(pt[0], pt[1]); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// трассы + swath
|
||||
const TYPE_RGB = { optical: [126,222,180], radar: [232,200,120], combo: [240,150,165] };
|
||||
const many = scList.length;
|
||||
const showSwath = layers.swath && many <= 8;
|
||||
const trackAlpha = many > 40 ? 0.14 : many > 14 ? 0.22 : many > 4 ? 0.4 : 0.72;
|
||||
for (const sc of scList) {
|
||||
const p = paramsBy[sc.id];
|
||||
const rgb = TYPE_RGB[sc.type];
|
||||
const segs = groundTrack(p, win.t0, win.t1, 900);
|
||||
const isSelSc = selectedPlan && selectedPlan.scId === sc.id;
|
||||
if (showSwath || isSelSc) {
|
||||
for (const seg of segs) {
|
||||
if (!seg.length) continue;
|
||||
const midLat = seg[Math.floor(seg.length / 2)].lat;
|
||||
const mpp = BM.metersPerPixel(midLat, ws);
|
||||
const wpx = Math.max(1.5, (p.swathKm * 1000) / mpp);
|
||||
ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${isSelSc ? 0.16 : 0.09})`;
|
||||
ctx.lineWidth = wpx; ctx.lineCap = "round"; ctx.lineJoin = "round";
|
||||
ctx.beginPath();
|
||||
seg.forEach((pt, i) => { const q = P(pt.lon, pt.lat); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
if (layers.tracks) {
|
||||
ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${isSelSc ? 0.98 : trackAlpha})`;
|
||||
ctx.lineWidth = isSelSc ? 2 : many > 14 ? 0.6 : 0.9;
|
||||
for (const seg of segs) {
|
||||
ctx.beginPath();
|
||||
seg.forEach((pt, i) => { const q = P(pt.lon, pt.lat); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [dims, view, scList, selectedPlan, win, now, layers, project, ws, provider, tileTick]);
|
||||
|
||||
// ---- overlay (SVG) ----
|
||||
const planOverlay = useMemoMV(() => {
|
||||
if (!selectedPlan) return null;
|
||||
const p = paramsBy[selectedPlan.scId];
|
||||
const sc = scById[selectedPlan.scId];
|
||||
const shoots = [], downs = [];
|
||||
for (const wk of selectedPlan.works) {
|
||||
if (wk.kind === "shoot" && layers.shoot) {
|
||||
const pts = trackPoints(p, wk.start, wk.end, 18);
|
||||
const poly = ribbon(pts, p.swathKm / 2);
|
||||
shoots.push({ id: wk.id, label: wk.label, poly: poly.map(c => project(c[0], c[1])) });
|
||||
}
|
||||
if (wk.kind === "downlink" && layers.downlink) {
|
||||
const pts = trackPoints(p, wk.start, wk.end, 14);
|
||||
const mid = pts[Math.floor(pts.length / 2)];
|
||||
const ns = nearestStation(mid.lat, mid.lon, STATIONS);
|
||||
downs.push({
|
||||
id: wk.id, label: wk.label,
|
||||
line: pts.map(pt => project(pt.lon, pt.lat)),
|
||||
st: ns.st, stPt: project(ns.st.lon, ns.st.lat), midPt: project(mid.lon, mid.lat),
|
||||
});
|
||||
}
|
||||
}
|
||||
return { shoots, downs, scType: sc.type };
|
||||
}, [selectedPlan, layers, project, paramsBy, scById]);
|
||||
|
||||
const nowMarkers = useMemoMV(() => {
|
||||
if (now < win.t0 || now > win.t1) return [];
|
||||
return scList.map((sc) => {
|
||||
const s = subPoint(paramsBy[sc.id], now);
|
||||
return { id: sc.id, short: sc.short, type: sc.type, ...project(s.lon, s.lat) };
|
||||
});
|
||||
}, [scList, now, win, project, paramsBy]);
|
||||
|
||||
const TYPE_HEX = { optical: "var(--t-optical)", radar: "var(--t-radar)", combo: "var(--t-combo)" };
|
||||
|
||||
// ---- pan / zoom ----
|
||||
const onWheel = (e) => {
|
||||
e.preventDefault();
|
||||
const rect = wrapRef.current.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
||||
const before = unproject(mx, my);
|
||||
setView((v) => {
|
||||
const nz = Math.max(0.7, Math.min(9, v.z + (e.deltaY < 0 ? 0.4 : -0.4)));
|
||||
const nws = TILE * Math.pow(2, nz);
|
||||
const cwx = BM.lonToWorldX(before.lon, nws) - (mx - dims.w / 2);
|
||||
const cwy = BM.latToWorldY(before.lat, nws) - (my - dims.h / 2);
|
||||
return { ...v, z: nz, centerLon: BM.worldXToLon(cwx, nws), centerLat: BM.worldYToLat(cwy, nws) };
|
||||
});
|
||||
};
|
||||
const onDown = (e) => {
|
||||
drag.current = { x: e.clientX, y: e.clientY, lon: view.centerLon, lat: view.centerLat, moved: false };
|
||||
};
|
||||
const onMove = (e) => {
|
||||
if (!drag.current) return;
|
||||
const dx = e.clientX - drag.current.x, dy = e.clientY - drag.current.y;
|
||||
if (Math.abs(dx) + Math.abs(dy) > 3) drag.current.moved = true;
|
||||
const cwx = BM.lonToWorldX(drag.current.lon, ws) - dx;
|
||||
const cwy = BM.latToWorldY(drag.current.lat, ws) - dy;
|
||||
setView((v) => ({ ...v, centerLon: BM.worldXToLon(cwx, ws), centerLat: Math.max(-82, Math.min(82, BM.worldYToLat(cwy, ws))) }));
|
||||
};
|
||||
const onUp = () => { drag.current = null; };
|
||||
const zoomBy = (f) => setView((v) => ({ ...v, z: Math.max(0.7, Math.min(9, v.z + f)) }));
|
||||
const resetView = () => setView((v) => ({ ...v, init: false }));
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} style={{ position: "absolute", inset: 0, overflow: "hidden", cursor: drag.current ? "grabbing" : "grab", background: "#0a1410" }}
|
||||
onWheel={onWheel} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}>
|
||||
<canvas ref={canvasRef} style={{ position: "absolute", inset: 0 }} />
|
||||
|
||||
<svg style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none", overflow: "visible" }}>
|
||||
{planOverlay && planOverlay.shoots.map((s) => (
|
||||
<polygon key={s.id} points={s.poly.map(p => `${p.x},${p.y}`).join(" ")}
|
||||
fill={`color-mix(in oklch, ${TYPE_HEX[planOverlay.scType]} 32%, transparent)`}
|
||||
stroke={TYPE_HEX[planOverlay.scType]} strokeWidth="1.4" style={{ pointerEvents: "all", cursor: "pointer" }}
|
||||
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: s.label, kind: "Съёмка / контур маршрута" })}
|
||||
onMouseLeave={() => setHover(null)} />
|
||||
))}
|
||||
{planOverlay && planOverlay.downs.map((d) => (
|
||||
<g key={d.id}>
|
||||
<line x1={d.midPt.x} y1={d.midPt.y} x2={d.stPt.x} y2={d.stPt.y} stroke="var(--accent)" strokeWidth="1" strokeDasharray="2 3" opacity="0.6" />
|
||||
<polyline points={d.line.map(p => `${p.x},${p.y}`).join(" ")} fill="none"
|
||||
stroke="var(--accent)" strokeWidth="3.4" strokeLinecap="round"
|
||||
style={{ pointerEvents: "all", cursor: "pointer", filter: "drop-shadow(0 0 4px var(--accent))" }}
|
||||
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: `${d.label} → ${d.st.name}`, kind: "Сброс на НКПОР" })}
|
||||
onMouseLeave={() => setHover(null)} />
|
||||
</g>
|
||||
))}
|
||||
|
||||
{layers.stations && STATIONS.map((st) => {
|
||||
const q = project(st.lon, st.lat);
|
||||
const active = planOverlay && planOverlay.downs.some(d => d.st.id === st.id);
|
||||
return (
|
||||
<g key={st.id} transform={`translate(${q.x},${q.y})`} style={{ pointerEvents: "all", cursor: "default" }}
|
||||
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: st.name, kind: "Станция приёма (НКПОР)" })}
|
||||
onMouseLeave={() => setHover(null)}>
|
||||
<path d="M0,-6 L5,4 L-5,4 Z" fill={active ? "var(--accent)" : "rgba(190,210,220,0.92)"} stroke="#0a1410" strokeWidth="1" />
|
||||
{view.z > 3.6 && <text x="7" y="3" fontSize="9" fill={provider.dark ? "var(--ink-2)" : "#dfeaf0"} style={{ fontFamily: "var(--mono)", paintOrder: "stroke", stroke: "#0a1410", strokeWidth: 2 }}>{st.name}</text>}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{nowMarkers.map((m) => (
|
||||
<g key={m.id} transform={`translate(${m.x},${m.y})`} style={{ pointerEvents: "all", cursor: "pointer" }}
|
||||
onClick={() => onSelectSc(m.id)}
|
||||
onMouseEnter={(e) => setHover({ x: e.clientX, y: e.clientY, title: m.short, kind: "Подспутниковая точка · сейчас" })}
|
||||
onMouseLeave={() => setHover(null)}>
|
||||
<circle r="4.5" fill={TYPE_HEX[m.type]} stroke="#0a1410" strokeWidth="1.5" />
|
||||
<circle r="8" fill="none" stroke={TYPE_HEX[m.type]} strokeWidth="1" opacity="0.5" />
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
{hover && (
|
||||
<div style={{ position: "fixed", left: hover.x + 12, top: hover.y + 12, zIndex: 50, pointerEvents: "none",
|
||||
background: "var(--bg-0)", border: "1px solid var(--line)", borderRadius: 6, padding: "6px 9px", boxShadow: "var(--shadow)", maxWidth: 220 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--ink)" }}>{hover.title}</div>
|
||||
<div className="mono" style={{ fontSize: 9.5, color: "var(--ink-3)", marginTop: 2 }}>{hover.kind}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* атрибуция подложки */}
|
||||
<div className="mono" style={{ position: "absolute", left: 8, bottom: 6, fontSize: 9, color: provider.dark ? "var(--ink-3)" : "rgba(255,255,255,0.8)", background: "rgba(10,20,16,0.55)", padding: "2px 6px", borderRadius: 4, pointerEvents: "none" }}>{provider.label}</div>
|
||||
|
||||
<div style={{ position: "absolute", right: 12, bottom: 12, display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<ZoomBtn onClick={() => zoomBy(0.5)}>+</ZoomBtn>
|
||||
<ZoomBtn onClick={() => zoomBy(-0.5)}>−</ZoomBtn>
|
||||
<ZoomBtn onClick={resetView} title="Сбросить вид">⊡</ZoomBtn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Векторная схема континентов (режим «Схема», оффлайн).
|
||||
function drawVectorWorld(ctx, P, dims) {
|
||||
for (const name in window.WorldGeo.LAND) {
|
||||
const poly = window.WorldGeo.LAND[name];
|
||||
ctx.beginPath();
|
||||
poly.forEach((pt, i) => { const q = P(pt[0], pt[1]); i ? ctx.lineTo(q.x, q.y) : ctx.moveTo(q.x, q.y); });
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "#173228"; ctx.fill();
|
||||
ctx.strokeStyle = "rgba(150,215,180,0.5)"; ctx.lineWidth = 0.9; ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function ZoomBtn({ children, onClick, title }) {
|
||||
return (
|
||||
<button onClick={onClick} title={title} style={{ width: 30, height: 30, borderRadius: 6, border: "1px solid var(--line)", background: "var(--bg-1)", color: "var(--ink-1)", fontSize: 16, lineHeight: 1, display: "grid", placeItems: "center" }}>{children}</button>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MapView });
|
||||
@@ -0,0 +1,142 @@
|
||||
// Орбитальная геометрия: трассы КА (подспутниковая точка), полосы обзора (swath),
|
||||
// контуры маршрутов съёмки, участки сброса, терминатор день/ночь.
|
||||
const DEG = Math.PI / 180;
|
||||
const RAD = 180 / Math.PI;
|
||||
const RE_KM = 6371;
|
||||
const MU = 398600.4418; // км^3/с^2
|
||||
const SIDEREAL = 86164; // период вращения Земли, с
|
||||
|
||||
// Детерминированный хэш строки → [0,1).
|
||||
function hash01(str, salt) {
|
||||
let h = 2166136261 ^ (salt || 0);
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
}
|
||||
|
||||
// Параметры орбиты КА (ССО). Высоту берём из sc.orbit, наклонение ~98°.
|
||||
function orbitParams(sc) {
|
||||
const m = /(\d+)\s*км/.exec(sc.orbit);
|
||||
const h = m ? +m[1] : 550;
|
||||
const a = RE_KM + h;
|
||||
const T = 2 * Math.PI * Math.sqrt((a * a * a) / MU); // период, с
|
||||
const incl = (97.4 + hash01(sc.id, 7) * 1.6) * DEG; // 97.4–99.0°
|
||||
const node0 = hash01(sc.id, 3) * 360 * DEG; // долгота восх. узла @ эпоха
|
||||
const u0 = hash01(sc.id, 11) * 2 * Math.PI; // фаза
|
||||
// Ширина полосы обзора (км) по типу аппаратуры.
|
||||
const swathKm = sc.type === "radar" ? 90 + hash01(sc.id, 5) * 110
|
||||
: sc.type === "combo" ? 50 + hash01(sc.id, 5) * 70
|
||||
: 24 + hash01(sc.id, 5) * 36;
|
||||
return { h, a, T, incl, node0, u0, swathKm };
|
||||
}
|
||||
|
||||
const EPOCH = (window.MissionData ? window.MissionData.RANGE_START : 0);
|
||||
|
||||
// Подспутниковая точка в момент t (мс).
|
||||
function subPoint(p, tMs) {
|
||||
const dt = (tMs - EPOCH) / 1000; // с
|
||||
const u = p.u0 + 2 * Math.PI * (dt / p.T);
|
||||
const lat = Math.asin(Math.sin(p.incl) * Math.sin(u)) * RAD;
|
||||
let lonRel = Math.atan2(Math.cos(p.incl) * Math.sin(u), Math.cos(u));
|
||||
let lon = (p.node0 + lonRel) * RAD - (360 / SIDEREAL) * dt;
|
||||
lon = ((((lon + 180) % 360) + 360) % 360) - 180;
|
||||
// курс (для перпендикуляра swath) — численно
|
||||
return { lat, lon, u };
|
||||
}
|
||||
|
||||
// Трасса за [t0,t1] с адаптивным шагом. Возвращает сегменты (разрыв на ±180°).
|
||||
function groundTrack(p, t0, t1, maxPts) {
|
||||
const span = t1 - t0;
|
||||
const idealStep = p.T * 1000 / 36; // ~36 точек на виток
|
||||
let step = idealStep;
|
||||
const cap = maxPts || 900;
|
||||
if (span / step > cap) step = span / cap;
|
||||
const segs = [];
|
||||
let cur = [];
|
||||
let prevLon = null;
|
||||
for (let t = t0; t <= t1; t += step) {
|
||||
const s = subPoint(p, t);
|
||||
if (prevLon !== null && Math.abs(s.lon - prevLon) > 180) {
|
||||
if (cur.length) segs.push(cur);
|
||||
cur = [];
|
||||
}
|
||||
cur.push({ t, lat: s.lat, lon: s.lon });
|
||||
prevLon = s.lon;
|
||||
}
|
||||
if (cur.length) segs.push(cur);
|
||||
return segs;
|
||||
}
|
||||
|
||||
// Точки трассы строго внутри [t0,t1] (для footprint работы), без разрыва.
|
||||
function trackPoints(p, t0, t1, n) {
|
||||
const pts = [];
|
||||
const k = Math.max(2, n || 24);
|
||||
for (let i = 0; i <= k; i++) {
|
||||
const t = t0 + (t1 - t0) * (i / k);
|
||||
const s = subPoint(p, t);
|
||||
pts.push({ t, lat: s.lat, lon: s.lon });
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// Лента (ribbon) шириной halfKm по обе стороны трассы → замкнутый полигон [lon,lat].
|
||||
function ribbon(points, halfKm) {
|
||||
const half = halfKm / 111; // градусы широты
|
||||
const left = [], right = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const a = points[Math.max(0, i - 1)];
|
||||
const b = points[Math.min(points.length - 1, i + 1)];
|
||||
const cosLat = Math.cos(points[i].lat * DEG) || 0.01;
|
||||
let dx = (b.lon - a.lon) * cosLat;
|
||||
let dy = (b.lat - a.lat);
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
dx /= len; dy /= len;
|
||||
// перпендикуляр
|
||||
const px = -dy, py = dx;
|
||||
const p = points[i];
|
||||
left.push([p.lon + (px * half) / cosLat, p.lat + py * half]);
|
||||
right.push([p.lon - (px * half) / cosLat, p.lat - py * half]);
|
||||
}
|
||||
return left.concat(right.reverse());
|
||||
}
|
||||
|
||||
// Видимость станции из подспутниковой точки (грубо: по угловому расстоянию).
|
||||
function nearestStation(lat, lon, stations) {
|
||||
let best = null, bd = 1e9;
|
||||
for (const st of stations) {
|
||||
const d = Math.hypot((st.lon - lon) * Math.cos(lat * DEG), st.lat - lat);
|
||||
if (d < bd) { bd = d; best = st; }
|
||||
}
|
||||
return { st: best, distDeg: bd };
|
||||
}
|
||||
|
||||
// Подсолнечная точка (примитивно) для терминатора в момент tMs.
|
||||
function subsolar(tMs) {
|
||||
const d = new Date(tMs);
|
||||
const dayMs = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
||||
const secs = (tMs - dayMs) / 1000;
|
||||
const lon = 180 - (secs / 86400) * 360; // движется на запад
|
||||
const N = Math.floor((tMs - Date.UTC(d.getUTCFullYear(), 0, 0)) / 86400000);
|
||||
const decl = 23.44 * Math.sin(DEG * (360 / 365) * (N - 81)); // склонение
|
||||
return { lon: ((lon + 180) % 360 + 360) % 360 - 180, lat: decl };
|
||||
}
|
||||
|
||||
// Кривая терминатора: для каждого lon — широта раздела день/ночь.
|
||||
function terminatorCurve(tMs) {
|
||||
const ss = subsolar(tMs);
|
||||
const decl = ss.lat * DEG;
|
||||
const out = [];
|
||||
for (let lon = -180; lon <= 180; lon += 4) {
|
||||
const H = (lon - ss.lon) * DEG;
|
||||
// граница: tan(lat) = -cos(H)/tan(decl)
|
||||
let lat;
|
||||
if (Math.abs(decl) < 0.01) lat = 0;
|
||||
else lat = Math.atan(-Math.cos(H) / Math.tan(decl)) * RAD;
|
||||
out.push([lon, lat]);
|
||||
}
|
||||
return { curve: out, nightInNorth: ss.lat < 0, subsolar: ss };
|
||||
}
|
||||
|
||||
Object.assign(window, {
|
||||
orbitParams, subPoint, groundTrack, trackPoints, ribbon,
|
||||
nearestStation, subsolar, terminatorCurve, DEG, RAD,
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user