From eb4beb50c4048a49ab25f94644ad3f9a22e573f6 Mon Sep 17 00:00:00 2001 From: Yuki Guo Date: Tue, 27 Oct 2026 23:07:21 +0800 Subject: [PATCH] fix: docker image pull and stop (#14202) --- agent/app/service/runtime.go | 25 ++++++++- agent/app/service/runtime_update.go | 86 +++++++++++++++++++++++++++++ agent/utils/compose/compose.go | 1 + agent/utils/docker/docker.go | 6 +- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/agent/app/service/runtime.go b/agent/app/service/runtime.go index 03297d6ca..c7cf6a00e 100644 --- a/agent/app/service/runtime.go +++ b/agent/app/service/runtime.go @@ -647,7 +647,7 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) { return &res, nil } -func (r *RuntimeService) Update(req request.RuntimeUpdate) error { +func (r *RuntimeService) Update(req request.RuntimeUpdate) (err error) { runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(req.ID)) if err != nil { return err @@ -656,6 +656,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error { runtime.Version = req.Version return runtimeRepo.Save(runtime) } + oldRuntime := *runtime appDetail, err := getRuntimeUpdateAppDetail(runtime, req) if err != nil { return err @@ -700,6 +701,20 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error { if err = downloadApp(app, appDetail, nil, nil); err != nil { return err } + var snapshot *runtimeUpdateSnapshot + if runtime.Type != constant.RuntimePHP { + snapshot, err = snapshotRuntimeUpdate(&oldRuntime) + if err != nil { + return err + } + defer func() { + if err != nil { + if restoreErr := snapshot.restoreFiles(); restoreErr != nil { + err = fmt.Errorf("%w; restore runtime configuration: %v", err, restoreErr) + } + } + }() + } if versionChanged { if err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil { return err @@ -773,7 +788,13 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error { if err = runtimeRepo.Save(runtime); err != nil { return err } - go reCreateRuntime(runtime) + go func() { + if err := prepareRuntimeUpdate(runtime, snapshot); err != nil { + global.LOG.Errorf("prepare runtime [%s] update failed: %v", runtime.Name, err) + return + } + reCreateRuntime(runtime) + }() } return nil } diff --git a/agent/app/service/runtime_update.go b/agent/app/service/runtime_update.go index bd681fa45..9fb25e14f 100644 --- a/agent/app/service/runtime_update.go +++ b/agent/app/service/runtime_update.go @@ -1,6 +1,9 @@ package service import ( + "errors" + "fmt" + "os" "path/filepath" "github.com/1Panel-dev/1Panel/agent/app/dto/request" @@ -12,6 +15,89 @@ import ( "gopkg.in/yaml.v3" ) +// Keep the on-disk configuration as well as the model: files may contain edits +// that have not been reflected in the database. +type runtimeUpdateSnapshot struct { + runtime model.Runtime + files []runtimeUpdateFile +} + +type runtimeUpdateFile struct { + path string + content []byte + mode os.FileMode + exists bool +} + +func snapshotRuntimeUpdate(runtime *model.Runtime) (*runtimeUpdateSnapshot, error) { + snapshot := &runtimeUpdateSnapshot{runtime: *runtime} + for _, name := range []string{"docker-compose.yml", ".env", "run.sh"} { + file := runtimeUpdateFile{path: filepath.Join(runtime.GetPath(), name)} + info, err := os.Stat(file.path) + if errors.Is(err, os.ErrNotExist) { + snapshot.files = append(snapshot.files, file) + continue + } + if err != nil { + return nil, err + } + file.content, err = os.ReadFile(file.path) + if err != nil { + return nil, err + } + file.exists, file.mode = true, info.Mode().Perm() + snapshot.files = append(snapshot.files, file) + } + return snapshot, nil +} + +func (s *runtimeUpdateSnapshot) restoreFiles() error { + var result error + for _, file := range s.files { + var err error + if file.exists { + err = os.WriteFile(file.path, file.content, file.mode) + if err == nil { + err = os.Chmod(file.path, file.mode) + } + } else { + err = os.Remove(file.path) + if errors.Is(err, os.ErrNotExist) { + err = nil + } + } + if err != nil { + result = errors.Join(result, fmt.Errorf("restore %s: %w", file.path, err)) + } + } + return result +} + +// This must finish before reCreateRuntime stops the existing container. A +// failed preparation restores the previous version without touching Docker. +func prepareRuntimeUpdate(runtime *model.Runtime, snapshot *runtimeUpdateSnapshot) error { + if err := pullRuntimeComposeImages(runtime.GetComposePath(), false, nil); err != nil { + result := fmt.Errorf("prepare runtime images: %w", err) + restoreErr := snapshot.restoreFiles() + *runtime = snapshot.runtime + if restoreErr != nil { + result = errors.Join(result, restoreErr) + runtime.Status = constant.StatusError + } + runtime.Message = result.Error() + if saveErr := runtimeRepo.Save(runtime); saveErr != nil { + result = errors.Join(result, fmt.Errorf("restore runtime record: %w", saveErr)) + } + logFile, logErr := os.OpenFile(runtime.GetLogPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm) + if logErr == nil { + _, logErr = fmt.Fprintln(logFile, result.Error()) + logErr = errors.Join(logErr, logFile.Close()) + } + return errors.Join(result, logErr) + } + return nil +} + func getRuntimeUpdateAppDetail(runtime *model.Runtime, req request.RuntimeUpdate) (model.AppDetail, error) { current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID)) if err != nil { diff --git a/agent/utils/compose/compose.go b/agent/utils/compose/compose.go index ebc1d2677..c35669ff1 100644 --- a/agent/utils/compose/compose.go +++ b/agent/utils/compose/compose.go @@ -104,6 +104,7 @@ func pullComposeImages(filePath string, forcePull bool, task *task.Task, project if err != nil { return err } + defer dockerCLi.Close() for _, image := range images { if !forcePull { if exist, _ := dockerCLi.ImageExists(image); exist { diff --git a/agent/utils/docker/docker.go b/agent/utils/docker/docker.go index 92f95f3b9..cdab983c4 100644 --- a/agent/utils/docker/docker.go +++ b/agent/utils/docker/docker.go @@ -345,10 +345,12 @@ func PullImage(imageName string) error { if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok { options.RegistryAuth = authStr } - if _, err := cli.ImagePull(context.Background(), imageName, options); err != nil { + out, err := cli.ImagePull(context.Background(), imageName, options) + if err != nil { return err } - return nil + defer out.Close() + return handlePullImageProcess(out, nil) } func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) {