fix: resolve compose project names consistently (#13468)

This commit is contained in:
ssongliu
2026-08-04 17:17:07 +08:00
committed by GitHub
parent 6584e3b868
commit 1eb429631d
32 changed files with 330 additions and 68 deletions

View File

@@ -315,6 +315,7 @@ type ComposeContainer struct {
type ComposeCreate struct {
TaskID string `json:"taskID"`
Name string `json:"name"`
DirName string `json:"dirName"`
From string `json:"from" validate:"required,oneof=edit path template"`
File string `json:"file"`
Path string `json:"path"`

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
@@ -23,8 +24,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"gopkg.in/yaml.v3"
)
const composeProjectLabel = "com.docker.compose.project"
@@ -212,12 +215,16 @@ func composeFileExists(workdir, configFile string) bool {
}
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if cmd.CheckIllegal(req.Path) {
if err := validateComposeCreateName(req); err != nil {
return false, err
}
if cmd.CheckIllegal(req.Name, req.DirName, req.Path) {
return false, buserr.New("ErrCmdIllegal")
}
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
return false, buserr.New("ErrRecordExist")
if req.From != "path" {
if err := checkComposeRecordName(composeCreateDirName(req)); err != nil {
return false, err
}
}
if err := u.loadPath(&req); err != nil {
return false, err
@@ -225,37 +232,52 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
if err := newComposeEnv(req.Path, req.Env); err != nil {
return false, err
}
cmd := getComposeCmd(req.Path, "config")
stdout, err := cmd.CombinedOutput()
projectName, err := resolveComposeProjectName(req.Path, req.Name)
if err != nil {
return false, fmt.Errorf("docker-compose config failed, std: %s, err: %v", string(stdout), err)
return false, err
}
if err := checkComposeRecordName(projectName); err != nil {
return false, err
}
return true, nil
}
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
if cmd.CheckIllegal(req.Name, req.Path) {
if err := validateComposeCreateName(req); err != nil {
return err
}
if cmd.CheckIllegal(req.Name, req.DirName, req.Path) {
return buserr.New("ErrCmdIllegal")
}
if req.From != "path" {
if err := checkComposeRecordName(composeCreateDirName(req)); err != nil {
return err
}
}
if err := u.loadPath(&req); err != nil {
return err
}
if req.From == "path" {
req.Name = path.Base(path.Dir(req.Path))
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)
}
if err := newComposeEnv(req.Path, req.Env); err != nil {
return err
}
go func() {
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
err := compose.UpWithTask(req.Path, t, req.ForcePull)
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name)
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
if err != nil {
_, _ = compose.Down(req.Path)
_, _ = compose.Down(req.Path, req.Name)
return err
}
recordName := strings.ToLower(req.Name)
@@ -273,6 +295,142 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
return nil
}
func checkComposeRecordName(name string) error {
composeItem, _ := composeRepo.GetRecord(repo.WithByName(name))
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
return buserr.New("ErrRecordExist")
}
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")
}
if req.From != "path" && !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
return buserr.New("ErrComposeNameInvalid")
}
return nil
}
func composeCreateDirName(req dto.ComposeCreate) string {
dirName := strings.TrimSpace(req.DirName)
if dirName == "" {
// Keep compatibility with callers that used name as both the directory and
// Compose project name before dirName was introduced.
return strings.TrimSpace(req.Name)
}
return dirName
}
func resolveComposeProjectName(composePath, fallbackName 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, "")
if err == nil {
projectName, parseErr := loadComposeProjectName(stdout)
if parseErr != nil {
return "", parseErr
}
if projectName != "" {
if !re.GetRegex(re.ComposeNamePattern).MatchString(projectName) {
return "", buserr.New("ErrComposeNameInvalid")
}
return projectName, nil
}
if parentName != "" {
return parentName, nil
}
if fallbackName != "" {
if _, fallbackErr := runComposeConfig(composePath, fallbackName); fallbackErr != nil {
return "", fallbackErr
}
return fallbackName, nil
}
return "", buserr.New("ErrComposeProjectNameEmpty")
}
if !isComposeProjectNameEmptyError(err) {
return "", err
}
resolveErr := err
if parentName != "" {
if _, parentErr := runComposeConfig(composePath, parentName); parentErr == nil {
return parentName, nil
} else {
resolveErr = parentErr
}
}
if fallbackName != "" && fallbackName != parentName {
if _, fallbackErr := runComposeConfig(composePath, fallbackName); fallbackErr == nil {
return fallbackName, nil
} else {
return "", fallbackErr
}
}
if parentName == "" && fallbackName == "" {
return "", buserr.New("ErrComposeProjectNameEmpty")
}
return "", resolveErr
}
func runComposeConfig(composePath, projectName string) ([]byte, error) {
configCmd := getComposeCmd(composePath, "config", projectName)
stdout, err := configCmd.Output()
if err != nil {
var stderr []byte
if exitErr, ok := err.(*exec.ExitError); ok {
stderr = exitErr.Stderr
}
return nil, fmt.Errorf("docker-compose config failed, std: %s, err: %v", mergeComposeOutput(stdout, stderr), err)
}
return stdout, nil
}
func mergeComposeOutput(stdout, stderr []byte) string {
outputs := make([]string, 0, 2)
if output := strings.TrimSpace(string(stdout)); output != "" {
outputs = append(outputs, output)
}
if output := strings.TrimSpace(string(stderr)); output != "" {
outputs = append(outputs, output)
}
return strings.Join(outputs, "\n")
}
func loadComposeProjectName(config []byte) (string, error) {
var project struct {
Name string `yaml:"name"`
}
if err := yaml.Unmarshal(config, &project); err != nil {
return "", buserr.WithDetail("ErrComposeProjectNameParse", err.Error(), err)
}
return strings.TrimSpace(project.Name), nil
}
func primaryComposePath(composePath string) string {
if index := strings.Index(composePath, ","); index >= 0 {
return composePath[:index]
}
return composePath
}
func normalizeComposeProjectName(name string) string {
name = re.GetRegex(re.ComposeDisallowedCharsPattern).
ReplaceAllString(strings.ToLower(strings.TrimSpace(name)), "")
return strings.TrimLeft(name, "_-")
}
func isComposeProjectNameEmptyError(err error) bool {
message := strings.ToLower(err.Error())
return strings.Contains(message, "project name must not be empty") ||
strings.Contains(message, "project name can't be empty")
}
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if len(req.Path) == 0 && req.Operation == "delete" {
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
@@ -296,15 +454,15 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
return nil
}
if req.Operation == "up" {
if stdout, err := compose.Up(req.Path); err != nil {
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
}
} else if req.Operation == "rebuild" {
if stdout, err := compose.DownAndUp(req.Path); err != nil {
if stdout, err := compose.DownAndUp(req.Path, req.Name); err != nil {
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
}
} else {
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
if stdout, err := compose.Operate(req.Path, req.Operation, req.Name); err != nil {
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
}
}
@@ -340,9 +498,9 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
return err
}
if err := compose.UpWithTask(req.Path, t, req.ForcePull); err != nil {
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
if err := recreateCompose(string(oldFile), req.Path); err != nil {
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
}
return fmt.Errorf("update failed when handle compose up, err: %s", err)
@@ -432,7 +590,7 @@ 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, req.Name)
dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, composeCreateDirName(*req))
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return err
@@ -455,7 +613,7 @@ 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 {
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
return errors.New(stdout)
}
return nil
@@ -480,7 +638,7 @@ func removeContainerForCompose(composeName, composePath string) error {
return nil
}
func recreateCompose(content, path string) error {
func recreateCompose(content, path, projectName string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
@@ -490,7 +648,7 @@ func recreateCompose(content, path string) error {
_, _ = write.WriteString(content)
write.Flush()
if stdout, err := compose.Up(path); err != nil {
if stdout, err := compose.Up(path, projectName); err != nil {
return errors.New(string(stdout))
}
return nil

View File

@@ -166,21 +166,21 @@ func reCreateRuntime(runtime *model.Runtime) {
}
}
func getComposeCmd(composePath, operate string) *exec.Cmd {
func getComposeCmd(composePath, operate string, projectName ...string) *exec.Cmd {
dockerCommand := global.CONF.DockerConfig.Command
args := make([]string, 0, 7)
if len(projectName) > 0 && strings.TrimSpace(projectName[0]) != "" {
args = append(args, "--project-name", projectName[0])
}
args = append(args, "-f", composePath, operate)
if operate == "up" {
args = append(args, "-d")
}
var cmd *exec.Cmd
if dockerCommand == "docker-compose" {
if operate == "up" {
cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d")
} else {
cmd = exec.Command("docker-compose", "-f", composePath, operate)
}
cmd = exec.Command("docker-compose", args...)
} else {
if operate == "up" {
cmd = exec.Command("docker", "compose", "-f", composePath, operate, "-d")
} else {
cmd = exec.Command("docker", "compose", "-f", composePath, operate)
}
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
}
return cmd
}

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'The application name already exists'
ErrAppInstallNameExist: 'App install name {{ .name }} already exists'
AppStoreIsSyncing: 'App Store is syncing; try again later'
ErrGetCompose: 'Failed to read docker-compose.yml: {{ .detail }}'
ErrComposeProjectNameParse: 'Failed to parse the project name from Docker Compose config output: {{ .detail }}'
ErrComposeProjectNameEmpty: 'Unable to derive a project name from Docker Compose config or the file parent directory. Enter a name and try again.'
ErrComposeNameInvalid: 'Invalid Compose project name: it must start with a lowercase letter or number, contain only lowercase letters, numbers, hyphens, and underscores, and be 1-256 characters long.'
ErrAppVersionUnavailable: 'This application version has been removed from the remote service. Please select another version and try again.'
ErrAppWarn: 'App status abnormal; check logs'
ErrAppParamKey: 'Invalid app parameter: {{ .name }}'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'El nombre de la aplicación ya existe'
ErrAppInstallNameExist: 'El nombre de instalación de la aplicación {{ .name }} ya existe'
AppStoreIsSyncing: 'La tienda de aplicaciones está sincronizando, inténtelo más tarde'
ErrGetCompose: 'Fallo al obtener el archivo docker-compose.yml {{ .detail }}'
ErrComposeProjectNameParse: 'No se pudo analizar el nombre del proyecto desde la salida de configuración de Docker Compose: {{ .detail }}'
ErrComposeProjectNameEmpty: 'No se pudo derivar un nombre de proyecto desde la configuración de Docker Compose ni desde el directorio padre del archivo. Introduzca un nombre e inténtelo de nuevo.'
ErrComposeNameInvalid: 'Nombre de proyecto Compose no válido: debe comenzar con una letra minúscula o un número, contener solo letras minúsculas, números, guiones y guiones bajos, y tener entre 1 y 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versión de la aplicación se ha eliminado del servicio remoto. Seleccione otra versión e inténtelo de nuevo.'
ErrAppWarn: 'Estado anómalo, revise el log'
ErrAppParamKey: 'El campo de parámetro {{ .name }} es anómalo'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'نام برنامه از قبل وجود دارد'
ErrAppInstallNameExist: 'نام نصب برنامه {{ .name }} از قبل وجود دارد'
AppStoreIsSyncing: 'فروشگاه برنامه در حال همگام‌سازی است؛ بعداً دوباره تلاش کنید'
ErrGetCompose: 'خواندن docker-compose.yml ناموفق بود: {{ .detail }}'
ErrComposeProjectNameParse: 'تجزیه نام پروژه از خروجی پیکربندی Docker Compose ناموفق بود: {{ .detail }}'
ErrComposeProjectNameEmpty: 'نام پروژه از پیکربندی Docker Compose یا پوشه والد فایل قابل استخراج نیست. یک نام وارد کرده و دوباره تلاش کنید.'
ErrComposeNameInvalid: 'نام پروژه Compose نامعتبر است: باید با حرف کوچک یا عدد شروع شود، فقط شامل حروف کوچک، اعداد، خط تیره و زیرخط باشد و ۱ تا ۲۵۶ نویسه داشته باشد.'
ErrAppVersionUnavailable: 'این نسخه برنامه از سرویس راه دور حذف شده است. لطفاً نسخه دیگری را انتخاب کرده و دوباره تلاش کنید.'
ErrAppWarn: 'وضعیت برنامه غیرعادی است؛ لاگ‌ها را بررسی کنید'
ErrAppParamKey: 'پارامتر برنامه نامعتبر است: {{ .name }}'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'アプリケーション名がすでに存在します'
ErrAppInstallNameExist: 'アプリケーションインストール名 {{ .name }} はすでに存在します'
AppStoreIsSyncing: 'App Store が同期中です。しばらくしてからもう一度お試しください'
ErrGetCompose: 'docker-compose.yml ファイルの取得に失敗しました {{ .detail }}'
ErrComposeProjectNameParse: 'Docker Compose 設定出力からプロジェクト名を解析できませんでした:{{ .detail }}'
ErrComposeProjectNameEmpty: 'Docker Compose 設定またはファイルの親ディレクトリからプロジェクト名を推定できません。名前を入力して再試行してください。'
ErrComposeNameInvalid: 'Compose プロジェクト名の形式が正しくありません。小文字または数字で始まり、小文字、数字、ハイフン、アンダースコアのみを使用した 1256 文字にしてください。'
ErrAppVersionUnavailable: '現在のアプリケーションバージョンはリモートサービスから削除されています。別のバージョンを選択して再試行してください。'
ErrAppWarn: '異常な状態です。ログを確認してください'
ErrAppParamKey: 'パラメータ {{ .name }} フィールドが異常です'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: '응용 프로그램 이름이 이미 존재합니다'
ErrAppInstallNameExist: '앱 설치 이름 {{ .name }} 이(가) 이미 존재합니다'
AppStoreIsSyncing: 'App Store가 동기화 중입니다. 나중에 다시 시도하세요.'
ErrGetCompose: 'docker-compose.yml 파일을 가져오지 못했습니다 {{ .detail }}'
ErrComposeProjectNameParse: 'Docker Compose 구성 출력에서 프로젝트 이름을 구문 분석하지 못했습니다: {{ .detail }}'
ErrComposeProjectNameEmpty: 'Docker Compose 구성 또는 파일의 상위 디렉터리에서 프로젝트 이름을 확인할 수 없습니다. 이름을 입력한 후 다시 시도하세요.'
ErrComposeNameInvalid: 'Compose 프로젝트 이름 형식이 잘못되었습니다. 소문자 또는 숫자로 시작하고 소문자, 숫자, 하이픈, 밑줄만 사용하여 1~256자로 입력하세요.'
ErrAppVersionUnavailable: '현재 애플리케이션 버전이 원격 서비스에서 제거되었습니다. 다른 버전을 선택한 후 다시 시도해 주세요.'
ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.'
ErrAppParamKey: '매개변수 {{ .name }} 필드가 비정상입니다.'

View File

@@ -122,6 +122,9 @@ ErrAppNameExist: 'ຊື່ແອັບພລິເຄຊັນມີຢູ່
ErrAppInstallNameExist: 'ຊື່ການຕິດຕັ້ງແອັບ {{ .name }} ມີຢູ່ໃນລະບົບແລ້ວ'
AppStoreIsSyncing: 'App Store ກຳລັງຊິງຄ໌ຂໍ້ມູນ; ກະລຸນາລອງໃໝ່ພາຍຫຼັງ'
ErrGetCompose: 'ອ່ານໄຟລ໌ docker-compose.yml ລົ້ມເຫຼວ: {{ .detail }}'
ErrComposeProjectNameParse: 'ບໍ່ສາມາດວິເຄາະຊື່ໂຄງການຈາກຜົນລັບການກຳນົດຄ່າ Docker Compose: {{ .detail }}'
ErrComposeProjectNameEmpty: 'ບໍ່ສາມາດກຳນົດຊື່ໂຄງການຈາກການຕັ້ງຄ່າ Docker Compose ຫຼື ໂຟນເດີແມ່ຂອງໄຟລ໌ໄດ້. ກະລຸນາປ້ອນຊື່ແລ້ວລອງໃໝ່.'
ErrComposeNameInvalid: 'ຊື່ໂຄງການ Compose ບໍ່ຖືກຕ້ອງ: ຕ້ອງເລີ່ມດ້ວຍຕົວພິມນ້ອຍ ຫຼື ຕົວເລກ, ໃຊ້ໄດ້ສະເພາະຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດກາງ ແລະ ຂີດລຸ່ມ, ຄວາມຍາວ 1-256 ຕົວອັກສອນ.'
ErrAppVersionUnavailable: 'ເວີຊັນແອັບນີ້ຖືກລຶບອອກຈາກບໍລິການທາງໄກແລ້ວ ກະລຸນາເລືອກເວີຊັນອື່ນແລ້ວລອງໃໝ່'
ErrAppWarn: 'ສະຖານະແອັບຜິດປົກກະຕິ; ກະລຸນາກວດສອບບັນທຶກ'
ErrAppParamKey: 'ພາຣາມິເຕີຂອງແອັບບໍ່ຖືກຕ້ອງ: {{ .name }}'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'Nama aplikasi sudah wujud'
ErrAppInstallNameExist: 'Nama pemasangan aplikasi {{ .name }} sudah wujud'
AppStoreIsSyncing: 'App Store sedang menyegerak, sila cuba sebentar lagi'
ErrGetCompose: 'Gagal mendapatkan fail docker-compose.yml {{ .detail }}'
ErrComposeProjectNameParse: 'Gagal menghurai nama projek daripada output konfigurasi Docker Compose: {{ .detail }}'
ErrComposeProjectNameEmpty: 'Nama projek tidak dapat diperoleh daripada konfigurasi Docker Compose atau direktori induk fail. Masukkan nama dan cuba lagi.'
ErrComposeNameInvalid: 'Nama projek Compose tidak sah: mesti bermula dengan huruf kecil atau nombor, hanya mengandungi huruf kecil, nombor, tanda sempang dan garis bawah, serta sepanjang 1-256 aksara.'
ErrAppVersionUnavailable: 'Versi aplikasi ini telah dialih keluar daripada perkhidmatan jauh. Sila pilih versi lain dan cuba lagi.'
ErrAppWarn: 'Status tidak normal, sila semak log'
ErrAppParamKey: 'Parameter {{ .name }} medan tidak normal'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'O nome do aplicativo já existe'
ErrAppInstallNameExist: 'O nome de instalação do aplicativo {{ .name }} já existe'
AppStoreIsSyncing: 'A App Store está sincronizando mais tarde'
ErrGetCompose: 'Falha ao obter o arquivo docker-compose.yml {{ .detail }}'
ErrComposeProjectNameParse: 'Falha ao analisar o nome do projeto na saída de configuração do Docker Compose: {{ .detail }}'
ErrComposeProjectNameEmpty: 'Não foi possível derivar um nome de projeto da configuração do Docker Compose nem do diretório pai do arquivo. Informe um nome e tente novamente.'
ErrComposeNameInvalid: 'Nome de projeto Compose inválido: deve começar com uma letra minúscula ou número, conter apenas letras minúsculas, números, hífens e sublinhados e ter de 1 a 256 caracteres.'
ErrAppVersionUnavailable: 'Esta versão do aplicativo foi removida do serviço remoto. Selecione outra versão e tente novamente.'
ErrAppWarn: 'Status anormal, verifique o log'
ErrAppParamKey: 'O campo de parâmetro {{ .name }} está anormal'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'Имя приложения уже существует'
ErrAppInstallNameExist: 'Имя установки приложения {{ .name }} уже существует'
AppStoreIsSyncing: 'App Store синхронизируется, повторите попытку позже'
ErrGetCompose: 'Не удалось получить файл docker-compose.yml {{ .detail }}'
ErrComposeProjectNameParse: 'Не удалось определить имя проекта из вывода конфигурации Docker Compose: {{ .detail }}'
ErrComposeProjectNameEmpty: 'Не удалось получить имя проекта из конфигурации Docker Compose или родительского каталога файла. Введите имя и повторите попытку.'
ErrComposeNameInvalid: 'Недопустимое имя проекта Compose: оно должно начинаться со строчной буквы или цифры, содержать только строчные буквы, цифры, дефисы и подчёркивания и иметь длину от 1 до 256 символов.'
ErrAppVersionUnavailable: 'Эта версия приложения удалена из удаленного сервиса. Выберите другую версию и повторите попытку.'
ErrAppWarn: 'Ненормальное состояние, проверьте журнал'
ErrAppParamKey: 'Поле параметра {{ .name }} ненормально'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: 'Uygulama adı zaten mevcut'
ErrAppInstallNameExist: '{{ .name }} uygulama kurulum adı zaten mevcut'
AppStoreIsSyncing: 'Uygulama Mağazası senkronize ediliyor, lütfen daha sonra tekrar deneyin'
ErrGetCompose: 'docker-compose.yml dosyası alınamadı {{ .detail }}'
ErrComposeProjectNameParse: 'Docker Compose yapılandırma çıktısından proje adı ayrıştırılamadı: {{ .detail }}'
ErrComposeProjectNameEmpty: 'Docker Compose yapılandırmasından veya dosyanın üst dizininden proje adı türetilemedi. Bir ad girip yeniden deneyin.'
ErrComposeNameInvalid: 'Geçersiz Compose proje adı: küçük harf veya rakamla başlamalı, yalnızca küçük harf, rakam, kısa çizgi ve alt çizgi içermeli ve 1-256 karakter uzunluğunda olmalıdır.'
ErrAppVersionUnavailable: 'Bu uygulama sürümü uzak hizmetten kaldırıldı. Lütfen başka bir sürüm seçip tekrar deneyin.'
ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin'
ErrAppParamKey: 'Parametre {{ .name }} alanı anormal'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: '應用程式名稱已存在'
ErrAppInstallNameExist: '應用程式安裝名稱 {{ .name }} 已存在'
AppStoreIsSyncing: '應用程式商店正在同步中,請稍後重試'
ErrGetCompose: 'docker-compose.yml 檔案取得失敗!{{ .detail }}'
ErrComposeProjectNameParse: '解析 Docker Compose 設定輸出中的專案名稱失敗:{{ .detail }}'
ErrComposeProjectNameEmpty: '無法從 Docker Compose 設定或檔案父目錄中推導出專案名稱,請填寫名稱後重試'
ErrComposeNameInvalid: 'Compose 專案名稱格式錯誤:必須以小寫字母或數字開頭,僅支援小寫字母、數字、- 和 _長度為 1-256 個字元'
ErrAppVersionUnavailable: '目前應用程式版本已從遠端服務下架,請選擇其他版本後重試'
ErrAppWarn: '狀態異常,請檢視日誌'
ErrAppParamKey: '參數{{ .name }} 欄位異常'

View File

@@ -132,6 +132,9 @@ ErrAppNameExist: '应用名称已存在'
ErrAppInstallNameExist: '应用安装名称 {{ .name }} 已存在'
AppStoreIsSyncing: '应用商店同步中,请稍后重试'
ErrGetCompose: "docker-compose.yml 文件获取失败!{{ .detail }}"
ErrComposeProjectNameParse: "解析 Docker Compose 配置输出中的项目名称失败:{{ .detail }}"
ErrComposeProjectNameEmpty: "无法从 Docker Compose 配置或文件父目录中推导出项目名称,请填写名称后重试"
ErrComposeNameInvalid: "Compose 项目名称格式错误:必须以小写字母或数字开头,仅支持小写字母、数字、- 和 _长度为 1-256 个字符"
ErrAppVersionUnavailable: "当前应用版本已从远程服务下架,请选择其他版本后重试"
ErrAppWarn: "状态异常,请查看日志"
ErrAppParamKey: "参数 {{ .name }} 字段异常"

View File

@@ -38,21 +38,23 @@ func getComposeBaseCmd() (string, []string) {
return parts[0], parts[1:]
}
func Up(filePath string) (string, error) {
func Up(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
base, extra := getComposeBaseCmd()
args := append(extra, upArgs(filePath, false)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, upArgs(filePath, false)...)
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}
func UpWithoutPull(filePath string) (string, error) {
func UpWithoutPull(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
base, extra := getComposeBaseCmd()
args := append(extra, upArgs(filePath, true)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, upArgs(filePath, true)...)
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}
@@ -65,12 +67,13 @@ func upArgs(filePath string, withoutPull bool) []string {
return args
}
func UpWithTask(filePath string, task *task.Task, forcePull bool) error {
if err := PullComposeImages(filePath, forcePull, task); err != nil {
func UpWithTask(filePath string, task *task.Task, forcePull bool, projectName ...string) error {
if err := PullComposeImages(filePath, forcePull, task, projectName...); err != nil {
return err
}
base, extra := getComposeBaseCmd()
args := append(extra, upArgs(filePath, false)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, upArgs(filePath, false)...)
return cmd.NewCommandMgr(cmd.WithTask(*task), cmd.WithTimeout(20*time.Minute)).Run(base, args...)
}
@@ -88,12 +91,12 @@ func BuildWithTask(filePath, projectName string, task *task.Task) error {
return cmd.NewCommandMgr(cmd.WithTask(*task), cmd.WithTimeout(120*time.Minute)).Run(base, args...)
}
func PullComposeImages(filePath string, forcePull bool, task *task.Task) error {
return pullComposeImages(filePath, forcePull, task)
func PullComposeImages(filePath string, forcePull bool, task *task.Task, projectName ...string) error {
return pullComposeImages(filePath, forcePull, task, projectName...)
}
func pullComposeImages(filePath string, forcePull bool, task *task.Task) error {
images, err := GetComposeImages(filePath)
func pullComposeImages(filePath string, forcePull bool, task *task.Task, projectName ...string) error {
images, err := GetComposeImages(filePath, projectName...)
if err != nil {
return err
}
@@ -150,8 +153,8 @@ func pullComposeImages(filePath string, forcePull bool, task *task.Task) error {
return nil
}
func GetComposeImages(filePath string) ([]string, error) {
images, err := getComposeImagesByCommand(filePath)
func GetComposeImages(filePath string, projectName ...string) ([]string, error) {
images, err := getComposeImagesByCommand(filePath, projectName...)
if err == nil {
return images, nil
}
@@ -168,12 +171,13 @@ func GetComposeImages(filePath string) ([]string, error) {
return images, nil
}
func getComposeImagesByCommand(filePath string) ([]string, error) {
func getComposeImagesByCommand(filePath string, projectName ...string) ([]string, error) {
if err := checkCmd(); err != nil {
return nil, err
}
base, extra := getComposeBaseCmd()
args := append(extra, loadFiles(filePath)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, loadFiles(filePath)...)
args = append(args, "config", "--format", "json", "--no-normalize")
stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).
RunWithStdout(base, args...)
@@ -209,64 +213,77 @@ func getComposeImagesByCommand(filePath string) ([]string, error) {
return images, nil
}
func Down(filePath string) (string, error) {
func Down(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
base, extra := getComposeBaseCmd()
args := append(extra, loadFiles(filePath)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, loadFiles(filePath)...)
args = append(args, "down", "--remove-orphans")
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}
func Stop(filePath string) (string, error) {
func Stop(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
base, extra := getComposeBaseCmd()
args := append(extra, loadFiles(filePath)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, loadFiles(filePath)...)
args = append(args, "stop")
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}
func Restart(filePath string) (string, error) {
func Restart(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
base, extra := getComposeBaseCmd()
args := append(extra, loadFiles(filePath)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, loadFiles(filePath)...)
args = append(args, "restart")
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}
func Operate(filePath, operation string) (string, error) {
func Operate(filePath, operation string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
base, extra := getComposeBaseCmd()
args := append(extra, loadFiles(filePath)...)
args := appendProjectName(append([]string(nil), extra...), projectName)
args = append(args, loadFiles(filePath)...)
args = append(args, operation)
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}
func DownAndUp(filePath string) (string, error) {
func DownAndUp(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Minute))
base, extra := getComposeBaseCmd()
argsDown := append(extra, loadFiles(filePath)...)
argsDown := appendProjectName(append([]string(nil), extra...), projectName)
argsDown = append(argsDown, loadFiles(filePath)...)
argsDown = append(argsDown, "down")
stdout, err := cmdMgr.RunWithStdout(base, argsDown...)
if err != nil {
return stdout, err
}
argsUp := append(extra, loadFiles(filePath)...)
argsUp := appendProjectName(append([]string(nil), extra...), projectName)
argsUp = append(argsUp, loadFiles(filePath)...)
argsUp = append(argsUp, "up", "-d")
stdout, err = cmdMgr.RunWithStdout(base, argsUp...)
return stdout, err
}
func appendProjectName(args []string, projectName []string) []string {
if len(projectName) > 0 && strings.TrimSpace(projectName[0]) != "" {
args = append(args, "--project-name", projectName[0])
}
return args
}
func loadFiles(filePath string) []string {
var fileItem []string
for _, item := range strings.Split(filePath, ",") {

View File

@@ -8,6 +8,7 @@ import (
const (
NumberAlphaPattern = `(\d+)([A-Za-z]+)`
ComposeDisallowedCharsPattern = `[^a-z0-9_-]+`
ComposeNamePattern = `^[a-z0-9][a-z0-9_-]{0,255}$`
ComposeEnvVarPattern = `\$\{([^}]+)\}`
DiskKeyValuePattern = `([A-Za-z0-9_]+)=("([^"\\]|\\.)*"|[^ \t]+)`
ValidatorNamePattern = `^[a-zA-Z\p{Han}]{1}[a-zA-Z0-9_\p{Han}]{0,30}$`
@@ -59,6 +60,7 @@ func Init() {
patterns := []string{
NumberAlphaPattern,
ComposeDisallowedCharsPattern,
ComposeNamePattern,
ComposeEnvVarPattern,
DiskKeyValuePattern,
ValidatorNamePattern,

View File

@@ -352,6 +352,7 @@ export namespace Container {
export interface ComposeCreate {
taskID: string;
name: string;
dirName: string;
from: string;
file: string;
path: string;

View File

@@ -246,7 +246,7 @@ const checkComposeName: RuleValidator = (_rule, value, callback) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.composeName')));
} else {
const reg = /^[a-z0-9]{1}[a-z0-9_-]{0,256}$/;
const reg = /^[a-z0-9]{1}[a-z0-9_-]{0,255}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.composeName')));
} else {

View File

@@ -1744,6 +1744,8 @@ const message = {
composeFile: 'Compose File',
fromChangeHelper: 'Switching the source will clean the current edited content. Continue?',
composePathHelper: 'Configuration file save path: {0}',
composeNamePriorityHelper:
'Project name priority: name specified in Docker Compose config > file parent directory > name entered here.',
composeHelper:
'The composition created through 1Panel editor or template will be saved in the {0}/docker/compose directory.',
deleteFile: 'Delete file',

View File

@@ -1771,6 +1771,8 @@ const message = {
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}',
composeNamePriorityHelper:
'Prioridad del nombre del proyecto: nombre especificado en la configuración de Docker Compose > directorio padre del archivo > nombre indicado aquí.',
composeHelper:
'La composición creada mediante el editor o plantilla de 1Panel se guardará en el directorio {0}/docker/compose.',
deleteFile: 'Eliminar archivo',

View File

@@ -1731,6 +1731,8 @@ const message = {
composeFile: 'فایل Compose',
fromChangeHelper: 'تغییر منبع محتوای ویرایش شده فعلی را پاک می‌کند. ادامه می‌دهید؟',
composePathHelper: 'مسیر ذخیره فایل پیکربندی: {0}',
composeNamePriorityHelper:
'اولویت نام پروژه: نام مشخص‌شده در پیکربندی Docker Compose > پوشه والد فایل > نام واردشده در اینجا.',
composeHelper: 'ترکیب ایجاد شده از طریق ویرایشگر یا قالب 1Panel در دایرکتوری {0}/docker/compose ذخیره می‌شود.',
deleteFile: 'حذف فایل',
deleteComposeHelper:

View File

@@ -1734,6 +1734,8 @@ const message = {
composeFile: 'オーケストレーションファイル',
fromChangeHelper: 'ソースを切り替えると、現在の編集されたコンテンツがきれいになります。続けたいですか?',
composePathHelper: '構成ファイル保存パス:{0}',
composeNamePriorityHelper:
'プロジェクト名の優先順位Docker Compose 設定で指定された名前 > ファイルの親ディレクトリ名 > ここで入力した名前。',
composeHelper:
'1パネルの編集者またはテンプレートを介して作成された構成は、{0}/docker/composeディレクトリに保存されます。',
deleteFile: 'ファイルを削除します',

View File

@@ -1715,6 +1715,8 @@ const message = {
composeFile: '컴포즈 파일',
fromChangeHelper: '소스를 변경하면 현재 편집한 내용이 삭제됩니다. 계속 하시겠습니까?',
composePathHelper: '구성 파일 저장 경로: {0}',
composeNamePriorityHelper:
'프로젝트 이름 우선순위: Docker Compose 구성에 지정된 이름 > 파일의 상위 디렉터리 이름 > 여기에 입력한 이름.',
composeHelper: '1Panel 에디터나 템플릿을 통해 생성된 컴포지션은 {0}/docker/compose 디렉토리에 저장됩니다.',
deleteFile: '파일 삭제',
deleteComposeHelper:

View File

@@ -1705,6 +1705,8 @@ const message = {
composeFile: 'ໄຟລ໌ Compose',
fromChangeHelper: 'ການປ່ຽນແຫຼ່ງທີ່ມາຈະລ້າງເນື້ອຫາທີ່ແກ້ໄຂໄວ້. ຕ້ອງການຕໍ່ຫຼືບໍ່?',
composePathHelper: 'ເສັ້ນທາງບັນທຶກໄຟລ໌ກຳນົດຄ່າ: {0}',
composeNamePriorityHelper:
'ລຳດັບຊື່ໂຄງການ: ຊື່ທີ່ລະບຸໃນການຕັ້ງຄ່າ Docker Compose > ໂຟນເດີແມ່ຂອງໄຟລ໌ > ຊື່ທີ່ປ້ອນຢູ່ນີ້.',
composeHelper: 'Compose ທີ່ສ້າງຜ່ານ 1Panel editor ຫຼື template ຈະຖືກບັນທຶກໄວ້ໃນໂຟນເດີ {0}/docker/compose.',
deleteFile: 'ລຶບໄຟລ໌',
deleteComposeHelper:

View File

@@ -1762,6 +1762,8 @@ const message = {
composeFile: 'Fail Susunan',
fromChangeHelper: 'Menukar sumber akan membersihkan kandungan yang sedang diedit. Adakah anda mahu meneruskan?',
composePathHelper: 'Laluan simpan fail konfigurasi: {0}',
composeNamePriorityHelper:
'Keutamaan nama projek: nama dalam konfigurasi Docker Compose > direktori induk fail > nama yang dimasukkan di sini.',
composeHelper:
'Komposisi yang dicipta melalui editor atau templat 1Panel akan disimpan dalam direktori {0}/docker/compose.',
deleteFile: 'Padam fail',

View File

@@ -1763,6 +1763,8 @@ const message = {
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}',
composeNamePriorityHelper:
'Prioridade do nome do projeto: nome definido na configuração do Docker Compose > diretório pai do arquivo > nome informado aqui.',
composeHelper:
'A composição criada através do editor ou template do 1Panel será salva no diretório {0}/docker/compose.',
deleteFile: 'Excluir arquivo',

View File

@@ -1752,6 +1752,8 @@ const message = {
composeFile: 'Файл Оркестрации',
fromChangeHelper: 'Переключение источника очистит текущее отредактированное содержимое. Хотите продолжить?',
composePathHelper: 'Путь сохранения файла конфигурации: {0}',
composeNamePriorityHelper:
'Приоритет имени проекта: имя из конфигурации Docker Compose > родительский каталог файла > имя, введённое здесь.',
composeHelper:
'Композиция, созданная через редактор 1Panel или шаблон, будет сохранена в директории {0}/docker/compose.',
deleteFile: 'Удалить файл',

View File

@@ -1763,6 +1763,8 @@ const message = {
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}',
composeNamePriorityHelper:
'Proje adı önceliği: Docker Compose yapılandırmasında belirtilen ad > dosyanın üst dizini > buraya girilen ad.',
composeHelper:
'1Panel editörü veya şablonu aracılığıyla oluşturulan kompozisyon {0}/docker/compose dizinine kaydedilecektir.',
deleteFile: 'Dosyayı sil',

View File

@@ -1656,6 +1656,8 @@ const message = {
composeFile: '編排檔案',
fromChangeHelper: '切換來源將清空當前已編輯內容,是否繼續?',
composePathHelper: '設定檔儲存路徑: {0}',
composeNamePriorityHelper:
'專案名稱優先順序Docker Compose 設定中指定的名稱 > 檔案父目錄名稱 > 此處填寫的名稱。',
composeHelper: '通過 1Panel 編輯或範本建立的編排,將儲存在 {0}/docker/compose 路徑下',
deleteFile: '刪除檔案',
deleteComposeHelper: '刪除容器編排的所有檔案,包括設定檔和持久化檔案,請謹慎操作',

View File

@@ -1680,6 +1680,8 @@ const message = {
composeFile: '编排文件',
fromChangeHelper: '切换来源将清空当前已编辑内容,是否继续?',
composePathHelper: '配置文件保存路径: {0}',
composeNamePriorityHelper:
'项目名称优先级Docker Compose 配置中指定的名称 > 文件父目录名称 > 此处填写的名称。',
composeHelper: '通过 1Panel 编辑或模版创建的编排,将保存在 {0}/docker/compose 路径下',
deleteFile: '删除文件',
deleteComposeHelper: '删除容器编排的所有文件,包括配置文件和持久化文件,请谨慎操作!',

View File

@@ -455,8 +455,8 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="form.from === 'edit' || form.from === 'template'" prop="name">
<el-input @input="changePath" @change="onEdit('')" v-model.trim="form.name">
<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>
</template>
@@ -465,6 +465,16 @@
{{ $t('container.composePathHelper', [composeFile]) }}
</span>
</el-form-item>
<el-form-item prop="name">
<el-input v-model.trim="form.name">
<template #prefix>
<span style="margin-right: 8px">{{ $t('commons.table.name') }}</span>
</template>
</el-input>
<span class="input-help">
{{ $t('container.composeNamePriorityHelper') }}
</span>
</el-form-item>
<el-form-item>
<div v-if="form.from === 'edit' || form.from === 'template'" class="w-full">
<CodemirrorPro
@@ -582,6 +592,7 @@ const formRef = ref<FormInstance>();
const form = reactive({
taskID: '',
name: '',
dirName: '',
from: 'edit',
path: '',
file: '',
@@ -589,11 +600,22 @@ const form = reactive({
env: '',
forcePull: false,
});
const rules = reactive({
name: [Rules.requiredInput, Rules.composeName],
const optionalComposeNameRule = {
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (!value || /^[a-z0-9][a-z0-9_-]{0,255}$/.test(value)) {
callback();
return;
}
callback(new Error(i18n.global.t('commons.rule.composeName')));
},
trigger: 'blur',
};
const rules = computed(() => ({
name: [optionalComposeNameRule],
dirName: [Rules.requiredInput, Rules.composeName],
path: [Rules.requiredInput],
template: [Rules.requiredSelect],
});
}));
const isActive = ref(false);
const isExist = ref(false);
@@ -747,6 +769,7 @@ const onOpenDialog = async () => {
isOnCreate.value = true;
loadTemplates();
form.name = '';
form.dirName = '';
form.from = 'edit';
form.path = '';
form.file = '';
@@ -806,7 +829,7 @@ const loadPath = async () => {
changePath();
};
const changePath = async () => {
composeFile.value = baseDir.value + '/docker/compose/' + form.name;
composeFile.value = baseDir.value + '/docker/compose/' + form.dirName;
};
const loadDir = async (path: string) => {
form.path = path;