diff --git a/backend/app/dto/container.go b/backend/app/dto/container.go index b78ec1b30..cadc1c780 100644 --- a/backend/app/dto/container.go +++ b/backend/app/dto/container.go @@ -200,6 +200,7 @@ type ComposeInfo struct { Workdir string `json:"workdir"` Path string `json:"path"` Containers []ComposeContainer `json:"containers"` + Env []string `json:"env"` } type ComposeContainer struct { ContainerID string `json:"containerID"` diff --git a/backend/app/service/container_compose.go b/backend/app/service/container_compose.go index 545f7b12d..953d01b29 100644 --- a/backend/app/service/container_compose.go +++ b/backend/app/service/container_compose.go @@ -4,12 +4,11 @@ import ( "bufio" "errors" "fmt" - "gopkg.in/yaml.v3" "io" - "io/ioutil" "os" "os/exec" "path" + "path/filepath" "sort" "strings" "time" @@ -161,7 +160,26 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface } BackDatas = records[start:end] } - return int64(total), BackDatas, nil + listItem := loadEnv(BackDatas) + return int64(total), listItem, nil +} + +func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo { + for i := 0; i < len(list); i++ { + envFilePath := filepath.Join(path.Dir(list[i].Path), "1panel.env") + file, err := os.ReadFile(envFilePath) + if err != nil { + continue + } + lines := strings.Split(string(file), "\n") + for _, line := range lines { + lineItem := strings.TrimSpace(line) + if len(lineItem) != 0 && !strings.HasPrefix(lineItem, "#") { + list[i].Env = append(list[i].Env, lineItem) + } + } + } + return list } func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { @@ -175,6 +193,9 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) { if err := u.loadPath(&req); err != nil { return false, err } + if err := newComposeEnv(req.Path, req.Env); err != nil { + return false, err + } cmd := exec.Command("docker-compose", "-f", req.Path, "config") stdout, err := cmd.CombinedOutput() if err != nil { @@ -183,59 +204,6 @@ 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) @@ -260,11 +228,8 @@ 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 - } + if err := newComposeEnv(req.Path, req.Env); err != nil { + return "", err } go func() { defer file.Close() @@ -320,60 +285,6 @@ 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) @@ -382,21 +293,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error { if err != nil { return fmt.Errorf("load file with path %s failed, %v", req.Path, 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) + 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, now start to docker-compose restart", req.Path) + if err := newComposeEnv(req.Path, req.Env); err != nil { + return err + } + 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) @@ -445,3 +355,26 @@ func recreateCompose(content, path string) error { } return nil } + +func newComposeEnv(pathItem string, env []string) error { + if len(env) == 0 { + return nil + } + envFilePath := path.Join(path.Dir(pathItem), "1panel.env") + + file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + global.LOG.Errorf("failed to create env file: %v", err) + return err + } + defer file.Close() + for _, env := range env { + envItem := strings.TrimSpace(env) + if _, err := file.WriteString(fmt.Sprintf("%s\n", envItem)); err != nil { + global.LOG.Errorf("failed to write env to file: %v", err) + return err + } + } + global.LOG.Infof("1panel.env file successfully created or updated with env variables in %s", envFilePath) + return nil +} diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index 5c9b63c97..bad9293cd 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -241,6 +241,8 @@ export namespace Container { path: string; containers: Array; expand: boolean; + envStr: string; + env: Array; } export interface ComposeContainer { name: string; @@ -268,6 +270,7 @@ export namespace Container { path: string; content: string; env: Array; + createdBy: string; } export interface TemplateCreate { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 4efdf1fbd..10a7dab0b 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -655,7 +655,7 @@ const message = { 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.', + 'Note: The environment variables set will be written to the 1panel.env file by default.\nIf you want to use these parameters in the container, you also need to manually add an env_file reference in the compose file.', 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 5692fc6fa..a684d860f 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -629,7 +629,8 @@ const message = { emptyUser: '為空時,將使用容器默認的用戶登錄', privileged: '特權模式', privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟!', - editComposeHelper: '在菜單中手動輸入的環境變量會覆蓋原有的同名變量,若不存在則新增。', + editComposeHelper: + '注意:設置的環境變數會默認寫入 1panel.env 文件。\n若需在容器中使用這些參數,還需在 compose 文件中手動添加 env_file 引用。', upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本', upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index bd0a29f8c..90247e846 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -632,8 +632,8 @@ const message = { emptyUser: '为空时,将使用容器默认的用户登录', privileged: '特权模式', privilegedHelper: '允许容器在主机上执行某些特权操作,可能会增加容器风险,谨慎开启!', - editComposeHelper: '菜单中手动输入的环境变量会覆盖原有的同名变量,如果不存在则新增', - + editComposeHelper: + '注意:设置的环境变量会默认写入 1panel.env 文件。\n如需在容器中使用这些参数,还需在 compose 文件中手动添加 env_file 引用。', upgradeHelper: '仓库名称/镜像名称:镜像版本', upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?', oldImage: '当前镜像', diff --git a/frontend/src/views/container/compose/create/index.vue b/frontend/src/views/container/compose/create/index.vue index 107023cd3..334e4551b 100644 --- a/frontend/src/views/container/compose/create/index.vue +++ b/frontend/src/views/container/compose/create/index.vue @@ -66,7 +66,7 @@ placeholder="#Define or paste the content of your docker-compose file here" :indent-with-tab="true" :tabSize="4" - style="width: 100%; height: calc(100vh - 376px)" + style="width: 100%; height: calc(100vh - 400px)" :lineWrapping="true" :matchBrackets="true" theme="cobalt" @@ -94,6 +94,18 @@ v-model="form.envStr" /> + {{ $t('container.editComposeHelper') }} + @@ -153,6 +165,7 @@ const form = reactive({ template: null as number, env: [], envStr: '', + envFileContent: `env_file:\n - 1panel.env`, }); const rules = reactive({ name: [Rules.requiredInput, Rules.imageName], diff --git a/frontend/src/views/container/compose/edit/index.vue b/frontend/src/views/container/compose/edit/index.vue index 33227d5b5..a4b00af4a 100644 --- a/frontend/src/views/container/compose/edit/index.vue +++ b/frontend/src/views/container/compose/edit/index.vue @@ -19,7 +19,7 @@ placeholder="#Define or paste the content of your docker-compose file here" :indent-with-tab="true" :tabSize="4" - style="width: 100%; height: calc(100vh - 175px)" + style="width: 100%; height: calc(100vh - 300px)" :lineWrapping="true" :matchBrackets="true" theme="cobalt" @@ -28,7 +28,7 @@ v-model="content" /> - + - {{ $t('container.editComposeHelper') }} + {{ $t('container.editComposeHelper') }} + @@ -64,6 +75,7 @@ import { MsgSuccess } from '@/utils/message'; import DrawerHeader from '@/components/drawer-header/index.vue'; import { ElForm } from 'element-plus'; +const emit = defineEmits<{ (e: 'search'): void }>(); const loading = ref(false); const composeVisible = ref(false); const extensions = [javascript(), oneDark]; @@ -71,6 +83,9 @@ const path = ref(); const content = ref(); const name = ref(); const environmentStr = ref(); +const environmentEnv = ref(); +const createdBy = ref(); +const envFileContent = `env_file:\n - 1panel.env`; const onSubmitEdit = async () => { const param = { @@ -78,6 +93,7 @@ const onSubmitEdit = async () => { path: path.value, content: content.value, env: environmentStr.value, + createdBy: createdBy.value, }; if (environmentStr.value != undefined) { param.env = environmentStr.value.split('\n'); @@ -88,6 +104,7 @@ const onSubmitEdit = async () => { loading.value = false; MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); composeVisible.value = false; + emit('search'); }) .catch(() => { loading.value = false; @@ -98,6 +115,9 @@ interface DialogProps { name: string; path: string; content: string; + env: Array; + envStr: string; + createdBy: string; } const acceptParams = (props: DialogProps): void => { @@ -105,7 +125,9 @@ const acceptParams = (props: DialogProps): void => { path.value = props.path; name.value = props.name; content.value = props.content; - environmentStr.value = ''; + createdBy.value = props.createdBy; + environmentEnv.value = props.env || []; + environmentStr.value = environmentEnv.value.join('\n'); }; const handleClose = () => { composeVisible.value = false; diff --git a/frontend/src/views/container/compose/index.vue b/frontend/src/views/container/compose/index.vue index 87eb05efd..a8680b5a8 100644 --- a/frontend/src/views/container/compose/index.vue +++ b/frontend/src/views/container/compose/index.vue @@ -79,7 +79,7 @@ - + @@ -207,6 +207,8 @@ const onEdit = async (row: Container.ComposeInfo) => { name: row.name, path: row.path, content: res.data, + env: row.env, + createdBy: row.createdBy, }; dialogEditRef.value!.acceptParams(params); };