Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent/app/dto/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
206 changes: 182 additions & 24 deletions agent/app/service/container_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
Expand All @@ -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"
Expand Down Expand Up @@ -212,50 +215,69 @@ 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
}
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
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
projectName, err := resolveComposeProjectName(req.Path, req.Name)
if err != nil {
return fmt.Errorf("new task for image build failed, err: %v", err)
return err
}
if err := newComposeEnv(req.Path, req.Env); err != nil {
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)
}
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)
Expand All @@ -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
}
Comment thread
Copilot marked this conversation as resolved.
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))
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 11 additions & 11 deletions agent/app/service/runtime_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions agent/i18n/lang/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
Expand Down
3 changes: 3 additions & 0 deletions agent/i18n/lang/es-ES.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions agent/i18n/lang/fa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
Expand Down
3 changes: 3 additions & 0 deletions agent/i18n/lang/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }} フィールドが異常です'
Expand Down
Loading
Loading