feat(gateway): сервис pcp-gateway-service + сворачивание маршрутизации фронта на него

- новый Spring Cloud Gateway (WebFlux): доменные маршруты /api/pcp-<x>/**
  со StripPrefix=2, /api/stations без среза пути, catch-all /api/** → ui-service;
  опциональная JWT-валидация на edge (gateway.security.jwt-enabled, по умолчанию off)
- config-repo: pcp-gateway-service.yaml (маршруты) + порт/адрес gateway в реестрах
  application-{local,docker-local,dev}
- build: репозитории переведены на mavenCentral (nstart-proxy закомментирован),
  включены mavenLocal/gradlePluginPortal; settings: include модуля
- pcp-tgu-ui-service: vite-proxy и nginx.conf свёрнуты на единый /api → gateway
  (вместо набора per-service правил)
- deploy: сервис gateway в docker-compose; helm-чарт pcp-gateway-service
- CI: .gitlab/ci/pcp-gateway-service.yml + проводка в .gitlab-ci.yml
This commit is contained in:
Дмитрий Соловьев
2026-06-10 13:34:16 +03:00
parent 6c2491cbb9
commit ab3a787da5
24 changed files with 652 additions and 95 deletions
+9
View File
@@ -0,0 +1,9 @@
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
FROM ${RUNTIME_IMAGE}
ENV JAVA_OPTS=""
ADD ./build/libs/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
@@ -0,0 +1,50 @@
group = "space.nstart.pcp"
plugins {
kotlin("jvm")
kotlin("plugin.spring")
id("org.springframework.boot")
id("io.spring.dependency-management")
}
version = "0.0.1"
java {
toolchain {
languageVersion = JavaLanguageVersion.of((property("versions.java") as String).toInt())
}
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
dependencies {
// Реактивный edge: Spring Cloud Gateway (WebFlux-вариант) + конфиг из config-server.
// Маршруты живут в config-repo/pcp-gateway-service.yaml — единый источник правды.
implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webflux")
implementation("org.springframework.cloud:spring-cloud-starter-config")
implementation("org.springframework.boot:spring-boot-starter-actuator")
// Валидация JWT на edge (Keycloak/new-start-id) — включается флагом gateway.security.jwt-enabled.
// Тянет reactive Spring Security (ServerHttpSecurity/@EnableWebFluxSecurity).
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test> {
useJUnitPlatform()
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("versions.spring.cloud")}")
}
}
@@ -0,0 +1,16 @@
package space.nstart.pcp_gateway_service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
/**
* Единая точка входа в бэкенд для фронта (pcp-tgu-ui-service): reverse-proxy поверх
* микросервисов. Маршрутная таблица — в config-repo/pcp-gateway-service.yaml и ссылается
* на реестр `pcp.services.*`, так что топологию знает только gateway, а не браузер.
*/
@SpringBootApplication
class GatewayApplication
fun main(args: Array<String>) {
runApplication<GatewayApplication>(*args)
}
@@ -0,0 +1,51 @@
package space.nstart.pcp_gateway_service.config
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
/**
* Политика edge. Две взаимоисключающие цепочки, переключаемые флагом
* `gateway.security.jwt-enabled` (завязан на режим входа фронта):
* - выключено (anonymous, текущий дефолт) — пропускаем всё: поведение как при прямых вызовах;
* - включено (закрытый контур new-start-id) — валидируем Bearer JWT один раз здесь, дальше
* gateway по умолчанию пробрасывает заголовок Authorization вниз к сервисам.
*
* Когда JWT выключен, `spring.security.oauth2.resourceserver.jwt.issuer-uri` не задаётся,
* поэтому JwtDecoder не создаётся и старт не падает из-за пустого issuer.
*/
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
@Bean
@ConditionalOnProperty(prefix = "gateway.security", name = ["jwt-enabled"], havingValue = "true")
fun securedChain(http: ServerHttpSecurity): SecurityWebFilterChain =
http
.csrf { it.disable() }
.authorizeExchange { exchanges ->
exchanges
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/healthz", "/actuator/**").permitAll()
.anyExchange().authenticated()
}
.oauth2ResourceServer { it.jwt {} }
.build()
@Bean
@ConditionalOnProperty(
prefix = "gateway.security",
name = ["jwt-enabled"],
havingValue = "false",
matchIfMissing = true,
)
fun openChain(http: ServerHttpSecurity): SecurityWebFilterChain =
http
.csrf { it.disable() }
.authorizeExchange { it.anyExchange().permitAll() }
.build()
}
@@ -0,0 +1,13 @@
spring:
application:
name: pcp-gateway-service
profiles:
default: local
config:
import: "configserver:"
cloud:
config:
uri: ${CONFIG_SERVER_URI:http://localhost:8888}
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
label: ${SPRING_CLOUD_CONFIG_LABEL:master}
+7 -57
View File
@@ -21,64 +21,14 @@ server {
try_files $uri =404;
}
# --- ТГУ: маршрутизация на бэкенды (зеркало server.proxy из vite.config.ts).
# СЕМАНТИКА (важно повторить точно, иначе бэкенды получат неверный путь):
# - префиксы pcp-* СРЕЗАЮТСЯ: trailing-slash в proxy_pass отбрасывает префикс
# location (как rewrite в vite). Пример: /api/pcp-tgu/api/plans → бэкенд /api/plans.
# - /api/stations путь СОХРАНЯЕТ (в vite без rewrite): proxy_pass без URI-части.
# - общий /api/ — catch-all (tgu-planning, current-plans, auth) на ui-service/gateway.
# Порядок важен: специфичные префиксы идут РАНЬШЕ общего /api/.
# Цели (UPSTREAM_*) — заглушки: подставьте реальные адреса кластера ЛИБО перенесите
# маршрутизацию на gateway/ingress. Порты dev (vite) даны как ориентир в комментариях.
# Раскомментируйте после подстановки реальных upstream (нерезолвимый хост ломает старт nginx).
# Заголовки proxy_set_header в каждом блоке — те же, что в активном /api/ ниже.
#
# location /api/pcp-request/ { # dev :7005
# proxy_pass http://UPSTREAM_REQUEST/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# location /api/pcp-ballistics/ { # dev :7003
# proxy_pass http://UPSTREAM_BALLISTICS/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# location /api/pcp-mission/ { # dev :7010
# proxy_pass http://UPSTREAM_MISSION/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# location /api/pcp-tgu/ { # dev :7011
# proxy_pass http://UPSTREAM_TGU/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# location /api/pcp-complex/ { # dev :7002
# proxy_pass http://UPSTREAM_COMPLEX/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# location /api/stations/ { # dev :7009 — путь СОХРАНЯЕМ (proxy_pass без / и без URI)
# proxy_pass http://UPSTREAM_STATIONS;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
#
# Общий catch-all. В деве это ui-service (:7008): /api/tgu-planning, /api/current-plans, /api/auth.
# --- ТГУ: вся маршрутизация /api/* уходит на gateway (Spring Cloud Gateway).
# Gateway сам срезает префиксы pcp-* (StripPrefix) и выбирает бэкенд —
# см. config-repo/pcp-gateway-service.yaml. Здесь прозрачный reverse-proxy:
# proxy_pass БЕЗ URI-части → путь /api/... передаётся gateway без изменений
# (он разбирает его сам). Это заменяет прежний набор per-service location'ов.
# Адрес gateway в кластере — pcp-gateway-service:8080 (dev/master-профиль).
location /api/ {
proxy_pass http://127.0.0.1:8000/api/; # ЗАГЛУШКА: замените на ui-service/gateway
proxy_pass http://pcp-gateway-service:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+4 -32
View File
@@ -21,40 +21,12 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 5174,
// ТГУ ходит в несколько бэкендов через префиксы. В проде эти маршруты
// повторяются в nginx.conf (зеркало) либо на gateway/ingress.
// Всё /api/* уходит на gateway (Spring Cloud Gateway, dev :7080). Gateway сам
// срезает префиксы pcp-* (StripPrefix) и выбирает бэкенд — ровно то, что раньше
// делали правила-зеркала здесь. В проде то же делает nginx.conf.
proxy: {
'/api/pcp-request': {
target: 'http://localhost:7005',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api\/pcp-request/, ''),
},
'/api/pcp-ballistics': {
target: 'http://localhost:7003',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api\/pcp-ballistics/, ''),
},
'/api/pcp-mission': {
target: 'http://localhost:7010',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api\/pcp-mission/, ''),
},
'/api/pcp-tgu': {
target: 'http://localhost:7011',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api\/pcp-tgu/, ''),
},
'/api/pcp-complex': {
target: 'http://localhost:7002',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api\/pcp-complex/, ''),
},
'/api/stations': {
target: 'http://localhost:7009',
changeOrigin: true,
},
'/api': {
target: 'http://localhost:7008',
target: 'http://localhost:7080',
changeOrigin: true,
},
},