diff --git a/docker-compose/multiwar/.env b/docker-compose/multiwar/.env new file mode 100644 index 00000000..2633c881 --- /dev/null +++ b/docker-compose/multiwar/.env @@ -0,0 +1,6 @@ +SHOULD_INIT_GEO_DATA=true +SHOULD_INIT_LINES_CATALOG=true + +COMPOSE_PROJECT_NAME=gridstudy + +TOMCAT_PORT=12345 diff --git a/docker-compose/multiwar/README.md b/docker-compose/multiwar/README.md new file mode 100644 index 00000000..895e5eaa --- /dev/null +++ b/docker-compose/multiwar/README.md @@ -0,0 +1,58 @@ +# WAR Deploy Tool + +Generated with little supervision by opus4.6. + +Builds GridSuite microservices as WAR files for docker-compose deployment, **without modifying any original source files**. + +## How it works + +Instead of patching server sources, this tool generates **wrapper Maven projects** that: +1. Depend on the original server JAR (classes artifact) +2. Add a generated `SpringBootServletInitializer` class that delegates to the original Application class +3. Package everything as a WAR with a clean context-path name + +``` +Original server (unmodified) Wrapper project (generated) +┌─────────────────────────┐ ┌─────────────────────────┐ +│ report-server/ │ │ war-wrappers/report-server/ +│ pom.xml (jar) │◄─────────│ pom.xml (war) │ +│ ReportApplication.java│ │ WarInitializer.java │ +└─────────────────────────┘ └─────────────────────────┘ + │ │ + ▼ ▼ + gridsuite-report-server.jar report-server.war +``` + +## Quick Start + +```bash +# Build all WARs and prepare for docker-compose +./wars.sh + +# Start the stack +docker compose up +``` + +## Enabling/disabling servers + +Edit the `MANIFEST` array in `war-deploy.sh`. Comment lines with `#` to disable: + +```bash +"actions-server|actions-server||..." # enabled +#"timeseries-server|timeseries-server||..." # disabled +``` + +## Prerequisites + +- Java 21+ +- Maven 3.8+ +- Server submodules checked out and buildable in the layout of Gridsuite Aggregator + +## Excluded (Boot WebFlux — incompatible(??) with WAR) + +- config-server +- config-notification-server +- directory-notification-server +- merge-notification-server +- study-notification-server +- gateway diff --git a/docker-compose/multiwar/docker-compose.multiwar.yml b/docker-compose/multiwar/docker-compose.multiwar.yml new file mode 100644 index 00000000..8f9e1ab8 --- /dev/null +++ b/docker-compose/multiwar/docker-compose.multiwar.yml @@ -0,0 +1,252 @@ +services: + + # =========================================================================== + # SINGLE TOMCAT — all WAR-deployed microservices (33 webmvc servers) + # =========================================================================== + tomcat: + # JDK 21 required: servers are compiled with Java 21 (class file version 65) + image: tomcat:10.1-jdk21 + ports: + - "${TOMCAT_PORT:-12345}:8080" + volumes: + # Custom server.xml: sets Host startStopThreads="10" for parallel WAR deployment + - ./server.xml:/usr/local/tomcat/conf/server.xml:Z + - ./gen/wars:/usr/local/tomcat/webapps:Z + # Per-WAR config overrides (base-uri rewrites) loaded via spring.config.additional-location + # in each WAR's SpringBootServletInitializer. Only contains service base-uri mappings. + - ./gen/externalized-war-configs:/config:Z + # Merged powsybl itools config (loadflow params, writeReferenceTerminals: false, etc.) + - ./itools-config.yml:/root/.itools/config.yml:Z + environment: + # spring.profiles.active=default: disables "local" profile so application-local.yml + # files embedded in server JARs are NOT loaded. Infrastructure defaults (rabbitmq, + # postgres, elasticsearch hosts) come from powsybl-ws-commons.jar application.yaml + # which already has the correct docker service names. + - CATALINA_OPTS=-Dspring.profiles.active=default -Xmx2g + depends_on: + - postgres + - rabbitmq + - elasticsearch + - s3-storage + # Wait for elasticsearch before starting Tomcat: WARs that depend on elasticsearch + # (study-server, directory-server, case-server, network-conversion-server) fail + # immediately if ES is not reachable at startup (no built-in retry). + entrypoint: ["/bin/bash", "-c", "until curl -sf http://elasticsearch:9200/_cluster/health; do echo 'Waiting for elasticsearch...'; sleep 3; done && catalina.sh run"] + restart: unless-stopped + memswap_limit: 5g + deploy: + resources: + limits: + memory: 5g + # Future: move shared JARs (Spring, Hibernate, powsybl-*) to Tomcat lib/ so + # they are loaded once by the common classloader, reducing Metaspace usage. + # Current setup: each WAR bundles ~200MB of dependencies, many identical across + # WARs. Shared classloading could cut memory by 30-50%. + + # =========================================================================== + # NON-WAR SERVICES (WebFlux / notification servers) + # Future: avoid redeclaring these services here — find a way to reuse the + # definitions from docker-compose.base.yml directly. + # =========================================================================== + config-server: + image: gridsuite/config-server:latest + ports: + - 5025:80 + volumes: + - ../../k8s/resources/common/config/config-server-application.yml:/config/specific/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + config-notification-server: + image: gridsuite/config-notification-server:latest + ports: + - 5024:80 + volumes: + - ../../k8s/resources/common/config/config-notification-server-application.yml:/config/specific/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + study-notification-server: + image: gridsuite/study-notification-server:latest + ports: + - 5009:80 + volumes: + - ../../k8s/resources/study/config/study-notification-server-application.yml:/config/specific/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + directory-notification-server: + image: gridsuite/directory-notification-server:latest + ports: + - 5004:80 + volumes: + - ../../k8s/resources/study/config/directory-notification-server-application.yml:/config/specific/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + gateway: + image: gridsuite/gateway:latest + ports: + - 9000:80 + volumes: + - ../../k8s/resources/common/config/gateway-application.yml:/config/specific/application.yml:Z + - ./nonwars-to-tomcat-config/common-application.yml:/config/common/application.yml:Z + - ../allowed-issuers.yml:/config/allowed-issuers.yml:Z + restart: unless-stopped + environment: + - JAVA_TOOL_OPTIONS=-Xmx96m + command: --server.port=80 --spring.config.additional-location=/config/ + sysctls: + - net.ipv4.ip_unprivileged_port_start=0 + memswap_limit: 384m + deploy: + resources: + limits: + memory: 384m + + # =========================================================================== + # FRONTEND APPS + # =========================================================================== + gridstudy-app: + image: gridsuite/gridstudy-app:latest + ports: + - 84:8080 + volumes: + - ../study/gridstudy-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridstudy/idpSettings.json:Z + - ../env.json:/opt/bitnami/apache/htdocs/gridstudy/env.json:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped + + gridexplore-app: + image: gridsuite/gridexplore-app:latest + ports: + - 80:8080 + volumes: + - ../study/gridexplore-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridexplore/idpSettings.json:Z + - ../env.json:/opt/bitnami/apache/htdocs/gridexplore/env.json:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped + + gridadmin-app: + image: gridsuite/gridadmin-app:latest + ports: + - 82:8080 + volumes: + - ../study/gridadmin-app-idpSettings.json:/opt/bitnami/apache/htdocs/gridadmin/idpSettings.json:Z + - ../env.json:/opt/bitnami/apache/htdocs/gridadmin/env.json:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped + + # =========================================================================== + # MOCK & METADATA + # =========================================================================== + mock-user-service: + image: gridsuite/oidc-mock-server + ports: + - 9090:9090 + environment: + - PORT=9090 + - ISSUER_HOST=172.17.0.1:9090 + - USERS_PROFILE=UTILISATEURS|ADMIN|ADMIN_EXPLORE + - CLIENT_COUNT=8 + - CLIENT_ID=gridexplore-client + - CLIENT_REDIRECT_URI=http://localhost:80/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI=http://localhost:80/logout-callback + - CLIENT_SILENT_REDIRECT_URI=http://localhost:80/silent-renew-callback + - CLIENT_ID_2=gridadmin-client + - CLIENT_REDIRECT_URI_2=http://localhost:82/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_2=http://localhost:82/logout-callback + - CLIENT_SILENT_REDIRECT_URI_2=http://localhost:82/silent-renew-callback + - CLIENT_ID_3=griddyna-client + - CLIENT_REDIRECT_URI_3=http://localhost:83/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_3=http://localhost:83/logout-callback + - CLIENT_SILENT_REDIRECT_URI_3=http://localhost:83/silent-renew-callback + - CLIENT_ID_4=gridstudy-client + - CLIENT_REDIRECT_URI_4=http://localhost:84/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_4=http://localhost:84/logout-callback + - CLIENT_SILENT_REDIRECT_URI_4=http://localhost:84/silent-renew-callback + - CLIENT_ID_5=gridexplore-local + - CLIENT_REDIRECT_URI_5=http://localhost:3000/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_5=http://localhost:3000/logout-callback + - CLIENT_SILENT_REDIRECT_URI_5=http://localhost:3000/silent-renew-callback + - CLIENT_ID_6=gridadmin-local + - CLIENT_REDIRECT_URI_6=http://localhost:3002/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_6=http://localhost:3002/logout-callback + - CLIENT_SILENT_REDIRECT_URI_6=http://localhost:3002/silent-renew-callback + - CLIENT_ID_7=griddyna-local + - CLIENT_REDIRECT_URI_7=http://localhost:3003/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_7=http://localhost:3003/logout-callback + - CLIENT_SILENT_REDIRECT_URI_7=http://localhost:3003/silent-renew-callback + - CLIENT_ID_8=gridstudy-local + - CLIENT_REDIRECT_URI_8=http://localhost:3004/sign-in-callback + - CLIENT_LOGOUT_REDIRECT_URI_8=http://localhost:3004/logout-callback + - CLIENT_SILENT_REDIRECT_URI_8=http://localhost:3004/silent-renew-callback + restart: unless-stopped + + apps-metadata-server: + image: bitnami/apache:2.4.55-debian-11-r3@sha256:bbe50190eb3bbf3be6f61318004480b3230846bfd52dec9286bd1862254c1719 + ports: + - 8070:8080 + volumes: + - ../apps-metadata.json:/opt/bitnami/apache/htdocs/apps-metadata.json:Z + - ../../k8s/resources/common/config/apps-metadata-base-voltages.json:/opt/bitnami/apache/htdocs/apps-metadata-base-voltages.json:Z + - ../version.json:/opt/bitnami/apache/htdocs/version.json:Z + - ../gridapps-metadata-httpd.conf:/opt/bitnami/apache/conf/bitnami/bitnami.conf:Z + memswap_limit: 128m + deploy: + resources: + limits: + memory: 128m + restart: unless-stopped diff --git a/docker-compose/multiwar/docker-compose.yml b/docker-compose/multiwar/docker-compose.yml new file mode 100644 index 00000000..ceb04af5 --- /dev/null +++ b/docker-compose/multiwar/docker-compose.yml @@ -0,0 +1,3 @@ +include: + - ../technical/docker-compose.technical.yml + - docker-compose.multiwar.yml diff --git a/docker-compose/multiwar/itools-config.yml b/docker-compose/multiwar/itools-config.yml new file mode 100644 index 00000000..ff2716b4 --- /dev/null +++ b/docker-compose/multiwar/itools-config.yml @@ -0,0 +1,66 @@ +# Merged itools config for all WAR-deployed servers in Tomcat. +# Mounted at /root/.itools/config.yml (Tomcat container runs as root). +# +# In k8s, each server has its own config.yml. Here we merge them all because +# all 33 WARs share a single JVM. Key settings: +# - writeReferenceTerminals: false — workaround for ReferenceTerminals extension +# not implemented in network-store (causes InvalidClassException without it) +# - network.default-impl-name: NetworkStore — forces iidm to use network-store +# instead of in-memory impl (which is on classpath due to cvg-extension) +# - computation-local.available-core: 4 — limits concurrent computation processes +# +# Source files merged: +# k8s/resources/common/config/loadflow-server-config.yml +# k8s/resources/common/config/network-conversion-server-config.yml +# k8s/resources/study/config/network-modification-server-config.yml +# k8s/resources/study/config/dynawo-itools-config.yml +# k8s/resources/study/config/voltage-init-server-config.yml +# k8s/resources/study/config/sensitivity-analysis-server-config.yml +# k8s/resources/study/config/security-analysis-server-config.yml +# k8s/resources/study/config/shortcircuit-server-config.yml +# k8s/resources/monitor/config/monitor-lf-worker-server-config.yml +# k8s/resources/monitor/config/monitor-sa-worker-server-config.yml + +hades2: + homeDir: /hades2 + debug: false + +dynaflow: + homeDir: /dynaflow-launcher + debug: false + +dynawo: + homeDir: /dynaflow-launcher + debug: false + +dynawo-algorithms: + homeDir: /dynaflow-launcher + debug: false + +network: + default-impl-name: NetworkStore + +dynaflow-default-parameters: + mergeLoads: false + +computation-local: + available-core: 4 + +load-flow-default-parameters: + voltageInitMode: DC_VALUES + phaseShifterRegulationOn: true + twtSplitShuntAdmittance: true + dcUseTransformerRatio: false + countriesToBalance: + - FR + +open-loadflow-default-parameters: + writeReferenceTerminals: false + slackDistributionFailureBehavior: FAIL + +open-security-analysis-default-parameters: + threadCount: 2 + +import-export-parameters-default-value: + iidm.import.cgmes.cgm-with-subnetworks: false + ucte.import.create-areas: false diff --git a/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml b/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml new file mode 100644 index 00000000..45cb98c0 --- /dev/null +++ b/docker-compose/multiwar/nonwars-to-tomcat-config/common-application.yml @@ -0,0 +1,56 @@ +# Common application config for non-WAR services in multiwar deployment. +# Points service base-uris to the single Tomcat instance. + +powsybl: + services: + case-server: + base-uri: http://tomcat:8080/case-server/ + network-store-server: + base-uri: http://tomcat:8080/network-store-server/ + network-conversion-server: + base-uri: http://tomcat:8080/network-conversion-server/ + single-line-diagram-server: + base-uri: http://tomcat:8080/single-line-diagram-server/ + +gridsuite: + services: + directory-server: + base-uri: http://tomcat:8080/directory-server/ + explore-server: + base-uri: http://tomcat:8080/explore-server/ + study-server: + base-uri: http://tomcat:8080/study-server/ + actions-server: + base-uri: http://tomcat:8080/actions-server/ + filter-server: + base-uri: http://tomcat:8080/filter-server/ + user-admin-server: + base-uri: http://tomcat:8080/user-admin-server/ + config-server: + base-uri: http://config-server:80 + report-server: + base-uri: http://tomcat:8080/report-server/ + network-modification-server: + base-uri: http://tomcat:8080/network-modification-server/ + geo-data-server: + base-uri: http://tomcat:8080/geo-data-server/ + network-map-server: + base-uri: http://tomcat:8080/network-map-server/ + loadflow-server: + base-uri: http://tomcat:8080/loadflow-server/ + security-analysis-server: + base-uri: http://tomcat:8080/security-analysis-server/ + sensitivity-analysis-server: + base-uri: http://tomcat:8080/sensitivity-analysis-server/ + shortcircuit-server: + base-uri: http://tomcat:8080/shortcircuit-server/ + voltage-init-server: + base-uri: http://tomcat:8080/voltage-init-server/ + dynamic-simulation-server: + base-uri: http://tomcat:8080/dynamic-simulation-server/ + timeseries-server: + base-uri: http://tomcat:8080/timeseries-server/ + study-config-server: + base-uri: http://tomcat:8080/study-config-server/ + user-identity-server: + base-uri: http://tomcat:8080/user-identity-server/ diff --git a/docker-compose/multiwar/server.xml b/docker-compose/multiwar/server.xml new file mode 100644 index 00000000..957a5a26 --- /dev/null +++ b/docker-compose/multiwar/server.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose/multiwar/wars.sh b/docker-compose/multiwar/wars.sh new file mode 100755 index 00000000..3a5f26cf --- /dev/null +++ b/docker-compose/multiwar/wars.sh @@ -0,0 +1,516 @@ +#!/usr/bin/env bash +# +# war-deploy.sh — Build GridSuite microservices as WAR files for docker-compose. +# Zero modifications to original server source files. +# +# Word of Caution, the whole script with comments (except this sentence) +# has been generated by opus4.6 without much look at the details. +# +# Usage: +# ./war-deploy.sh [OPTIONS] +# +# Options: +# --only SRV Only process this server (repeatable) +# --help Show this help +# +# Strategy ("wrapper POM" approach): +# Instead of modifying original Spring Boot server sources to produce WAR files, +# we generate lightweight wrapper Maven projects that: +# 1. Depend on the original server's classes JAR (available because powsybl-parent-ws +# configures spring-boot-maven-plugin with exec, so +# `mvn install` installs both the executable fat JAR and a plain classes JAR) +# 2. Provide a SpringBootServletInitializer subclass (required for WAR deployment) +# 3. Set spring.config.additional-location for per-WAR config overrides +# 4. Package as WAR with = desired context path +# +# This keeps all server submodules UNMODIFIED while producing deployable WARs. +# +# Future improvements: +# - Move shared JARs (Spring, Hibernate, powsybl-commons, etc.) into Tomcat's +# lib/ directory so they are loaded once by the common classloader. This would +# significantly reduce Metaspace usage (each WAR currently loads its own copy +# of every dependency class). +# - Reduce duplication in config override generation: many servers share the same +# service references (e.g. network-store, report-server). A template or merge +# system could replace per-server YAML generation. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +AGGREGATOR_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +SERVERS_DIR="$AGGREGATOR_ROOT/backend/servers" +WRAPPERS_DIR="$SCRIPT_DIR/gen/war-wrappers" +WEBAPPS_DIR="$SCRIPT_DIR/gen/wars" +CONFIG_DIR="$SCRIPT_DIR/gen/externalized-war-configs" + +# Base URL used inside docker network (container-to-container) +TOMCAT_BASE_URL="http://tomcat:8080" + +# Defaults +ONLY_SERVERS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --only) ONLY_SERVERS+=("$2"); shift 2 ;; + --help) sed -n '2,/^$/p' "$0" | sed 's/^# \?//'; exit 0 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +log() { echo -e "\033[1;34m[WAR]\033[0m $*"; } +err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; } + +# =========================================================================== +# SERVER MANIFEST +# Format: context_name|server_folder|pom_subpath|app_class_fqn +# +# context_name: WAR filename / Tomcat context path +# server_folder: folder under backend/servers/ +# pom_subpath: submodule path within server_folder (empty = root) +# app_class_fqn: fully qualified Application class name +# Lines starting with # are disabled. Uncomment to include in the build. +# =========================================================================== +MANIFEST=( +"actions-server|actions-server||org.gridsuite.actions.server.ActionsApplication" +#"balances-adjustment-server|balances-adjustment-server||org.gridsuite.balances.adjustment.server.BalancesAdjustmentApplication" +#"case-import-server|case-import-server||org.gridsuite.caseimport.server.CaseImportApplication" +#"case-validation-server|case-validation-server||org.gridsuite.casevalidation.server.CaseValidationApplication" +#"cgmes-boundary-server|cgmes-boundary-server||org.gridsuite.cgmes.boundary.server.CgmesBoundaryApplication" +#"cgmes-gl-server|cgmes-gl-server||org.gridsuite.cgmes.gl.server.CgmesGlApplication" +"directory-server|directory-server||org.gridsuite.directory.server.DirectoryApplication" +#"dynamic-mapping-server|dynamic-mapping-server||org.gridsuite.mapping.server.MappingApplication" +#"dynamic-margin-calculation-server|dynamic-margin-calculation-server||org.gridsuite.dynamicmargincalculation.server.DynamicMarginCalculationApplication" +#"dynamic-security-analysis-server|dynamic-security-analysis-server||org.gridsuite.dynamicsecurityanalysis.server.DynamicSecurityAnalysisApplication" +#"dynamic-simulation-server|dynamic-simulation-server||org.gridsuite.ds.server.DynamicSimulationApplication" +"explore-server|explore-server||org.gridsuite.explore.server.ExploreApplication" +"filter-server|filter-server||org.gridsuite.filter.server.FilterApplication" +"geo-data-server|geo-data-server||org.gridsuite.geodata.server.GeoDataApplication" +"loadflow-server|loadflow-server||org.gridsuite.loadflow.server.LoadFlowApplication" +#"merge-orchestrator-server|merge-orchestrator-server||org.gridsuite.merge.orchestrator.server.MergeOrchestratorApplication" +"network-map-server|network-map-server||org.gridsuite.network.map.NetworkMapApplication" +"network-modification-server|network-modification-server||org.gridsuite.modification.server.NetworkModificationApplication" +"network-store-server|powsybl-network-store-server|network-store-server|com.powsybl.network.store.server.NetworkStoreApplication" +#"odre-server|odre-server||org.gridsuite.odre.server.OdreApplication" +"case-server|powsybl-case-server||com.powsybl.caseserver.CaseApplication" +"network-conversion-server|powsybl-network-conversion-server||com.powsybl.network.conversion.server.NetworkConversionApplication" +"single-line-diagram-server|powsybl-single-line-diagram-server||com.powsybl.sld.server.SingleLineDiagramApplication" +"report-server|report-server||org.gridsuite.report.server.ReportApplication" +"security-analysis-server|security-analysis-server||org.gridsuite.securityanalysis.server.SecurityAnalysisApplication" +"sensitivity-analysis-server|sensitivity-analysis-server||org.gridsuite.sensitivityanalysis.server.SensitivityAnalysisApplication" +"shortcircuit-server|shortcircuit-server||org.gridsuite.shortcircuit.server.ShortCircuitApplication" +"study-config-server|study-config-server||org.gridsuite.studyconfig.server.StudyConfigApplication" +"study-server|study-server||org.gridsuite.study.server.StudyApplication" +#"timeseries-server|timeseries-server||org.gridsuite.timeseries.server.TimeSeriesApplication" +"user-admin-server|user-admin-server||org.gridsuite.useradmin.server.UserAdminApplication" +"user-identity-server|user-identity-oidc-replication-server||org.gridsuite.useridentity.oidcreplication.server.UserIdentityOidcReplicationApplication" +"voltage-init-server|voltage-init-server||org.gridsuite.voltageinit.server.VoltageInitApplication" +) + +# Filter out commented entries (lines starting with #) +_filtered=() +for _e in "${MANIFEST[@]}"; do + [[ "$_e" =~ ^# ]] || _filtered+=("$_e") +done +MANIFEST=("${_filtered[@]}") +unset _filtered _e + +should_process() { + local ctx="$1" + if [[ ${#ONLY_SERVERS[@]} -gt 0 ]]; then + for s in "${ONLY_SERVERS[@]}"; do + [[ "$s" == "$ctx" ]] && return 0 + done + return 1 + fi + return 0 +} + +# Resolve a service key to its base-uri. +# Trailing slash is required: clients use UriComponentsBuilder.fromUriString(baseUri) +# which concatenates paths like "v1/networks/..." directly after the base URI. +# Without trailing slash: "http://tomcat:8080/network-store-serverv1/..." (broken) +# With trailing slash: "http://tomcat:8080/network-store-server/v1/..." (correct) +resolve_base_uri() { + local svc_key="$1" + echo "${TOMCAT_BASE_URL}/${svc_key}/" +} + +# Generate per-WAR config override with base-uri values pointing to Tomcat. +# Since we disable the "local" profile, we only need base-uri overrides in application.yml. +# Reads the original application-local file to discover which services are referenced, +# then generates a clean application.yml with only the base-uri section. +generate_local_config_override() { + local ctx="$1" + local server_folder="$2" + local pom_subpath="$3" + local wrapper_dir="$4" + + local module_dir="$SERVERS_DIR/$server_folder" + [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath" + + # Find the original application-local file to discover service references + local orig_file="" + for ext in yaml yml; do + if [[ -f "$module_dir/src/main/resources/application-local.$ext" ]]; then + orig_file="$module_dir/src/main/resources/application-local.$ext" + break + fi + done + [[ -z "$orig_file" ]] && return 0 + + # Detect which format gridsuite: services: uses (list-style vs map-style) + local uses_list_style=false + if grep -qE '^\s+-\s*(name:|$)' "$orig_file" 2>/dev/null; then + uses_list_style=true + fi + + # Extract service keys referenced in the file + local powsybl_services=() gridsuite_services=() + local current_section="" + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^powsybl: ]]; then + current_section="powsybl" + elif [[ "$line" =~ ^gridsuite: ]]; then + current_section="gridsuite" + elif [[ ! "$line" =~ ^[[:space:]] && ! "$line" =~ ^# && -n "$line" ]]; then + current_section="other" + fi + + # Map-style key: " server-name:" + if [[ "$line" =~ ^[[:space:]]{4}([a-z][a-z0-9-]+):$ ]]; then + local key="${BASH_REMATCH[1]}" + if [[ "$current_section" == "powsybl" ]]; then + powsybl_services+=("$key") + elif [[ "$current_section" == "gridsuite" ]]; then + gridsuite_services+=("$key") + fi + fi + # List-style: "- name: xxx" or " name: xxx" + if [[ "$line" =~ ^[[:space:]]+-[[:space:]]*name:[[:space:]]*(.+) ]]; then + local key="${BASH_REMATCH[1]}" + key="${key%%[[:space:]]}" + [[ "$current_section" == "gridsuite" ]] && gridsuite_services+=("$key") + elif [[ "$line" =~ ^[[:space:]]+name:[[:space:]]*(.+) && "$current_section" == "gridsuite" ]]; then + local key="${BASH_REMATCH[1]}" + key="${key%%[[:space:]]}" + gridsuite_services+=("$key") + fi + done < "$orig_file" + + # If no services referenced, no config needed + if [[ ${#powsybl_services[@]} -eq 0 && ${#gridsuite_services[@]} -eq 0 ]]; then + return 0 + fi + + local out_file="$CONFIG_DIR/$ctx/application.yml" + mkdir -p "$CONFIG_DIR/$ctx" + + local output="" + + # Generate powsybl: services: section (always map-style) + if [[ ${#powsybl_services[@]} -gt 0 ]]; then + output+="powsybl:"$'\n' + output+=" services:"$'\n' + for svc in "${powsybl_services[@]}"; do + local uri + uri=$(resolve_base_uri "$svc") + output+=" ${svc}:"$'\n' + output+=" base-uri: ${uri}"$'\n' + done + fi + + # Generate gridsuite: services: section + if [[ ${#gridsuite_services[@]} -gt 0 ]]; then + [[ -n "$output" ]] && output+=$'\n' + output+="gridsuite:"$'\n' + output+=" services:"$'\n' + if $uses_list_style; then + for svc in "${gridsuite_services[@]}"; do + local uri + uri=$(resolve_base_uri "$svc") + output+=" -"$'\n' + output+=" name: ${svc}"$'\n' + output+=" base-uri: ${uri}"$'\n' + done + else + for svc in "${gridsuite_services[@]}"; do + local uri + uri=$(resolve_base_uri "$svc") + output+=" ${svc}:"$'\n' + output+=" base-uri: ${uri}"$'\n' + done + fi + fi + + echo -n "$output" > "$out_file" +} + +# =========================================================================== +# PHASE 1: GENERATE WRAPPER PROJECTS +# =========================================================================== +generate_wrappers() { + log "=== Generating wrapper projects ===" + rm -rf "$WRAPPERS_DIR" + mkdir -p "$WRAPPERS_DIR" + + # Detect gridsuite-dependencies BOM version + local first_entry="${MANIFEST[0]}" + IFS='|' read -r _ctx first_folder first_sub _ <<< "$first_entry" + local first_dir="$SERVERS_DIR/$first_folder" + [[ -n "$first_sub" ]] && first_dir="$first_dir/$first_sub" + local gridsuite_deps_version + gridsuite_deps_version=$(grep -m1 "gridsuite-dependencies.version" "$first_dir/pom.xml" 2>/dev/null | sed 's/.*>\(.*\)<.*/\1/' || true) + [[ -z "$gridsuite_deps_version" ]] && gridsuite_deps_version="50.0.0" + log " gridsuite-dependencies version: $gridsuite_deps_version" + + local modules="" + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local module_dir="$SERVERS_DIR/$server_folder" + [[ -n "$pom_subpath" ]] && module_dir="$module_dir/$pom_subpath" + + # Get groupId, artifactId, version from the server's pom + local group_id artifact_id version parent_group parent_version + + parent_group=$(sed -n '//,/<\/parent>/p' "$module_dir/pom.xml" | grep '' | sed 's/.*//;s/<.*//' | tr -d ' ') + parent_version=$(sed -n '//,/<\/parent>/p' "$module_dir/pom.xml" | grep '' | sed 's/.*//;s/<.*//' | tr -d ' ') + artifact_id=$(sed -n '/<\/parent>/,$ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ') + version=$(sed -n '/<\/parent>/,/<\(build\|dependencies\|dependencyManagement\|properties\)>/ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ' || true) + group_id=$(sed -n '/<\/parent>/,/<\(build\|dependencies\|dependencyManagement\|properties\)>/ p' "$module_dir/pom.xml" | grep -m1 '' | sed 's/.*//;s/<.*//' | tr -d ' ' || true) + + # Fall back to parent values if not specified at project level + [[ -z "$group_id" ]] && group_id="$parent_group" + [[ -z "$version" ]] && version="$parent_version" + + local wrapper_dir="$WRAPPERS_DIR/$ctx" + mkdir -p "$wrapper_dir/src/main/java/org/gridsuite/war" + + # Generate WarInitializer.java + # Sets spring.config.additional-location to load per-WAR base-uri overrides from + # an external directory (/config//application.yml). "optional:" prefix means + # servers with no service references (e.g. report-server) won't fail if the + # directory doesn't exist. This is the Spring Boot guaranteed way to override + # config — additional-location properties always win over classpath configs. + local simple_class="${app_class##*.}" + local initializer_class="${simple_class/Application/WarInitializer}" + cat > "$wrapper_dir/src/main/java/org/gridsuite/war/${initializer_class}.java" << JAVA +package org.gridsuite.war; + +import ${app_class}; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ${initializer_class} extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(${simple_class}.class) + .properties("spring.config.additional-location=optional:file:/config/${ctx}/"); + } +} +JAVA + + # Generate wrapper pom.xml + local extra_deps="" + # antlr4 version override: these servers depend on powsybl-open-loadflow which + # pulls in graphviz-builder → antlr4:4.5.1. But Spring Data JPA's HqlLexer was + # compiled with antlr4 4.13.0 (ATN version 4). The old antlr4-4.5.1.jar contains + # ATNDeserializer that only understands ATN version 3, causing: + # InvalidClassException: org.antlr.v4.runtime.atn.ATN; Could not deserialize ATN with version 4 + # Fix: force antlr4 (the full tool jar, which includes the runtime) to 4.13.0. + case "$ctx" in + loadflow-server|security-analysis-server|sensitivity-analysis-server|network-modification-server) + extra_deps=' + + org.antlr + antlr4 + 4.13.0 + ' + ;; + esac + + cat > "$wrapper_dir/pom.xml" << POM + + + 4.0.0 + + + org.gridsuite.war + war-wrappers + 1.0.0 + + + ${ctx}-war + war + + + + ${group_id} + ${artifact_id} + ${version} + ${extra_deps} + + + + ${ctx} + + +POM + + modules="$modules $ctx\n" + log " ${ctx}-war -> $module_dir" + done + + # Generate reactor parent pom + cat > "$WRAPPERS_DIR/pom.xml" << POM + + + 4.0.0 + + org.gridsuite.war + war-wrappers + 1.0.0 + pom + + + 21 + 21 + + + + + + + org.gridsuite + gridsuite-dependencies + ${gridsuite_deps_version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + +$(echo -e "$modules") + +POM + + # Generate per-WAR config overrides directly into gen/externalized-war-configs/ + rm -rf "$CONFIG_DIR" + log "=== Generating externalized WAR additional-configs ===" + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + local wrapper_dir="$WRAPPERS_DIR/$ctx" + generate_local_config_override "$ctx" "$server_folder" "$pom_subpath" "$wrapper_dir" + done + (cd "$SCRIPT_DIR" && find gen/externalized-war-configs -type f 2>/dev/null) || true + + +} + +# =========================================================================== +# PHASE 2: BUILD ALL (SINGLE REACTOR) +# Builds ALL server source modules + wrapper projects in one Maven invocation. +# Server source modules are added to the reactor so that their classes JARs +# are available in the local repo for wrapper dependency resolution. +# -T2.0C = 2 threads per CPU core for parallel module builds. +# =========================================================================== +build_wars() { + # Prefer mvnd (Maven Daemon) for faster builds; fall back to mvn -q + local mvn_cmd + if command -v mvnd &>/dev/null; then + mvn_cmd="mvnd" + log "=== Building all with mvnd ===" + else + mvn_cmd="mvn -T2.0C -q" + log "=== Building all with mvn (mvnd not found, using -T2.0C -q) ===" + fi + + # Add server source modules to reactor POM for a single build + local server_modules="" + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local rel_path="../../../../../../backend/servers/$server_folder" + [[ -n "$pom_subpath" ]] && rel_path="$rel_path/$pom_subpath" + server_modules+=" ${rel_path}\n" + done + + # Inject server modules into reactor POM (before wrapper modules) + local wrapper_pom="$WRAPPERS_DIR/pom.xml" + sed -i "s| | \n \n${server_modules} |" "$wrapper_pom" + + log "Running: $mvn_cmd package -DskipTests -f $WRAPPERS_DIR/pom.xml" + (cd "$WRAPPERS_DIR" && $mvn_cmd package -DskipTests) || { + err "Reactor build failed" + return 1 + } + + # Verify + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + if [[ ! -f "$WRAPPERS_DIR/$ctx/target/${ctx}.war" ]]; then + err "Missing: $WRAPPERS_DIR/$ctx/target/${ctx}.war" + fi + done +} + +# =========================================================================== +# PHASE 3: COPY WARS TO DOCKER-COMPOSE MOUNT DIR +# =========================================================================== +deploy_to_compose_dirs() { + log "=== Copying WARs to gen/wars/ ===" + rm -f "$WEBAPPS_DIR"/*.war 2>/dev/null || true + mkdir -p "$WEBAPPS_DIR" + + for entry in "${MANIFEST[@]}"; do + IFS='|' read -r ctx server_folder pom_subpath app_class <<< "$entry" + should_process "$ctx" || continue + + local war="$WRAPPERS_DIR/$ctx/target/${ctx}.war" + if [[ -f "$war" ]]; then + cp "$war" "$WEBAPPS_DIR/" + else + err "Missing: $war" + fi + done + + (cd "$SCRIPT_DIR" && find gen/wars -maxdepth 1 -name '*.war' -type f 2>/dev/null) || true + echo "" + echo "Folders gen/wars/ and gen/externalized-war-configs/ ready for \$ docker compose up" +} + +# =========================================================================== +# MAIN +# =========================================================================== +generate_wrappers +build_wars +deploy_to_compose_dirs