feat: Support display and operations for multi-YAML compose startup (#11421)

Refs #11405
This commit is contained in:
ssongliu 2025-12-22 13:57:30 +08:00 committed by GitHub
parent 74f29e3dc3
commit 5674673c5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 107 additions and 60 deletions

View file

@ -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 {

View file

@ -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...)
}

View file

@ -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

View file

@ -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, " ")
}

View file

@ -173,6 +173,7 @@ export namespace Container {
export interface ContainerInspect {
id: string;
type: string;
detail: string;
}
export interface ContainerPrune {
pruneType: string;

View file

@ -190,7 +190,7 @@ export const upCompose = (params: Container.ComposeCreate) => {
export const testCompose = (params: Container.ComposeCreate) => {
return http.post<boolean>(`/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) => {

View file

@ -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:

View file

@ -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:

View file

@ -995,6 +995,7 @@ const message = {
registrieHelper: '複数のプライベートリポジトリが存在する場合たとえばnewlinesを表示する必要があります',
compose: '構成|作曲',
composeFile: 'オーケストレーションファイル',
fromChangeHelper: 'ソースを切り替えると現在の編集されたコンテンツがきれいになります続けたいですか',
composePathHelper: '構成ファイル保存パス:{0}',
composeHelper:

View file

@ -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 디렉토리에 저장됩니다.',

View file

@ -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:

View file

@ -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:

View file

@ -1007,6 +1007,7 @@ const message = {
'Если существует несколько частных репозиториев, они должны быть разделены новой строкой, например:\n172.16.10.111:8081 \n172.16.10.112:8081',
compose: 'Compose | Composes',
composeFile: 'Файл Оркестрации',
fromChangeHelper: 'Переключение источника очистит текущее отредактированное содержимое. Хотите продолжить?',
composePathHelper: 'Путь сохранения файла конфигурации: {0}',
composeHelper:

View file

@ -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}',

View file

@ -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 路徑下',

View file

@ -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 路径下',

View file

@ -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');

View file

@ -221,10 +221,24 @@
</el-table-column>
</el-table>
<el-radio-group size="small" class="mt-1 mb-1" v-model="showType">
<el-radio-group class="mt-1 mb-1" v-model="showType">
<el-radio-button value="compose">{{ $t('container.compose') }}</el-radio-button>
<el-radio-button value="log">{{ $t('commons.button.log') }}</el-radio-button>
</el-radio-group>
<el-select
class="p-w-300 mt-2 ml-2"
v-model="currentYamlPath"
@change="inspectCompose(currentCompose.name, currentYamlPath)"
v-if="currentCompose.path.indexOf(',') !== -1"
>
<template #prefix>{{ $t('container.composeFile') }}</template>
<el-option
v-for="item in currentCompose.path.split(',')"
:key="item"
:value="item"
:label="item.split('/').pop()"
/>
</el-select>
<div v-show="showType === 'compose'">
<CodemirrorPro
v-model="composeContent"
@ -357,7 +371,7 @@ import TerminalDialog from '@/views/container/container/terminal/index.vue';
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
import DeleteDialog from '@/views/container/compose/delete/index.vue';
import {
composeOperator,
composeOperate,
composeUpdate,
containerItemStats,
containerListStats,
@ -381,6 +395,7 @@ const data = ref<any[]>([]);
const loading = ref(false);
const detailLoading = ref(false);
const currentCompose = ref<Container.ComposeInfo | null>(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) => {