feat(tenant-service): 一键更新自动修复配置文件
- 更新前执行幂等配置修复:nginx location /file/ → /api/file/, docker-compose.yml 补齐 FILE_UPLOAD_DIR 和 FILE_BASE_URL - nginx 移至 OTHER_SERVICES 末尾,最后重启以应用修复后的配置 - docker login 读取 .env 中的仓库凭据,解决私有镜像拉取 403 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
7a530eb35b
当前提交
4a38147cb9
@ -6,9 +6,12 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@ -17,8 +20,9 @@ public class SystemUpdateService {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class);
|
private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class);
|
||||||
|
|
||||||
|
// nginx is restarted last so it picks up any patched config files.
|
||||||
private static final List<String> OTHER_SERVICES = List.of(
|
private static final List<String> OTHER_SERVICES = List.of(
|
||||||
"file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service"
|
"file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service", "nginx"
|
||||||
);
|
);
|
||||||
|
|
||||||
@Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}")
|
@Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}")
|
||||||
@ -27,9 +31,13 @@ public class SystemUpdateService {
|
|||||||
public void runUpdate(Consumer<String> emit) {
|
public void runUpdate(Consumer<String> emit) {
|
||||||
String composeFile = deployRoot + "/docker-compose.yml";
|
String composeFile = deployRoot + "/docker-compose.yml";
|
||||||
|
|
||||||
// Authenticate to the registry before pulling (credentials stored in .env by deploy.sh)
|
// Step 1: authenticate to registry
|
||||||
dockerLogin(emit);
|
dockerLogin(emit);
|
||||||
|
|
||||||
|
// Step 2: apply any pending config patches (idempotent)
|
||||||
|
patchConfigs(emit);
|
||||||
|
|
||||||
|
// Step 3: pull images
|
||||||
emit.accept(">>> 拉取最新镜像...");
|
emit.accept(">>> 拉取最新镜像...");
|
||||||
for (String svc : OTHER_SERVICES) {
|
for (String svc : OTHER_SERVICES) {
|
||||||
if (isRunning(svc)) {
|
if (isRunning(svc)) {
|
||||||
@ -41,6 +49,7 @@ public class SystemUpdateService {
|
|||||||
exec(emit, "docker", "compose", "-f", composeFile, "pull", "--quiet", "tenant-service");
|
exec(emit, "docker", "compose", "-f", composeFile, "pull", "--quiet", "tenant-service");
|
||||||
emit.accept(">>> 镜像拉取完成");
|
emit.accept(">>> 镜像拉取完成");
|
||||||
|
|
||||||
|
// Step 4: restart other services (nginx last so patched conf is applied)
|
||||||
emit.accept(">>> 重启各服务...");
|
emit.accept(">>> 重启各服务...");
|
||||||
for (String svc : OTHER_SERVICES) {
|
for (String svc : OTHER_SERVICES) {
|
||||||
if (isRunning(svc)) {
|
if (isRunning(svc)) {
|
||||||
@ -51,10 +60,7 @@ public class SystemUpdateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant-service 自身的重建:不能直接用 docker compose,因为一旦发出 stop 指令,
|
// Step 5: self-update tenant-service via detached helper container
|
||||||
// 当前容器(含 docker compose 进程)会立即被杀死,后续的 rm/create/start 步骤不会执行。
|
|
||||||
// 解决方案:先用 docker run -d 启动一个独立助手容器,它不依附于 tenant-service,
|
|
||||||
// 能在 tenant-service 停止后继续完成重建。
|
|
||||||
emit.accept(">>> 启动自更新助手容器...");
|
emit.accept(">>> 启动自更新助手容器...");
|
||||||
String selfImage = getCurrentImage();
|
String selfImage = getCurrentImage();
|
||||||
if (selfImage == null) {
|
if (selfImage == null) {
|
||||||
@ -74,6 +80,75 @@ public class SystemUpdateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idempotent config patches — fixes known misconfigurations introduced in earlier deploy versions.
|
||||||
|
* Each patch checks before writing so running this multiple times is safe.
|
||||||
|
*/
|
||||||
|
private void patchConfigs(Consumer<String> emit) {
|
||||||
|
emit.accept(">>> 检查并修复配置文件...");
|
||||||
|
patchNginxFileRoute(emit);
|
||||||
|
patchDockerComposeFileService(emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** nginx: location /file/ must be location /api/file/ to match the file controller path. */
|
||||||
|
private void patchNginxFileRoute(Consumer<String> emit) {
|
||||||
|
Path conf = Paths.get(deployRoot, "config", "nginx", "conf.d", "xuqm.conf");
|
||||||
|
if (!Files.exists(conf)) return;
|
||||||
|
try {
|
||||||
|
String content = Files.readString(conf);
|
||||||
|
if (!content.contains("location /file/")) return;
|
||||||
|
String patched = content.replace("location /file/", "location /api/file/");
|
||||||
|
Files.writeString(conf, patched, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
emit.accept(" [已修复] nginx: location /file/ → /api/file/");
|
||||||
|
} catch (IOException e) {
|
||||||
|
emit.accept(" [警告] nginx 配置修复失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* docker-compose: ensure FILE_UPLOAD_DIR and FILE_BASE_URL are set for file-service.
|
||||||
|
* Reads CONSOLE_DOMAIN from xuqm.env to determine the correct base URL.
|
||||||
|
*/
|
||||||
|
private void patchDockerComposeFileService(Consumer<String> emit) {
|
||||||
|
Path composeFile = Paths.get(deployRoot, "docker-compose.yml");
|
||||||
|
if (!Files.exists(composeFile)) return;
|
||||||
|
try {
|
||||||
|
String content = Files.readString(composeFile);
|
||||||
|
if (content.contains("FILE_UPLOAD_DIR") && content.contains("FILE_BASE_URL")) return;
|
||||||
|
|
||||||
|
String consoleDomain = readEnvValue(Paths.get(deployRoot, "config", "xuqm.env"), "CONSOLE_DOMAIN");
|
||||||
|
if (consoleDomain == null) consoleDomain = "";
|
||||||
|
|
||||||
|
// Inject the missing env vars directly after SPRING_DATA_REDIS_DATABASE line in file-service block.
|
||||||
|
// This pattern is stable — the line is unique in the file-service environment section.
|
||||||
|
String anchor = " SPRING_DATA_REDIS_DATABASE: \"${REDIS_DATABASE:-0}\"\n";
|
||||||
|
if (!content.contains(anchor)) {
|
||||||
|
emit.accept(" [跳过] docker-compose 文件-服务补丁锚点未找到,请手动检查");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String injection = anchor
|
||||||
|
+ " FILE_UPLOAD_DIR: \"/data/uploads\"\n"
|
||||||
|
+ " FILE_BASE_URL: \"" + consoleDomain + "\"\n";
|
||||||
|
String patched = content.replace(anchor, injection);
|
||||||
|
Files.writeString(composeFile, patched, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
emit.accept(" [已修复] docker-compose: 补齐 FILE_UPLOAD_DIR 和 FILE_BASE_URL");
|
||||||
|
} catch (IOException e) {
|
||||||
|
emit.accept(" [警告] docker-compose 修复失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readEnvValue(Path envFile, String key) {
|
||||||
|
if (!Files.exists(envFile)) return null;
|
||||||
|
try {
|
||||||
|
for (String line : Files.readAllLines(envFile)) {
|
||||||
|
if (line.startsWith(key + "=")) {
|
||||||
|
return line.substring(key.length() + 1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read REGISTRY / REGISTRY_USER / REGISTRY_PASSWORD from deployRoot/.env and
|
* Read REGISTRY / REGISTRY_USER / REGISTRY_PASSWORD from deployRoot/.env and
|
||||||
* run "docker login" so that subsequent pulls succeed on private registries.
|
* run "docker login" so that subsequent pulls succeed on private registries.
|
||||||
@ -87,7 +162,6 @@ public class SystemUpdateService {
|
|||||||
else if (line.startsWith("REGISTRY_PASSWORD=")) password = line.substring("REGISTRY_PASSWORD=".length()).trim();
|
else if (line.startsWith("REGISTRY_PASSWORD=")) password = line.substring("REGISTRY_PASSWORD=".length()).trim();
|
||||||
}
|
}
|
||||||
if (registry == null || user == null || password == null || password.isEmpty()) return;
|
if (registry == null || user == null || password == null || password.isEmpty()) return;
|
||||||
// REGISTRY format: host/namespace → extract host
|
|
||||||
String host = registry.contains("/") ? registry.substring(0, registry.indexOf('/')) : registry;
|
String host = registry.contains("/") ? registry.substring(0, registry.indexOf('/')) : registry;
|
||||||
ProcessBuilder pb = new ProcessBuilder("docker", "login", host, "-u", user, "--password-stdin")
|
ProcessBuilder pb = new ProcessBuilder("docker", "login", host, "-u", user, "--password-stdin")
|
||||||
.redirectErrorStream(true);
|
.redirectErrorStream(true);
|
||||||
@ -109,15 +183,12 @@ public class SystemUpdateService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动一个独立的 detached 容器,在 tenant-service 被停止后重建它。
|
* 启动一个独立的 detached 容器,在 tenant-service 被停止后重建它。
|
||||||
* 助手容器与 tenant-service 无父子关系,不会随 tenant-service 终止。
|
|
||||||
*/
|
*/
|
||||||
private boolean spawnSelfUpdater(String composeFile, String image) {
|
private boolean spawnSelfUpdater(String composeFile, String image) {
|
||||||
try {
|
try {
|
||||||
// 清理上次残留(若有)
|
|
||||||
new ProcessBuilder("docker", "rm", "-f", "xuqm-self-updater")
|
new ProcessBuilder("docker", "rm", "-f", "xuqm-self-updater")
|
||||||
.redirectErrorStream(true).start().waitFor();
|
.redirectErrorStream(true).start().waitFor();
|
||||||
|
|
||||||
// 等待 8 秒确保 tenant-service 已完全停止,然后执行 force-recreate
|
|
||||||
String shellCmd = "sleep 8 && docker compose -f " + composeFile
|
String shellCmd = "sleep 8 && docker compose -f " + composeFile
|
||||||
+ " up -d --no-deps --force-recreate tenant-service";
|
+ " up -d --no-deps --force-recreate tenant-service";
|
||||||
|
|
||||||
@ -141,7 +212,6 @@ public class SystemUpdateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter only by service label — avoids dependency on project name which varies by deploy root dir.
|
|
||||||
private boolean isRunning(String service) {
|
private boolean isRunning(String service) {
|
||||||
try {
|
try {
|
||||||
Process p = new ProcessBuilder(
|
Process p = new ProcessBuilder(
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户