From 44312a34784ce9411773c82a0145377bfbd2b1ca Mon Sep 17 00:00:00 2001 From: ssongliu Date: Thu, 30 Oct 2025 17:26:06 +0800 Subject: [PATCH] feat: Container operations support task logs --- agent/app/dto/container.go | 1 + agent/app/dto/image.go | 4 +- agent/app/service/container.go | 45 +++++---- agent/app/service/image.go | 93 ++++++++++++------- agent/app/task/task.go | 2 + agent/i18n/lang/en.yaml | 7 ++ agent/i18n/lang/es-ES.yaml | 7 ++ agent/i18n/lang/ja.yaml | 7 ++ agent/i18n/lang/ko.yaml | 7 ++ agent/i18n/lang/ms.yaml | 7 ++ agent/i18n/lang/pt-BR.yaml | 7 ++ agent/i18n/lang/ru.yaml | 7 ++ agent/i18n/lang/tr.yaml | 8 ++ agent/i18n/lang/zh-Hant.yaml | 7 ++ agent/i18n/lang/zh.yaml | 7 ++ frontend/src/api/interface/container.ts | 3 + .../src/views/container/container/index.vue | 41 ++++++-- .../src/views/container/image/load/index.vue | 11 +++ .../src/views/container/image/save/index.vue | 11 +++ 19 files changed, 221 insertions(+), 61 deletions(-) diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index dfa950080e25..ca19cca86c06 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -153,6 +153,7 @@ type PortHelper struct { } type ContainerOperation struct { + TaskID string `json:"taskID"` Names []string `json:"names" validate:"required"` Operation string `json:"operation" validate:"required,oneof=up start stop restart kill pause unpause remove"` } diff --git a/agent/app/dto/image.go b/agent/app/dto/image.go index 7838bd0c870d..304dd0f835bb 100644 --- a/agent/app/dto/image.go +++ b/agent/app/dto/image.go @@ -18,7 +18,8 @@ type ImageInfo struct { } type ImageLoad struct { - Path string `json:"path" validate:"required"` + TaskID string `json:"taskID"` + Path string `json:"path" validate:"required"` } type ImageBuild struct { @@ -48,6 +49,7 @@ type ImagePush struct { } type ImageSave struct { + TaskID string `json:"taskID"` TagName string `json:"tagName" validate:"required"` Path string `json:"path" validate:"required"` Name string `json:"name" validate:"required"` diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 3c8f33f337ae..f3dec08803c1 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -895,25 +895,36 @@ func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error return err } defer client.Close() + taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1) + if err != nil { + return fmt.Errorf("new task for container commit failed, err: %v", err) + } + for _, item := range req.Names { - global.LOG.Infof("start container %s operation %s", item, req.Operation) - switch req.Operation { - case constant.ContainerOpStart: - err = client.ContainerStart(ctx, item, container.StartOptions{}) - case constant.ContainerOpStop: - err = client.ContainerStop(ctx, item, container.StopOptions{}) - case constant.ContainerOpRestart: - err = client.ContainerRestart(ctx, item, container.StopOptions{}) - case constant.ContainerOpKill: - err = client.ContainerKill(ctx, item, "SIGKILL") - case constant.ContainerOpPause: - err = client.ContainerPause(ctx, item) - case constant.ContainerOpUnpause: - err = client.ContainerUnpause(ctx, item) - case constant.ContainerOpRemove: - err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) - } + taskItem.AddSubTask(item, func(t *task.Task) error { + switch req.Operation { + case constant.ContainerOpStart: + err = client.ContainerStart(ctx, item, container.StartOptions{}) + case constant.ContainerOpStop: + err = client.ContainerStop(ctx, item, container.StopOptions{}) + case constant.ContainerOpRestart: + err = client.ContainerRestart(ctx, item, container.StopOptions{}) + case constant.ContainerOpKill: + err = client.ContainerKill(ctx, item, "SIGKILL") + case constant.ContainerOpPause: + err = client.ContainerPause(ctx, item) + case constant.ContainerOpUnpause: + err = client.ContainerUnpause(ctx, item) + case constant.ContainerOpRemove: + err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) + } + return err + }, nil) } + + go func() { + _ = taskItem.Execute() + }() return err } diff --git a/agent/app/service/image.go b/agent/app/service/image.go index f3f88f5475f2..e8b31e02d76f 100644 --- a/agent/app/service/image.go +++ b/agent/app/service/image.go @@ -308,51 +308,72 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error { } func (u *ImageService) ImageLoad(req dto.ImageLoad) error { - file, err := os.Open(req.Path) + taskItem, err := task.NewTaskWithOps(req.Path, task.TaskImport, task.TaskScopeImage, req.TaskID, 1) if err != nil { - return err - } - defer file.Close() - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - res, err := client.ImageLoad(context.TODO(), file) - if err != nil { - return err - } - defer res.Body.Close() - content, err := io.ReadAll(res.Body) - if err != nil { - return err - } - if strings.Contains(string(content), "Error") { - return errors.New(string(content)) + return fmt.Errorf("new task for image import failed, err: %v", err) } + taskItem.AddSubTask(i18n.GetWithName("TaskImport", req.Path), func(t *task.Task) error { + file, err := os.Open(req.Path) + if err != nil { + return err + } + defer file.Close() + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + res, err := client.ImageLoad(context.TODO(), file) + if err != nil { + return err + } + defer res.Body.Close() + content, err := io.ReadAll(res.Body) + if err != nil { + return err + } + if strings.Contains(string(content), "Error") { + return errors.New(string(content)) + } + return nil + }, nil) + go func() { + _ = taskItem.Execute() + }() return nil } func (u *ImageService) ImageSave(req dto.ImageSave) error { - client, err := docker.NewDockerClient() + taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1) if err != nil { - return err + return fmt.Errorf("new task for image export failed, err: %v", err) } - defer client.Close() + taskItem.AddSubTask(i18n.GetWithName("TaskExport", req.Name), func(t *task.Task) error { + taskItem.Log(req.TagName) + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() - out, err := client.ImageSave(context.TODO(), []string{req.TagName}) - if err != nil { - return err - } - defer out.Close() - file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, constant.FilePerm) - if err != nil { - return err - } - defer file.Close() - if _, err = io.Copy(file, out); err != nil { - return err - } + out, err := client.ImageSave(context.TODO(), []string{req.TagName}) + if err != nil { + return err + } + defer out.Close() + file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, constant.FilePerm) + if err != nil { + return err + } + defer file.Close() + if _, err = io.Copy(file, out); err != nil { + return err + } + return nil + }, nil) + go func() { + _ = taskItem.Execute() + }() return nil } diff --git a/agent/app/task/task.go b/agent/app/task/task.go index 7584bd0821b7..d3431e5abf7f 100644 --- a/agent/app/task/task.go +++ b/agent/app/task/task.go @@ -65,6 +65,8 @@ const ( TaskSync = "TaskSync" TaskBuild = "TaskBuild" TaskPull = "TaskPull" + TaskImport = "TaskImport" + TaskExport = "TaskExport" TaskCommit = "TaskCommit" TaskPush = "TaskPush" TaskClean = "TaskClean" diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 755f773a4e0c..68b44c53aa04 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -146,6 +146,11 @@ ErrRuntimeNoPort: "The runtime environment is not set with a port, please edit t Status: 'Status' start: 'Start' stop: 'Stop' +restart: 'Restart' +kill: 'Force Stop' +pause: 'Pause' +unpause: 'Resume' +remove: 'Delete' delete: 'Delete' ErrDefaultWebsite: 'Default website has been set, please cancel it before setting!' @@ -343,6 +348,8 @@ TaskBuild: 'Build' TaskPush: 'Push' TaskClean: "Cleanup" TaskHandle: 'Execute' +TaskImport: "Import" +TaskExport: "Export" Website: 'Website' App: 'Application' Runtime: 'Runtime environment' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index f578f6e322c8..7a2bf5e68e30 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -145,6 +145,11 @@ ErrRuntimeNoPort: "El entorno de ejecución no tiene configurado un puerto, edí Status: 'Estado' start: 'Iniciar' stop: 'Detener' +restart: 'Reiniciar' +kill: 'Detener Forzosamente' +pause: 'Pausar' +unpause: 'Reanudar' +remove: 'Eliminar' delete: 'Eliminar' ErrDefaultWebsite: 'El sitio web predeterminado ya está configurado, ¡cancélelo antes de configurar!' @@ -341,6 +346,8 @@ TaskCommit: 'Commit' TaskBuild: 'Construir' TaskPush: 'Subir' TaskHandle: 'Ejecutar' +TaskImport: "Importar" +TaskExport: "Exportar" Website: 'Sitio web' App: 'Aplicación' Runtime: 'Entorno de ejecución' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 5114437ef0cf..0318e500ca2c 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -145,6 +145,11 @@ ErrRuntimeNoPort: "ランタイム環境にポートが設定されていませ Status: 'ステータス' start: '開始' stop: '停止' +restart: '再起動' +kill: '強制停止' +pause: '一時停止' +unpause: '再開' +remove: '削除' delete: '削除' ErrDefaultWebsite: 'デフォルト Web サイトが既に設定されています。設定する前にキャンセルしてください!' @@ -342,6 +347,8 @@ TaskBuild: 'ビルド' TaskPush: 'プッシュ' TaskClean: "クリーンアップ" TaskHandle: '実行' +TaskImport: "インポート" +TaskExport: "エクスポート" Website: 'ウェブサイト' App: 'アプリケーション' Runtime: 'ランタイム環境' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 0585ea1d2ae3..af54651d7548 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -146,6 +146,11 @@ ErrRuntimeNoPort: "런타임 환경에 포트가 설정되지 않았습니다. Status: '상태' start: '시작' stop: '중지' +restart: '재시작' +kill: '강제 중지' +pause: '일시 정지' +unpause: '재개' +remove: '삭제' delete: '삭제' ErrDefaultWebsite: '기본 웹사이트가 이미 설정되었습니다. 설정하기 전에 취소하세요!' @@ -343,6 +348,8 @@ TaskBuild: '빌드' TaskPush: '푸시' TaskClean: "정리" TaskHandle: '실행' +TaskImport: "가져오기" +TaskExport: "내보내기" Website: '웹사이트' App: '애플리케이션' Runtime: '런타임 환경' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index f1e53f407fc4..dad5705912a2 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -149,6 +149,11 @@ ErrRuntimeNoPort: "Persekitaran runtime tidak diset dengan port, sila edit perse Status: 'Status' start: 'Mulakan' stop: 'Berhenti' +restart: 'Mulakan Semula' +kill: 'Hentikan Paksa' +pause: 'Jeda' +unpause: 'Sambung Semula' +remove: 'Padam' delete: 'Padam' ErrDefaultWebsite: 'Laman web lalai telah ditetapkan, sila batalkan sebelum menetapkan!' @@ -343,6 +348,8 @@ TaskBuild: 'Bina' TaskPush: 'Tolak' TaskClean: "Pembersihan" TaskHandle: 'Laksanakan' +TaskImport: "Import" +TaskExport: "Eksport" Website: 'Laman web' App: 'Aplikasi' Runtime: 'Persekitaran masa jalan' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 8c4dc918f117..624e6cb7522e 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -149,6 +149,11 @@ ErrRuntimeNoPort: "O ambiente de tempo de execução não está configurado com Status: 'Status' start: 'Iniciar' stop: 'Parar' +restart: 'Reiniciar' +kill: 'Parar Forçadamente' +pause: 'Pausar' +unpause: 'Retomar' +remove: 'Excluir' delete: 'Excluir' ErrDefaultWebsite: 'O site padrão já foi definido, cancele-o antes de definir!' @@ -343,6 +348,8 @@ TaskBuild: 'Construir' TaskPush: 'Empurrar' TaskClean: "Limpeza" TaskHandle: 'Executar' +TaskImport: "Importar" +TaskExport: "Exportar" Website: 'Site' App: 'Aplicativo' Runtime: 'Ambiente de tempo de execução' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 9bc621867e93..c602bdc12b57 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -149,6 +149,11 @@ ErrRuntimeNoPort: "Среда выполнения не настроена с п Status: 'Статус' start: 'Запустить' stop: 'Остановить' +restart: 'Перезапустить' +kill: 'Принудительно Остановить' +pause: 'Приостановить' +unpause: 'Возобновить' +remove: 'Удалить' delete: 'Удалить' ErrDefaultWebsite: 'Веб-сайт по умолчанию уже установлен, отмените его перед настройкой!' @@ -343,6 +348,8 @@ TaskCommit: 'Kоммит' TaskPush: 'Push' TaskClean: "Очистка" TaskHandle: 'Выполнить' +TaskImport: "Импорт" +TaskExport: "Экспорт" Website: 'Be6-сайт' App: 'Приложение' Runtime: 'Среда выполнения' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index b8caa03c21ac..9b3c78449d6d 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -150,7 +150,13 @@ ErrRuntimeNoPort: "Çalışma zamanı ortamı bir porta sahip değil, lütfen ö Status: 'Durum' start: 'Başlat' stop: 'Durdur' +restart: 'Yeniden Başlat' +kill: 'Zorla Durdur' +pause: 'Duraklat' +unpause: 'Devam Et' +remove: 'Sil' delete: 'Sil' +ErrDefaultWebsite: "Varsayılan bir web sitesi zaten ayarlanmış. Yeni bir tane ayarlamadan önce lütfen iptal edin!" #ssl ErrSSLCannotDelete: '{{ .name }} sertifikası bir web sitesi tarafından kullanılıyor ve silinemez' @@ -343,6 +349,8 @@ TaskBuild: 'Yapı' TaskPush: 'Gönder' TaskClean: "Temizleme" TaskHandle: 'Yürüt' +TaskImport: "İçe Aktar" +TaskExport: "Dışa Aktar" Website: 'Web Sitesi' App: 'Uygulama' Runtime: 'Çalışma ortamı' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 9e554a4ee486..7da3e890c703 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -145,6 +145,11 @@ ErrRuntimeNoPort: "執行環境未設定埠,請先編輯執行環境" Status: '狀態' start: '開啟' stop: '關閉' +restart: '重啟' +kill: '強制停止' +pause: '暫停' +unpause: '恢復' +remove: '刪除' delete: '刪除' ErrDefaultWebsite: '已經設置默認網站,請取消後再設置!' @@ -342,6 +347,8 @@ TaskBuild: '建置' TaskPush: '推送' TaskClean: "清理" TaskHandle: '執行' +TaskImport: "導入" +TaskExport: "導出" Website: '網站' App: '應用程式' Runtime: '運作環境' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 9dbb00b0b1b3..0aee05903b0e 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -145,6 +145,11 @@ ErrRuntimeNoPort: '运行环境未设置端口,请先编辑运行环境' Status: '状态' start: '开启' stop: '关闭' +restart: '重启' +kill: '强制停止' +pause: '暂停' +unpause: '恢复' +remove: '删除' delete: '删除' ErrDefaultWebsite: "已经设置默认网站,请取消后再设置!" @@ -343,6 +348,8 @@ TaskBuild: "构建" TaskPush: "推送" TaskClean: "清理" TaskHandle: "执行" +TaskImport: "导入" +TaskExport: "导出" Website: "网站" App: "应用" Runtime: "运行环境" diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index 64fa49ce2ac0..461902dd31e8 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -2,6 +2,7 @@ import { ReqPage } from '.'; export namespace Container { export interface ContainerOperate { + taskID: string; names: Array; operation: string; } @@ -196,9 +197,11 @@ export namespace Container { tagName: string; } export interface ImageLoad { + taskID: string; path: string; } export interface ImageSave { + taskID: string; tagName: string; path: string; name: string; diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index 7b574d11c808..04b2a15e227c 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -335,7 +335,7 @@ - + @@ -348,6 +348,7 @@ + @@ -360,6 +361,7 @@ import MonitorDialog from '@/views/container/container/monitor/index.vue'; import TerminalDialog from '@/views/container/container/terminal/index.vue'; import ContainerInspectDialog from '@/views/container/container/inspect/index.vue'; import PortJumpDialog from '@/components/port-jump/index.vue'; +import TaskLog from '@/components/log/task/index.vue'; import DockerStatus from '@/views/container/docker-status/index.vue'; import ContainerLogDialog from '@/components/log/container-drawer/index.vue'; import Status from '@/components/status/index.vue'; @@ -373,10 +375,11 @@ import { } from '@/api/modules/container'; import { Container } from '@/api/interface/container'; import i18n from '@/lang'; -import { MsgWarning } from '@/utils/message'; +import { MsgSuccess, MsgWarning } from '@/utils/message'; import { GlobalStore } from '@/store'; import { routerToName, routerToNameWithQuery } from '@/utils/router'; import router from '@/routers'; +import { newUUID } from '@/utils/util'; const globalStore = GlobalStore(); const mobile = computed(() => { @@ -405,6 +408,10 @@ const opRef = ref(); const includeAppStore = ref(true); const columns = ref([]); +const batchNames = ref(); +const batchOp = ref(); +const taskLogRef = ref(); + const tags = ref([]); const activeTag = ref('all'); @@ -658,9 +665,10 @@ const checkStatus = (operation: string, row: Container.ContainerInfo | null) => const onOperate = async (op: string, row: Container.ContainerInfo | null) => { let opList = row ? [row] : selects.value; let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + op)]); - let names = []; + batchNames.value = []; + batchOp.value = op; for (const item of opList) { - names.push(item.name); + batchNames.value.push(item.name); if (item.isFromApp) { msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + op)]); } @@ -668,14 +676,33 @@ const onOperate = async (op: string, row: Container.ContainerInfo | null) => { const successMsg = `${i18n.global.t('container.' + op)}${i18n.global.t('commons.status.success')}`; opRef.value.acceptParams({ title: i18n.global.t('container.' + op), - names: names, + names: batchNames.value, msg: msg, - api: containerOperator, - params: { names: names, operation: op }, + api: null, + params: null, successMsg, }); }; +const onSubmitOperate = async () => { + loading.value = true; + let taskID = newUUID(); + await containerOperator({ names: batchNames.value, operation: batchOp.value, taskID: taskID }) + .then(() => { + loading.value = false; + search(); + openTaskLog(taskID); + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + }) + .catch(() => { + loading.value = false; + }); +}; + +const openTaskLog = (taskID: string) => { + taskLogRef.value.openWithTaskID(taskID); +}; + const buttons = [ { label: i18n.global.t('menu.terminal'), diff --git a/frontend/src/views/container/image/load/index.vue b/frontend/src/views/container/image/load/index.vue index c3ab872035fb..2e249caeb2d4 100644 --- a/frontend/src/views/container/image/load/index.vue +++ b/frontend/src/views/container/image/load/index.vue @@ -21,23 +21,28 @@ +