diff --git a/backend/app/api/v1/app_install.go b/backend/app/api/v1/app_install.go index babc9cbdd..f0b63ab6c 100644 --- a/backend/app/api/v1/app_install.go +++ b/backend/app/api/v1/app_install.go @@ -205,14 +205,13 @@ func (b *BaseApi) GetServices(c *gin.Context) { // @Param appInstallId path integer true "request" // @Success 200 {array} dto.AppVersion // @Security ApiKeyAuth -// @Router /apps/installed/:appInstallId/versions [get] +// @Router /apps/installed/update/versions [post] func (b *BaseApi) GetUpdateVersions(c *gin.Context) { - appInstallId, err := helper.GetIntParamByKey(c, "appInstallId") - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + var req request.AppUpdateVersion + if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - versions, err := appInstallService.GetUpdateVersions(appInstallId) + versions, err := appInstallService.GetUpdateVersions(req) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return diff --git a/backend/app/dto/app.go b/backend/app/dto/app.go index 9839d450c..73b075c91 100644 --- a/backend/app/dto/app.go +++ b/backend/app/dto/app.go @@ -37,8 +37,9 @@ type AppOssConfig struct { } type AppVersion struct { - Version string `json:"version"` - DetailId uint `json:"detailId"` + Version string `json:"version"` + DetailId uint `json:"detailId"` + DockerCompose string `json:"dockerCompose"` } type AppList struct { diff --git a/backend/app/dto/request/app.go b/backend/app/dto/request/app.go index b06cbf96e..e6e61870d 100644 --- a/backend/app/dto/request/app.go +++ b/backend/app/dto/request/app.go @@ -61,15 +61,24 @@ type AppBackupDelete struct { } type AppInstalledOperate struct { - InstallId uint `json:"installId" validate:"required"` - BackupId uint `json:"backupId"` - DetailId uint `json:"detailId"` - Operate constant.AppOperate `json:"operate" validate:"required"` - ForceDelete bool `json:"forceDelete"` - DeleteBackup bool `json:"deleteBackup"` - DeleteDB bool `json:"deleteDB"` - Backup bool `json:"backup"` - PullImage bool `json:"pullImage"` + InstallId uint `json:"installId" validate:"required"` + BackupId uint `json:"backupId"` + DetailId uint `json:"detailId"` + Operate constant.AppOperate `json:"operate" validate:"required"` + ForceDelete bool `json:"forceDelete"` + DeleteBackup bool `json:"deleteBackup"` + DeleteDB bool `json:"deleteDB"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + DockerCompose string `json:"dockerCompose"` +} + +type AppInstallUpgrade struct { + InstallID uint `json:"installId"` + DetailID uint `json:"detailId"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + DockerCompose string `json:"dockerCompose"` } type AppInstalledUpdate struct { @@ -88,3 +97,8 @@ type PortUpdate struct { Name string `json:"name"` Port int64 `json:"port"` } + +type AppUpdateVersion struct { + AppInstallID uint `json:"appInstallID" validate:"required"` + UpdateVersion string `json:"updateVersion"` +} diff --git a/backend/app/dto/response/app.go b/backend/app/dto/response/app.go index 630244473..824c9d1ba 100644 --- a/backend/app/dto/response/app.go +++ b/backend/app/dto/response/app.go @@ -31,7 +31,7 @@ type AppDTO struct { type AppDto struct { Name string `json:"name"` Key string `json:"key"` - ID uint `json:"ID"` + ID uint `json:"id"` ShortDescZh string `json:"shortDescZh"` ShortDescEn string `json:"shortDescEn"` Icon string `json:"icon"` @@ -88,24 +88,25 @@ type AppInstalledDTO struct { } type AppInstallDTO struct { - ID uint `json:"id"` - Name string `json:"name"` - AppID uint `json:"appID"` - AppDetailID uint `json:"appDetailID"` - Version string `json:"version"` - Status string `json:"status"` - Message string `json:"message"` - HttpPort int `json:"httpPort"` - HttpsPort int `json:"httpsPort"` - Path string `json:"path"` - CanUpdate bool `json:"canUpdate"` - Icon string `json:"icon"` - AppName string `json:"appName"` - Ready int `json:"ready"` - Total int `json:"total"` - AppKey string `json:"appKey"` - AppType string `json:"appType"` - AppStatus string `json:"appStatus"` + ID uint `json:"id"` + Name string `json:"name"` + AppID uint `json:"appID"` + AppDetailID uint `json:"appDetailID"` + Version string `json:"version"` + Status string `json:"status"` + Message string `json:"message"` + HttpPort int `json:"httpPort"` + HttpsPort int `json:"httpsPort"` + Path string `json:"path"` + CanUpdate bool `json:"canUpdate"` + Icon string `json:"icon"` + AppName string `json:"appName"` + Ready int `json:"ready"` + Total int `json:"total"` + AppKey string `json:"appKey"` + AppType string `json:"appType"` + AppStatus string `json:"appStatus"` + DockerCompose string `json:"dockerCompose"` } type DatabaseConn struct { diff --git a/backend/app/service/app_install.go b/backend/app/service/app_install.go index 7888fb6f2..a51cbaa65 100644 --- a/backend/app/service/app_install.go +++ b/backend/app/service/app_install.go @@ -5,11 +5,14 @@ import ( "encoding/json" "fmt" "github.com/1Panel-dev/1Panel/backend/utils/files" + httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" "github.com/docker/docker/api/types" "gopkg.in/yaml.v3" "math" + "net/http" "os" "path" + "path/filepath" "reflect" "sort" "strconv" @@ -49,7 +52,7 @@ type IAppInstallService interface { IgnoreUpgrade(req request.AppInstalledIgnoreUpgrade) error SyncAll(systemInit bool) error GetServices(key string) ([]response.AppService, error) - GetUpdateVersions(installId uint) ([]dto.AppVersion, error) + GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) GetParams(id uint) (*response.AppConfig, error) ChangeAppPort(req request.PortUpdate) error GetDefaultConfigByKey(key, name string) (string, error) @@ -262,7 +265,14 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error { case constant.Sync: return syncAppInstallStatus(&install) case constant.Upgrade: - return upgradeInstall(install.ID, req.DetailId, req.Backup, req.PullImage) + upgradeReq := request.AppInstallUpgrade{ + InstallID: install.ID, + DetailID: req.DetailId, + Backup: req.Backup, + PullImage: req.PullImage, + DockerCompose: req.DockerCompose, + } + return upgradeInstall(upgradeReq) case constant.Reload: return opNginx(install.ContainerName, constant.NginxReload) default: @@ -484,8 +494,8 @@ func (a *AppInstallService) GetServices(key string) ([]response.AppService, erro return res, nil } -func (a *AppInstallService) GetUpdateVersions(installId uint) ([]dto.AppVersion, error) { - install, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId)) +func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error) { + install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.AppInstallID)) var versions []dto.AppVersion if err != nil { return versions, err @@ -506,9 +516,28 @@ func (a *AppInstallService) GetUpdateVersions(installId uint) ([]dto.AppVersion, continue } if common.CompareVersion(detail.Version, install.Version) { + var newCompose string + if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { + filename := filepath.Base(detail.DownloadUrl) + dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") + statusCode, composeRes, err := httpUtil.HandleGet(dockerComposeUrl, http.MethodGet) + if err != nil { + return versions, err + } + if statusCode > 200 { + return versions, err + } + detail.DockerCompose = string(composeRes) + _ = appDetailRepo.Update(context.Background(), detail) + } + newCompose, err = getUpgradeCompose(install, detail) + if err != nil { + return versions, err + } versions = append(versions, dto.AppVersion{ - Version: detail.Version, - DetailId: detail.ID, + Version: detail.Version, + DetailId: detail.ID, + DockerCompose: newCompose, }) } } diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index f86f10ae8..cd8b7a01c 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -439,12 +439,65 @@ func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, f return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID)) } -func upgradeInstall(installID uint, detailID uint, backup, pullImage bool) error { - install, err := appInstallRepo.GetFirst(commonRepo.WithByID(installID)) +func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { + if detail.DockerCompose == "" { + return "", nil + } + composeMap := make(map[string]interface{}) + if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil { + return "", err + } + value, ok := composeMap["services"] + if !ok { + return "", buserr.New(constant.ErrFileParse) + } + servicesMap := value.(map[string]interface{}) + if len(servicesMap) == 1 { + index := 0 + oldServiceName := "" + for k := range servicesMap { + oldServiceName = k + index++ + if index > 0 { + break + } + } + servicesMap[install.ServiceName] = servicesMap[oldServiceName] + if install.ServiceName != oldServiceName { + delete(servicesMap, oldServiceName) + } + } + envs := make(map[string]interface{}) + if err := json.Unmarshal([]byte(install.Env), &envs); err != nil { + return "", err + } + config := getAppCommonConfig(envs) + if config.ContainerName == "" { + config.ContainerName = install.ContainerName + envs[constant.ContainerName] = install.ContainerName + } + config.Advanced = true + if err := addDockerComposeCommonParam(composeMap, install.ServiceName, config, envs); err != nil { + return "", err + } + paramByte, err := json.Marshal(envs) + if err != nil { + return "", err + } + install.Env = string(paramByte) + composeByte, err := yaml.Marshal(composeMap) + if err != nil { + return "", err + } + return string(composeByte), nil +} + +func upgradeInstall(req request.AppInstallUpgrade) error { + install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) if err != nil { return err } - detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(detailID)) + detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(req.DetailID)) if err != nil { return err } @@ -459,7 +512,7 @@ func upgradeInstall(installID uint, detailID uint, backup, pullImage bool) error backupFile string ) global.LOG.Infof(i18n.GetMsgWithName("UpgradeAppStart", install.Name, nil)) - if backup { + if req.Backup { backupRecord, err := NewIBackupService().AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name}) if err == nil { localDir, err := loadLocalDir() @@ -476,13 +529,13 @@ func upgradeInstall(installID uint, detailID uint, backup, pullImage bool) error defer func() { if upErr != nil { global.LOG.Infof(i18n.GetMsgWithName("ErrAppUpgrade", install.Name, upErr)) - if backup { + if req.Backup { global.LOG.Infof(i18n.GetMsgWithName("AppRecover", install.Name, nil)) if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", Source: constant.ResourceLocal, File: backupFile}); err != nil { global.LOG.Errorf("recover app [%s] [%s] failed %v", install.App.Key, install.Name, err) } } - existInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(installID)) + existInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallID)) if existInstall.ID > 0 { existInstall.Status = constant.UpgradeErr existInstall.Message = upErr.Error() @@ -529,59 +582,19 @@ func upgradeInstall(installID uint, detailID uint, backup, pullImage bool) error _, _ = scriptCmd.CombinedOutput() } - composeMap := make(map[string]interface{}) - if upErr = yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); upErr != nil { - return - } - value, ok := composeMap["services"] - if !ok { - upErr = buserr.New(constant.ErrFileParse) - return - } - servicesMap := value.(map[string]interface{}) - if len(servicesMap) == 1 { - index := 0 - oldServiceName := "" - for k := range servicesMap { - oldServiceName = k - index++ - if index > 0 { - break - } + var newCompose string + if req.DockerCompose == "" { + newCompose, upErr = getUpgradeCompose(install, detail) + if upErr != nil { + return } - servicesMap[install.ServiceName] = servicesMap[oldServiceName] - if install.ServiceName != oldServiceName { - delete(servicesMap, oldServiceName) - } - } - envs := make(map[string]interface{}) - if upErr = json.Unmarshal([]byte(install.Env), &envs); upErr != nil { - return - } - config := getAppCommonConfig(envs) - if config.ContainerName == "" { - config.ContainerName = install.ContainerName - envs[constant.ContainerName] = install.ContainerName - } - config.Advanced = true - if upErr = addDockerComposeCommonParam(composeMap, install.ServiceName, config, envs); upErr != nil { - return - } - paramByte, err := json.Marshal(envs) - if err != nil { - upErr = err - return - } - install.Env = string(paramByte) - composeByte, err := yaml.Marshal(composeMap) - if err != nil { - upErr = err - return + } else { + newCompose = req.DockerCompose } - install.DockerCompose = string(composeByte) + install.DockerCompose = newCompose install.Version = detail.Version - install.AppDetailId = detailID + install.AppDetailId = req.DetailID content, err := fileOp.GetContent(install.GetEnvPath()) if err != nil { @@ -589,7 +602,7 @@ func upgradeInstall(installID uint, detailID uint, backup, pullImage bool) error return } - if pullImage { + if req.PullImage { projectName := strings.ToLower(install.Name) images, err := composeV2.GetDockerComposeImages(projectName, content, []byte(detail.DockerCompose)) if err != nil { @@ -621,6 +634,10 @@ func upgradeInstall(installID uint, detailID uint, backup, pullImage bool) error upErr = err return } + envs := make(map[string]interface{}) + if upErr = json.Unmarshal([]byte(install.Env), &envs); upErr != nil { + return + } envParams := make(map[string]string, len(envs)) handleMap(envs, envParams) if upErr = env.Write(envParams, install.GetEnvPath()); upErr != nil { @@ -1134,6 +1151,7 @@ func synAppInstall(containers map[string]types.Container, appInstall *model.AppI if len(containers) == 0 { appInstall.Status = constant.Error appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error() + _ = appInstallRepo.Save(context.Background(), appInstall) return } notFoundNames := make([]string, 0) @@ -1178,6 +1196,7 @@ func synAppInstall(containers map[string]types.Container, appInstall *model.AppI appInstall.Message = msg appInstall.Status = constant.UnHealthy } + _ = appInstallRepo.Save(context.Background(), appInstall) } func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool) ([]response.AppInstallDTO, error) { @@ -1223,6 +1242,10 @@ func handleInstalled(appInstallList []model.AppInstall, updated bool, sync bool) AppName: installed.App.Name, AppKey: installed.App.Key, AppType: installed.App.Type, + Path: installed.GetPath(), + } + if updated { + installDTO.DockerCompose = installed.DockerCompose } app, err := appRepo.GetFirst(commonRepo.WithByID(installed.AppId)) if err != nil { diff --git a/backend/router/ro_app.go b/backend/router/ro_app.go index 127139fd9..70bc790d3 100644 --- a/backend/router/ro_app.go +++ b/backend/router/ro_app.go @@ -23,7 +23,6 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) { appRouter.GET("/details/:id", baseApi.GetAppDetailByID) appRouter.POST("/install", baseApi.InstallApp) appRouter.GET("/tags", baseApi.GetAppTags) - appRouter.GET("/installed/:appInstallId/versions", baseApi.GetUpdateVersions) appRouter.POST("/installed/check", baseApi.CheckAppInstalled) appRouter.POST("/installed/loadport", baseApi.LoadPort) appRouter.POST("/installed/conninfo", baseApi.LoadConnInfo) @@ -39,5 +38,6 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) { appRouter.POST("/installed/params/update", baseApi.UpdateInstalled) appRouter.POST("/installed/ignore", baseApi.IgnoreUpgrade) appRouter.GET("/ignored/detail", baseApi.GetIgnoredApp) + appRouter.POST("/installed/update/versions", baseApi.GetUpdateVersions) } } diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index fa44aea49..cf27cb27f 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -236,4 +236,9 @@ export namespace App { version: string; icon: string; } + + export interface AppUpdateVersionReq { + appInstallID: number; + updateVersion?: string; + } } diff --git a/frontend/src/api/modules/app.ts b/frontend/src/api/modules/app.ts index cb49c832c..f337b9920 100644 --- a/frontend/src/api/modules/app.ts +++ b/frontend/src/api/modules/app.ts @@ -79,8 +79,8 @@ export const GetAppService = (key: string | undefined) => { return http.get(`apps/services/${key}`); }; -export const GetAppUpdateVersions = (id: number) => { - return http.get(`apps/installed/${id}/versions`); +export const GetAppUpdateVersions = (req: App.AppUpdateVersionReq) => { + return http.post(`apps/installed/update/versions`, req); }; export const GetAppDefaultConfig = (key: string, name: string) => { diff --git a/frontend/src/assets/images/theworld.png b/frontend/src/assets/images/theworld.png new file mode 100644 index 000000000..d0a639610 Binary files /dev/null and b/frontend/src/assets/images/theworld.png differ diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 8af4d5ac4..9b94b1b85 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1744,7 +1744,16 @@ const message = { showLocal: 'Show Local Application', reload: 'Reload', upgradeWarn: - 'Upgrading the application will replace the docker-compose.yml file. If there is any change, please replace it after upgrading', + 'Upgrading the application will replace the docker-compose.yml file. If there are any changes, you can click to view the file comparison', + newVersion: 'New version', + oldVersion: 'Current version', + composeDiff: 'File comparison', + showDiff: 'View comparison', + useNew: 'Use custom version', + useDefault: 'Use default version', + useCustom: 'Customize docker-compose.yml', + useCustomHelper: + 'Using a custom docker-compose.yml file may cause the application upgrade to fail. If it is not necessary, do not check it', }, website: { website: 'Website', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index adab00347..0dc5ed97d 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1624,7 +1624,15 @@ const message = { hostModeHelper: '目前應用網路模式為 host 模式,如需放開端口,請在防火牆頁面手動放開', showLocal: '顯示本機應用程式', reload: '重載', - upgradeWarn: '升級應用程式會取代 docker-compose.yml 文件,如有更改,請升級之後替換', + upgradeWarn: '升級應用程式會取代 docker-compose.yml 文件,如有更改,可以點擊查看文件對比', + newVersion: '新版本', + oldVersion: '目前版本', + composeDiff: '文件對比', + showDiff: '看對比', + useNew: '使用自訂版本', + useDefault: '使用預設版本', + useCustom: '自訂 docker-compose.yml', + useCustomHelper: '使用自訂 docker-compose.yml 文件,可能會導致應用程式升級失敗,如無必要,請勿勾選', }, website: { website: '網站', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 5c1ef93b1..6d4956644 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1624,7 +1624,15 @@ const message = { hostModeHelper: '当前应用网络模式为 host 模式,如需放开端口,请在防火墙页面手动放开', showLocal: '显示本地应用', reload: '重载', - upgradeWarn: '升级应用会替换 docker-compose.yml 文件,如有更改,请升级之后替换', + upgradeWarn: '升级应用会替换 docker-compose.yml 文件,如有更改,可以点击查看文件对比', + newVersion: '新版本', + oldVersion: '当前版本', + composeDiff: '文件对比', + showDiff: '查看对比', + useNew: '使用自定义版本', + useDefault: '使用默认版本', + useCustom: '自定义 docker-compose.yml', + useCustomHelper: '使用自定义 docker-compose.yml 文件,可能会导致应用升级失败,如无必要,请勿勾选', }, website: { website: '网站', diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 3fd2a6391..75a008ded 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -21,10 +21,15 @@ import Components from '@/components'; import ElementPlus from 'element-plus'; import Fit2CloudPlus from 'fit2cloud-ui-plus'; import * as Icons from '@element-plus/icons-vue'; +import VueDiff from 'vue-diff'; +import 'vue-diff/dist/index.css'; +import yaml from 'highlight.js/lib/languages/yaml'; +VueDiff.hljs.registerLanguage('yaml', yaml); + const app = createApp(App); app.component('SvgIcon', SvgIcon); app.use(ElementPlus); - +app.use(VueDiff); app.use(Fit2CloudPlus, { locale: i18n.global.messages.value[localStorage.getItem('lang') || 'zh'] }); Object.keys(Icons).forEach((key) => { diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index 13f3d5cfc..8cd627c1d 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -435,7 +435,7 @@ const openOperate = (row: any, op: string) => { operateReq.installId = row.id; operateReq.operate = op; if (op == 'upgrade' || op == 'ignore') { - upgradeRef.value.acceptParams(row.id, row.name, op, row.app); + upgradeRef.value.acceptParams(row.id, row.name, row.dockerCompose, op, row.app); } else if (op == 'delete') { AppInstalledDeleteCheck(row.id).then(async (res) => { const items = res.data; diff --git a/frontend/src/views/app-store/installed/upgrade/diff/index.vue b/frontend/src/views/app-store/installed/upgrade/diff/index.vue new file mode 100644 index 000000000..44b1a0cab --- /dev/null +++ b/frontend/src/views/app-store/installed/upgrade/diff/index.vue @@ -0,0 +1,88 @@ + + + diff --git a/frontend/src/views/app-store/installed/upgrade/index.vue b/frontend/src/views/app-store/installed/upgrade/index.vue index b8a83675a..0cebf9757 100644 --- a/frontend/src/views/app-store/installed/upgrade/index.vue +++ b/frontend/src/views/app-store/installed/upgrade/index.vue @@ -1,5 +1,5 @@