diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index f0c035cc5..75566736b 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -15,8 +15,9 @@ type PageContainer struct { } type InspectReq struct { - ID string `json:"id" validate:"required"` - Type string `json:"type" validate:"required"` + ID string `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` + Detail string `json:"detail"` } type ContainerInfo struct { @@ -284,17 +285,19 @@ type ComposeOperation struct { Path string `json:"path"` Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"` WithFile bool `json:"withFile"` - Force bool `josn:"force"` + Force bool `json:"force"` } type ComposeUpdate struct { - Name string `json:"name" validate:"required"` - Path string `json:"path" validate:"required"` - Content string `json:"content" validate:"required"` - Env string `json:"env"` + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + DetailPath string `json:"detailPath"` + Content string `json:"content" validate:"required"` + Env string `json:"env"` } type ComposeLogClean struct { - Name string `json:"name" validate:"required"` - Path string `json:"path" validate:"required"` + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + DetailPath string `json:"detailPath"` } type ContainerLog struct { diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 50b2b6b6a..d2b05defe 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -19,12 +19,8 @@ import ( "syscall" "time" - "github.com/1Panel-dev/1Panel/agent/app/repo" - "github.com/gin-gonic/gin" - - "github.com/pkg/errors" - "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" @@ -45,7 +41,9 @@ import ( "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" + "github.com/gin-gonic/gin" v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/mem" ) @@ -362,6 +360,9 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { } for _, container := range containers { config := container.Labels[composeConfigLabel] + if len(req.Detail) != 0 && strings.Contains(config, req.Detail) { + config = req.Detail + } workdir := container.Labels[composeWorkdirLabel] if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { filePath = config @@ -970,13 +971,20 @@ func collectLogs(done <-chan struct{}, params dto.StreamLog, messageChan chan<- var dockerCmd *exec.Cmd if params.Type == "compose" { dockerComposCmd := common.GetDockerComposeCommand() + var yamlFiles []string + for _, item := range strings.Split(params.Compose, ",") { + if len(item) != 0 { + yamlFiles = append(yamlFiles, "-f", item) + } + } if dockerComposCmd == "docker-compose" { - newCmdArgs := append([]string{"-f", params.Compose}, cmdArgs...) + newCmdArgs := append(yamlFiles, cmdArgs...) dockerCmd = exec.Command(dockerComposCmd, newCmdArgs...) } else { - newCmdArgs := append([]string{"compose", "-f", params.Compose}, cmdArgs...) + newCmdArgs := append(append([]string{"compose"}, yamlFiles...), cmdArgs...) dockerCmd = exec.Command("docker", newCmdArgs...) } + global.LOG.Debug("Docker command:", dockerCmd.Args) } else { dockerCmd = exec.Command("docker", cmdArgs...) } diff --git a/agent/app/service/container_compose.go b/agent/app/service/container_compose.go index e8319e91f..7f80c287f 100644 --- a/agent/app/service/container_compose.go +++ b/agent/app/service/container_compose.go @@ -2,6 +2,7 @@ package service import ( "bufio" + "context" "errors" "fmt" "os" @@ -24,7 +25,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" - "golang.org/x/net/context" ) const composeProjectLabel = "com.docker.compose.project" @@ -233,14 +233,15 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { return err } if req.WithFile { - _ = os.RemoveAll(path.Dir(req.Path)) + for _, item := range strings.Split(req.Path, ",") { + if len(item) != 0 { + _ = os.RemoveAll(path.Dir(item)) + } + } } _ = composeRepo.DeleteRecord(repo.WithByName(req.Name)) return nil } - if _, err := os.Stat(req.Path); err != nil { - return fmt.Errorf("load file with path %s failed, %v", req.Path, err) - } if req.Operation == "up" { if stdout, err := compose.Up(req.Path); err != nil { return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err) @@ -257,11 +258,11 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { if cmd.CheckIllegal(req.Name, req.Path) { return buserr.New("ErrCmdIllegal") } - oldFile, err := os.ReadFile(req.Path) + oldFile, err := os.ReadFile(req.DetailPath) if err != nil { - return fmt.Errorf("load file with path %s failed, %v", req.Path, err) + return fmt.Errorf("load file with path %s failed, %v", req.DetailPath, err) } - file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640) + file, err := os.OpenFile(req.DetailPath, os.O_WRONLY|os.O_TRUNC, 0640) if err != nil { return err } @@ -270,8 +271,8 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { _, _ = write.WriteString(req.Content) write.Flush() - global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path) - if err := newComposeEnv(req.Path, req.Env); err != nil { + global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath) + if err := newComposeEnv(req.DetailPath, req.Env); err != nil { return err } @@ -361,11 +362,8 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error { } func removeContainerForCompose(composeName, composePath string) error { - if _, err := os.Stat(composePath); err == nil { - if stdout, err := compose.Operate(composePath, "down"); err != nil { - return errors.New(stdout) - } - return nil + if stdout, err := compose.Operate(composePath, "down"); err != nil { + return errors.New(stdout) } var options container.ListOptions options.All = true @@ -405,7 +403,11 @@ func recreateCompose(content, path string) error { func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo { for i := 0; i < len(list); i++ { - envFilePath := path.Join(path.Dir(list[i].Path), ".env") + tmpPath := list[i].Path + if strings.Contains(list[i].Path, ",") { + tmpPath = strings.Split(list[i].Path, ",")[0] + } + envFilePath := path.Join(path.Dir(tmpPath), ".env") file, err := os.ReadFile(envFilePath) if err != nil { continue diff --git a/agent/utils/compose/compose.go b/agent/utils/compose/compose.go index e0dfc6af2..cb3bf9c73 100644 --- a/agent/utils/compose/compose.go +++ b/agent/utils/compose/compose.go @@ -33,8 +33,7 @@ func Up(filePath string) (string, error) { if err := checkCmd(); err != nil { return "", err } - stdout, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(global.CONF.DockerConfig.Command+" -f %s up -d", 20*time.Minute, filePath) - return stdout, err + return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdoutBashCf("%s %s up -d", global.CONF.DockerConfig.Command, loadFiles(filePath)) } func UpWithTask(filePath string, task *task.Task) error { @@ -81,54 +80,56 @@ func UpWithTask(filePath string, task *task.Task) error { } } - dockerCommand := global.CONF.DockerConfig.Command - if dockerCommand == "docker-compose" { - return cmd.NewCommandMgr(cmd.WithTask(*task)).Run("docker-compose", "-f", filePath, "up", "-d") - } else { - return cmd.NewCommandMgr(cmd.WithTask(*task)).Run("docker", "compose", "-f", filePath, "up", "-d") - } + return cmd.NewCommandMgr(cmd.WithTask(*task)).Run("%s %s up -d", global.CONF.DockerConfig.Command, loadFiles(filePath)) } func Down(filePath string) (string, error) { if err := checkCmd(); err != nil { return "", err } - stdout, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(global.CONF.DockerConfig.Command+" -f %s down --remove-orphans", 20*time.Minute, filePath) - return stdout, err + return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdoutBashCf("%s %s down --remove-orphans", global.CONF.DockerConfig.Command, loadFiles(filePath)) } func Stop(filePath string) (string, error) { if err := checkCmd(); err != nil { return "", err } - stdout, err := cmd.RunDefaultWithStdoutBashCf(global.CONF.DockerConfig.Command+" -f %s stop", filePath) - return stdout, err + return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdoutBashCf("%s %s stop", global.CONF.DockerConfig.Command, loadFiles(filePath)) } func Restart(filePath string) (string, error) { if err := checkCmd(); err != nil { return "", err } - stdout, err := cmd.RunDefaultWithStdoutBashCf(global.CONF.DockerConfig.Command+" -f %s restart", filePath) - return stdout, err + return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdoutBashCf("%s %s restart", global.CONF.DockerConfig.Command, loadFiles(filePath)) } func Operate(filePath, operation string) (string, error) { if err := checkCmd(); err != nil { return "", err } - stdout, err := cmd.RunDefaultWithStdoutBashCf(global.CONF.DockerConfig.Command+" -f %s %s", filePath, operation) - return stdout, err + return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdoutBashCf("%s %s %s", global.CONF.DockerConfig.Command, loadFiles(filePath), operation) } func DownAndUp(filePath string) (string, error) { if err := checkCmd(); err != nil { return "", err } - stdout, err := cmd.RunDefaultWithStdoutBashCf(global.CONF.DockerConfig.Command+" -f %s down", filePath) + cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Minute)) + stdout, err := cmdMgr.RunWithStdoutBashCf("%s %s down", global.CONF.DockerConfig.Command, loadFiles(filePath)) if err != nil { return stdout, err } - stdout, err = cmd.RunDefaultWithStdoutBashCf(global.CONF.DockerConfig.Command+" -f %s up -d", filePath) + stdout, err = cmdMgr.RunWithStdoutBashCf("%s %s up -d", global.CONF.DockerConfig.Command, loadFiles(filePath)) return stdout, err } + +func loadFiles(filePath string) string { + var fileItem []string + for _, item := range strings.Split(filePath, ",") { + if len(item) != 0 { + fileItem = append(fileItem, fmt.Sprintf("-f %s", item)) + } + } + return strings.Join(fileItem, " ") +} diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index d83027d6d..b440d1fb0 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -173,6 +173,7 @@ export namespace Container { export interface ContainerInspect { id: string; type: string; + detail: string; } export interface ContainerPrune { pruneType: string; diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 76882cb38..d8ebe931b 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -190,7 +190,7 @@ export const upCompose = (params: Container.ComposeCreate) => { export const testCompose = (params: Container.ComposeCreate) => { return http.post(`/containers/compose/test`, params); }; -export const composeOperator = (params: Container.ComposeOperation) => { +export const composeOperate = (params: Container.ComposeOperation) => { return http.post(`/containers/compose/operate`, params); }; export const composeUpdate = (params: Container.ComposeUpdate) => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 47fc28920..50725f5d5 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1017,6 +1017,7 @@ const message = { 'If multiple private repositories exist, newlines must be displayed, for example:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: 'Compose | Composes', + composeFile: 'Compose File', fromChangeHelper: 'Switching the source will clean the current edited content. Do you want to continue?', composePathHelper: 'Configuration file save path: {0}', composeHelper: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 371856b0b..54739ccd6 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -1021,7 +1021,9 @@ const message = { 'Si hay varios mirrors, deben estar en líneas separadas. Ejemplo:\nhttp://xxxxxx.m.daocloud.io\nhttps://xxxxxx.mirror.aliyuncs.com', registrieHelper: 'Si existen varios repositorios privados, deben estar en líneas separadas. Ejemplo:\n172.16.10.111:8081\n172.16.10.112:8081', + compose: 'Compose | Composes', + composeFile: 'Archivo de Orquestación', fromChangeHelper: 'Cambiar la fuente limpiará el contenido actualmente editado. ¿Desea continuar?', composePathHelper: 'Ruta de guardado del archivo de configuración: {0}', composeHelper: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 3a27f53ce..d74c1d4f6 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -995,6 +995,7 @@ const message = { registrieHelper: '複数のプライベートリポジトリが存在する場合、たとえばnewlinesを表示する必要があります。', compose: '構成|作曲', + composeFile: 'オーケストレーションファイル', fromChangeHelper: 'ソースを切り替えると、現在の編集されたコンテンツがきれいになります。続けたいですか?', composePathHelper: '構成ファイル保存パス:{0}', composeHelper: diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index c17f0a7e5..bcd4fa850 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -983,6 +983,7 @@ const message = { '개인 레지스트리가 여러 개 있을 경우 각 줄에 하나씩 표시해야 합니다. 예시:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: '컴포즈 | 컴포즈들', + composeFile: '컴포즈 파일', fromChangeHelper: '소스를 변경하면 현재 편집한 내용이 삭제됩니다. 계속 하시겠습니까?', composePathHelper: '구성 파일 저장 경로: {0}', composeHelper: '1Panel 에디터나 템플릿을 통해 생성된 컴포지션은 {0}/docker/compose 디렉토리에 저장됩니다.', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 6e7ec8d76..18808811f 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1012,6 +1012,7 @@ const message = { 'Jika terdapat banyak repositori persendirian, baris baru mesti dipaparkan, contohnya:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: 'Compose | Compose-compose', + composeFile: 'Fail Susunan', fromChangeHelper: 'Menukar sumber akan membersihkan kandungan yang sedang diedit. Adakah anda mahu meneruskan?', composePathHelper: 'Laluan simpan fail konfigurasi: {0}', composeHelper: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1e4674ba4..3ce83f409 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1008,6 +1008,7 @@ const message = { 'Se houver múltiplos repositórios privados, eles devem ser exibidos em novas linhas, por exemplo:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: 'Compose | Composições', + composeFile: 'Arquivo de Orquestração', fromChangeHelper: 'Trocar a origem limpará o conteúdo editado atual. Deseja continuar?', composePathHelper: 'Caminho de salvamento do arquivo de configuração: {0}', composeHelper: diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 52b9f689c..6aebe09a8 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1007,6 +1007,7 @@ const message = { 'Если существует несколько частных репозиториев, они должны быть разделены новой строкой, например:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: 'Compose | Composes', + composeFile: 'Файл Оркестрации', fromChangeHelper: 'Переключение источника очистит текущее отредактированное содержимое. Хотите продолжить?', composePathHelper: 'Путь сохранения файла конфигурации: {0}', composeHelper: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 9f01000c5..b7fcdd0ea 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -1029,6 +1029,7 @@ const message = { 'Birden fazla özel depo varsa, yeni satırlar gösterilmelidir, örneğin:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: 'Compose | Composelar', + composeFile: 'Düzenleme Dosyası', fromChangeHelper: 'Kaynağın değiştirilmesi mevcut düzenlenen içeriği temizleyecektir. Devam etmek istiyor musunuz?', composePathHelper: 'Yapılandırma dosyası kaydetme yolu: {0}', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 7327d8a1e..904f839ce 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -970,6 +970,7 @@ const message = { registrieHelper: '當存在多個私有倉庫時,需要換行顯示,例:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: '編排', + composeFile: '編排檔案', fromChangeHelper: '切換來源將清空檔前已編輯內容,是否繼續?', composePathHelper: '設定檔儲存路徑: {0}', composeHelper: '通過 1Panel 編輯或者模版建立的編排,將儲存在 {0}/docker/compose 路徑下', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 084c8af6b..d9e9d16c3 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -972,6 +972,7 @@ const message = { registrieHelper: '当存在多个私有仓库时,需要换行显示,例:\n172.16.10.111:8081 \n172.16.10.112:8081', compose: '编排', + composeFile: '编排文件', fromChangeHelper: '切换来源将清空当前已编辑内容,是否继续?', composePathHelper: '配置文件保存路径: {0}', composeHelper: '通过 1Panel 编辑或者模版创建的编排,将保存在 {0}/docker/compose 路径下', diff --git a/frontend/src/views/container/compose/delete/index.vue b/frontend/src/views/container/compose/delete/index.vue index 25a8c9c81..a90547efd 100644 --- a/frontend/src/views/container/compose/delete/index.vue +++ b/frontend/src/views/container/compose/delete/index.vue @@ -39,7 +39,7 @@ import { FormInstance } from 'element-plus'; import { ref } from 'vue'; import i18n from '@/lang'; import { MsgSuccess } from '@/utils/message'; -import { composeOperator } from '@/api/modules/container'; +import { composeOperate } from '@/api/modules/container'; let open = ref(false); let loading = ref(false); @@ -76,7 +76,7 @@ const submit = async () => { withFile: deleteFile.value, force: force.value, }; - await composeOperator(params) + await composeOperate(params) .then(() => { loading.value = false; emit('search'); diff --git a/frontend/src/views/container/compose/index.vue b/frontend/src/views/container/compose/index.vue index f010f4c8d..ac915a22f 100644 --- a/frontend/src/views/container/compose/index.vue +++ b/frontend/src/views/container/compose/index.vue @@ -221,10 +221,24 @@ - + {{ $t('container.compose') }} {{ $t('commons.button.log') }} + + + +
([]); const loading = ref(false); const detailLoading = ref(false); const currentCompose = ref(null); +const currentYamlPath = ref(''); const composeContainers = ref([]); const composeContent = ref(''); @@ -410,7 +425,7 @@ const form = reactive({ path: '', file: '', template: null as number, - env: [], + env: '', }); const rules = reactive({ name: [Rules.requiredInput, Rules.composeName], @@ -491,9 +506,14 @@ const loadDetail = async (row: Container.ComposeInfo, withRefresh: boolean) => { isOnCreate.value = false; detailLoading.value = true; currentCompose.value = row; + currentYamlPath.value = row.path.indexOf(',') !== -1 ? row.path.split(',')[0] : row.path; env.value = row.env || ''; composeContainers.value = row.containers || []; - await inspect({ id: currentCompose.value.name, type: 'compose' }) + inspectCompose(row.name, currentYamlPath.value); +}; + +const inspectCompose = async (name: string, detailPath: string) => { + await inspect({ id: name, type: 'compose', detail: detailPath }) .then((res) => { composeContent.value = res.data; detailLoading.value = false; @@ -521,7 +541,7 @@ const onOpenDialog = async () => { form.path = ''; form.file = ''; form.template = null; - form.env = []; + form.env = ''; loadPath(); loadTemplates(); }; @@ -613,7 +633,7 @@ const handleComposeOperate = async (operation: 'up' | 'stop' | 'restart', row: a withFile: false, force: false, }; - await composeOperator(params) + await composeOperate(params) .then(async () => { MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); await search(); @@ -642,6 +662,7 @@ const onSubmitEdit = async () => { const param = { name: currentCompose.value.name, path: currentCompose.value.path, + detailPath: currentYamlPath.value, content: composeContent.value, createdBy: currentCompose.value.createdBy, env: env.value || '', @@ -700,7 +721,7 @@ const onInspectContainer = async (item: any) => { if (!item.containerID) { return; } - const res = await inspect({ id: item.containerID, type: 'container' }); + const res = await inspect({ id: item.containerID, type: 'container', detail: '' }); containerInspectRef.value!.acceptParams({ data: res.data, ports: item.ports || [] }); }; const onOpenTerminal = (row: any) => {