feat(tenant-service): 一键更新接口 + Dockerfile 添加 docker-compose
- 新增 SystemUpdateController POST /api/system/update(PRIVATE 模式) - SystemUpdateService 通过 docker-compose 拉镜像并逐服务重建容器 - Dockerfile 添加 docker-cli + docker-compose(用于容器内调用 Docker API) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
8a3c41d5ff
当前提交
f2e126e2d0
@ -43,7 +43,7 @@ ARG SERVICE_MODULE
|
|||||||
|
|
||||||
# curl is required by update-service (MI store upload uses ProcessBuilder curl to handle
|
# curl is required by update-service (MI store upload uses ProcessBuilder curl to handle
|
||||||
# Expect:100-continue and force HTTP/1.1 for large multipart uploads)
|
# Expect:100-continue and force HTTP/1.1 for large multipart uploads)
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl docker-cli docker-compose
|
||||||
|
|
||||||
COPY --from=build /workspace/${SERVICE_MODULE}/target/${SERVICE_MODULE}-0.1.0-SNAPSHOT.jar /app/app.jar
|
COPY --from=build /workspace/${SERVICE_MODULE}/target/${SERVICE_MODULE}-0.1.0-SNAPSHOT.jar /app/app.jar
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.xuqm.tenant.controller;
|
||||||
|
|
||||||
|
import com.xuqm.tenant.config.PrivateDeploymentProperties;
|
||||||
|
import com.xuqm.tenant.service.SystemUpdateService;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/system")
|
||||||
|
public class SystemUpdateController {
|
||||||
|
|
||||||
|
private final PrivateDeploymentProperties deployProps;
|
||||||
|
private final SystemUpdateService updateService;
|
||||||
|
|
||||||
|
public SystemUpdateController(PrivateDeploymentProperties deployProps,
|
||||||
|
SystemUpdateService updateService) {
|
||||||
|
this.deployProps = deployProps;
|
||||||
|
this.updateService = updateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发私有化部署一键升级,以流式文本返回进度日志。
|
||||||
|
* 仅在 PRIVATE 模式下可用;需要 JWT 认证(租户账号即可)。
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/update", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
|
public ResponseEntity<StreamingResponseBody> update() {
|
||||||
|
if (!deployProps.isPrivate()) {
|
||||||
|
return ResponseEntity.status(403)
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.body(out -> out.write("此接口仅在私有化部署可用\n".getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamingResponseBody body = outputStream -> {
|
||||||
|
updateService.runUpdate(line -> {
|
||||||
|
try {
|
||||||
|
outputStream.write((line + "\n").getBytes());
|
||||||
|
outputStream.flush();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.body(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package com.xuqm.tenant.service;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SystemUpdateService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class);
|
||||||
|
|
||||||
|
private static final List<String> OTHER_SERVICES = List.of(
|
||||||
|
"file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}")
|
||||||
|
private String deployRoot;
|
||||||
|
|
||||||
|
public void runUpdate(Consumer<String> emit) {
|
||||||
|
String composeFile = deployRoot + "/docker-compose.yml";
|
||||||
|
|
||||||
|
emit.accept(">>> 拉取最新镜像...");
|
||||||
|
for (String svc : OTHER_SERVICES) {
|
||||||
|
if (isRunning(svc)) {
|
||||||
|
emit.accept(" pulling " + svc + " ...");
|
||||||
|
exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", svc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit.accept(" pulling tenant-service ...");
|
||||||
|
exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", "tenant-service");
|
||||||
|
emit.accept(">>> 镜像拉取完成");
|
||||||
|
|
||||||
|
emit.accept(">>> 重启各服务...");
|
||||||
|
for (String svc : OTHER_SERVICES) {
|
||||||
|
if (isRunning(svc)) {
|
||||||
|
emit.accept(" restarting " + svc + " ...");
|
||||||
|
exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm",
|
||||||
|
"up", "-d", "--no-deps", "--force-recreate", svc);
|
||||||
|
emit.accept(" " + svc + " ✓");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit.accept(">>> 即将重启 tenant-service,连接将短暂中断...");
|
||||||
|
emit.accept("RESTART_SELF");
|
||||||
|
|
||||||
|
// 延迟 3 秒后重启自身,确保 SSE 事件已发送到客户端
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(3000);
|
||||||
|
exec(msg -> log.info("[self-restart] {}", msg),
|
||||||
|
"docker-compose", "-f", composeFile, "-p", "xuqm",
|
||||||
|
"up", "-d", "--no-deps", "--force-recreate", "tenant-service");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("self-restart failed", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunning(String service) {
|
||||||
|
String containerName = "xuqm-" + service;
|
||||||
|
try {
|
||||||
|
Process p = new ProcessBuilder("docker", "inspect", "--type=container", containerName)
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start();
|
||||||
|
p.getInputStream().transferTo(java.io.OutputStream.nullOutputStream());
|
||||||
|
return p.waitFor() == 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exec(Consumer<String> emit, String... cmd) {
|
||||||
|
try {
|
||||||
|
Process p = new ProcessBuilder(cmd)
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (!line.isBlank()) emit.accept(" " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int code = p.waitFor();
|
||||||
|
if (code != 0) emit.accept(" [warn] exit code " + code);
|
||||||
|
} catch (Exception e) {
|
||||||
|
emit.accept(" [error] " + e.getMessage());
|
||||||
|
log.error("exec failed: {}", String.join(" ", cmd), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户