fix(*): 平台稳定性 - 修复会话隔离、运行时缓存与部署镜像更新链路
This commit is contained in:
1 parent
88abdd1b75
commit
e77c26d2b0
32 files changed
+788
-116
No files matched your search
@@ -264,13 +264,19 @@ func (m *Manager) runUpdate(fromVersion, targetVersion string) {
|
||||
m.failWithoutRollback(PhaseFailed, fmt.Errorf("保存旧 Compose 配置:%w", err))
|
||||
return
|
||||
}
|
||||
if err := replaceFile(m.envPath(), m.previousEnvPath(), 0o600); err != nil {
|
||||
m.failWithoutRollback(PhaseFailed, fmt.Errorf("保存旧部署环境:%w", err))
|
||||
return
|
||||
}
|
||||
|
||||
m.setPhase(PhasePulling, "拉取目标版本镜像")
|
||||
if err := m.compose(nextCompose, targetVersion, m.config.StepTimeout, nil, "pull", "backend", "web"); err != nil {
|
||||
targetImages := immutableImageRefs(m.config.Repository, targetVersion)
|
||||
if err := m.composeWithImages(nextCompose, targetVersion, targetImages, m.config.StepTimeout, nil, "pull", "backend", "web"); err != nil {
|
||||
m.failWithoutRollback(PhaseFailed, err)
|
||||
return
|
||||
}
|
||||
if err := m.verifyImages(targetVersion); err != nil {
|
||||
targetImages, err = m.verifyImages(targetVersion)
|
||||
if err != nil {
|
||||
m.failWithoutRollback(PhaseFailed, err)
|
||||
return
|
||||
}
|
||||
@@ -282,13 +288,13 @@ func (m *Manager) runUpdate(fromVersion, targetVersion string) {
|
||||
}
|
||||
|
||||
m.setPhase(PhaseMigrating, "执行目标版本数据库迁移")
|
||||
if err := m.compose(nextCompose, targetVersion, m.config.StepTimeout, nil, "run", "--rm", "migrate"); err != nil {
|
||||
if err := m.composeWithImages(nextCompose, targetVersion, targetImages, m.config.StepTimeout, nil, "run", "--rm", "migrate"); err != nil {
|
||||
m.failWithRollback(err, fromVersion, backup)
|
||||
return
|
||||
}
|
||||
|
||||
m.setPhase(PhaseSwitching, "切换 Compose 配置与镜像版本")
|
||||
if err := setEnvValue(m.envPath(), "CANVAS_IMAGE_TAG", strings.TrimPrefix(targetVersion, "v")); err != nil {
|
||||
if err := setDeploymentImages(m.envPath(), targetVersion, targetImages); err != nil {
|
||||
m.failWithRollback(err, fromVersion, backup)
|
||||
return
|
||||
}
|
||||
@@ -297,7 +303,7 @@ func (m *Manager) runUpdate(fromVersion, targetVersion string) {
|
||||
m.failWithRollback(err, fromVersion, backup)
|
||||
return
|
||||
}
|
||||
if err := m.compose(m.composePath(), targetVersion, m.config.StepTimeout, nil, "up", "-d", "--remove-orphans", "--wait", "--wait-timeout", "600"); err != nil {
|
||||
if err := m.composeWithImages(m.composePath(), targetVersion, targetImages, m.config.StepTimeout, nil, "up", "-d", "--remove-orphans", "--wait", "--wait-timeout", "600"); err != nil {
|
||||
m.failWithRollback(err, fromVersion, backup)
|
||||
return
|
||||
}
|
||||
@@ -368,10 +374,12 @@ func (m *Manager) runRollback(targetVersion string, backup Backup, automatic boo
|
||||
failures = append(failures, fmt.Errorf("恢复旧 Compose 配置:%w", err))
|
||||
}
|
||||
}
|
||||
if err := m.restoreDatabase(backup); err != nil {
|
||||
failures = append(failures, err)
|
||||
if _, err := os.Stat(m.previousEnvPath()); err == nil {
|
||||
if err := replaceFile(m.previousEnvPath(), m.envPath(), 0o600); err != nil {
|
||||
failures = append(failures, fmt.Errorf("恢复旧部署环境:%w", err))
|
||||
}
|
||||
}
|
||||
if err := setEnvValue(m.envPath(), "CANVAS_IMAGE_TAG", strings.TrimPrefix(targetVersion, "v")); err != nil {
|
||||
if err := m.restoreDatabase(backup); err != nil {
|
||||
failures = append(failures, err)
|
||||
}
|
||||
started := true
|
||||
@@ -513,6 +521,9 @@ func (m *Manager) envPath() string { return filepath.Join(m.config.InstallDir, m
|
||||
func (m *Manager) previousComposePath() string {
|
||||
return filepath.Join(m.config.StateDir, "previous-compose.yml")
|
||||
}
|
||||
func (m *Manager) previousEnvPath() string {
|
||||
return filepath.Join(m.config.StateDir, "previous.env")
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
buffer := make([]byte, 12)
|
||||
|
||||
@@ -133,7 +133,7 @@ func (m *Manager) preflight(composePath, targetVersion string) error {
|
||||
if err := checkBackupDiskSpace(m.config.BackupDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.compose(composePath, targetVersion, 2*time.Minute, nil, "config", "--quiet"); err != nil {
|
||||
if err := m.composeWithImages(composePath, targetVersion, immutableImageRefs(m.config.Repository, targetVersion), 2*time.Minute, nil, "config", "--quiet"); err != nil {
|
||||
return fmt.Errorf("目标 Compose 校验失败:%w", err)
|
||||
}
|
||||
current, err := m.currentVersion()
|
||||
@@ -243,7 +243,25 @@ func (m *Manager) restartSelf() {
|
||||
_ = m.runner.Run(ctx, "systemctl", []string{"restart", m.config.ServiceName}, nil, io.Discard, io.Discard)
|
||||
}
|
||||
|
||||
type deploymentImages struct {
|
||||
backend string
|
||||
web string
|
||||
}
|
||||
|
||||
func immutableImageRefs(repository, version string) deploymentImages {
|
||||
owner := strings.SplitN(repository, "/", 2)[0]
|
||||
tag := strings.TrimPrefix(version, "v")
|
||||
return deploymentImages{
|
||||
backend: "ghcr.io/" + owner + "/open-ai-canvas-backend:" + tag,
|
||||
web: "ghcr.io/" + owner + "/open-ai-canvas-web:" + tag,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) compose(composePath, imageTag string, timeout time.Duration, stdout io.Writer, arguments ...string) error {
|
||||
return m.composeWithImages(composePath, imageTag, deploymentImages{}, timeout, stdout, arguments...)
|
||||
}
|
||||
|
||||
func (m *Manager) composeWithImages(composePath, imageTag string, images deploymentImages, timeout time.Duration, stdout io.Writer, arguments ...string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
args := []string{"compose", "--env-file", m.envPath(), "-f", composePath}
|
||||
@@ -252,7 +270,11 @@ func (m *Manager) compose(composePath, imageTag string, timeout time.Duration, s
|
||||
if stdout == nil {
|
||||
stdout = io.Discard
|
||||
}
|
||||
err := m.runner.Run(ctx, "docker", args, []string{"CANVAS_IMAGE_TAG=" + strings.TrimPrefix(imageTag, "v")}, stdout, &stderr)
|
||||
environment := []string{"CANVAS_IMAGE_TAG=" + strings.TrimPrefix(imageTag, "v")}
|
||||
if images.backend != "" {
|
||||
environment = append(environment, "CANVAS_BACKEND_IMAGE="+images.backend, "CANVAS_WEB_IMAGE="+images.web)
|
||||
}
|
||||
err := m.runner.Run(ctx, "docker", args, environment, stdout, &stderr)
|
||||
if err != nil {
|
||||
message := strings.TrimSpace(stderr.String())
|
||||
if len(message) > 1000 {
|
||||
@@ -266,22 +288,47 @@ func (m *Manager) compose(composePath, imageTag string, timeout time.Duration, s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) verifyImages(targetVersion string) error {
|
||||
owner := strings.SplitN(m.config.Repository, "/", 2)[0]
|
||||
for _, image := range []string{"ghcr.io/" + owner + "/open-ai-canvas-backend:", "ghcr.io/" + owner + "/open-ai-canvas-web:"} {
|
||||
func (m *Manager) verifyImages(targetVersion string) (deploymentImages, error) {
|
||||
images := immutableImageRefs(m.config.Repository, targetVersion)
|
||||
refs := []string{images.backend, images.web}
|
||||
digests := make([]string, 0, len(refs))
|
||||
for _, image := range refs {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
var output bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
err := m.runner.Run(ctx, "docker", []string{"image", "inspect", image + strings.TrimPrefix(targetVersion, "v"), "--format", "{{json .RepoDigests}}"}, nil, &output, &stderr)
|
||||
err := m.runner.Run(ctx, "docker", []string{"image", "inspect", image, "--format", "{{json .RepoDigests}}"}, nil, &output, &stderr)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("校验目标镜像摘要失败:%s", strings.TrimSpace(stderr.String()))
|
||||
return deploymentImages{}, fmt.Errorf("校验目标镜像摘要失败:%s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
if !strings.Contains(output.String(), "@sha256:") {
|
||||
return fmt.Errorf("目标镜像 %s 未包含仓库摘要", image+targetVersion)
|
||||
var repositoryDigests []string
|
||||
if err := json.Unmarshal(output.Bytes(), &repositoryDigests); err != nil {
|
||||
return deploymentImages{}, fmt.Errorf("解析目标镜像摘要失败:%w", err)
|
||||
}
|
||||
repository := strings.Split(image, ":")[0]
|
||||
var digest string
|
||||
for _, candidate := range repositoryDigests {
|
||||
if strings.HasPrefix(candidate, repository+"@sha256:") {
|
||||
digest = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if digest == "" {
|
||||
return deploymentImages{}, fmt.Errorf("目标镜像 %s 未包含仓库摘要", image)
|
||||
}
|
||||
digests = append(digests, digest)
|
||||
}
|
||||
return nil
|
||||
return deploymentImages{backend: digests[0], web: digests[1]}, nil
|
||||
}
|
||||
|
||||
func setDeploymentImages(path, version string, images deploymentImages) error {
|
||||
if err := setEnvValue(path, "CANVAS_IMAGE_TAG", strings.TrimPrefix(version, "v")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setEnvValue(path, "CANVAS_BACKEND_IMAGE", images.backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return setEnvValue(path, "CANVAS_WEB_IMAGE", images.web)
|
||||
}
|
||||
|
||||
func (m *Manager) createBackup(version string) (Backup, error) {
|
||||
|
||||
Reference in new issue
Block a user