From 4f361f7cae704e891858c5331a41bda82ce028e8 Mon Sep 17 00:00:00 2001 From: leiyun Date: Thu, 11 Jun 2026 11:35:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=8F=8C=E7=8E=AF?= =?UTF-8?q?=E5=A2=83COS=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- isolated/README.md | 234 +++++++++++------------ isolated/apply-update.sh | 10 +- isolated/build-package.ps1 | 95 +++++++++- isolated/build-package.sh | 35 +++- isolated/build-update.sh | 23 ++- isolated/deploy-from-url.sh | 142 ++++++++++++++ isolated/install.sh | 26 +-- isolated/profiles/docker-compose.yml | 238 ++++++++++++++++++++++++ isolated/profiles/emp-test/.env.example | 108 +++++++++++ isolated/profiles/emp-uat/.env.example | 108 +++++++++++ isolated/publish-cos.sh | 120 ++++++++++++ isolated/update.MD | 228 +++++++++-------------- 12 files changed, 1075 insertions(+), 292 deletions(-) create mode 100644 isolated/deploy-from-url.sh create mode 100644 isolated/profiles/docker-compose.yml create mode 100644 isolated/profiles/emp-test/.env.example create mode 100644 isolated/profiles/emp-uat/.env.example create mode 100644 isolated/publish-cos.sh diff --git a/isolated/README.md b/isolated/README.md index 7e30df6..110e0cf 100644 --- a/isolated/README.md +++ b/isolated/README.md @@ -1,202 +1,184 @@ -# EMP 隔离测试环境部署说明 +# EMP 隔离部署说明 -这套部署方案用于“服务器不放源码,只运行本地打好的 Docker 镜像包”的场景。 +本目录用于在打包/测试服务器构建 Docker 镜像包,上传到腾讯云 COS,然后在甲方服务器通过包 URL 下载并部署。 -## 目录说明 - -本地项目中: - -```text -deploy/isolated/ - build-package.ps1 # Windows 本地打包 - build-package.sh # Linux / macOS / WSL 本地打包 - docker-compose.runtime.yml # 服务器运行用 compose 模板 - .env.example # 服务器运行配置模板 - install.sh # 服务器安装脚本 - dockerfiles/ # 本地构建镜像用 Dockerfile - nginx/admin.conf # 前端 nginx 代理配置 -``` +默认环境为 `emp-test`,用于兼容原有流程;UAT 环境使用 `DEPLOY_ENV=emp-uat`。`emp-test` 和 `emp-uat` 都有独立环境变量模板,并共用一份运行用 compose 模板。 -打包后产物在: +## 目录说明 ```text -deploy/isolated/dist/emp-test-runtime-时间戳.tar.gz +build-package.sh Linux / WSL 全量打包脚本 +build-package.ps1 Windows PowerShell 全量打包脚本 +build-update.sh 增量打包脚本 +publish-cos.sh 上传 COS 并输出签名 URL +deploy-from-url.sh 甲方服务器按 URL 下载并部署 +install.sh 甲方服务器全量安装脚本 +apply-update.sh 甲方服务器增量更新脚本 +profiles/docker-compose.yml emp-test / emp-uat 共用运行模板 +profiles/emp-test/.env.example +profiles/emp-uat/.env.example +docker-compose.runtime.yml 旧默认运行模板,保留兼容 ``` -服务器解压后只需要: +打包时 compose 选择顺序为: ```text -docker-compose.yml -.env -install.sh -images.tar -README.md +profiles//docker-compose.yml +-> profiles/docker-compose.yml +-> docker-compose.runtime.yml +-> test/docker-compose.yml ``` -## 本地打包 +正常情况下,`emp-test` 和 `emp-uat` 都使用 `profiles/docker-compose.yml`,差异只来自各自的 `.env.example`。 -Windows PowerShell: - -```powershell -cd E:\emp\deploy\isolated -.\build-package.ps1 -``` +## COS 配置 -Linux / WSL: +打包服务器启用 `COS_UPLOAD=1` 前,需要设置: ```bash -cd /path/to/emp/deploy/isolated -sh build-package.sh +export COS_SECRET_ID=change-me +export COS_SECRET_KEY=change-me +export COS_REGION=ap-chengdu +export COS_BUCKET=emp-example-bucket ``` -如果 `deploy` 是独立仓库,不在 `emp` 根目录下,需要指定业务代码根目录: +可选配置: ```bash -cd /home/git/emp_test_deploy/isolated -EMP_ROOT=/home/git/emp sh build-package.sh +export COS_SIGN_EXPIRE=604800 +export COS_PREFIX=deploy/emp-uat/runtime/custom +export COS_CONFIG_PATH=/path/to/.cos.yaml ``` -`EMP_ROOT` 指向的目录下必须包含: +`publish-cos.sh` 使用腾讯云 `coscli cp` 上传包,并用 `coscli signurl` 生成临时下载 URL。打包服务器需要提前安装 `coscli`。 -```text -emp_server/ -emp_admin/ -emp_ws/ -``` - -如果本机已经构建好所有业务镜像,只想重新生成安装包: +## UAT 全量打包并上传 ```bash -SKIP_BUILD=1 sh build-package.sh +cd /home/git/emp/deploy/isolated + +DEPLOY_ENV=emp-uat \ +COS_UPLOAD=1 \ +EMP_ROOT=/home/git/emp \ +./build-package.sh ``` -国内 npm 慢时可以指定源: +Windows PowerShell: -```bash -NPM_REGISTRY=https://registry.npmmirror.com sh build-package.sh +```powershell +cd E:\emp\deploy\isolated +.\build-package.ps1 -DeployEnv emp-uat -CosUpload ``` -默认会把 MySQL、Redis、Kafka、TDengine、Nacos 的中间件镜像一起打进 `images.tar`,服务器不需要访问 Docker Hub。 +输出示例: -## 上传服务器 - -```bash -scp deploy/isolated/dist/emp-test-runtime-*.tar.gz root@服务器IP:/opt/ +```text +Package: .../dist/emp-uat-runtime-20260611120000.tar.gz +COS Key: deploy/emp-uat/runtime/20260611120000/emp-uat-runtime-20260611120000.tar.gz +SHA256: ... +URL: https://... ``` -服务器执行: +## 甲方服务器全量部署 + +先将 `deploy-from-url.sh` 放到甲方服务器。之后执行: ```bash -mkdir -p /opt/emp-test -tar -xzf /opt/emp-test-runtime-*.tar.gz -C /opt/emp-test --strip-components=1 -cd /opt/emp-test -cp .env.example .env -vi .env -sh install.sh +DEPLOY_ENV=emp-uat \ +DEPLOY_HOME=/home/admin-x99/emp-uat \ +PACKAGE_SHA256=<打包输出的SHA256> \ +bash deploy-from-url.sh "<打包输出的URL>" ``` -## 必改配置 - -`.env` 中至少要改这些: +脚本会将包下载到: -```env -PUBLIC_HOST=服务器外网IP或域名 -MYSQL_ROOT_PASSWORD=强密码 -DB_PWD=同 MYSQL_ROOT_PASSWORD -SIMULATOR_DB_PASSWORD=同 MYSQL_ROOT_PASSWORD -REDIS_PASSWORD=强密码 -PDF_FRONTEND_BASE_URL=http://服务器外网IP:4081 +```text +/home/admin-x99/emp-uat/packages/<时间戳>/ ``` -如果端口被宿主机其他项目占用,改这些端口: +然后把运行文件复制到: -```env -ADMIN_HOST_PORT=4081 -GATEWAY_HOST_PORT=9000 -MYSQL_HOST_PORT=13306 -KAFKA_HOST_PORT=19094 -TDENGINE_REST_HOST_PORT=6041 +```text +/home/admin-x99/emp-uat/runtime/ ``` -## 对外连接 +并执行 `install.sh`。 -MySQL 8.0: +如果目标目录下还没有 `.env`,`install.sh` 会先从 `.env.example` 生成 `.env` 并停止。修改密码、`PUBLIC_HOST`、端口和第三方配置后,再执行: -```text -host: 服务器外网IP -port: MYSQL_HOST_PORT,默认 13306 -user: root -password: MYSQL_ROOT_PASSWORD -database: emp +```bash +cd /home/admin-x99/emp-uat/runtime +DEPLOY_ENV=emp-uat bash install.sh ``` -Kafka: +## 同一台服务器部署 test 和 uat -```text -bootstrap.servers=PUBLIC_HOST:KAFKA_HOST_PORT -默认端口:19094 -``` +同一台甲方服务器可以同时部署两套环境。两套环境使用不同 `DEPLOY_ENV`、`DEPLOY_HOME`、compose project 和宿主机端口。 -TDengine REST: +`profiles/docker-compose.yml` 中网关、PDF、Nacos、Redis 默认只在 Docker 内网访问;前端 Nginx 容器会在 Docker 内网代理 `/api/` 和 `/socket.io/`。 -```text -http://PUBLIC_HOST:TDENGINE_REST_HOST_PORT -默认端口:6041 -``` +| 环境 | 部署目录 | Compose 项目名 | 前端 | WS | MySQL | 本地 Kafka 可选 | TDengine REST | +| --- | --- | --- | --- | --- | --- | --- | --- | +| emp-test | `/home/admin-x99/emp-test` | `emp-test` | 4750 | 4751 | 4752 | 4753 | 4754 | +| emp-uat | `/home/admin-x99/emp-uat` | `emp-uat` | 4755 | 4756 | 4757 | 4758 | 4759 | -Nacos: +`4760` 预留备用。当前公共 compose 模板不对外暴露 Gateway、PDF、Nacos、Redis、TDengine RPC;如需额外暴露,再使用 `4760` 或向甲方申请新端口。 -```text -http://PUBLIC_HOST:NACOS_HOST_PORT/nacos -默认账号:nacos -默认密码:nacos -``` +甲方服务器系统重装后,`emp-test` 和 `emp-uat` 都按全量部署重新执行一次;不要只打增量包。 -前端: +Kafka 当前配置: -```text -http://PUBLIC_HOST:ADMIN_HOST_PORT -``` +- `emp-test` 模拟器推送:`ip-cld.cn:29362` / `test-vehicle-real-data`。 +- `emp-test` 后端消费:`ip-cld.cn:29362` / `YuanJing-test-vehicle-mock-data`。 +- `emp-uat` 模拟器推送:`ip-cld.cn:29362` / `uat-vehicle-real-data`。 +- `emp-uat` 后端消费:`ip-cld.cn:29362` / `YuanJing-uat-vehicle-mock-data`。 -## 数据导入 +内部 Kafka 镜像仍会打进离线包,但 `profiles/docker-compose.yml` 默认不启动 Kafka。需要本地联调内部 Kafka 时,再显式启用 compose profile:`COMPOSE_PROFILES=local-kafka`。 -MySQL 导入示例: +部署 test: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test exec -T mysql \ - mysql -uroot -p"$MYSQL_ROOT_PASSWORD" emp < emp.sql +DEPLOY_ENV=emp-test \ +DEPLOY_HOME=/home/admin-x99/emp-test \ +PACKAGE_SHA256=<打包输出的SHA256> \ +bash deploy-from-url.sh "" ``` -TDengine 导入建议仍使用 `taosdump`: +部署 uat: ```bash -docker cp td_dump_dir emp-test-tdengine-1:/tmp/td_dump -docker compose --env-file .env -f docker-compose.yml -p emp-test exec tdengine \ - taosdump -u root -p"$TDENGINE_PWD" -i /tmp/td_dump +DEPLOY_ENV=emp-uat \ +DEPLOY_HOME=/home/admin-x99/emp-uat \ +PACKAGE_SHA256=<打包输出的SHA256> \ +bash deploy-from-url.sh "" ``` -## 常用命令 +Docker compose 会按项目名隔离容器、网络和数据卷,所以只要 `PROJECT_NAME` 不同,`emp-test` 和 `emp-uat` 的数据卷不会互相覆盖。 -查看状态: +## 甲方服务器常用命令 ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test ps -``` - -看日志: +cd /home/admin-x99/emp-uat/runtime -```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test logs -f emp-gateway +docker compose --env-file .env -f docker-compose.yml -p emp-uat ps +docker compose --env-file .env -f docker-compose.yml -p emp-uat logs -f emp-gateway +docker compose --env-file .env -f docker-compose.yml -p emp-uat down ``` -停止: +查看 test 时把目录和项目名改为 `emp-test`: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test down +cd /home/admin-x99/emp-test/runtime +docker compose --env-file .env -f docker-compose.yml -p emp-test ps ``` -停止并删除数据卷: +## 手工兜底部署 + +如果 COS 不可用,仍然可以通过其他方式把 `dist/` 下的包传到甲方服务器。全量包手工部署示例: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test down -v +mkdir -p /home/admin-x99/emp-uat/runtime +tar -xzf emp-uat-runtime-*.tar.gz -C /home/admin-x99/emp-uat/runtime --strip-components=1 +cd /home/admin-x99/emp-uat/runtime +DEPLOY_ENV=emp-uat bash install.sh ``` diff --git a/isolated/apply-update.sh b/isolated/apply-update.sh index c723b6b..e54f72f 100644 --- a/isolated/apply-update.sh +++ b/isolated/apply-update.sh @@ -5,12 +5,14 @@ set -Eeuo pipefail # Usage: # bash apply-update.sh # bash apply-update.sh emp-admin emp-monitor -# PROJECT_NAME=emp-test ENV_FILE=.env COMPOSE_FILE=docker-compose.yml bash apply-update.sh +# DEPLOY_ENV=emp-uat ENV_FILE=.env COMPOSE_FILE=docker-compose.yml bash apply-update.sh SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -PROJECT_NAME="${PROJECT_NAME:-emp-test}" +DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" +PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}" +DEPLOY_HOME="${DEPLOY_HOME:-/home/admin-x99/$DEPLOY_ENV}" ENV_FILE="${ENV_FILE:-.env}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" IMAGE_TAR="${IMAGE_TAR:-images.tar}" @@ -66,7 +68,9 @@ resolve_runtime_file() { local candidates=( "../runtime/$default_name" - "/home/admin-x99/emp-test/runtime/$default_name" + "$DEPLOY_HOME/runtime/$default_name" + "$DEPLOY_HOME/$default_name" + "/opt/$DEPLOY_ENV/$default_name" ) local candidate for candidate in "${candidates[@]}"; do diff --git a/isolated/build-package.ps1 b/isolated/build-package.ps1 index be98966..22e8b1b 100644 --- a/isolated/build-package.ps1 +++ b/isolated/build-package.ps1 @@ -1,10 +1,13 @@ param( + [string]$DeployEnv = $(if ($env:DEPLOY_ENV) { $env:DEPLOY_ENV } else { "emp-test" }), [string]$ImageNamespace = $(if ($env:IMAGE_NAMESPACE) { $env:IMAGE_NAMESPACE } else { "emp-test" }), [string]$ImageTag = $(if ($env:IMAGE_TAG) { $env:IMAGE_TAG } else { Get-Date -Format "yyyyMMddHHmmss" }), [string]$NpmRegistry = $(if ($env:NPM_REGISTRY) { $env:NPM_REGISTRY } else { "https://registry.npmjs.org" }), [string]$EmpRoot = $(if ($env:EMP_ROOT) { $env:EMP_ROOT } else { "" }), + [string]$ProfileDir = $(if ($env:PROFILE_DIR) { $env:PROFILE_DIR } else { "" }), [switch]$SkipBuild, - [switch]$NoMiddlewareImages + [switch]$NoMiddlewareImages, + [switch]$CosUpload ) $ErrorActionPreference = "Stop" @@ -14,7 +17,10 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $RootDir = $null $DistDir = Join-Path $ScriptDir "dist" $BuildContextDir = Join-Path $ScriptDir ".build-context" -$PackageName = "emp-test-runtime-$ImageTag" +if (-not $env:IMAGE_NAMESPACE -and $ImageNamespace -eq "emp-test" -and $DeployEnv -ne "emp-test") { + $ImageNamespace = $DeployEnv +} +$PackageName = $(if ($env:PACKAGE_NAME) { $env:PACKAGE_NAME } else { "$DeployEnv-runtime-$ImageTag" }) $PackageDir = Join-Path $DistDir $PackageName $PackageArchive = Join-Path $DistDir "$PackageName.tar.gz" @@ -202,18 +208,34 @@ function Prepare-Package { $ComposeSource = Join-Path $ScriptDir "docker-compose.runtime.yml" $EnvSource = Join-Path $ScriptDir ".env.example" + $ResolvedProfileDir = $(if ($ProfileDir) { $ProfileDir } else { Join-Path $ScriptDir "profiles\$DeployEnv" }) + $CommonProfileComposeSource = Join-Path $ScriptDir "profiles\docker-compose.yml" + $ProfileComposeSource = Join-Path $ResolvedProfileDir "docker-compose.yml" + $ProfileEnvSource = Join-Path $ResolvedProfileDir ".env.example" + $ProfileEnvTxtSource = Join-Path $ResolvedProfileDir "env.txt" $TestComposeSource = Join-Path $ScriptDir "test\docker-compose.yml" $TestEnvSource = Join-Path $ScriptDir "test\env.txt" - if (Test-Path $TestComposeSource) { + + if (Test-Path $ProfileComposeSource) { + $ComposeSource = $ProfileComposeSource + } elseif (Test-Path $CommonProfileComposeSource) { + $ComposeSource = $CommonProfileComposeSource + } elseif ($DeployEnv -eq "emp-test" -and (Test-Path $TestComposeSource)) { $ComposeSource = $TestComposeSource } - if (Test-Path $TestEnvSource) { + + if (Test-Path $ProfileEnvSource) { + $EnvSource = $ProfileEnvSource + } elseif (Test-Path $ProfileEnvTxtSource) { + $EnvSource = $ProfileEnvTxtSource + } elseif ($DeployEnv -eq "emp-test" -and (Test-Path $TestEnvSource)) { $EnvSource = $TestEnvSource } Copy-Item $ComposeSource (Join-Path $PackageDir "docker-compose.yml") -Force Copy-Item $EnvSource (Join-Path $PackageDir ".env.example") -Force Copy-Item (Join-Path $ScriptDir "install.sh") (Join-Path $PackageDir "install.sh") -Force + Copy-Item (Join-Path $ScriptDir "deploy-from-url.sh") (Join-Path $PackageDir "deploy-from-url.sh") -Force Copy-Item (Join-Path $ScriptDir "README.md") (Join-Path $PackageDir "README.md") -Force } @@ -244,6 +266,69 @@ function Archive-Package { Write-Log "Package created: $PackageArchive" } +function Publish-Package { + if (-not $CosUpload -and $env:COS_UPLOAD -ne "1") { + return + } + + $CoscliBin = $(if ($env:COSCLI_BIN) { $env:COSCLI_BIN } else { "coscli" }) + Assert-Command $CoscliBin + + if (-not $env:COS_BUCKET) { + throw "Missing COS_BUCKET" + } + + $CoscliOptions = @() + if ($env:COS_CONFIG_PATH) { + $CoscliOptions += @("-c", $env:COS_CONFIG_PATH) + } else { + if (-not $env:COS_SECRET_ID) { throw "Missing COS_SECRET_ID or COS_CONFIG_PATH" } + if (-not $env:COS_SECRET_KEY) { throw "Missing COS_SECRET_KEY or COS_CONFIG_PATH" } + if (-not $env:COS_REGION) { throw "Missing COS_REGION" } + $CoscliOptions += @( + "--init-skip=true", + "-i", $env:COS_SECRET_ID, + "-k", $env:COS_SECRET_KEY, + "-e", "cos.$($env:COS_REGION).myqcloud.com" + ) + if ($env:COS_TOKEN) { + $CoscliOptions += @("--token", $env:COS_TOKEN) + } + } + + $RunId = Get-Date -Format "yyyyMMddHHmmss" + $CosSignExpire = $(if ($env:COS_SIGN_EXPIRE) { $env:COS_SIGN_EXPIRE } else { "604800" }) + $DefaultPrefix = "deploy/$DeployEnv/runtime/$RunId" + $CosPrefix = $(if ($env:COS_PREFIX) { $env:COS_PREFIX } else { $DefaultPrefix }).Trim("/") + $PackageBase = Split-Path -Leaf $PackageArchive + $CosKey = $(if ($env:COS_KEY) { $env:COS_KEY } else { "$CosPrefix/$PackageBase" }).TrimStart("/") + $CosUri = "cos://$($env:COS_BUCKET)/$CosKey" + $Sha256 = (Get-FileHash $PackageArchive -Algorithm SHA256).Hash.ToLowerInvariant() + + Write-Log "Upload package to COS: $CosUri" + & $CoscliBin @(@("cp", $PackageArchive, $CosUri) + $CoscliOptions) + if ($LASTEXITCODE -ne 0) { + throw "coscli cp failed, exit code: $LASTEXITCODE" + } + + Write-Log "Generate signed URL, expire seconds: $CosSignExpire" + $SignedOutput = & $CoscliBin @(@("signurl", $CosUri, "--time", $CosSignExpire, "--simple-output") + $CoscliOptions) + if ($LASTEXITCODE -ne 0) { + throw "coscli signurl failed, exit code: $LASTEXITCODE" + } + $SignedUrl = ($SignedOutput | Select-Object -Last 1).ToString() + + Write-Host "" + Write-Host "Package: $PackageArchive" + Write-Host "COS Key: $CosKey" + Write-Host "SHA256: $Sha256" + Write-Host "URL: $SignedUrl" + Write-Host "" + Write-Host "Target deploy command:" + Write-Host " DEPLOY_ENV=$DeployEnv PACKAGE_SHA256=$Sha256 bash deploy-from-url.sh `"$SignedUrl`"" + Write-Host "" +} + Add-KnownToolPaths Assert-Command "docker" Assert-DockerDaemon @@ -252,6 +337,7 @@ Assert-Command "tar" $RootDir = Resolve-EmpRoot Write-Log "EMP root: $RootDir" Write-Log "Deploy root: $ScriptDir" +Write-Log "Deploy env: $DeployEnv" if (-not $SkipBuild) { Assert-Command "mvn" @@ -266,3 +352,4 @@ if (-not $SkipBuild) { Prepare-Package Save-Images Archive-Package +Publish-Package diff --git a/isolated/build-package.sh b/isolated/build-package.sh index 8e09e9b..971c8f2 100644 --- a/isolated/build-package.sh +++ b/isolated/build-package.sh @@ -10,14 +10,16 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_DIR="$SCRIPT_DIR/dist" BUILD_CONTEXT_DIR="$SCRIPT_DIR/.build-context" -IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-emp-test}" +DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" +IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-$DEPLOY_ENV}" IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}" -PACKAGE_NAME="${PACKAGE_NAME:-emp-test-runtime-${IMAGE_TAG}}" +PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-runtime-${IMAGE_TAG}}" PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" SKIP_BUILD="${SKIP_BUILD:-0}" INCLUDE_MIDDLEWARE_IMAGES="${INCLUDE_MIDDLEWARE_IMAGES:-1}" +COS_UPLOAD="${COS_UPLOAD:-0}" JAVA_MODULES=(emp_gateway emp_auth emp_monitor emp_data) APP_IMAGES=( @@ -150,21 +152,35 @@ prepare_package() { rm -rf "$PACKAGE_DIR" mkdir -p "$PACKAGE_DIR" + local common_profile_compose="$SCRIPT_DIR/profiles/docker-compose.yml" local compose_src="$SCRIPT_DIR/docker-compose.runtime.yml" local env_src="$SCRIPT_DIR/.env.example" - if [[ -f "$SCRIPT_DIR/test/docker-compose.yml" ]]; then + local profile_dir="${PROFILE_DIR:-$SCRIPT_DIR/profiles/$DEPLOY_ENV}" + + if [[ -f "$profile_dir/docker-compose.yml" ]]; then + compose_src="$profile_dir/docker-compose.yml" + elif [[ -f "$common_profile_compose" ]]; then + compose_src="$common_profile_compose" + elif [[ "$DEPLOY_ENV" == "emp-test" && -f "$SCRIPT_DIR/test/docker-compose.yml" ]]; then compose_src="$SCRIPT_DIR/test/docker-compose.yml" fi - if [[ -f "$SCRIPT_DIR/test/env.txt" ]]; then + + if [[ -f "$profile_dir/.env.example" ]]; then + env_src="$profile_dir/.env.example" + elif [[ -f "$profile_dir/env.txt" ]]; then + env_src="$profile_dir/env.txt" + elif [[ "$DEPLOY_ENV" == "emp-test" && -f "$SCRIPT_DIR/test/env.txt" ]]; then env_src="$SCRIPT_DIR/test/env.txt" fi cp "$compose_src" "$PACKAGE_DIR/docker-compose.yml" cp "$env_src" "$PACKAGE_DIR/.env.example" cp "$SCRIPT_DIR/install.sh" "$PACKAGE_DIR/install.sh" + cp "$SCRIPT_DIR/deploy-from-url.sh" "$PACKAGE_DIR/deploy-from-url.sh" cp "$SCRIPT_DIR/README.md" "$PACKAGE_DIR/README.md" chmod +x "$PACKAGE_DIR/install.sh" + chmod +x "$PACKAGE_DIR/deploy-from-url.sh" } save_images() { @@ -189,11 +205,21 @@ archive_package() { log "Package created: $PACKAGE_ARCHIVE" } +publish_package() { + if [[ "$COS_UPLOAD" != "1" ]]; then + return + fi + + log "Upload package to COS" + DEPLOY_ENV="$DEPLOY_ENV" PACKAGE_KIND=runtime bash "$SCRIPT_DIR/publish-cos.sh" "$PACKAGE_ARCHIVE" +} + need_cmd docker need_cmd tar log "EMP root: $ROOT_DIR" log "Deploy root: $SCRIPT_DIR" +log "Deploy env: $DEPLOY_ENV" if [[ "$SKIP_BUILD" != "1" ]]; then need_cmd mvn @@ -208,3 +234,4 @@ fi prepare_package save_images archive_package +publish_package diff --git a/isolated/build-update.sh b/isolated/build-update.sh index 083e83c..9a3927c 100644 --- a/isolated/build-update.sh +++ b/isolated/build-update.sh @@ -8,16 +8,18 @@ set -Eeuo pipefail # EMP_ROOT=/path/to/emp IMAGE_TAG=20260602153000 ./build-update.sh emp-admin emp-monitor # # Output: -# dist/emp-test-update-${IMAGE_TAG}-${services}.tar.gz +# dist/${DEPLOY_ENV}-update-${IMAGE_TAG}-${services}.tar.gz SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_DIR="${DIST_DIR:-$SCRIPT_DIR/dist}" BUILD_CONTEXT_DIR="$SCRIPT_DIR/.build-context" -IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-emp-test}" +DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" +IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-$DEPLOY_ENV}" IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}" NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" SKIP_BUILD="${SKIP_BUILD:-0}" +COS_UPLOAD="${COS_UPLOAD:-0}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" @@ -212,7 +214,7 @@ build_admin_image() { prepare_package() { local services_slug="$1" - PACKAGE_NAME="${PACKAGE_NAME:-emp-test-update-${IMAGE_TAG}-${services_slug}}" + PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-update-${IMAGE_TAG}-${services_slug}}" PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" @@ -228,6 +230,7 @@ prepare_package() { done > "$PACKAGE_DIR/services.txt" { + echo "deploy_env=$DEPLOY_ENV" echo "image_namespace=$IMAGE_NAMESPACE" echo "image_tag=$IMAGE_TAG" echo "services=${REQUESTED_SERVICES[*]}" @@ -255,6 +258,15 @@ archive_package() { log "Update package created: $PACKAGE_ARCHIVE" } +publish_package() { + if [[ "$COS_UPLOAD" != "1" ]]; then + return + fi + + log "Upload update package to COS" + DEPLOY_ENV="$DEPLOY_ENV" PACKAGE_KIND=update bash "$SCRIPT_DIR/publish-cos.sh" "$PACKAGE_ARCHIVE" +} + need_cmd docker need_cmd tar @@ -266,6 +278,7 @@ SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")" log "EMP root: $ROOT_DIR" log "Deploy root: $SCRIPT_DIR" +log "Deploy env: $DEPLOY_ENV" log "Update services: ${REQUESTED_SERVICES[*]}" log "Image tag: $IMAGE_TAG" @@ -304,10 +317,14 @@ fi prepare_package "$SERVICES_SLUG" save_images archive_package +publish_package cat <" + +Manual fallback: mkdir -p /tmp/emp-update tar -xzf $(basename "$PACKAGE_ARCHIVE") -C /tmp/emp-update --strip-components=1 cd /tmp/emp-update diff --git a/isolated/deploy-from-url.sh b/isolated/deploy-from-url.sh new file mode 100644 index 0000000..50bc8cd --- /dev/null +++ b/isolated/deploy-from-url.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Download a package URL and deploy it on the target server. +# Usage: +# DEPLOY_ENV=emp-uat bash deploy-from-url.sh "" +# DEPLOY_ENV=emp-uat PACKAGE_SHA256= bash deploy-from-url.sh "" + +DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" +PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}" +DEPLOY_HOME="${DEPLOY_HOME:-/home/admin-x99/$DEPLOY_ENV}" +PACKAGE_URL="${1:-}" +EXPECTED_SHA256="${PACKAGE_SHA256:-${2:-}}" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d%H%M%S')}" +PACKAGE_ROOT="${PACKAGE_ROOT:-$DEPLOY_HOME/packages}" +RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" +WORK_DIR="$PACKAGE_ROOT/$RUN_ID" +DOWNLOAD_FILE="$WORK_DIR/package.tar.gz" +EXTRACT_DIR="$WORK_DIR/extracted" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" +} + +download_package() { + local url="$1" + local output="$2" + + if command -v curl >/dev/null 2>&1; then + curl -fL --retry 3 --connect-timeout 20 -o "$output" "$url" + elif command -v wget >/dev/null 2>&1; then + wget -O "$output" "$url" + else + die "Missing curl or wget" + fi +} + +calc_sha256() { + local file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + else + die "Missing sha256sum or shasum for checksum verification" + fi +} + +verify_checksum() { + local file="$1" + local expected="$2" + [[ -n "$expected" ]] || return + + local actual + actual="$(calc_sha256 "$file")" + if [[ "$actual" != "$expected" ]]; then + die "SHA256 mismatch. expected=$expected actual=$actual" + fi + log "SHA256 verified: $actual" +} + +copy_runtime_package() { + mkdir -p "$RUNTIME_DIR" + cp "$EXTRACT_DIR/docker-compose.yml" "$RUNTIME_DIR/docker-compose.yml" + cp "$EXTRACT_DIR/install.sh" "$RUNTIME_DIR/install.sh" + cp "$EXTRACT_DIR/images.tar" "$RUNTIME_DIR/images.tar" + + if [[ -f "$EXTRACT_DIR/deploy-from-url.sh" ]]; then + cp "$EXTRACT_DIR/deploy-from-url.sh" "$RUNTIME_DIR/deploy-from-url.sh" + fi + if [[ -f "$EXTRACT_DIR/.env.example" ]]; then + cp "$EXTRACT_DIR/.env.example" "$RUNTIME_DIR/.env.example" + fi + if [[ -f "$EXTRACT_DIR/README.md" ]]; then + cp "$EXTRACT_DIR/README.md" "$RUNTIME_DIR/README.md" + fi +} + +deploy_runtime_package() { + log "Deploy runtime package to $RUNTIME_DIR" + copy_runtime_package + + ( + cd "$RUNTIME_DIR" + DEPLOY_ENV="$DEPLOY_ENV" \ + PROJECT_NAME="$PROJECT_NAME" \ + ENV_FILE=.env \ + COMPOSE_FILE=docker-compose.yml \ + IMAGE_TAR=images.tar \ + bash install.sh + ) +} + +deploy_update_package() { + [[ -f "$RUNTIME_DIR/.env" ]] || die "Missing runtime env file: $RUNTIME_DIR/.env" + [[ -f "$RUNTIME_DIR/docker-compose.yml" ]] || die "Missing runtime compose file: $RUNTIME_DIR/docker-compose.yml" + + log "Apply update package with runtime dir: $RUNTIME_DIR" + ( + cd "$EXTRACT_DIR" + DEPLOY_ENV="$DEPLOY_ENV" \ + DEPLOY_HOME="$DEPLOY_HOME" \ + PROJECT_NAME="$PROJECT_NAME" \ + ENV_FILE="$RUNTIME_DIR/.env" \ + COMPOSE_FILE="$RUNTIME_DIR/docker-compose.yml" \ + IMAGE_TAR=images.tar \ + bash apply-update.sh + ) +} + +[[ -n "$PACKAGE_URL" ]] || die "Usage: DEPLOY_ENV=emp-uat bash deploy-from-url.sh " +need_cmd tar + +mkdir -p "$WORK_DIR" "$EXTRACT_DIR" + +log "Deploy env: $DEPLOY_ENV" +log "Deploy home: $DEPLOY_HOME" +log "Download package" +download_package "$PACKAGE_URL" "$DOWNLOAD_FILE" +verify_checksum "$DOWNLOAD_FILE" "$EXPECTED_SHA256" + +log "Extract package" +tar -xzf "$DOWNLOAD_FILE" -C "$EXTRACT_DIR" --strip-components=1 + +if [[ -f "$EXTRACT_DIR/install.sh" && -f "$EXTRACT_DIR/docker-compose.yml" ]]; then + deploy_runtime_package +elif [[ -f "$EXTRACT_DIR/apply-update.sh" && -f "$EXTRACT_DIR/images.tar" ]]; then + deploy_update_package +else + die "Unknown package structure: $EXTRACT_DIR" +fi + +log "Done. Package workspace: $WORK_DIR" diff --git a/isolated/install.sh b/isolated/install.sh index a7652f6..1f4b9a7 100644 --- a/isolated/install.sh +++ b/isolated/install.sh @@ -1,11 +1,14 @@ #!/usr/bin/env bash set -Eeuo pipefail -# 服务器运行脚本:只加载本地镜像包并启动 compose,不做源码构建。 +# Target server runtime installer. It only loads packaged Docker images and +# starts docker compose; it does not build source code. + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -PROJECT_NAME="${PROJECT_NAME:-emp-test}" +DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" +PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}" ENV_FILE="${ENV_FILE:-.env}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" IMAGE_TAR="${IMAGE_TAR:-images.tar}" @@ -20,7 +23,7 @@ die() { } need_cmd() { - command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1" + command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" } need_cmd docker @@ -30,38 +33,37 @@ if docker compose version >/dev/null 2>&1; then elif command -v docker-compose >/dev/null 2>&1; then DC=(docker-compose) else - die "未安装 docker compose" + die "Missing docker compose" fi if [[ ! -f "$ENV_FILE" ]]; then cp .env.example "$ENV_FILE" - die "已生成 $ENV_FILE,请先修改密码、PUBLIC_HOST 和端口后重新执行:sh install.sh" + die "Generated $ENV_FILE. Update passwords, PUBLIC_HOST and ports, then rerun: bash install.sh" fi if [[ ! -f "$COMPOSE_FILE" ]]; then - die "未找到 $COMPOSE_FILE" + die "Missing compose file: $COMPOSE_FILE" fi if [[ -f "$IMAGE_TAR" ]]; then - log "加载镜像包:$IMAGE_TAR" + log "Load images: $IMAGE_TAR" docker load -i "$IMAGE_TAR" else - log "未找到 $IMAGE_TAR,将尝试使用本机已有镜像或在线拉取镜像" + log "Image tar not found: $IMAGE_TAR. Compose will use local or remote images." fi -log "启动隔离测试环境:$PROJECT_NAME" +log "Start deploy env: $DEPLOY_ENV, project: $PROJECT_NAME" "${DC[@]}" \ --env-file "$ENV_FILE" \ -f "$COMPOSE_FILE" \ -p "$PROJECT_NAME" \ up -d -log "当前容器状态" +log "Current service status" "${DC[@]}" \ --env-file "$ENV_FILE" \ -f "$COMPOSE_FILE" \ -p "$PROJECT_NAME" \ ps -log "完成。前端地址请访问 .env 中的 PUBLIC_HOST:ADMIN_HOST_PORT。" - +log "Done. Visit http://PUBLIC_HOST:ADMIN_HOST_PORT from the configured .env values." diff --git a/isolated/profiles/docker-compose.yml b/isolated/profiles/docker-compose.yml new file mode 100644 index 0000000..aed320c --- /dev/null +++ b/isolated/profiles/docker-compose.yml @@ -0,0 +1,238 @@ +x-app-env: &app-env + env_file: + - .env + restart: unless-stopped + networks: + - emp-net + +x-java-depends: &java-depends + nacos: + condition: service_healthy + mysql: + condition: service_healthy + redis: + condition: service_healthy + +services: + mysql: + image: ${MYSQL_IMAGE:-mysql:8.0} + restart: unless-stopped + ports: + - "0.0.0.0:${MYSQL_HOST_PORT:-23306}:3306" + environment: + TZ: Asia/Shanghai + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE:-emp} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_0900_ai_ci + - --default-time-zone=+08:00 + - --max-connections=1000 + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + redis: + image: ${REDIS_IMAGE:-redis:7-alpine} + restart: unless-stopped + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD} + command: ["sh", "-c", "redis-server --appendonly yes --requirepass \"$${REDIS_PASSWORD}\""] + volumes: + - redis_data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + kafka: + image: ${KAFKA_IMAGE:-bitnami/kafka:3.7.0} + profiles: + - local-kafka + restart: unless-stopped + ports: + - "0.0.0.0:${KAFKA_HOST_PORT:-29362}:9094" + environment: + ALLOW_PLAINTEXT_LISTENER: "yes" + KAFKA_CFG_NODE_ID: 1 + KAFKA_CFG_PROCESS_ROLES: controller,broker + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_CFG_LISTENERS: INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094 + KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://${PUBLIC_HOST}:${KAFKA_HOST_PORT:-29362} + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" + KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 + volumes: + - kafka_data:/bitnami/kafka + healthcheck: + test: ["CMD-SHELL", "/opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --list >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + kafka-init: + image: ${KAFKA_IMAGE:-bitnami/kafka:3.7.0} + profiles: + - local-kafka + restart: "no" + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/bash", "-ec"] + environment: + KAFKA_TOPIC: ${KAFKA_TOPIC:-vehicle-data} + command: | + echo "create kafka topic: $${KAFKA_TOPIC}" + /opt/bitnami/kafka/bin/kafka-topics.sh \ + --bootstrap-server kafka:9092 \ + --create \ + --if-not-exists \ + --topic "$${KAFKA_TOPIC}" \ + --partitions 3 \ + --replication-factor 1 + /opt/bitnami/kafka/bin/kafka-topics.sh \ + --bootstrap-server kafka:9092 \ + --describe \ + --topic "$${KAFKA_TOPIC}" + networks: + - emp-net + + tdengine: + image: ${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.0} + hostname: tdengine + privileged: true + restart: unless-stopped + ports: + - "0.0.0.0:${TDENGINE_REST_HOST_PORT:-37363}:6041" + environment: + TZ: Asia/Shanghai + TAOS_FQDN: tdengine + TDENGINE_DATABASE: ${TDENGINE_DATABASE:-emp} + volumes: + - tdengine_data:/var/lib/taos + - tdengine_log:/var/log/taos + healthcheck: + test: ["CMD-SHELL", "taos -s \"create database if not exists $${TDENGINE_DATABASE}; show databases;\" >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + nacos: + image: ${NACOS_IMAGE:-nacos/nacos-server:v2.3.2-slim} + restart: unless-stopped + environment: + MODE: standalone + JVM_XMS: 256m + JVM_XMX: 512m + SPRING_DATASOURCE_PLATFORM: "" + NACOS_AUTH_ENABLE: ${NACOS_AUTH_ENABLE:-true} + NACOS_AUTH_IDENTITY_KEY: ${NACOS_AUTH_IDENTITY_KEY:-emp} + NACOS_AUTH_IDENTITY_VALUE: ${NACOS_AUTH_IDENTITY_VALUE:-emp2026} + NACOS_AUTH_TOKEN: ${NACOS_AUTH_TOKEN} + volumes: + - nacos_data:/home/nacos/data + - nacos_logs:/home/nacos/logs + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8848/nacos/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + emp-gateway: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-gateway:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + + emp-auth: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-auth:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + + emp-monitor: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-monitor:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + tdengine: + condition: service_healthy + emp-pdf: + condition: service_healthy + + emp-data: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-data:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + tdengine: + condition: service_healthy + emp-ws: + condition: service_started + + emp-pdf: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-pdf:${IMAGE_TAG:-latest} + environment: + PORT: 3100 + healthcheck: + test: ["CMD-SHELL", "node -e \"require('http').get('http://127.0.0.1:3100/pdf', r => { r.resume(); process.exit(r.statusCode < 500 ? 0 : 1) }).on('error', () => process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 30 + + emp-ws: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-ws:${IMAGE_TAG:-latest} + ports: + - "0.0.0.0:${WS_HOST_PORT:-37362}:3000" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + + emp-admin: + image: ${IMAGE_NAMESPACE:-emp-test}/emp-admin:${IMAGE_TAG:-latest} + restart: unless-stopped + ports: + - "0.0.0.0:${ADMIN_HOST_PORT:-37361}:80" + depends_on: + emp-gateway: + condition: service_started + emp-ws: + condition: service_started + networks: + - emp-net + +networks: + emp-net: + driver: bridge + +volumes: + mysql_data: + redis_data: + kafka_data: + tdengine_data: + tdengine_log: + nacos_data: + nacos_logs: diff --git a/isolated/profiles/emp-test/.env.example b/isolated/profiles/emp-test/.env.example new file mode 100644 index 0000000..95cc7eb --- /dev/null +++ b/isolated/profiles/emp-test/.env.example @@ -0,0 +1,108 @@ +# EMP test runtime variables. +# Copy to .env on the target server and update passwords, PUBLIC_HOST and ports. + +COMPOSE_PROJECT_NAME=emp-test +CONTAINER_PREFIX=emp-test +IMAGE_NAMESPACE=emp-test +IMAGE_TAG=latest + +PUBLIC_HOST=127.0.0.1 + +ADMIN_HOST_PORT=4750 +WS_HOST_PORT=4751 +MYSQL_HOST_PORT=4752 +KAFKA_HOST_PORT=4753 +TDENGINE_REST_HOST_PORT=4754 + +REDIS_BIND_HOST=127.0.0.1 + +# profiles/docker-compose.yml 不对外暴露 Gateway/PDF/Nacos/Redis/TDengine RPC。 +# 4760 预留备用,确需额外暴露服务时再单独分配。 + +MYSQL_IMAGE=mysql:8.0 +REDIS_IMAGE=redis:7-alpine +KAFKA_IMAGE=bitnami/kafka:3.7.0 +TDENGINE_IMAGE=tdengine/tdengine:3.3.6.0 +NACOS_IMAGE=nacos/nacos-server:v2.3.2-slim + +MYSQL_DATABASE=emp +MYSQL_ROOT_PASSWORD=change-me-mysql-root +DB_URL=jdbc:mysql://mysql:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +DB_USER=root +DB_PWD=change-me-mysql-root +# DB_READ_URL=jdbc:mysql://mysql-read:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +# DB_READ_USER=root +# DB_READ_PWD=change-me-mysql-root + +SIMULATOR_DB_HOST=mysql +SIMULATOR_DB_PORT=3306 +SIMULATOR_DB_USER=root +SIMULATOR_DB_PASSWORD=change-me-mysql-root +SIMULATOR_DB_DATABASE=emp +SIMULATOR_DB_LIMIT=0 + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=change-me-redis +REDIS_DB=0 + +KAFKA_BROKERS=ip-cld.cn:29362 +KAFKA_GROUP_ID=emp-test-data-group +KAFKA_TOPIC=YuanJing-test-vehicle-mock-data +KAFKA_USER= +KAFKA_PWD= + +SIMULATOR_KAFKA_BROKERS=ip-cld.cn:29362 +SIMULATOR_KAFKA_TOPIC=test-vehicle-real-data +SIMULATOR_KAFKA_USER= +SIMULATOR_KAFKA_PASSWORD= +SIMULATOR_KAFKA_CLIENT_ID=emp-test-simulator +SIMULATOR_KAFKA_BATCH_SIZE=500 + +TDENGINE_DATABASE=emp +TDENGINE_USER=root +TDENGINE_PWD=taosdata +TDENGINE_URL=jdbc:TAOS-RS://tdengine:6041/emp + +NACOS_ADDR=nacos:8848 +NACOS_USER=nacos +NACOS_PWD=nacos +NACOS_AUTH_ENABLE=true +NACOS_AUTH_IDENTITY_KEY=emp +NACOS_AUTH_IDENTITY_VALUE=emp2026 +NACOS_AUTH_TOKEN=ZW1wLXBsYXRmb3JtLW5hY29zLXNlY3JldC1rZXktMjAyNg== + +SPRING_PROFILES_ACTIVE=prod +JWT_SECRET=change-me-jwt-secret +JWT_EXPIRATION=86400000 +SCHEDULER_ENABLED=true + +EMP_WS_ENV=production +NODE_ENV=production +PORT=3000 +WS_INSTANCES=1 +WS_HOST=emp-ws +SERVER_API_BASE_URL=http://emp-gateway:9000/api +SIMULATOR_ADMIN_USERNAME=admin +SIMULATOR_LOGIN_AUTH=change-me-login-auth +SIMULATOR_JWT_SECRET=change-me-jwt-secret + +PDF_SERVICE_URL=http://emp-pdf:3100 +PDF_FRONTEND_BASE_URL=http://127.0.0.1:4750 + +AMAP_KEY= +COS_SECRET_ID=change-me +COS_SECRET_KEY=change-me +COS_REGION=ap-chengdu +COS_BUCKET=emp-example-bucket +COS_PUBLIC_BASE_URL= + +SYNC_BASE_URL=https://example.com +SYNC_TK=change-me +SYNC_TENANT_ID=change-me +SYNC_REPORT_CRON=0 30 2 * * ? +SYNC_REPORT_SYNC_ENABLED=false +SYNC_REPORT_SYNC_CONCURRENCY=3 +SYNC_REPORT_SYNC_GROUP_NAMES= +SYNC_REPORT_CACHE_MISS_FETCH_ENABLED=false +GROUP_REPORT_CRON=0 30 4 ? * THU,SUN diff --git a/isolated/profiles/emp-uat/.env.example b/isolated/profiles/emp-uat/.env.example new file mode 100644 index 0000000..5122fde --- /dev/null +++ b/isolated/profiles/emp-uat/.env.example @@ -0,0 +1,108 @@ +# EMP UAT runtime variables. +# Copy to .env on the target server and update passwords, PUBLIC_HOST and ports. + +COMPOSE_PROJECT_NAME=emp-uat +CONTAINER_PREFIX=emp-uat +IMAGE_NAMESPACE=emp-uat +IMAGE_TAG=latest + +PUBLIC_HOST=127.0.0.1 + +ADMIN_HOST_PORT=4755 +WS_HOST_PORT=4756 +MYSQL_HOST_PORT=4757 +KAFKA_HOST_PORT=4758 +TDENGINE_REST_HOST_PORT=4759 + +REDIS_BIND_HOST=127.0.0.1 + +# profiles/docker-compose.yml 不对外暴露 Gateway/PDF/Nacos/Redis/TDengine RPC。 +# 4760 预留备用,确需额外暴露服务时再单独分配。 + +MYSQL_IMAGE=mysql:8.0 +REDIS_IMAGE=redis:7-alpine +KAFKA_IMAGE=bitnami/kafka:3.7.0 +TDENGINE_IMAGE=tdengine/tdengine:3.3.6.0 +NACOS_IMAGE=nacos/nacos-server:v2.3.2-slim + +MYSQL_DATABASE=emp +MYSQL_ROOT_PASSWORD=change-me-mysql-root +DB_URL=jdbc:mysql://mysql:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +DB_USER=root +DB_PWD=change-me-mysql-root +# DB_READ_URL=jdbc:mysql://mysql-read:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +# DB_READ_USER=root +# DB_READ_PWD=change-me-mysql-root + +SIMULATOR_DB_HOST=mysql +SIMULATOR_DB_PORT=3306 +SIMULATOR_DB_USER=root +SIMULATOR_DB_PASSWORD=change-me-mysql-root +SIMULATOR_DB_DATABASE=emp +SIMULATOR_DB_LIMIT=0 + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=change-me-redis +REDIS_DB=0 + +KAFKA_BROKERS=ip-cld.cn:29362 +KAFKA_GROUP_ID=emp-uat-data-group +KAFKA_TOPIC=YuanJing-uat-vehicle-mock-data +KAFKA_USER= +KAFKA_PWD= + +SIMULATOR_KAFKA_BROKERS=ip-cld.cn:29362 +SIMULATOR_KAFKA_TOPIC=uat-vehicle-real-data +SIMULATOR_KAFKA_USER= +SIMULATOR_KAFKA_PASSWORD= +SIMULATOR_KAFKA_CLIENT_ID=emp-uat-simulator +SIMULATOR_KAFKA_BATCH_SIZE=500 + +TDENGINE_DATABASE=emp +TDENGINE_USER=root +TDENGINE_PWD=taosdata +TDENGINE_URL=jdbc:TAOS-RS://tdengine:6041/emp + +NACOS_ADDR=nacos:8848 +NACOS_USER=nacos +NACOS_PWD=nacos +NACOS_AUTH_ENABLE=true +NACOS_AUTH_IDENTITY_KEY=emp +NACOS_AUTH_IDENTITY_VALUE=emp2026 +NACOS_AUTH_TOKEN=ZW1wLXBsYXRmb3JtLW5hY29zLXNlY3JldC1rZXktMjAyNg== + +SPRING_PROFILES_ACTIVE=prod +JWT_SECRET=change-me-jwt-secret +JWT_EXPIRATION=86400000 +SCHEDULER_ENABLED=true + +EMP_WS_ENV=production +NODE_ENV=production +PORT=3000 +WS_INSTANCES=1 +WS_HOST=emp-ws +SERVER_API_BASE_URL=http://emp-gateway:9000/api +SIMULATOR_ADMIN_USERNAME=admin +SIMULATOR_LOGIN_AUTH=change-me-login-auth +SIMULATOR_JWT_SECRET=change-me-jwt-secret + +PDF_SERVICE_URL=http://emp-pdf:3100 +PDF_FRONTEND_BASE_URL=http://127.0.0.1:4755 + +AMAP_KEY= +COS_SECRET_ID=change-me +COS_SECRET_KEY=change-me +COS_REGION=ap-chengdu +COS_BUCKET=emp-example-bucket +COS_PUBLIC_BASE_URL= + +SYNC_BASE_URL=https://example.com +SYNC_TK=change-me +SYNC_TENANT_ID=change-me +SYNC_REPORT_CRON=0 30 2 * * ? +SYNC_REPORT_SYNC_ENABLED=false +SYNC_REPORT_SYNC_CONCURRENCY=3 +SYNC_REPORT_SYNC_GROUP_NAMES= +SYNC_REPORT_CACHE_MISS_FETCH_ENABLED=false +GROUP_REPORT_CRON=0 30 4 ? * THU,SUN diff --git a/isolated/publish-cos.sh b/isolated/publish-cos.sh new file mode 100644 index 0000000..d9fb715 --- /dev/null +++ b/isolated/publish-cos.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Upload a runtime/update package to Tencent COS and print a deployable URL. +# Required when COSCLI is not preconfigured: +# COS_SECRET_ID, COS_SECRET_KEY, COS_REGION, COS_BUCKET +# +# Optional: +# DEPLOY_ENV=emp-uat +# PACKAGE_KIND=runtime|update +# COS_PREFIX=deploy/emp-uat/update/20260611120000 +# COS_KEY=deploy/emp-uat/update/package.tar.gz +# COS_SIGN_EXPIRE=604800 +# COS_CONFIG_PATH=/path/to/.cos.yaml + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" +PACKAGE_KIND="${PACKAGE_KIND:-}" +COSCLI_BIN="${COSCLI_BIN:-coscli}" +COS_SIGN_EXPIRE="${COS_SIGN_EXPIRE:-604800}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" +} + +calc_sha256() { + local file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + else + echo "" + fi +} + +infer_package_kind() { + local name="$1" + if [[ -n "$PACKAGE_KIND" ]]; then + echo "$PACKAGE_KIND" + elif [[ "$name" == *"-update-"* ]]; then + echo "update" + else + echo "runtime" + fi +} + +build_coscli_opts() { + COSCLI_OPTS=() + + if [[ -n "${COS_CONFIG_PATH:-}" ]]; then + COSCLI_OPTS+=("-c" "$COS_CONFIG_PATH") + return + fi + + [[ -n "${COS_SECRET_ID:-}" ]] || die "Missing COS_SECRET_ID or COS_CONFIG_PATH" + [[ -n "${COS_SECRET_KEY:-}" ]] || die "Missing COS_SECRET_KEY or COS_CONFIG_PATH" + [[ -n "${COS_REGION:-}" ]] || die "Missing COS_REGION" + + COSCLI_OPTS+=( + "--init-skip=true" + "-i" "$COS_SECRET_ID" + "-k" "$COS_SECRET_KEY" + "-e" "cos.$COS_REGION.myqcloud.com" + ) + + if [[ -n "${COS_TOKEN:-}" ]]; then + COSCLI_OPTS+=("--token" "$COS_TOKEN") + fi +} + +[[ "$#" -eq 1 ]] || die "Usage: bash $SCRIPT_DIR/publish-cos.sh " +PACKAGE_FILE="$1" +[[ -f "$PACKAGE_FILE" ]] || die "Package not found: $PACKAGE_FILE" +[[ -n "${COS_BUCKET:-}" ]] || die "Missing COS_BUCKET" + +need_cmd "$COSCLI_BIN" +build_coscli_opts + +PACKAGE_FILE="$(cd "$(dirname "$PACKAGE_FILE")" && pwd)/$(basename "$PACKAGE_FILE")" +PACKAGE_BASE="$(basename "$PACKAGE_FILE")" +PACKAGE_KIND="$(infer_package_kind "$PACKAGE_BASE")" +RUN_ID="$(date '+%Y%m%d%H%M%S')" +DEFAULT_PREFIX="deploy/$DEPLOY_ENV/$PACKAGE_KIND/$RUN_ID" +COS_PREFIX="${COS_PREFIX:-$DEFAULT_PREFIX}" +COS_PREFIX="${COS_PREFIX#/}" +COS_PREFIX="${COS_PREFIX%/}" +COS_KEY="${COS_KEY:-$COS_PREFIX/$PACKAGE_BASE}" +COS_KEY="${COS_KEY#/}" +COS_URI="cos://$COS_BUCKET/$COS_KEY" +SHA256="$(calc_sha256 "$PACKAGE_FILE")" + +log "Upload package: $PACKAGE_FILE" +log "COS object: $COS_URI" +"$COSCLI_BIN" cp "$PACKAGE_FILE" "$COS_URI" "${COSCLI_OPTS[@]}" + +log "Generate signed URL, expire seconds: $COS_SIGN_EXPIRE" +SIGNED_URL="$("$COSCLI_BIN" signurl "$COS_URI" --time "$COS_SIGN_EXPIRE" --simple-output "${COSCLI_OPTS[@]}")" + +cat <-admin-monitor.tar.gz +export COS_SECRET_ID=change-me +export COS_SECRET_KEY=change-me +export COS_REGION=ap-chengdu +export COS_BUCKET=emp-example-bucket ``` -也可以更新其他服务: +构建指定服务,上传 COS,并输出签名 URL: ```bash -# 只更新前端 -EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh admin - -# 只更新数据服务 -EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh data +cd /home/git/emp/deploy/isolated -# 更新模拟器 / WebSocket -EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh ws - -# 更新 PDF 服务 -EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh pdf -``` - -如果镜像已经在本机构建好,只想重新打包已有镜像: - -```bash +DEPLOY_ENV=emp-uat \ +COS_UPLOAD=1 \ EMP_ROOT=/home/git/emp \ -IMAGE_NAMESPACE=emp-test \ -SKIP_BUILD=1 \ ./build-update.sh admin monitor ``` -## 三、传输到甲方服务器 - -将增量包传到甲方服务器任意目录,例如: - -```bash -/home/admin-x99/emp-test/update/emp-test-update-20260602153000-admin-monitor.tar.gz -``` - -## 四、甲方服务器应用增量包 - -进入服务器: - -```bash -cd /home/admin-x99/emp-test -mkdir -p update-runtime -tar -xzf update/emp-test-update-20260602153000-admin-monitor.tar.gz \ - -C update-runtime \ - --strip-components=1 -cd update-runtime -``` - -执行增量更新: +输出示例: -```bash -bash apply-update.sh +```text +Package: .../dist/emp-uat-update-20260611123000-admin-monitor.tar.gz +COS Key: deploy/emp-uat/update/20260611123000/emp-uat-update-20260611123000-admin-monitor.tar.gz +SHA256: ... +URL: https://... ``` -脚本会优先使用当前目录下的 `.env`、`docker-compose.yml`;如果当前目录没有,会自动查找 `../runtime/` 和 `/home/admin-x99/emp-test/runtime/` 下的运行配置。 - -脚本会自动执行: +如果本机镜像已经构建好,只需要重新打包并上传: ```bash -docker load -i images.tar -docker compose --env-file .env -f docker-compose.yml -p emp-test \ - up -d --no-deps --force-recreate emp-admin emp-monitor +DEPLOY_ENV=emp-uat \ +COS_UPLOAD=1 \ +SKIP_BUILD=1 \ +EMP_ROOT=/home/git/emp \ +./build-update.sh admin monitor ``` -如果当前目录没有 `.env` 和 `docker-compose.yml`,可以显式指定运行环境目录中的文件: - -```bash -PROJECT_NAME=emp-test \ -ENV_FILE=/home/admin-x99/emp-test/runtime/.env \ -COMPOSE_FILE=/home/admin-x99/emp-test/runtime/docker-compose.yml \ -bash apply-update.sh -``` +## 甲方服务器按 URL 应用增量包 -也可以手动指定更新服务,覆盖包内 `services.txt`: +先确认目标环境已完成全量部署,并存在: -```bash -PROJECT_NAME=emp-test \ -ENV_FILE=/home/admin-x99/emp-test/runtime/.env \ -COMPOSE_FILE=/home/admin-x99/emp-test/runtime/docker-compose.yml \ -bash apply-update.sh emp-admin emp-monitor +```text +/home/admin-x99/emp-uat/runtime/.env +/home/admin-x99/emp-uat/runtime/docker-compose.yml ``` -## 五、验证 - -查看服务状态: +执行: ```bash -cd /home/admin-x99/emp-test/runtime -docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-admin emp-monitor +DEPLOY_ENV=emp-uat \ +DEPLOY_HOME=/home/admin-x99/emp-uat \ +PACKAGE_SHA256=<打包输出的SHA256> \ +bash deploy-from-url.sh "<打包输出的URL>" ``` -查看后端日志: +同一台服务器上更新不同环境时,必须使用对应环境参数: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test logs --tail=100 emp-monitor -``` - -验证前端: +# 更新 test +DEPLOY_ENV=emp-test \ +DEPLOY_HOME=/home/admin-x99/emp-test \ +PACKAGE_SHA256=<打包输出的SHA256> \ +bash deploy-from-url.sh "" -```bash -curl -I http://127.0.0.1:37361 +# 更新 uat +DEPLOY_ENV=emp-uat \ +DEPLOY_HOME=/home/admin-x99/emp-uat \ +PACKAGE_SHA256=<打包输出的SHA256> \ +bash deploy-from-url.sh "" ``` -## 六、注意事项 - -1. 增量更新默认同时打 `latest` 和时间戳 tag。 -2. 当前测试环境 `.env` 建议继续使用 `IMAGE_TAG=latest`,不要为了增量包修改成时间戳。 -3. 如果把 `.env` 的 `IMAGE_TAG` 改成某个新时间戳,但只传了部分服务镜像,其他服务会因为缺少该 tag 而无法重建。 -4. 如果修改了 `docker-compose.yml`、`.env.example`、中间件初始化逻辑,建议重新全量打包或单独同步配置文件。 -5. 数据库结构变更不包含在镜像增量包中,需要单独执行 SQL 迁移。 - -## 七、PDF 导出故障排查 - -报错: +`deploy-from-url.sh` 会将包下载到: ```text -PDF导出失败: I/O error on GET request for "http://emp-pdf:3100/pdf": Connection refused +$DEPLOY_HOME/packages/<时间戳>/ ``` -含义:`emp-monitor` 已经访问到 Docker 内网地址 `emp-pdf:3100`,但 PDF 服务端口没有进程监听,常见原因是 `emp-pdf` 容器未启动、启动后退出、正在重启,或 Node 服务未正常监听 3100。 +然后调用包内的 `apply-update.sh`,并自动使用运行目录中的 `.env` 和 `docker-compose.yml`。 -新版 `docker-compose.yml` 已给 `emp-pdf` 增加健康检查,并让 `emp-monitor` 等待 `emp-pdf` 可访问后再启动。健康检查访问 `/pdf`,400 也算通过,因为不带 url 参数时返回 400 代表服务已正常监听。若服务器仍使用旧 compose,需要先同步新的 `docker-compose.yml` 或按下面命令手动重启 PDF 服务。 +## 手工应用增量包 -先看容器状态: +如果已经手工解压增量包,可以在解压目录执行: ```bash -cd /home/admin-x99/emp-test/runtime -docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-pdf emp-monitor -``` - -查看 PDF 服务日志: +cd /tmp/emp-update -```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test logs --tail=200 emp-pdf +DEPLOY_ENV=emp-uat \ +DEPLOY_HOME=/home/admin-x99/emp-uat \ +ENV_FILE=/home/admin-x99/emp-uat/runtime/.env \ +COMPOSE_FILE=/home/admin-x99/emp-uat/runtime/docker-compose.yml \ +bash apply-update.sh emp-admin emp-monitor ``` -在 PDF 容器内检查 PDF 服务是否监听: +如果不传服务名,`apply-update.sh` 会读取增量包内的 `services.txt`。 -```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test exec emp-pdf \ - node -e "require('http').get('http://127.0.0.1:3100/pdf', r => { console.log(r.statusCode); r.resume(); process.exit(r.statusCode < 500 ? 0 : 1) }).on('error', e => { console.error(e.message); process.exit(1) })" -``` +## 验证 -在同一个 Docker 网络里检查 `emp-monitor` 到 `emp-pdf` 的访问: +查看 UAT 服务状态: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test exec emp-monitor \ - sh -lc "curl -i http://emp-pdf:3100/pdf || wget -S -O- http://emp-pdf:3100/pdf" +cd /home/admin-x99/emp-uat/runtime +docker compose --env-file .env -f docker-compose.yml -p emp-uat ps emp-admin emp-monitor ``` -如果 `emp-pdf` 未运行或健康检查失败,先重启 PDF 服务: +查看日志: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test up -d --no-deps --force-recreate emp-pdf +docker compose --env-file .env -f docker-compose.yml -p emp-uat logs --tail=100 emp-monitor ``` -再重启 monitor,使其重新调用可用的 PDF 服务: +查看 test 时把目录和项目名改为 `emp-test`: ```bash -docker compose --env-file .env -f docker-compose.yml -p emp-test restart emp-monitor +cd /home/admin-x99/emp-test/runtime +docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-admin emp-monitor ``` -如果日志中出现 Chromium/Puppeteer 相关错误,重新构建并增量更新 `pdf`: +## 注意事项 -```bash -EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh pdf -``` +1. 增量包默认同时包含 `latest` 和时间戳两个镜像 tag。 +2. 目标服务器 `.env` 建议保持 `IMAGE_TAG=latest`,除非确定所有服务都已经按同一个时间戳 tag 部署。 +3. 如果修改了 `docker-compose.yml`、`.env.example` 或中间件初始化逻辑,建议重新打全量包,或单独同步运行配置。 +4. 数据库结构变更不包含在镜像增量包中,需要单独执行 SQL 迁移。 +5. 同一台服务器同时部署 `emp-test` 和 `emp-uat` 时,更新命令里的 `DEPLOY_ENV`、`DEPLOY_HOME`、包 URL 必须匹配同一个环境。