fix(updater): 复用原 Compose 项目名并保护非 Compose 部署

This commit is contained in:
2026-09-28 11:24:51 +08:00
parent f63c28e10e
commit 4e948f65d1
8 changed files with 41 additions and 4 deletions
+4
View File
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
- **示例页面新增独立升级验证区块**:页面增加「本次升级验证」高亮区块,显示 `v1.3.7`,验证标识更新为 `v1.3.7-example-page`,用于直观确认升级后的前端资源已切换 - **示例页面新增独立升级验证区块**:页面增加「本次升级验证」高亮区块,显示 `v1.3.7`,验证标识更新为 `v1.3.7-example-page`,用于直观确认升级后的前端资源已切换
### 修复
- **复用原 Compose 项目名重建容器**:updater 在通过 helper 容器执行 `docker compose` 时,先从现有 `fonu` 容器读取 `com.docker.compose.project` 标签,并显式传入同一个项目名,避免 Compose 将现有容器视为另一个项目后重新创建网络、数据卷并撞上 `/fonu` 容器名冲突。现有容器不是 Compose 管理时,updater 会拒绝升级并提示按 Compose 方式重新部署,避免误建空数据卷
### 变更 ### 变更
- 版本号提升到 1.3.7;`web/package.json` 与 `web/package-lock.json` 同步对齐 - 版本号提升到 1.3.7;`web/package.json` 与 `web/package-lock.json` 同步对齐
+1
View File
@@ -21,6 +21,7 @@ func main() {
Branch: envOr("FONU_UPDATE_BRANCH", "main"), Branch: envOr("FONU_UPDATE_BRANCH", "main"),
AppURL: envOr("FONU_UPDATE_APP_URL", "http://fonu:6893"), AppURL: envOr("FONU_UPDATE_APP_URL", "http://fonu:6893"),
HostProjectDir: envOr("FONU_UPDATE_HOST_DIR", ""), HostProjectDir: envOr("FONU_UPDATE_HOST_DIR", ""),
ProjectName: envOr("FONU_UPDATE_PROJECT_NAME", ""),
SelfImage: envOr("FONU_UPDATE_SELF_IMAGE", ""), SelfImage: envOr("FONU_UPDATE_SELF_IMAGE", ""),
DockerSocket: envOr("FONU_UPDATE_DOCKER_SOCK", "/var/run/docker.sock"), DockerSocket: envOr("FONU_UPDATE_DOCKER_SOCK", "/var/run/docker.sock"),
}, nil) }, nil)
+1
View File
@@ -82,6 +82,7 @@ services:
FONU_UPDATE_BRANCH: main FONU_UPDATE_BRANCH: main
FONU_UPDATE_APP_URL: http://fonu:6893 FONU_UPDATE_APP_URL: http://fonu:6893
FONU_UPDATE_HOST_DIR: ${FONU_UPDATE_HOST_DIR:-} FONU_UPDATE_HOST_DIR: ${FONU_UPDATE_HOST_DIR:-}
FONU_UPDATE_PROJECT_NAME: ${FONU_UPDATE_PROJECT_NAME:-fonu}
FONU_UPDATE_SELF_IMAGE: fonu-updater:${FONU_VERSION:-local} FONU_UPDATE_SELF_IMAGE: fonu-updater:${FONU_VERSION:-local}
FONU_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock} FONU_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock}
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
+1
View File
@@ -64,6 +64,7 @@ services:
FONU_UPDATE_MODE: local FONU_UPDATE_MODE: local
FONU_UPDATE_APP_URL: http://fonu:6893 FONU_UPDATE_APP_URL: http://fonu:6893
FONU_UPDATE_HOST_DIR: ${FONU_UPDATE_HOST_DIR:-} FONU_UPDATE_HOST_DIR: ${FONU_UPDATE_HOST_DIR:-}
FONU_UPDATE_PROJECT_NAME: ${FONU_UPDATE_PROJECT_NAME:-fonu}
FONU_UPDATE_SELF_IMAGE: fonu-updater:${FONU_VERSION:-local} FONU_UPDATE_SELF_IMAGE: fonu-updater:${FONU_VERSION:-local}
FONU_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock} FONU_UPDATE_DOCKER_SOCK: ${DOCKER_SOCK:-/var/run/docker.sock}
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
+6
View File
@@ -182,6 +182,12 @@ docker compose -f /实际/的/Compose/目录/docker-compose.hub.yml up -d --buil
状态接口会明确提示 Compose 文件缺失,修复挂载或设置覆盖后按钮才会重新显示。 状态接口会明确提示 Compose 文件缺失,修复挂载或设置覆盖后按钮才会重新显示。
### 报 `container name "/fonu" is already in use`
这表示 Compose 没有识别到当前容器所属的原项目,可能把升级当成新项目并尝试新建网络与数据卷。v1.3.7 的更新版 updater 会读取现有容器的 `com.docker.compose.project` 标签,并显式复用原项目名。
如果现有 `fonu` 容器不是由 Compose 管理,updater 会拒绝升级,避免新建 `fonu_fonu-data` 等空数据卷。应先按 Compose 方式重新部署,再使用一键升级。
### `docker-compose.hub.yml` 模式无法读取远端版本 ### `docker-compose.hub.yml` 模式无法读取远端版本
检查 sidecar 容器内是否可以访问 Git 仓库: 检查 sidecar 容器内是否可以访问 Git 仓库:
+2 -2
View File
@@ -323,7 +323,7 @@ func (s *Server) compose(ctx context.Context, version string, args ...string) (s
if plan.helper { if plan.helper {
return s.composeViaHelper(ctx, plan, version, args...) return s.composeViaHelper(ctx, plan, version, args...)
} }
fullArgs := append([]string{"compose", "-f", plan.containerCompose}, args...) fullArgs := append([]string{"compose", "--project-name", plan.projectName, "-f", plan.containerCompose}, args...)
env := []string{"FONU_VERSION=" + version} env := []string{"FONU_VERSION=" + version}
return runCommand(ctx, s.cfg.ProjectDir, env, "docker", fullArgs...) return runCommand(ctx, s.cfg.ProjectDir, env, "docker", fullArgs...)
} }
@@ -340,7 +340,7 @@ func (s *Server) composeViaHelper(ctx context.Context, plan composePlan, version
"-w", "/workspace", "-w", "/workspace",
"-e", "FONU_VERSION=" + version, "-e", "FONU_VERSION=" + version,
image, image,
"compose", "-f", plan.helperComposePath, "compose", "--project-name", plan.projectName, "-f", plan.helperComposePath,
} }
helperArgs = append(helperArgs, args...) helperArgs = append(helperArgs, args...)
return runCommand(ctx, "", nil, "docker", helperArgs...) return runCommand(ctx, "", nil, "docker", helperArgs...)
+24 -1
View File
@@ -31,6 +31,7 @@ type Config struct {
Branch string Branch string
AppURL string AppURL string
HostProjectDir string HostProjectDir string
ProjectName string
SelfImage string SelfImage string
DockerSocket string DockerSocket string
BuildTimeout time.Duration BuildTimeout time.Duration
@@ -49,6 +50,7 @@ type Server struct {
type composePlan struct { type composePlan struct {
helper bool helper bool
projectName string
containerCompose string containerCompose string
hostProjectDir string hostProjectDir string
hostComposePath string hostComposePath string
@@ -83,6 +85,7 @@ func NewServer(cfg Config, logger *slog.Logger) (*Server, error) {
cfg.DockerSocket = "/var/run/docker.sock" cfg.DockerSocket = "/var/run/docker.sock"
} }
cfg.HostProjectDir = strings.TrimSpace(cfg.HostProjectDir) cfg.HostProjectDir = strings.TrimSpace(cfg.HostProjectDir)
cfg.ProjectName = strings.TrimSpace(cfg.ProjectName)
cfg.SelfImage = strings.TrimSpace(cfg.SelfImage) cfg.SelfImage = strings.TrimSpace(cfg.SelfImage)
if cfg.BuildTimeout <= 0 { if cfg.BuildTimeout <= 0 {
cfg.BuildTimeout = 30 * time.Minute cfg.BuildTimeout = 30 * time.Minute
@@ -184,9 +187,13 @@ func (s *Server) ensureComposeFile(ctx context.Context) error {
} }
func (s *Server) resolveComposePlan(ctx context.Context) (composePlan, error) { func (s *Server) resolveComposePlan(ctx context.Context) (composePlan, error) {
projectName, err := s.composeProjectName(ctx)
if err != nil {
return composePlan{}, err
}
info, err := os.Stat(s.composePath) info, err := os.Stat(s.composePath)
if err == nil && !info.IsDir() { if err == nil && !info.IsDir() {
return composePlan{containerCompose: s.composePath}, nil return composePlan{projectName: projectName, containerCompose: s.composePath}, nil
} }
if err == nil && info.IsDir() { if err == nil && info.IsDir() {
return composePlan{}, fmt.Errorf("Compose 路径是目录而不是文件:%s", s.composePath) return composePlan{}, fmt.Errorf("Compose 路径是目录而不是文件:%s", s.composePath)
@@ -197,6 +204,7 @@ func (s *Server) resolveComposePlan(ctx context.Context) (composePlan, error) {
if pathErr == nil { if pathErr == nil {
return composePlan{ return composePlan{
helper: true, helper: true,
projectName: projectName,
hostProjectDir: hostDir, hostProjectDir: hostDir,
hostComposePath: hostComposePath, hostComposePath: hostComposePath,
helperComposePath: helperPath, helperComposePath: helperPath,
@@ -206,6 +214,21 @@ func (s *Server) resolveComposePlan(ctx context.Context) (composePlan, error) {
return composePlan{}, fmt.Errorf("找不到 Compose 文件 %s,且无法自动定位宿主机目录;请设置 FONU_UPDATE_HOST_DIR", s.composePath) return composePlan{}, fmt.Errorf("找不到 Compose 文件 %s,且无法自动定位宿主机目录;请设置 FONU_UPDATE_HOST_DIR", s.composePath)
} }
func (s *Server) composeProjectName(ctx context.Context) (string, error) {
projectName, err := s.dockerLabel(ctx, "com.docker.compose.project")
projectName = strings.TrimSpace(projectName)
if projectName != "" {
return projectName, nil
}
if s.cfg.ProjectName != "" {
return s.cfg.ProjectName, nil
}
if err != nil {
return "", fmt.Errorf("无法读取现有容器的 Compose 项目标签:%w", err)
}
return "", fmt.Errorf("现有 fonu 容器不是由 Compose 管理,无法安全一键升级;请按 Compose 方式重新部署")
}
func (s *Server) discoverHostCompose(ctx context.Context) (string, string, error) { func (s *Server) discoverHostCompose(ctx context.Context) (string, string, error) {
if s.cfg.HostProjectDir != "" { if s.cfg.HostProjectDir != "" {
if !filepath.IsAbs(s.cfg.HostProjectDir) { if !filepath.IsAbs(s.cfg.HostProjectDir) {
+2 -1
View File
@@ -46,7 +46,7 @@ func TestNewServerDefaultsAndValidation(t *testing.T) {
func TestEnsureComposeFile(t *testing.T) { func TestEnsureComposeFile(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
server, err := NewServer(Config{ProjectDir: dir, ComposeFile: "docker-compose.yml"}, nil) server, err := NewServer(Config{ProjectDir: dir, ComposeFile: "docker-compose.yml", ProjectName: "fonu"}, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -69,6 +69,7 @@ func TestResolveComposePlanUsesHostProjectDir(t *testing.T) {
ProjectDir: containerDir, ProjectDir: containerDir,
ComposeFile: "docker-compose.hub.yml", ComposeFile: "docker-compose.hub.yml",
HostProjectDir: hostDir, HostProjectDir: hostDir,
ProjectName: "fonu",
}, nil) }, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)