fix: 重构容器编排环境变量设置逻辑 (#6600)

This commit is contained in:
John Bro 2024-09-27 22:25:37 +08:00 committed by GitHub
parent 1d326afa93
commit c557b737f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 114 additions and 139 deletions

View File

@ -200,6 +200,7 @@ type ComposeInfo struct {
Workdir string `json:"workdir"` Workdir string `json:"workdir"`
Path string `json:"path"` Path string `json:"path"`
Containers []ComposeContainer `json:"containers"` Containers []ComposeContainer `json:"containers"`
Env []string `json:"env"`
} }
type ComposeContainer struct { type ComposeContainer struct {
ContainerID string `json:"containerID"` ContainerID string `json:"containerID"`

View File

@ -4,12 +4,11 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"gopkg.in/yaml.v3"
"io" "io"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -161,7 +160,26 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
} }
BackDatas = records[start:end] 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) { 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 { if err := u.loadPath(&req); err != nil {
return false, err return false, err
} }
if err := newComposeEnv(req.Path, req.Env); err != nil {
return false, err
}
cmd := exec.Command("docker-compose", "-f", req.Path, "config") cmd := exec.Command("docker-compose", "-f", req.Path, "config")
stdout, err := cmd.CombinedOutput() stdout, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@ -183,59 +204,6 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
return true, nil 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) { func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) {
if cmd.CheckIllegal(req.Name, req.Path) { if cmd.CheckIllegal(req.Name, req.Path) {
return "", buserr.New(constant.ErrCmdIllegal) return "", buserr.New(constant.ErrCmdIllegal)
@ -260,11 +228,8 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error)
if err != nil { if err != nil {
return "", err return "", err
} }
if len(req.Env) > 0 { if err := newComposeEnv(req.Path, req.Env); err != nil {
if err := updateDockerComposeWithEnv(req); err != nil { return "", err
fmt.Printf("failed to update docker-compose.yml with env: %v\n", err)
return "", err
}
} }
go func() { go func() {
defer file.Close() defer file.Close()
@ -320,60 +285,6 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
return nil 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 { func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
if cmd.CheckIllegal(req.Name, req.Path) { if cmd.CheckIllegal(req.Name, req.Path) {
return buserr.New(constant.ErrCmdIllegal) return buserr.New(constant.ErrCmdIllegal)
@ -382,21 +293,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
if err != nil { if err != nil {
return fmt.Errorf("load file with path %s failed, %v", req.Path, err) return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
} }
if len(req.Env) > 0 { file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640)
if err := updateComposeWithEnv(req); err != nil { if err != nil {
return fmt.Errorf("failed to update docker-compose with env: %v", err) return 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 err := newComposeEnv(req.Path, req.Env); err != nil {
return err
}
if stdout, err := compose.Up(req.Path); err != nil { if stdout, err := compose.Up(req.Path); err != nil {
if err := recreateCompose(string(oldFile), 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) 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 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
}

View File

@ -241,6 +241,8 @@ export namespace Container {
path: string; path: string;
containers: Array<ComposeContainer>; containers: Array<ComposeContainer>;
expand: boolean; expand: boolean;
envStr: string;
env: Array<string>;
} }
export interface ComposeContainer { export interface ComposeContainer {
name: string; name: string;
@ -268,6 +270,7 @@ export namespace Container {
path: string; path: string;
content: string; content: string;
env: Array<string>; env: Array<string>;
createdBy: string;
} }
export interface TemplateCreate { export interface TemplateCreate {

View File

@ -655,7 +655,7 @@ const message = {
privilegedHelper: privilegedHelper:
'Allows the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!', 'Allows the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!',
editComposeHelper: 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', upgradeHelper: 'Repository Name/Image Name: Image Version',
upgradeWarning2: upgradeWarning2:

View File

@ -629,7 +629,8 @@ const message = {
emptyUser: '為空時將使用容器默認的用戶登錄', emptyUser: '為空時將使用容器默認的用戶登錄',
privileged: '特權模式', privileged: '特權模式',
privilegedHelper: '允許容器在主機上執行某些特權操作可能會增加容器風險請謹慎開啟', privilegedHelper: '允許容器在主機上執行某些特權操作可能會增加容器風險請謹慎開啟',
editComposeHelper: '在菜單中手動輸入的環境變量會覆蓋原有的同名變量若不存在則新增', editComposeHelper:
'注意設置的環境變數會默認寫入 1panel.env 文件\n若需在容器中使用這些參數還需在 compose 文件中手動添加 env_file 引用',
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本', upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
upgradeWarning2: '升級操作需要重建容器任何未持久化的數據將會丟失是否繼續', upgradeWarning2: '升級操作需要重建容器任何未持久化的數據將會丟失是否繼續',

View File

@ -632,8 +632,8 @@ const message = {
emptyUser: '为空时将使用容器默认的用户登录', emptyUser: '为空时将使用容器默认的用户登录',
privileged: '特权模式', privileged: '特权模式',
privilegedHelper: '允许容器在主机上执行某些特权操作可能会增加容器风险谨慎开启', privilegedHelper: '允许容器在主机上执行某些特权操作可能会增加容器风险谨慎开启',
editComposeHelper: '菜单中手动输入的环境变量会覆盖原有的同名变量如果不存在则新增', editComposeHelper:
'注意设置的环境变量会默认写入 1panel.env 文件\n如需在容器中使用这些参数还需在 compose 文件中手动添加 env_file 引用',
upgradeHelper: '仓库名称/镜像名称:镜像版本', upgradeHelper: '仓库名称/镜像名称:镜像版本',
upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续', upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续',
oldImage: '当前镜像', oldImage: '当前镜像',

View File

@ -66,7 +66,7 @@
placeholder="#Define or paste the content of your docker-compose file here" placeholder="#Define or paste the content of your docker-compose file here"
:indent-with-tab="true" :indent-with-tab="true"
:tabSize="4" :tabSize="4"
style="width: 100%; height: calc(100vh - 376px)" style="width: 100%; height: calc(100vh - 400px)"
:lineWrapping="true" :lineWrapping="true"
:matchBrackets="true" :matchBrackets="true"
theme="cobalt" theme="cobalt"
@ -94,6 +94,18 @@
v-model="form.envStr" v-model="form.envStr"
/> />
</el-form-item> </el-form-item>
<span class="input-help whitespace-break-spaces">{{ $t('container.editComposeHelper') }}</span>
<codemirror
v-model="form.envFileContent"
:autofocus="true"
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
></codemirror>
</el-form> </el-form>
</el-col> </el-col>
</el-row> </el-row>
@ -153,6 +165,7 @@ const form = reactive({
template: null as number, template: null as number,
env: [], env: [],
envStr: '', envStr: '',
envFileContent: `env_file:\n - 1panel.env`,
}); });
const rules = reactive({ const rules = reactive({
name: [Rules.requiredInput, Rules.imageName], name: [Rules.requiredInput, Rules.imageName],

View File

@ -19,7 +19,7 @@
placeholder="#Define or paste the content of your docker-compose file here" placeholder="#Define or paste the content of your docker-compose file here"
:indent-with-tab="true" :indent-with-tab="true"
:tabSize="4" :tabSize="4"
style="width: 100%; height: calc(100vh - 175px)" style="width: 100%; height: calc(100vh - 300px)"
:lineWrapping="true" :lineWrapping="true"
:matchBrackets="true" :matchBrackets="true"
theme="cobalt" theme="cobalt"
@ -28,7 +28,7 @@
v-model="content" v-model="content"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('container.env')" prop="environmentStr"> <el-form-item :label="$t('container.env')" prop="environmentStr" v-if="createdBy === '1Panel'">
<el-input <el-input
type="textarea" type="textarea"
:placeholder="$t('container.tagHelper')" :placeholder="$t('container.tagHelper')"
@ -36,7 +36,18 @@
v-model="environmentStr" v-model="environmentStr"
/> />
</el-form-item> </el-form-item>
<span class="input-help">{{ $t('container.editComposeHelper') }}</span> <span class="input-help whitespace-break-spaces">{{ $t('container.editComposeHelper') }}</span>
<codemirror
v-model="envFileContent"
:autofocus="true"
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
></codemirror>
</el-form> </el-form>
</el-col> </el-col>
</el-row> </el-row>
@ -64,6 +75,7 @@ import { MsgSuccess } from '@/utils/message';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
const emit = defineEmits<{ (e: 'search'): void }>();
const loading = ref(false); const loading = ref(false);
const composeVisible = ref(false); const composeVisible = ref(false);
const extensions = [javascript(), oneDark]; const extensions = [javascript(), oneDark];
@ -71,6 +83,9 @@ const path = ref();
const content = ref(); const content = ref();
const name = ref(); const name = ref();
const environmentStr = ref(); const environmentStr = ref();
const environmentEnv = ref();
const createdBy = ref();
const envFileContent = `env_file:\n - 1panel.env`;
const onSubmitEdit = async () => { const onSubmitEdit = async () => {
const param = { const param = {
@ -78,6 +93,7 @@ const onSubmitEdit = async () => {
path: path.value, path: path.value,
content: content.value, content: content.value,
env: environmentStr.value, env: environmentStr.value,
createdBy: createdBy.value,
}; };
if (environmentStr.value != undefined) { if (environmentStr.value != undefined) {
param.env = environmentStr.value.split('\n'); param.env = environmentStr.value.split('\n');
@ -88,6 +104,7 @@ const onSubmitEdit = async () => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
composeVisible.value = false; composeVisible.value = false;
emit('search');
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
@ -98,6 +115,9 @@ interface DialogProps {
name: string; name: string;
path: string; path: string;
content: string; content: string;
env: Array<string>;
envStr: string;
createdBy: string;
} }
const acceptParams = (props: DialogProps): void => { const acceptParams = (props: DialogProps): void => {
@ -105,7 +125,9 @@ const acceptParams = (props: DialogProps): void => {
path.value = props.path; path.value = props.path;
name.value = props.name; name.value = props.name;
content.value = props.content; content.value = props.content;
environmentStr.value = ''; createdBy.value = props.createdBy;
environmentEnv.value = props.env || [];
environmentStr.value = environmentEnv.value.join('\n');
}; };
const handleClose = () => { const handleClose = () => {
composeVisible.value = false; composeVisible.value = false;

View File

@ -79,7 +79,7 @@
</template> </template>
</LayoutContent> </LayoutContent>
<EditDialog ref="dialogEditRef" /> <EditDialog @search="search" ref="dialogEditRef" />
<CreateDialog @search="search" ref="dialogRef" /> <CreateDialog @search="search" ref="dialogRef" />
<DeleteDialog @search="search" ref="dialogDelRef" /> <DeleteDialog @search="search" ref="dialogDelRef" />
</div> </div>
@ -207,6 +207,8 @@ const onEdit = async (row: Container.ComposeInfo) => {
name: row.name, name: row.name,
path: row.path, path: row.path,
content: res.data, content: res.data,
env: row.env,
createdBy: row.createdBy,
}; };
dialogEditRef.value!.acceptParams(params); dialogEditRef.value!.acceptParams(params);
}; };