From ab3a787da55fa6a48e964e80e5aa67389efb4867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Wed, 10 Jun 2026 13:34:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8?= =?UTF-8?q?=D1=81=20pcp-gateway-service=20+=20=D1=81=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=B0=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - новый Spring Cloud Gateway (WebFlux): доменные маршруты /api/pcp-/** со 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 --- .gitlab-ci.yml | 7 + .gitlab/ci/pcp-gateway-service.yml | 191 ++++++++++++++++++ build.gradle.kts | 12 +- config-repo/application-dev.yaml | 2 + config-repo/application-docker-local.yaml | 2 + config-repo/application-local.yaml | 2 + config-repo/pcp-gateway-service.yaml | 88 ++++++++ deploy/local-bundle/docker-compose.yml | 8 + helm/pcp-gateway-service/Chart.yaml | 6 + .../templates/_helpers.tpl | 18 ++ .../templates/deployment.yaml | 68 +++++++ .../templates/ingress.yaml | 31 +++ .../templates/service.yaml | 16 ++ helm/pcp-gateway-service/values-dev.yaml | 10 + helm/pcp-gateway-service/values-master.yaml | 10 + helm/pcp-gateway-service/values.yaml | 36 ++++ services/pcp-gateway-service/Dockerfile | 9 + services/pcp-gateway-service/build.gradle.kts | 50 +++++ .../pcp_gateway_service/GatewayApplication.kt | 16 ++ .../config/SecurityConfig.kt | 51 +++++ .../src/main/resources/application.yml | 13 ++ services/pcp-tgu-ui-service/nginx.conf | 64 +----- services/pcp-tgu-ui-service/vite.config.ts | 36 +--- settings.gradle.kts | 1 + 24 files changed, 652 insertions(+), 95 deletions(-) create mode 100644 .gitlab/ci/pcp-gateway-service.yml create mode 100644 config-repo/pcp-gateway-service.yaml create mode 100644 helm/pcp-gateway-service/Chart.yaml create mode 100644 helm/pcp-gateway-service/templates/_helpers.tpl create mode 100644 helm/pcp-gateway-service/templates/deployment.yaml create mode 100644 helm/pcp-gateway-service/templates/ingress.yaml create mode 100644 helm/pcp-gateway-service/templates/service.yaml create mode 100644 helm/pcp-gateway-service/values-dev.yaml create mode 100644 helm/pcp-gateway-service/values-master.yaml create mode 100644 helm/pcp-gateway-service/values.yaml create mode 100644 services/pcp-gateway-service/Dockerfile create mode 100644 services/pcp-gateway-service/build.gradle.kts create mode 100644 services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/GatewayApplication.kt create mode 100644 services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/config/SecurityConfig.kt create mode 100644 services/pcp-gateway-service/src/main/resources/application.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a12e74f..b2bd229 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ workflow: - 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 @@ -43,6 +44,7 @@ workflow: - 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 @@ -64,6 +66,7 @@ workflow: - 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 @@ -87,6 +90,7 @@ workflow: - 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/**/* @@ -104,6 +108,7 @@ workflow: - 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 @@ -127,6 +132,7 @@ workflow: - helm/pcp-tgu-service/**/* - helm/pcp-tgu-ui-service/**/* - helm/pcp-dynamic-plan-service/**/* + - helm/pcp-gateway-service/**/* - when: never default: @@ -168,3 +174,4 @@ include: - 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 diff --git a/.gitlab/ci/pcp-gateway-service.yml b/.gitlab/ci/pcp-gateway-service.yml new file mode 100644 index 0000000..cfbb6fb --- /dev/null +++ b/.gitlab/ci/pcp-gateway-service.yml @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 333cb2d..2ba8cb3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,18 +16,18 @@ allprojects { group = "space.nstart.pcp" ext.set("kotlinVersion", "2.2.20") repositories { - maven { - url = uri("https://repo.nstart.cloud/repository/maven-proxy/") - } +// maven { +// url = uri("https://repo.nstart.cloud/repository/maven-proxy/") +// } mavenCentral { content { includeGroup("org.jacoco") includeGroup("org.ow2.asm") } } -// mavenCentral() -// mavenLocal() -// gradlePluginPortal() + mavenCentral() + mavenLocal() + gradlePluginPortal() maven { url = uri("https://jitpack.io") } exclusiveContent { diff --git a/config-repo/application-dev.yaml b/config-repo/application-dev.yaml index 947eea6..038880a 100644 --- a/config-repo/application-dev.yaml +++ b/config-repo/application-dev.yaml @@ -29,6 +29,7 @@ pcp: coverage-scheme: 8080 satellite-catalog: 8080 dynamic-plan: 8080 + gateway: 8080 services: config: http://${pcp.network.host}:${pcp.ports.config} @@ -47,6 +48,7 @@ pcp: 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: diff --git a/config-repo/application-docker-local.yaml b/config-repo/application-docker-local.yaml index 7b479db..d52924f 100644 --- a/config-repo/application-docker-local.yaml +++ b/config-repo/application-docker-local.yaml @@ -27,6 +27,7 @@ pcp: coverage-scheme: 7012 satellite-catalog: 7013 dynamic-plan: 7014 + gateway: 7080 services: config: http://spring-cloud-config-server:${pcp.ports.config} @@ -45,6 +46,7 @@ pcp: 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: diff --git a/config-repo/application-local.yaml b/config-repo/application-local.yaml index ccd2cc3..1954f96 100644 --- a/config-repo/application-local.yaml +++ b/config-repo/application-local.yaml @@ -31,6 +31,7 @@ pcp: coverage-scheme: 7012 satellite-catalog: 7013 dynamic-plan: 7014 + gateway: 7080 services: config: http://${pcp.network.host}:${pcp.ports.config} @@ -49,6 +50,7 @@ pcp: 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: diff --git a/config-repo/pcp-gateway-service.yaml b/config-repo/pcp-gateway-service.yaml new file mode 100644 index 0000000..57ec2d5 --- /dev/null +++ b/config-repo/pcp-gateway-service.yaml @@ -0,0 +1,88 @@ +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: + routes: + # --- Доменные сервисы. Префикс /api/pcp- СРЕЗАЕТСЯ (StripPrefix=2 убирает + # 2 сегмента: api + pcp-), т.е. /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 + # --- Станции: путь СОХРАНЯЕМ (в vite было без rewrite) — без StripPrefix. + - id: stations + uri: ${pcp.services.stations} + predicates: + - Path=/api/stations/** + # --- Общий catch-all /api/** -> ui-service. Матчится ПОСЛЕДНИМ (order выше). + - id: ui-catch-all + uri: ${pcp.services.ui} + predicates: + - Path=/api/** + order: 1 + +# Валидация JWT на edge. По умолчанию ВЫКЛЮЧЕНА (anonymous, как сейчас работает фронт). +# Для закрытого контура: GATEWAY_JWT_ENABLED=true И задать issuer через env +# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=. +# 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} diff --git a/deploy/local-bundle/docker-compose.yml b/deploy/local-bundle/docker-compose.yml index a43c1d6..dd52411 100644 --- a/deploy/local-bundle/docker-compose.yml +++ b/deploy/local-bundle/docker-compose.yml @@ -182,6 +182,14 @@ services: 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: diff --git a/helm/pcp-gateway-service/Chart.yaml b/helm/pcp-gateway-service/Chart.yaml new file mode 100644 index 0000000..4b01f97 --- /dev/null +++ b/helm/pcp-gateway-service/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: pcp-gateway-service +description: Helm chart for deploying pcp-gateway-service from GitLab CI/CD. +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/helm/pcp-gateway-service/templates/_helpers.tpl b/helm/pcp-gateway-service/templates/_helpers.tpl new file mode 100644 index 0000000..614ddb6 --- /dev/null +++ b/helm/pcp-gateway-service/templates/_helpers.tpl @@ -0,0 +1,18 @@ +{{- define "pcp-gateway-service.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "pcp-gateway-service.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- include "pcp-gateway-service.name" . | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "pcp-gateway-service.labels" -}} +app.kubernetes.io/name: {{ include "pcp-gateway-service.name" . }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} diff --git a/helm/pcp-gateway-service/templates/deployment.yaml b/helm/pcp-gateway-service/templates/deployment.yaml new file mode 100644 index 0000000..d380133 --- /dev/null +++ b/helm/pcp-gateway-service/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "pcp-gateway-service.fullname" . }} + labels: + {{- include "pcp-gateway-service.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "pcp-gateway-service.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + {{- include "pcp-gateway-service.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- range . }} + - name: {{ . }} + {{- end }} + {{- end }} + containers: + - name: {{ include "pcp-gateway-service.name" . }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + env: + - name: SPRING_PROFILES_ACTIVE + value: {{ .Values.app.profile | quote }} + - name: CONFIG_SERVER_URI + value: {{ .Values.app.configServerUri | quote }} + - name: SPRING_CLOUD_CONFIG_LABEL + value: {{ .Values.app.configLabel | quote }} + - name: CONFIG_SERVER_FAIL_FAST + value: {{ .Values.app.configServerFailFast | quote }} + - name: SERVER_PORT + value: {{ .Values.service.port | quote }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + httpGet: + path: /actuator/health + port: http + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /actuator/health + port: http + initialDelaySeconds: 40 + periodSeconds: 20 + timeoutSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} diff --git a/helm/pcp-gateway-service/templates/ingress.yaml b/helm/pcp-gateway-service/templates/ingress.yaml new file mode 100644 index 0000000..03f1b55 --- /dev/null +++ b/helm/pcp-gateway-service/templates/ingress.yaml @@ -0,0 +1,31 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "pcp-gateway-service.fullname" . }} + labels: + {{- include "pcp-gateway-service.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className | quote }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: {{ .Values.ingress.path | quote }} + pathType: {{ .Values.ingress.pathType }} + backend: + service: + name: {{ include "pcp-gateway-service.fullname" . }} + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/helm/pcp-gateway-service/templates/service.yaml b/helm/pcp-gateway-service/templates/service.yaml new file mode 100644 index 0000000..32633a6 --- /dev/null +++ b/helm/pcp-gateway-service/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "pcp-gateway-service.fullname" . }} + labels: + {{- include "pcp-gateway-service.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + app.kubernetes.io/name: {{ include "pcp-gateway-service.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/helm/pcp-gateway-service/values-dev.yaml b/helm/pcp-gateway-service/values-dev.yaml new file mode 100644 index 0000000..e2fadb8 --- /dev/null +++ b/helm/pcp-gateway-service/values-dev.yaml @@ -0,0 +1,10 @@ +app: + profile: dev + configLabel: dev + +service: + port: 8080 + +ingress: + enabled: true + host: pcp-gateway-service.dev.k8s.264.nstart.cloud diff --git a/helm/pcp-gateway-service/values-master.yaml b/helm/pcp-gateway-service/values-master.yaml new file mode 100644 index 0000000..dee393f --- /dev/null +++ b/helm/pcp-gateway-service/values-master.yaml @@ -0,0 +1,10 @@ +app: + profile: master + configLabel: master + +service: + port: 8080 + +ingress: + enabled: true + host: pcp-gateway-service.master.k8s.264.nstart.cloud diff --git a/helm/pcp-gateway-service/values.yaml b/helm/pcp-gateway-service/values.yaml new file mode 100644 index 0000000..faf8c5c --- /dev/null +++ b/helm/pcp-gateway-service/values.yaml @@ -0,0 +1,36 @@ +replicaCount: 1 + +image: + repository: registry.example.com/pcp/pcp-gateway-service + tag: latest + pullPolicy: IfNotPresent + pullSecrets: [] + +service: + type: ClusterIP + port: 8080 + +app: + profile: master + configServerUri: "http://spring-cloud-config-server:38888" + configServerFailFast: "true" + configLabel: master + +ingress: + enabled: false + className: "" + annotations: {} + host: "" + path: / + pathType: Prefix + tls: [] + +extraEnv: [] + +resources: {} + +podAnnotations: {} +podLabels: {} + +nameOverride: "" +fullnameOverride: "" diff --git a/services/pcp-gateway-service/Dockerfile b/services/pcp-gateway-service/Dockerfile new file mode 100644 index 0000000..6fecd3c --- /dev/null +++ b/services/pcp-gateway-service/Dockerfile @@ -0,0 +1,9 @@ +ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5 +FROM ${RUNTIME_IMAGE} + +ENV JAVA_OPTS="" + +ADD ./build/libs/*.jar /app.jar + +EXPOSE 8080 +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"] diff --git a/services/pcp-gateway-service/build.gradle.kts b/services/pcp-gateway-service/build.gradle.kts new file mode 100644 index 0000000..7a75635 --- /dev/null +++ b/services/pcp-gateway-service/build.gradle.kts @@ -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 { + useJUnitPlatform() +} + +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("versions.spring.cloud")}") + } +} diff --git a/services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/GatewayApplication.kt b/services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/GatewayApplication.kt new file mode 100644 index 0000000..0dc5569 --- /dev/null +++ b/services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/GatewayApplication.kt @@ -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) { + runApplication(*args) +} diff --git a/services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/config/SecurityConfig.kt b/services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/config/SecurityConfig.kt new file mode 100644 index 0000000..295dba0 --- /dev/null +++ b/services/pcp-gateway-service/src/main/kotlin/space/nstart/pcp_gateway_service/config/SecurityConfig.kt @@ -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() +} diff --git a/services/pcp-gateway-service/src/main/resources/application.yml b/services/pcp-gateway-service/src/main/resources/application.yml new file mode 100644 index 0000000..fdb6af6 --- /dev/null +++ b/services/pcp-gateway-service/src/main/resources/application.yml @@ -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} diff --git a/services/pcp-tgu-ui-service/nginx.conf b/services/pcp-tgu-ui-service/nginx.conf index 78b61f1..45ea091 100644 --- a/services/pcp-tgu-ui-service/nginx.conf +++ b/services/pcp-tgu-ui-service/nginx.conf @@ -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; diff --git a/services/pcp-tgu-ui-service/vite.config.ts b/services/pcp-tgu-ui-service/vite.config.ts index f03e87c..9665873 100644 --- a/services/pcp-tgu-ui-service/vite.config.ts +++ b/services/pcp-tgu-ui-service/vite.config.ts @@ -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, }, }, diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cbf6b7..8033846 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,7 @@ include(":services:pcp-coverage-scheme-service") include(":services:pcp-dynamic-plan-service") include(":services:spring-cloud-config-server") include(":services:pcp-srpring-boot-admin-server") +include(":services:pcp-gateway-service") pluginManagement { repositories {