fix: align compose project name handling (#13500)

This commit is contained in:
ssongliu
2026-08-07 12:44:29 +08:00
committed by GitHub
parent b0d561e33b
commit e7ef35740c
16 changed files with 168 additions and 58 deletions
+6
View File
@@ -49,6 +49,12 @@ func WithByName(name string) DBOption {
}
}
func WithByPath(path string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("path = ?", path)
}
}
func WithByAddr(addr string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("addr = ?", addr)
+127 -42
View File
@@ -218,25 +218,14 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if err := validateComposeCreateName(req); err != nil {
return false, err
}
if cmd.CheckIllegal(req.Name, req.DirName, req.Path) {
if hasIllegalComposeCreateInput(req) {
return false, buserr.New("ErrCmdIllegal")
}
if req.From != "path" {
if err := checkComposeRecordName(composeCreateDirName(req)); err != nil {
return false, err
}
}
if err := u.loadPath(&req); err != nil {
return false, err
}
if err := newComposeEnv(req.Path, req.Env); err != nil {
return false, err
}
projectName, err := resolveComposeProjectName(req.Path, req.Name)
projectName, err := resolveComposeCreateProjectName(req)
if err != nil {
return false, err
}
if err := checkComposeRecordName(projectName); err != nil {
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
return false, err
}
return true, nil
@@ -246,13 +235,15 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
if err := validateComposeCreateName(req); err != nil {
return err
}
if cmd.CheckIllegal(req.Name, req.DirName, req.Path) {
if hasIllegalComposeCreateInput(req) {
return buserr.New("ErrCmdIllegal")
}
if req.From != "path" {
if err := checkComposeRecordName(composeCreateDirName(req)); err != nil {
return err
}
projectName, err := resolveComposeCreateProjectName(req)
if err != nil {
return err
}
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
return err
}
if err := u.loadPath(&req); err != nil {
return err
@@ -260,14 +251,7 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
if err := newComposeEnv(req.Path, req.Env); err != nil {
return err
}
projectName, err := resolveComposeProjectName(req.Path, req.Name)
if err != nil {
return err
}
req.Name = projectName
if err := checkComposeRecordName(req.Name); err != nil {
return err
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
if err != nil {
return fmt.Errorf("new task for image build failed, err: %v", err)
@@ -303,17 +287,41 @@ func checkComposeRecordName(name string) error {
return nil
}
func validateComposeCreateName(req dto.ComposeCreate) error {
name := strings.TrimSpace(req.Name)
if name != "" && !re.GetRegex(re.ComposeNamePattern).MatchString(name) {
return buserr.New("ErrComposeNameInvalid")
func checkComposeCreateDuplicate(req dto.ComposeCreate, projectName string) error {
if err := checkComposeRecordName(projectName); err != nil {
return err
}
if req.From != "path" && !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
if req.From == "path" {
return nil
}
composeItem, _ := composeRepo.GetRecord(repo.WithByPath(composeCreatePath(req)))
if composeItem.ID != 0 && composeItem.Path != "" {
return buserr.New("ErrRecordExist")
}
return nil
}
func validateComposeCreateName(req dto.ComposeCreate) error {
if req.From == "path" {
name := strings.TrimSpace(req.Name)
if name != "" && !re.GetRegex(re.ComposeNamePattern).MatchString(name) {
return buserr.New("ErrComposeNameInvalid")
}
return nil
}
if !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
return buserr.New("ErrComposeNameInvalid")
}
return nil
}
func hasIllegalComposeCreateInput(req dto.ComposeCreate) bool {
if req.From == "path" {
return cmd.CheckIllegal(req.Name, req.Path)
}
return cmd.CheckIllegal(composeCreateDirName(req))
}
func composeCreateDirName(req dto.ComposeCreate) string {
dirName := strings.TrimSpace(req.DirName)
if dirName == "" {
@@ -324,12 +332,89 @@ func composeCreateDirName(req dto.ComposeCreate) string {
return dirName
}
func resolveComposeProjectName(composePath, fallbackName string) (string, error) {
func composeCreatePath(req dto.ComposeCreate) string {
return filepath.Join(global.Dir.DataDir, "docker", "compose", composeCreateDirName(req), "docker-compose.yml")
}
func resolveComposeCreateProjectName(req dto.ComposeCreate) (string, error) {
if req.From == "path" {
envPath, err := createComposeTempFile(
filepath.Dir(primaryComposePath(req.Path)),
".1panel-compose-*.env",
req.Env,
)
if err != nil {
return "", err
}
defer os.Remove(envPath)
return resolveComposeProjectName(req.Path, req.Name, envPath)
}
dir := filepath.Dir(composeCreatePath(req))
cleanupDir, err := prepareComposeStagingDir(dir)
if err != nil {
return "", err
}
defer cleanupDir()
composePath, err := createComposeTempFile(dir, ".1panel-compose-*.yml", req.File)
if err != nil {
return "", err
}
defer os.Remove(composePath)
envPath, err := createComposeTempFile(dir, ".1panel-compose-*.env", req.Env)
if err != nil {
return "", err
}
defer os.Remove(envPath)
return resolveComposeProjectName(composePath, "", envPath)
}
func createComposeTempFile(dir, pattern, content string) (string, error) {
file, err := os.CreateTemp(dir, pattern)
if err != nil {
return "", err
}
filePath := file.Name()
if _, err := file.WriteString(content); err != nil {
_ = file.Close()
_ = os.Remove(filePath)
return "", err
}
if err := file.Close(); err != nil {
_ = os.Remove(filePath)
return "", err
}
return filePath, nil
}
func prepareComposeStagingDir(dir string) (func(), error) {
if err := os.MkdirAll(filepath.Dir(dir), os.ModePerm); err != nil {
return nil, err
}
created := false
if err := os.Mkdir(dir, os.ModePerm); err != nil {
if !errors.Is(err, os.ErrExist) {
return nil, err
}
} else {
created = true
}
return func() {
if created {
_ = os.Remove(dir)
}
}, nil
}
func resolveComposeProjectName(composePath, fallbackName, envFile string) (string, error) {
// Preserve the name resolved by Compose (including a top-level name) so the
// container label and the local record always use the same project identity.
parentName := normalizeComposeProjectName(path.Base(path.Dir(primaryComposePath(composePath))))
fallbackName = strings.TrimSpace(fallbackName)
stdout, err := runComposeConfig(composePath, "")
stdout, err := runComposeConfig(composePath, "", envFile)
if err == nil {
projectName, parseErr := loadComposeProjectName(stdout)
if parseErr != nil {
@@ -345,7 +430,7 @@ func resolveComposeProjectName(composePath, fallbackName string) (string, error)
return parentName, nil
}
if fallbackName != "" {
if _, fallbackErr := runComposeConfig(composePath, fallbackName); fallbackErr != nil {
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr != nil {
return "", fallbackErr
}
return fallbackName, nil
@@ -358,7 +443,7 @@ func resolveComposeProjectName(composePath, fallbackName string) (string, error)
resolveErr := err
if parentName != "" {
if _, parentErr := runComposeConfig(composePath, parentName); parentErr == nil {
if _, parentErr := runComposeConfig(composePath, parentName, envFile); parentErr == nil {
return parentName, nil
} else {
resolveErr = parentErr
@@ -366,7 +451,7 @@ func resolveComposeProjectName(composePath, fallbackName string) (string, error)
}
if fallbackName != "" && fallbackName != parentName {
if _, fallbackErr := runComposeConfig(composePath, fallbackName); fallbackErr == nil {
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr == nil {
return fallbackName, nil
} else {
return "", fallbackErr
@@ -378,8 +463,8 @@ func resolveComposeProjectName(composePath, fallbackName string) (string, error)
return "", resolveErr
}
func runComposeConfig(composePath, projectName string) ([]byte, error) {
configCmd := getComposeCmd(composePath, "config", projectName)
func runComposeConfig(composePath, projectName, envFile string) ([]byte, error) {
configCmd := getComposeCmdWithEnv(composePath, "config", envFile, projectName)
stdout, err := configCmd.Output()
if err != nil {
var stderr []byte
@@ -590,15 +675,15 @@ func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
if req.From == "template" || req.From == "edit" {
dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, composeCreateDirName(*req))
composePath := composeCreatePath(*req)
dir := filepath.Dir(composePath)
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
}
path := fmt.Sprintf("%s/docker-compose.yml", dir)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
if err != nil {
return err
}
@@ -606,7 +691,7 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
write := bufio.NewWriter(file)
_, _ = write.WriteString(string(req.File))
write.Flush()
req.Path = path
req.Path = composePath
}
return nil
}
+8 -1
View File
@@ -167,8 +167,15 @@ func reCreateRuntime(runtime *model.Runtime) {
}
func getComposeCmd(composePath, operate string, projectName ...string) *exec.Cmd {
return getComposeCmdWithEnv(composePath, operate, "", projectName...)
}
func getComposeCmdWithEnv(composePath, operate, envFile string, projectName ...string) *exec.Cmd {
dockerCommand := global.CONF.DockerConfig.Command
args := make([]string, 0, 7)
args := make([]string, 0, 9)
if envFile != "" {
args = append(args, "--env-file", envFile)
}
if len(projectName) > 0 && strings.TrimSpace(projectName[0]) != "" {
args = append(args, "--project-name", projectName[0])
}
+2 -1
View File
@@ -1745,7 +1745,8 @@ const message = {
compose: 'Compose | Composes',
composeFile: 'Compose File',
fromChangeHelper: 'Switching the source will clean the current edited content. Continue?',
composePathHelper: 'Configuration file save path: {0}',
composePathHelper:
'Compose file save path: {0}; if a project name is specified in Docker Compose, it takes precedence; otherwise, {1} is used.',
composeNamePriorityHelper:
'Project name priority: name specified in Docker Compose config > file parent directory > name entered here.',
composeHelper:
+2 -1
View File
@@ -1772,7 +1772,8 @@ const message = {
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}',
composePathHelper:
'Ruta de guardado del archivo Compose: {0}; si se especifica un nombre de proyecto en Docker Compose, tendrá prioridad; de lo contrario, se usará {1}.',
composeNamePriorityHelper:
'Prioridad del nombre del proyecto: nombre especificado en la configuración de Docker Compose > directorio padre del archivo > nombre indicado aquí.',
composeHelper:
+2 -1
View File
@@ -1732,7 +1732,8 @@ const message = {
compose: 'Compose | Compose‌ها',
composeFile: 'فایل Compose',
fromChangeHelper: 'تغییر منبع محتوای ویرایش شده فعلی را پاک می‌کند. ادامه می‌دهید؟',
composePathHelper: 'مسیر ذخیره فایل پیکربندی: {0}',
composePathHelper:
'مسیر ذخیره فایل Compose: {0}؛ اگر نام پروژه‌ای در Docker Compose مشخص شده باشد، در اولویت است؛ در غیر این صورت از {1} استفاده می‌شود.',
composeNamePriorityHelper:
'اولویت نام پروژه: نام مشخص‌شده در پیکربندی Docker Compose > پوشه والد فایل > نام واردشده در اینجا.',
composeHelper: 'ترکیب ایجاد شده از طریق ویرایشگر یا قالب 1Panel در دایرکتوری {0}/docker/compose ذخیره می‌شود.',
+2 -1
View File
@@ -1735,7 +1735,8 @@ const message = {
compose: '構成|作曲',
composeFile: 'オーケストレーションファイル',
fromChangeHelper: 'ソースを切り替えると現在の編集されたコンテンツがきれいになります続けたいですか',
composePathHelper: '構成ファイル保存パス:{0}',
composePathHelper:
'Compose ファイルの保存パス{0}Docker Compose でプロジェクト名が指定されている場合はその名前を優先し指定されていない場合は {1} を使用します',
composeNamePriorityHelper:
'プロジェクト名の優先順位Docker Compose 設定で指定された名前 > ファイルの親ディレクトリ名 > ここで入力した名前',
composeHelper:
+2 -1
View File
@@ -1716,7 +1716,8 @@ const message = {
compose: '컴포즈 | 컴포즈들',
composeFile: '컴포즈 파일',
fromChangeHelper: '소스를 변경하면 현재 편집한 내용이 삭제됩니다. 계속 하시겠습니까?',
composePathHelper: '구성 파일 저장 경로: {0}',
composePathHelper:
'Compose 파일 저장 경로: {0}. Docker Compose에 프로젝트 이름이 지정되어 있으면 해당 이름을 우선 사용하고, 지정되어 있지 않으면 {1}() 사용합니다.',
composeNamePriorityHelper:
'프로젝트 이름 우선순위: Docker Compose 구성에 지정된 이름 > 파일의 상위 디렉터리 이름 > 여기에 입력한 이름.',
composeHelper: '1Panel 에디터나 템플릿을 통해 생성된 컴포지션은 {0}/docker/compose 디렉토리에 저장됩니다.',
+2 -1
View File
@@ -1706,7 +1706,8 @@ const message = {
compose: 'Compose',
composeFile: 'ໄຟລ໌ Compose',
fromChangeHelper: 'ການປ່ຽນແຫຼ່ງທີ່ມາຈະລ້າງເນື້ອຫາທີ່ແກ້ໄຂໄວ້. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
composePathHelper: 'ເສັ້ນທາງບັນທຶກໄຟລ໌ກຳນົດຄ່າ: {0}',
composePathHelper:
'ເສັ້ນທາງບັນທຶກໄຟລ໌ Compose: {0}; ຖ້າມີການລະບຸຊື່ໂຄງການໃນ Docker Compose ຈະໃຊ້ຊື່ນັ້ນກ່ອນ, ຖ້າບໍ່ມີຈະໃຊ້ {1}.',
composeNamePriorityHelper:
'ລຳດັບຊື່ໂຄງການ: ຊື່ທີ່ລະບຸໃນການຕັ້ງຄ່າ Docker Compose > ໂຟນເດີແມ່ຂອງໄຟລ໌ > ຊື່ທີ່ປ້ອນຢູ່ນີ້.',
composeHelper: 'Compose ທີ່ສ້າງຜ່ານ 1Panel editor ຫຼື template ຈະຖືກບັນທຶກໄວ້ໃນໂຟນເດີ {0}/docker/compose.',
+2 -1
View File
@@ -1763,7 +1763,8 @@ const message = {
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}',
composePathHelper:
'Laluan simpan fail Compose: {0}; jika nama projek ditentukan dalam Docker Compose, nama tersebut akan diutamakan; jika tidak, {1} akan digunakan.',
composeNamePriorityHelper:
'Keutamaan nama projek: nama dalam konfigurasi Docker Compose > direktori induk fail > nama yang dimasukkan di sini.',
composeHelper:
+2 -1
View File
@@ -1764,7 +1764,8 @@ const message = {
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}',
composePathHelper:
'Caminho de salvamento do arquivo Compose: {0}; se um nome de projeto for definido no Docker Compose, ele terá prioridade; caso contrário, será usado {1}.',
composeNamePriorityHelper:
'Prioridade do nome do projeto: nome definido na configuração do Docker Compose > diretório pai do arquivo > nome informado aqui.',
composeHelper:
+2 -1
View File
@@ -1753,7 +1753,8 @@ const message = {
compose: 'Compose | Composes',
composeFile: 'Файл Оркестрации',
fromChangeHelper: 'Переключение источника очистит текущее отредактированное содержимое. Хотите продолжить?',
composePathHelper: 'Путь сохранения файла конфигурации: {0}',
composePathHelper:
'Путь сохранения файла Compose: {0}; если имя проекта указано в Docker Compose, оно имеет приоритет; иначе используется {1}.',
composeNamePriorityHelper:
'Приоритет имени проекта: имя из конфигурации Docker Compose > родительский каталог файла > имя, введённое здесь.',
composeHelper:
+2 -1
View File
@@ -1764,7 +1764,8 @@ const message = {
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}',
composePathHelper:
'Compose dosyası kaydetme yolu: {0}; Docker Compose içinde bir proje adı belirtilmişse bu ad önceliklidir; aksi halde {1} kullanılır.',
composeNamePriorityHelper:
'Proje adı önceliği: Docker Compose yapılandırmasında belirtilen ad > dosyanın üst dizini > buraya girilen ad.',
composeHelper:
+2 -1
View File
@@ -1656,7 +1656,8 @@ const message = {
compose: '編排',
composeFile: '編排檔案',
fromChangeHelper: '切換來源將清空當前已編輯內容是否繼續',
composePathHelper: '設定檔儲存路徑: {0}',
composePathHelper:
'編排檔案儲存路徑{0} Docker Compose 中指定了專案名稱則優先使用該名稱否則使用 {1}',
composeNamePriorityHelper:
'專案名稱優先順序Docker Compose 設定中指定的名稱 > 檔案父目錄名稱 > 此處填寫的名稱',
composeHelper: '通過 1Panel 編輯或範本建立的編排將儲存在 {0}/docker/compose 路徑下',
+2 -1
View File
@@ -1680,7 +1680,8 @@ const message = {
compose: '编排',
composeFile: '编排文件',
fromChangeHelper: '切换来源将清空当前已编辑内容是否继续',
composePathHelper: '配置文件保存路径: {0}',
composePathHelper:
'编排文件保存路径{0} Docker Compose 中指定了项目名称则优先使用该名称否则使用 {1}',
composeNamePriorityHelper:
'项目名称优先级Docker Compose 配置中指定的名称 > 文件父目录名称 > 此处填写的名称',
composeHelper: '通过 1Panel 编辑或模版创建的编排将保存在 {0}/docker/compose 路径下',
@@ -458,14 +458,14 @@
<el-form-item v-if="form.from === 'edit' || form.from === 'template'" prop="dirName">
<el-input @input="changePath" @change="onEdit('')" v-model.trim="form.dirName">
<template #prefix>
<span style="margin-right: 8px">{{ $t('file.dir') }}</span>
<span style="margin-right: 8px">{{ $t('commons.table.name') }}</span>
</template>
</el-input>
<span class="input-help">
{{ $t('container.composePathHelper', [composeFile]) }}
{{ $t('container.composePathHelper', [composeFile, form.dirName]) }}
</span>
</el-form-item>
<el-form-item prop="name">
<el-form-item v-if="form.from === 'path'" prop="name">
<el-input v-model.trim="form.name">
<template #prefix>
<span style="margin-right: 8px">{{ $t('commons.table.name') }}</span>