mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-19 05:49:02 +08:00
feat: Support application migration to other nodes. (#10832)
Refs https://github.com/1Panel-dev/1Panel/issues/10509
This commit is contained in:
parent
e86bc9a8db
commit
18ab07ec4f
28 changed files with 133 additions and 28 deletions
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ ErrInternalServerKey: "サーバー内部エラー: "
|
|||
# app
|
||||
CustomAppStoreFileValid: "アプリストアパッケージは .tar.gz 形式である必要があります"
|
||||
ErrFileNotFound: "{{ .name }} ファイルが存在しません"
|
||||
AppBackup: 'アプリケーションバックアップ',
|
||||
AppBackupPush: 'アプリケーションバックアップファイル {{.file}} をノード {{ .name }} に転送',
|
||||
ErrSourceTargetSame: 'ソースノードとターゲットノードは同じにできません!',
|
||||
AppInstall: 'ノード {{ .targetNode }} にアプリケーション {{ .name }} をインストール',
|
||||
AppInstallCheck: 'アプリケーションインストール環境を確認',
|
||||
|
||||
# backup
|
||||
ErrBackupInUsed: "このバックアップアカウントはスケジュールタスクで使用されており、削除できません"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ ErrInternalServerKey: "서버 내부 오류:"
|
|||
# app
|
||||
CustomAppStoreFileValid: "앱 스토어 패키지는 .tar.gz 형식이어야 합니다"
|
||||
ErrFileNotFound: "{{ .name }} 파일이 존재하지 않습니다"
|
||||
AppBackup: '애플리케이션 백업',
|
||||
AppBackupPush: '애플리케이션 백업 파일 {{.file}}을(를) 노드 {{ .name }}(으)로 전송',
|
||||
ErrSourceTargetSame: '소스 노드와 대상 노드는 동일할 수 없습니다!',
|
||||
AppInstall: '노드 {{ .targetNode }}에 애플리케이션 {{ .name }} 설치',
|
||||
AppInstallCheck: '애플리케이션 설치 환경 확인',
|
||||
|
||||
# backup
|
||||
ErrBackupInUsed: "이 백업 계정은 예약된 작업에 사용 중이며 삭제할 수 없습니다"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ ErrInternalServerKey: "Внутренняя ошибка сервера:"
|
|||
# app
|
||||
CustomAppStoreFileValid: "Пакет магазина приложений должен быть в формате .tar.gz"
|
||||
ErrFileNotFound: "Файл {{ .name }} не найден"
|
||||
AppBackup: 'Резервная копия приложения',
|
||||
AppBackupPush: 'Передать файл резервной копии приложения {{.file}} на узел {{ .name }}',
|
||||
ErrSourceTargetSame: 'Исходный узел и целевой узел не могут быть одинаковыми!',
|
||||
AppInstall: 'Установить приложение {{ .name }} на узел {{ .targetNode }}',
|
||||
AppInstallCheck: 'Проверить среду установки приложения',
|
||||
|
||||
# backup
|
||||
ErrBackupInUsed: "Эта учетная запись резервного копирования используется в запланированных задачах и не может быть удалена"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ ErrInternalServerKey: "服務內部錯誤: "
|
|||
#app
|
||||
CustomAppStoreFileValid: "應用商店包需要 .tar.gz 格式"
|
||||
ErrFileNotFound: "{{ .name }} 檔案不存在"
|
||||
AppBackup: '應用備份',
|
||||
AppBackupPush: '傳輸應用備份文件 {{.file}} 到節點 {{ .name }}',
|
||||
ErrSourceTargetSame: '源節點和目標節點不能相同!',
|
||||
AppInstall: '在 {{ .targetNode }} 節點安裝應用 {{ .name }}',
|
||||
AppInstallCheck: '檢查應用安裝環境',
|
||||
|
||||
#backup
|
||||
ErrBackupInUsed: "該備份帳號已在排程任務中使用,無法刪除"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ ErrInternalServerKey: "服务内部错误:"
|
|||
#app
|
||||
CustomAppStoreFileValid: "应用商店包需要 .tar.gz 格式"
|
||||
ErrFileNotFound: '{{ .name }} 文件不存在'
|
||||
AppBackup: "应用备份"
|
||||
AppBackupPush: "传输应用备份文件 {{.file}} 到节点 {{ .name }}"
|
||||
ErrSourceTargetSame: "源节点和目标节点不能相同!"
|
||||
AppInstall: "在 {{ .targetNode }} 节点安装应用 {{ .name }}"
|
||||
AppInstallCheck: "检查应用安装环境"
|
||||
|
||||
#backup
|
||||
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export namespace App {
|
|||
favorite: boolean;
|
||||
app: App;
|
||||
webUI: string;
|
||||
appKey?: string;
|
||||
}
|
||||
|
||||
export interface AppInstalledInfo {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@
|
|||
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<TaskLog ref="taskLogRef" @close="search" />
|
||||
<PushApp ref="pushAppRef" v-if="isProductPro" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -147,14 +148,24 @@ import i18n from '@/lang';
|
|||
import { Backup } from '@/api/interface/backup';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { routerToFileWithPath } from '@/utils/router';
|
||||
const globalStore = GlobalStore();
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
const { isProductPro, currentNode } = useGlobalStore();
|
||||
|
||||
const PushApp = defineAsyncComponent(async () => {
|
||||
const modules = import.meta.glob('@/xpack/views/appstore/push-app/index.vue');
|
||||
const loader = modules['/src/xpack/views/appstore/push-app/index.vue'];
|
||||
if (loader) {
|
||||
return ((await loader()) as any).default;
|
||||
}
|
||||
return { template: '<div></div>' };
|
||||
});
|
||||
|
||||
const selects = ref<any>([]);
|
||||
const loading = ref();
|
||||
const opRef = ref();
|
||||
const taskLogRef = ref();
|
||||
const pushAppRef = ref();
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
|
|
@ -175,16 +186,19 @@ const description = ref();
|
|||
const open = ref();
|
||||
const isBackup = ref();
|
||||
const recordInfo = ref();
|
||||
const appInstallID = ref();
|
||||
|
||||
interface DialogProps {
|
||||
type: string;
|
||||
name: string;
|
||||
detailName: string;
|
||||
status: string;
|
||||
appInstallID?: number;
|
||||
}
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
type.value = params.type;
|
||||
if (type.value === 'app') {
|
||||
appInstallID.value = params.appInstallID || 0;
|
||||
loadBackupDir();
|
||||
}
|
||||
name.value = params.name;
|
||||
|
|
@ -365,7 +379,7 @@ const onDownload = async (row: Backup.RecordInfo) => {
|
|||
fileName: row.fileName,
|
||||
};
|
||||
await downloadBackupRecord(params).then(async (res) => {
|
||||
downloadFile(res.data, globalStore.currentNode);
|
||||
downloadFile(res.data, currentNode.value);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -412,6 +426,21 @@ const buttons = [
|
|||
onRecover(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.migrate'),
|
||||
disabled: (row: any) => {
|
||||
return row.size === 0 || row.status === 'Failed' || row.accountType !== 'LOCAL';
|
||||
},
|
||||
show: () => {
|
||||
return type.value === 'app';
|
||||
},
|
||||
click: (row: Backup.RecordInfo) => {
|
||||
pushAppRef.value.acceptParams({
|
||||
appInstallID: appInstallID.value,
|
||||
appBackupID: row.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.download'),
|
||||
disabled: (row: any) => {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const message = {
|
|||
show: 'Show',
|
||||
hide: 'Hide',
|
||||
visit: 'Visit',
|
||||
migrate: 'Migrate',
|
||||
},
|
||||
operate: {
|
||||
start: 'Start',
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const message = {
|
|||
sure: 'Confirmar',
|
||||
show: 'Mostrar',
|
||||
hide: 'Ocultar',
|
||||
migrate: 'Migrar',
|
||||
},
|
||||
operate: {
|
||||
start: 'Iniciar',
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const message = {
|
|||
show: '表示する',
|
||||
hide: '隠す',
|
||||
visit: '訪問',
|
||||
migrate: '移行',
|
||||
},
|
||||
operate: {
|
||||
start: '開始',
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const message = {
|
|||
show: '보기',
|
||||
hide: '숨기기',
|
||||
visit: '방문',
|
||||
migrate: '마이그레이션',
|
||||
},
|
||||
operate: {
|
||||
start: '시작',
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const message = {
|
|||
show: 'Tunjukkan',
|
||||
hide: 'Sembunyikan',
|
||||
visit: 'Lawati',
|
||||
migrate: 'Migrasi',
|
||||
},
|
||||
operate: {
|
||||
start: 'Mula',
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const message = {
|
|||
show: 'Exibir',
|
||||
hide: 'Ocultar',
|
||||
visit: 'Visitar',
|
||||
migrate: 'Migrar',
|
||||
},
|
||||
operate: {
|
||||
start: 'Iniciar',
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ const message = {
|
|||
show: 'Показать',
|
||||
hide: 'Скрыть',
|
||||
visit: 'Посетить',
|
||||
migrate: 'Мигрировать',
|
||||
},
|
||||
operate: {
|
||||
start: 'Запустить',
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const message = {
|
|||
show: 'Göster',
|
||||
hide: 'Gizle',
|
||||
visit: 'Visit',
|
||||
migrate: 'Taşı',
|
||||
},
|
||||
operate: {
|
||||
start: 'Başlat',
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const message = {
|
|||
show: '顯示',
|
||||
hide: '隱藏',
|
||||
visit: '訪問',
|
||||
migrate: '遷移',
|
||||
},
|
||||
operate: {
|
||||
start: '啟動',
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const message = {
|
|||
show: '显示',
|
||||
hide: '隐藏',
|
||||
visit: '访问',
|
||||
migrate: '迁移',
|
||||
},
|
||||
operate: {
|
||||
start: '启动',
|
||||
|
|
|
|||
|
|
@ -211,13 +211,7 @@
|
|||
plain
|
||||
round
|
||||
size="small"
|
||||
@click="
|
||||
openBackups(
|
||||
installed.appKey,
|
||||
installed.name,
|
||||
installed.status,
|
||||
)
|
||||
"
|
||||
@click="openBackups(installed)"
|
||||
v-if="mode === 'installed'"
|
||||
>
|
||||
{{ $t('commons.button.backup') }}
|
||||
|
|
@ -709,12 +703,13 @@ const toContainer = async (row: App.AppInstalled) => {
|
|||
routerToNameWithQuery('ContainerItem', { filters: 'com.docker.compose.project=' + row.name, uncached: true });
|
||||
};
|
||||
|
||||
const openBackups = (key: string, name: string, status: string) => {
|
||||
const openBackups = (row: App.AppInstalled) => {
|
||||
let params = {
|
||||
type: 'app',
|
||||
name: key,
|
||||
detailName: name,
|
||||
status: status,
|
||||
name: row.appKey,
|
||||
detailName: row.name,
|
||||
status: row.status,
|
||||
appInstallID: row.id,
|
||||
};
|
||||
backupRef.value.acceptParams(params);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue