diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index e7af44110..4cabc8961 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -336,7 +336,6 @@ func deleteAppInstall(install model.AppInstall, deleteBackup bool, forceDelete b } func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, forceDelete bool, deleteBackup bool) error { - resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) if len(resources) == 0 { return nil @@ -383,33 +382,66 @@ func upgradeInstall(installId uint, detailId uint, backup bool) error { } } var upErr error + defer func() { if upErr != nil { + if backup { + if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", Source: constant.ResourceLocal}); err != nil { + global.LOG.Errorf("recover app [%s] [%s] failed %v", install.App.Key, install.Name, err) + } + } install.Status = constant.UpgradeErr install.Message = upErr.Error() _ = appInstallRepo.Save(context.Background(), &install) } }() + fileOp := files.NewFileOp() detailDir := path.Join(constant.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version) if install.App.Resource == constant.AppResourceRemote { if upErr = downloadApp(install.App, detail, &install); upErr != nil { return } + if detail.DockerCompose == "" { + composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml")) + if err != nil { + upErr = err + return + } + detail.DockerCompose = string(composeDetail) + _ = appDetailRepo.Update(context.Background(), detail) + } go func() { _, _ = http.Get(detail.DownloadCallBackUrl) }() } + if install.App.Resource == constant.AppResourceLocal { detailDir = path.Join(constant.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version) } + images, err := composeV2.GetDockerComposeImages([]byte(detail.DockerCompose)) + if err != nil { + upErr = err + return + } + dockerCli, err := composeV2.NewClient() + if err != nil { + upErr = err + return + } + for _, image := range images { + if err = dockerCli.PullImage(image, true); err != nil { + upErr = buserr.WithNameAndErr("ErrDockerPullImage", "", err) + return + } + } + command := exec.Command("/bin/bash", "-c", fmt.Sprintf("cp -rn %s/* %s || true", detailDir, install.GetPath())) stdout, _ := command.CombinedOutput() if stdout != nil { global.LOG.Infof("upgrade app [%s] [%s] cp file log : %s ", install.App.Key, install.Name, string(stdout)) } - fileOp := files.NewFileOp() sourceScripts := path.Join(detailDir, "scripts") if fileOp.Stat(sourceScripts) { dstScripts := path.Join(install.GetPath(), "scripts") @@ -419,16 +451,6 @@ func upgradeInstall(installId uint, detailId uint, backup bool) error { _, _ = scriptCmd.CombinedOutput() } - if detail.DockerCompose == "" { - composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml")) - if err != nil { - upErr = err - return - } - detail.DockerCompose = string(composeDetail) - _ = appDetailRepo.Update(context.Background(), detail) - } - composeMap := make(map[string]interface{}) if upErr = yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); upErr != nil { return @@ -481,24 +503,6 @@ func upgradeInstall(installId uint, detailId uint, backup bool) error { install.Version = detail.Version install.AppDetailId = detailId - images, err := getImages(install) - if err != nil { - upErr = err - return - } - dockerCli, err := composeV2.NewClient() - if err != nil { - upErr = err - return - } - - for _, image := range images { - if err = dockerCli.PullImage(image, true); err != nil { - upErr = buserr.WithNameAndErr("ErrDockerPullImage", "", err) - return - } - } - if out, err := compose.Down(install.GetComposePath()); err != nil { if out != "" { upErr = errors.New(out) @@ -560,26 +564,6 @@ func getContainerNames(install model.AppInstall) ([]string, error) { return containerNames, nil } -func getImages(install model.AppInstall) ([]string, error) { - envStr, err := coverEnvJsonToStr(install.Env) - if err != nil { - return nil, err - } - project, err := composeV2.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), []byte(envStr), true) - if err != nil { - return nil, err - } - imagesMap := make(map[string]struct{}) - for _, service := range project.AllServices() { - imagesMap[service.Image] = struct{}{} - } - var images []string - for k := range imagesMap { - images = append(images, k) - } - return images, nil -} - func coverEnvJsonToStr(envJson string) (string, error) { envMap := make(map[string]interface{}) _ = json.Unmarshal([]byte(envJson), &envMap) @@ -897,28 +881,28 @@ func handleLocalAppDetail(versionDir string, appDetail *model.AppDetail) error { fileOp := files.NewFileOp() dockerComposePath := path.Join(versionDir, "docker-compose.yml") if !fileOp.Stat(dockerComposePath) { - return errors.New(i18n.GetMsgWithMap("ErrFileNotFound", map[string]interface{}{"name": "docker-compose.yml"})) + return buserr.WithName(constant.ErrFileNotFound, "docker-compose.yml") } dockerComposeByte, _ := fileOp.GetContent(dockerComposePath) if dockerComposeByte == nil { - return errors.New(i18n.GetMsgWithMap("ErrFileParseApp", map[string]interface{}{"name": "docker-compose.yml"})) + return buserr.WithName(constant.ErrFileParseApp, "docker-compose.yml") } appDetail.DockerCompose = string(dockerComposeByte) paramPath := path.Join(versionDir, "data.yml") if !fileOp.Stat(paramPath) { - return errors.New(i18n.GetMsgWithMap("ErrFileNotFound", map[string]interface{}{"name": "data.yml"})) + return buserr.WithName(constant.ErrFileNotFound, "data.yml") } paramByte, _ := fileOp.GetContent(paramPath) if paramByte == nil { - return errors.New(i18n.GetMsgWithMap("ErrFileParseApp", map[string]interface{}{"name": "data.yml"})) + return buserr.WithName(constant.ErrFileNotFound, "data.yml") } appParamConfig := dto.LocalAppParam{} if err := yaml.Unmarshal(paramByte, &appParamConfig); err != nil { - return errors.New(i18n.GetMsgWithMap("ErrFileParseApp", map[string]interface{}{"name": "data.yml"})) + return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) } dataJson, err := json.Marshal(appParamConfig.AppParams) if err != nil { - return errors.New(i18n.GetMsgWithMap("ErrFileParseApp", map[string]interface{}{"name": "data.yml", "err": err.Error()})) + return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) } appDetail.Params = string(dataJson) return nil @@ -928,22 +912,22 @@ func handleLocalApp(appDir string) (app *model.App, err error) { fileOp := files.NewFileOp() configYamlPath := path.Join(appDir, "data.yml") if !fileOp.Stat(configYamlPath) { - err = errors.New(i18n.GetMsgWithMap("ErrFileNotFound", map[string]interface{}{"name": "data.yml"})) + err = buserr.WithName(constant.ErrFileNotFound, "data.yml") return } iconPath := path.Join(appDir, "logo.png") if !fileOp.Stat(iconPath) { - err = errors.New(i18n.GetMsgWithMap("ErrFileNotFound", map[string]interface{}{"name": "logo.png"})) + err = buserr.WithName(constant.ErrFileNotFound, "logo.png") return } configYamlByte, err := fileOp.GetContent(configYamlPath) if err != nil { - err = errors.New(i18n.GetMsgWithMap("ErrFileParseApp", map[string]interface{}{"name": "data.yml", "err": err.Error()})) + err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) return } localAppDefine := dto.LocalAppAppDefine{} if err = yaml.Unmarshal(configYamlByte, &localAppDefine); err != nil { - err = errors.New(i18n.GetMsgWithMap("ErrFileParseApp", map[string]interface{}{"name": "data.yml", "err": err.Error()})) + err = buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err) return } app = &localAppDefine.AppProperty diff --git a/backend/constant/errs.go b/backend/constant/errs.go index 11606f860..db5b97f37 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -60,6 +60,8 @@ var ( ErrInstallDirNotFound = "ErrInstallDirNotFound" ErrContainerName = "ErrContainerName" ErrAppNameExist = "ErrAppNameExist" + ErrFileNotFound = "ErrFileNotFound" + ErrFileParseApp = "ErrFileParseApp" ) // website diff --git a/backend/utils/docker/compose.go b/backend/utils/docker/compose.go index c7a1c5a85..e0f859730 100644 --- a/backend/utils/docker/compose.go +++ b/backend/utils/docker/compose.go @@ -6,6 +6,7 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/docker/compose/v2/pkg/api" "github.com/joho/godotenv" + "gopkg.in/yaml.v3" "path" "regexp" "strings" @@ -89,7 +90,7 @@ func GetComposeProject(projectName, workDir string, yml []byte, env []byte, skip projectName = strings.ToLower(projectName) reg, _ := regexp.Compile(`[^a-z0-9_-]+`) projectName = reg.ReplaceAllString(projectName, "") - project, err := loader.Load(details, func(options *loader.Options) { + project, err := loader.LoadWithContext(context.Background(), details, func(options *loader.Options) { options.SetProjectName(projectName, true) options.ResolvePaths = true options.SkipNormalization = skipNormalization @@ -105,3 +106,26 @@ func getComposeTimeout() *time.Duration { timeout := time.Minute * time.Duration(10) return &timeout } + +type ComposeProject struct { + Version string + Services map[string]Service `yaml:"services"` +} + +type Service struct { + Image string `yaml:"image"` +} + +func GetDockerComposeImages(data []byte) ([]string, error) { + var dc ComposeProject + err := yaml.Unmarshal(data, &dc) + if err != nil { + return nil, err + } + + var images []string + for _, service := range dc.Services { + images = append(images, service.Image) + } + return images, nil +}