From 18ab07ec4fcb231ffac0b9dac271becf9253922a Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:34:20 +0800 Subject: [PATCH] feat: Support application migration to other nodes. (#10832) Refs https://github.com/1Panel-dev/1Panel/issues/10509 --- agent/app/dto/response/app.go | 2 ++ agent/app/repo/app.go | 9 ----- agent/app/repo/backup.go | 3 +- agent/app/service/app_install.go | 18 ++++++++++ core/app/task/task.go | 9 ++--- core/i18n/lang/en.yaml | 5 +++ core/i18n/lang/es-ES.yaml | 5 +++ core/i18n/lang/ja.yaml | 5 +++ core/i18n/lang/ko.yaml | 5 +++ core/i18n/lang/pt-BR.yaml | 5 +++ core/i18n/lang/ru.yaml | 5 +++ core/i18n/lang/tr.yaml | 5 +++ core/i18n/lang/zh-Hant.yaml | 5 +++ core/i18n/lang/zh.yaml | 5 +++ core/utils/req_helper/requset.go | 12 +++++++ frontend/src/api/interface/app.ts | 1 + frontend/src/components/backup/index.vue | 35 +++++++++++++++++-- frontend/src/lang/modules/en.ts | 1 + frontend/src/lang/modules/es-es.ts | 1 + frontend/src/lang/modules/ja.ts | 1 + frontend/src/lang/modules/ko.ts | 1 + frontend/src/lang/modules/ms.ts | 1 + frontend/src/lang/modules/pt-br.ts | 1 + frontend/src/lang/modules/ru.ts | 1 + frontend/src/lang/modules/tr.ts | 1 + frontend/src/lang/modules/zh-Hant.ts | 1 + frontend/src/lang/modules/zh.ts | 1 + .../src/views/app-store/installed/index.vue | 17 ++++----- 28 files changed, 133 insertions(+), 28 deletions(-) diff --git a/agent/app/dto/response/app.go b/agent/app/dto/response/app.go index d9966fe86..d23922813 100644 --- a/agent/app/dto/response/app.go +++ b/agent/app/dto/response/app.go @@ -139,6 +139,8 @@ type AppInstallInfo struct { HttpPort int `json:"HttpPort"` Container string `json:"container"` ComposePath string `json:"composePath"` + AppKey string `json:"appKey"` + AppPorts []int `json:"appPorts"` Env map[string]interface{} `json:"env"` } diff --git a/agent/app/repo/app.go b/agent/app/repo/app.go index 67bf3105b..331ed8e21 100644 --- a/agent/app/repo/app.go +++ b/agent/app/repo/app.go @@ -27,7 +27,6 @@ type IAppRepo interface { GetFirst(opts ...DBOption) (model.App, error) GetBy(opts ...DBOption) ([]model.App, error) BatchCreate(ctx context.Context, apps []model.App) error - GetByKey(ctx context.Context, key string) (model.App, error) Create(ctx context.Context, app *model.App) error Save(ctx context.Context, app *model.App) error BatchDelete(ctx context.Context, apps []model.App) error @@ -144,14 +143,6 @@ func (a AppRepo) BatchCreate(ctx context.Context, apps []model.App) error { return getTx(ctx).Omit(clause.Associations).Create(&apps).Error } -func (a AppRepo) GetByKey(ctx context.Context, key string) (model.App, error) { - var app model.App - if err := getTx(ctx).Where("key = ?", key).First(&app).Error; err != nil { - return app, err - } - return app, nil -} - func (a AppRepo) Create(ctx context.Context, app *model.App) error { return getTx(ctx).Omit(clause.Associations).Create(app).Error } diff --git a/agent/app/repo/backup.go b/agent/app/repo/backup.go index 80213446a..2f726000c 100644 --- a/agent/app/repo/backup.go +++ b/agent/app/repo/backup.go @@ -20,6 +20,7 @@ type IBackupRepo interface { WithByPublic(isPublic bool) DBOption ListRecord(opts ...DBOption) ([]model.BackupRecord, error) + GetRecord(opts ...DBOption) (*model.BackupRecord, error) PageRecord(page, size int, opts ...DBOption) (int64, []model.BackupRecord, error) CreateRecord(record *model.BackupRecord) error DeleteRecord(ctx context.Context, opts ...DBOption) error @@ -160,7 +161,7 @@ func (u *BackupRepo) WithByCronID(cronjobID uint) DBOption { } func (u *BackupRepo) GetRecord(opts ...DBOption) (*model.BackupRecord, error) { - var record *model.BackupRecord + record := &model.BackupRecord{} db := global.DB.Model(&model.BackupRecord{}) for _, opt := range opts { db = opt(db) diff --git a/agent/app/service/app_install.go b/agent/app/service/app_install.go index bf23626fb..031b8f311 100644 --- a/agent/app/service/app_install.go +++ b/agent/app/service/app_install.go @@ -940,8 +940,26 @@ func (a *AppInstallService) GetAppInstallInfo(installID uint) (*response.AppInst HttpPort: appInstall.HttpPort, Status: appInstall.Status, Message: appInstall.Message, + AppKey: appInstall.App.Key, Env: envMap, ComposePath: appInstall.GetComposePath(), } + for k, v := range envMap { + if strings.Contains(strings.ToUpper(k), "PANEL_APP_PORT") { + var port int + switch val := v.(type) { + case int: + port = val + case string: + port, err = strconv.Atoi(val) + if err != nil { + continue + } + default: + continue + } + res.AppPorts = append(res.AppPorts, port) + } + } return res, nil } diff --git a/core/app/task/task.go b/core/app/task/task.go index 4f2b6a1cc..546fd8686 100644 --- a/core/app/task/task.go +++ b/core/app/task/task.go @@ -55,10 +55,11 @@ const ( ) const ( - TaskScopeSystem = "System" - TaskScopeScript = "Script" - TaskScopeNodeFile = "NodeFile" - TaskScopeCluster = "Cluster" + TaskScopeSystem = "System" + TaskScopeScript = "Script" + TaskScopeNodeFile = "NodeFile" + TaskScopeAppBackup = "AppBackup" + TaskScopeCluster = "Cluster" ) func GetTaskName(resourceName, operate, scope string) string { diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index e4c3f69ef..d8f0e62be 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "Internal server error:" # app CustomAppStoreFileValid: "Application store package requires .tar.gz format" ErrFileNotFound: "{{ .name }} file does not exist" +AppBackup: 'Application backup', +AppBackupPush: 'Transfer application backup file {{.file}} to node {{ .name }}', +ErrSourceTargetSame: 'Source node and target node cannot be the same!', +AppInstall: 'Install application {{ .name }} on node {{ .targetNode }}', +AppInstallCheck: 'Check application installation environment', # backup ErrBackupInUsed: "This backup account is used in scheduled tasks and cannot be deleted" diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index d2a455e20..c6f6a3b70 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "Error interno del servidor:" # app CustomAppStoreFileValid: "El paquete de la tienda de aplicaciones debe tener formato .tar.gz" ErrFileNotFound: "El archivo {{ .name }} no existe" +AppBackup: 'Copia de seguridad de aplicación', +AppBackupPush: 'Transferir archivo de copia de seguridad de aplicación {{.file}} al nodo {{ .name }}', +ErrSourceTargetSame: '¡El nodo de origen y el nodo de destino no pueden ser el mismo!', +AppInstall: 'Instalar aplicación {{ .name }} en nodo {{ .targetNode }}', +AppInstallCheck: 'Verificar entorno de instalación de aplicación', # backup ErrBackupInUsed: "Esta cuenta de respaldo se utiliza en tareas programadas y no se puede eliminar" diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index d64f34023..01991ee98 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "サーバー内部エラー: " # app CustomAppStoreFileValid: "アプリストアパッケージは .tar.gz 形式である必要があります" ErrFileNotFound: "{{ .name }} ファイルが存在しません" +AppBackup: 'アプリケーションバックアップ', +AppBackupPush: 'アプリケーションバックアップファイル {{.file}} をノード {{ .name }} に転送', +ErrSourceTargetSame: 'ソースノードとターゲットノードは同じにできません!', +AppInstall: 'ノード {{ .targetNode }} にアプリケーション {{ .name }} をインストール', +AppInstallCheck: 'アプリケーションインストール環境を確認', # backup ErrBackupInUsed: "このバックアップアカウントはスケジュールタスクで使用されており、削除できません" diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 4250775a0..67cbb87d7 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "서버 내부 오류:" # app CustomAppStoreFileValid: "앱 스토어 패키지는 .tar.gz 형식이어야 합니다" ErrFileNotFound: "{{ .name }} 파일이 존재하지 않습니다" +AppBackup: '애플리케이션 백업', +AppBackupPush: '애플리케이션 백업 파일 {{.file}}을(를) 노드 {{ .name }}(으)로 전송', +ErrSourceTargetSame: '소스 노드와 대상 노드는 동일할 수 없습니다!', +AppInstall: '노드 {{ .targetNode }}에 애플리케이션 {{ .name }} 설치', +AppInstallCheck: '애플리케이션 설치 환경 확인', # backup ErrBackupInUsed: "이 백업 계정은 예약된 작업에 사용 중이며 삭제할 수 없습니다" diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index 4dba30ec6..8fcf47b14 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "Erro interno do servidor:" # app CustomAppStoreFileValid: "O pacote da loja de aplicativos deve estar no formato .tar.gz" ErrFileNotFound: "Arquivo {{ .name }} não encontrado" +AppBackup: 'Backup de aplicação', +AppBackupPush: 'Transferir arquivo de backup de aplicação {{.file}} para o nó {{ .name }}', +ErrSourceTargetSame: 'O nó de origem e o nó de destino não podem ser os mesmos!', +AppInstall: 'Instalar aplicação {{ .name }} no nó {{ .targetNode }}', +AppInstallCheck: 'Verificar ambiente de instalação da aplicação', # backup ErrBackupInUsed: "Esta conta de backup está em uso em tarefas agendadas e não pode ser excluída" diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index c7a4d4ec4..a929cf09a 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "Внутренняя ошибка сервера:" # app CustomAppStoreFileValid: "Пакет магазина приложений должен быть в формате .tar.gz" ErrFileNotFound: "Файл {{ .name }} не найден" +AppBackup: 'Резервная копия приложения', +AppBackupPush: 'Передать файл резервной копии приложения {{.file}} на узел {{ .name }}', +ErrSourceTargetSame: 'Исходный узел и целевой узел не могут быть одинаковыми!', +AppInstall: 'Установить приложение {{ .name }} на узел {{ .targetNode }}', +AppInstallCheck: 'Проверить среду установки приложения', # backup ErrBackupInUsed: "Эта учетная запись резервного копирования используется в запланированных задачах и не может быть удалена" diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index b8a15520e..c6b311f39 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "İç sunucu hatası:" # app CustomAppStoreFileValid: "Uygulama mağazası paketi .tar.gz formatında olmalıdır" ErrFileNotFound: "{{ .name }} dosyası mevcut değil" +AppBackup: 'Uygulama yedekleme', +AppBackupPush: 'Uygulama yedek dosyası {{.file}} düğüm {{ .name }} a aktar', +ErrSourceTargetSame: 'Kaynak düğüm ve hedef düğüm aynı olamaz!', +AppInstall: 'Düğüm {{ .targetNode }} üzerine {{ .name }} uygulamasını yükle', +AppInstallCheck: 'Uygulama kurulum ortamını kontrol et', # backup ErrBackupInUsed: "Bu yedekleme hesabı zamanlanmış görevlerde kullanılıyor ve silinemez" diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 51106d6a4..191d60240 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "服務內部錯誤: " #app CustomAppStoreFileValid: "應用商店包需要 .tar.gz 格式" ErrFileNotFound: "{{ .name }} 檔案不存在" +AppBackup: '應用備份', +AppBackupPush: '傳輸應用備份文件 {{.file}} 到節點 {{ .name }}', +ErrSourceTargetSame: '源節點和目標節點不能相同!', +AppInstall: '在 {{ .targetNode }} 節點安裝應用 {{ .name }}', +AppInstallCheck: '檢查應用安裝環境', #backup ErrBackupInUsed: "該備份帳號已在排程任務中使用,無法刪除" diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index b74b156e1..da97ba7ba 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -36,6 +36,11 @@ ErrInternalServerKey: "服务内部错误:" #app CustomAppStoreFileValid: "应用商店包需要 .tar.gz 格式" ErrFileNotFound: '{{ .name }} 文件不存在' +AppBackup: "应用备份" +AppBackupPush: "传输应用备份文件 {{.file}} 到节点 {{ .name }}" +ErrSourceTargetSame: "源节点和目标节点不能相同!" +AppInstall: "在 {{ .targetNode }} 节点安装应用 {{ .name }}" +AppInstallCheck: "检查应用安装环境" #backup ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除" diff --git a/core/utils/req_helper/requset.go b/core/utils/req_helper/requset.go index 776093dc1..9d15d13f8 100644 --- a/core/utils/req_helper/requset.go +++ b/core/utils/req_helper/requset.go @@ -4,6 +4,10 @@ import ( "context" "crypto/tls" "errors" + "github.com/1Panel-dev/1Panel/core/utils/req_helper/proxy_local" + "github.com/1Panel-dev/1Panel/core/xpack/app/model" + "github.com/1Panel-dev/1Panel/core/xpack/utils/node_helper" + "github.com/gin-gonic/gin" "io" "net" "net/http" @@ -111,3 +115,11 @@ func handleGetWithTransport(url string, transport *http.Transport) (*http.Respon return resp, nil } + +func RequestToNode(ctx *gin.Context, reqUrl, reqMethod string, body io.Reader, isLocal bool, timeout int, node model.Node) (res interface{}, resErr error) { + if isLocal { + return proxy_local.NewLocalClient(reqUrl, reqMethod, body, ctx) + } else { + return node_helper.NewRequestToAgent(reqUrl, reqMethod, body, timeout, node) + } +} diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index 0e3731d0a..c66dd4974 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -150,6 +150,7 @@ export namespace App { favorite: boolean; app: App; webUI: string; + appKey?: string; } export interface AppInstalledInfo { diff --git a/frontend/src/components/backup/index.vue b/frontend/src/components/backup/index.vue index 43994da8d..16d8ce510 100644 --- a/frontend/src/components/backup/index.vue +++ b/frontend/src/components/backup/index.vue @@ -128,6 +128,7 @@ +