diff --git a/backend/app/dto/container.go b/backend/app/dto/container.go index 936db57bd..b78ec1b30 100644 --- a/backend/app/dto/container.go +++ b/backend/app/dto/container.go @@ -208,11 +208,12 @@ type ComposeContainer struct { State string `json:"state"` } type ComposeCreate struct { - Name string `json:"name"` - From string `json:"from" validate:"required,oneof=edit path template"` - File string `json:"file"` - Path string `json:"path"` - Template uint `json:"template"` + Name string `json:"name"` + From string `json:"from" validate:"required,oneof=edit path template"` + File string `json:"file"` + Path string `json:"path"` + Template uint `json:"template"` + Env []string `json:"env"` } type ComposeOperation struct { Name string `json:"name" validate:"required"` @@ -221,9 +222,10 @@ type ComposeOperation struct { WithFile bool `json:"withFile"` } type ComposeUpdate struct { - Name string `json:"name" validate:"required"` - Path string `json:"path" validate:"required"` - Content string `json:"content" validate:"required"` + Name string `json:"name" validate:"required"` + Path string `json:"path" validate:"required"` + Content string `json:"content" validate:"required"` + Env []string `json:"env"` } type ContainerLog struct { diff --git a/backend/app/service/container_compose.go b/backend/app/service/container_compose.go index b98734a9d..545f7b12d 100644 --- a/backend/app/service/container_compose.go +++ b/backend/app/service/container_compose.go @@ -4,7 +4,9 @@ import ( "bufio" "errors" "fmt" + "gopkg.in/yaml.v3" "io" + "io/ioutil" "os" "os/exec" "path" @@ -31,6 +33,12 @@ const composeConfigLabel = "com.docker.compose.project.config_files" const composeWorkdirLabel = "com.docker.compose.project.working_dir" const composeCreatedBy = "createdBy" +type DockerCompose struct { + Version string `yaml:"version"` + Services map[string]map[string]interface{} `yaml:"services"` + Networks map[string]interface{} `yaml:"networks"` +} + func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) { var ( records []dto.ComposeInfo @@ -175,6 +183,59 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { return true, nil } +func formatYAML(data []byte) []byte { + return []byte(strings.ReplaceAll(string(data), "\t", " ")) +} + +func updateDockerComposeWithEnv(req dto.ComposeCreate) error { + data, err := ioutil.ReadFile(req.Path) + if err != nil { + return fmt.Errorf("failed to read docker-compose.yml: %v", err) + } + var composeItem DockerCompose + if err := yaml.Unmarshal(data, &composeItem); err != nil { + return fmt.Errorf("failed to parse docker-compose.yml: %v", err) + } + for serviceName, service := range composeItem.Services { + envMap := make(map[string]string) + if existingEnv, exists := service["environment"].([]interface{}); exists { + for _, env := range existingEnv { + envStr := env.(string) + parts := strings.SplitN(envStr, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + } + for _, env := range req.Env { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + envVars := []string{} + for key, value := range envMap { + envVars = append(envVars, key+"="+value) + } + service["environment"] = envVars + composeItem.Services[serviceName] = service + } + if composeItem.Networks != nil { + for key := range composeItem.Networks { + composeItem.Networks[key] = map[string]interface{}{} + } + } + newData, err := yaml.Marshal(&composeItem) + if err != nil { + return fmt.Errorf("failed to marshal docker-compose.yml: %v", err) + } + formattedData := formatYAML(newData) + if err := ioutil.WriteFile(req.Path, formattedData, 0644); err != nil { + return fmt.Errorf("failed to write docker-compose.yml: %v", err) + } + return nil +} + func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) { if cmd.CheckIllegal(req.Name, req.Path) { return "", buserr.New(constant.ErrCmdIllegal) @@ -199,6 +260,12 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) if err != nil { return "", err } + if len(req.Env) > 0 { + if err := updateDockerComposeWithEnv(req); err != nil { + fmt.Printf("failed to update docker-compose.yml with env: %v\n", err) + return "", err + } + } go func() { defer file.Close() cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d") @@ -253,6 +320,60 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { return nil } +func updateComposeWithEnv(req dto.ComposeUpdate) error { + var composeItem DockerCompose + if err := yaml.Unmarshal([]byte(req.Content), &composeItem); err != nil { + return fmt.Errorf("failed to parse docker-compose content: %v", err) + } + for serviceName, service := range composeItem.Services { + envMap := make(map[string]string) + for _, env := range req.Env { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + newEnvVars := []string{} + if existingEnv, exists := service["environment"].([]interface{}); exists { + for _, env := range existingEnv { + envStr := env.(string) + parts := strings.SplitN(envStr, "=", 2) + if len(parts) == 2 { + key := parts[0] + if value, found := envMap[key]; found { + newEnvVars = append(newEnvVars, key+"="+value) + delete(envMap, key) + } else { + newEnvVars = append(newEnvVars, envStr) + } + } + } + } + for key, value := range envMap { + newEnvVars = append(newEnvVars, key+"="+value) + } + if len(newEnvVars) > 0 { + service["environment"] = newEnvVars + } else { + delete(service, "environment") + } + composeItem.Services[serviceName] = service + } + if composeItem.Networks != nil { + for key := range composeItem.Networks { + composeItem.Networks[key] = map[string]interface{}{} + } + } + newData, err := yaml.Marshal(&composeItem) + if err != nil { + return fmt.Errorf("failed to marshal docker-compose.yml: %v", err) + } + if err := ioutil.WriteFile(req.Path, newData, 0644); err != nil { + return fmt.Errorf("failed to write docker-compose.yml to path: %v", err) + } + return nil +} + func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { if cmd.CheckIllegal(req.Name, req.Path) { return buserr.New(constant.ErrCmdIllegal) @@ -261,16 +382,21 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { if err != nil { return fmt.Errorf("load file with path %s failed, %v", req.Path, err) } - file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640) - if err != nil { - return err + if len(req.Env) > 0 { + if err := updateComposeWithEnv(req); err != nil { + return fmt.Errorf("failed to update docker-compose with env: %v", err) + } + } else { + file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(req.Content) + write.Flush() + global.LOG.Infof("docker-compose.yml %s has been replaced", req.Path) } - defer file.Close() - write := bufio.NewWriter(file) - _, _ = 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 stdout, err := compose.Up(req.Path); err != nil { if err := recreateCompose(string(oldFile), req.Path); err != nil { return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err) diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index fb9563405..5c9b63c97 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -254,6 +254,8 @@ export namespace Container { file: string; path: string; template: number; + env: Array; + envStr: string; } export interface ComposeOperation { name: string; @@ -265,6 +267,7 @@ export namespace Container { name: string; path: string; content: string; + env: Array; } export interface TemplateCreate { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 3215cd9f7..60be3d407 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -654,6 +654,8 @@ const message = { privileged: 'Privileged', privilegedHelper: 'Allows the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!', + editComposeHelper: + 'The environment variables manually entered in the menu will override existing variables with the same name. If they do not exist, they will be added.', upgradeHelper: 'Repository Name/Image Name: Image Version', upgradeWarning2: diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 588f17ffb..1e56dd184 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -629,6 +629,7 @@ const message = { emptyUser: '為空時,將使用容器默認的用戶登錄', privileged: '特權模式', privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟!', + editComposeHelper: '在菜單中手動輸入的環境變量會覆蓋原有的同名變量,若不存在則新增。', upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本', upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index fa32d2805..7710ca8a0 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -632,6 +632,7 @@ const message = { emptyUser: '为空时,将使用容器默认的用户登录', privileged: '特权模式', privilegedHelper: '允许容器在主机上执行某些特权操作,可能会增加容器风险,谨慎开启!', + editComposeHelper: '菜单中手动输入的环境变量会覆盖原有的同名变量,如果不存在则新增', upgradeHelper: '仓库名称/镜像名称:镜像版本', upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?', diff --git a/frontend/src/views/container/compose/create/index.vue b/frontend/src/views/container/compose/create/index.vue index ad1d4e83b..107023cd3 100644 --- a/frontend/src/views/container/compose/create/index.vue +++ b/frontend/src/views/container/compose/create/index.vue @@ -86,6 +86,14 @@ /> + + + @@ -143,6 +151,8 @@ const form = reactive({ path: '', file: '', template: null as number, + env: [], + envStr: '', }); const rules = reactive({ name: [Rules.requiredInput, Rules.imageName], @@ -163,6 +173,8 @@ const acceptParams = (): void => { form.path = ''; form.file = ''; form.template = null; + form.envStr = ''; + form.env = []; loadTemplates(); loadPath(); isStartReading.value = false; @@ -242,6 +254,9 @@ const onSubmit = async (formEl: FormInstance | undefined) => { MsgError(i18n.global.t('container.contentEmpty')); return; } + if (form.envStr) { + form.env = form.envStr.split('\n'); + } loading.value = true; await testCompose(form) .then(async (res) => { diff --git a/frontend/src/views/container/compose/edit/index.vue b/frontend/src/views/container/compose/edit/index.vue index 21cc76830..33227d5b5 100644 --- a/frontend/src/views/container/compose/edit/index.vue +++ b/frontend/src/views/container/compose/edit/index.vue @@ -9,20 +9,37 @@ -
- +
+ + + + + + + + + + {{ $t('container.editComposeHelper') }} + + +