mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
fix: 创建和编辑 compose 文件支持设置环境变量 (#6503)
This commit is contained in:
parent
641812e7c1
commit
43f95fd40e
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -254,6 +254,8 @@ export namespace Container {
|
||||
file: string;
|
||||
path: string;
|
||||
template: number;
|
||||
env: Array<string>;
|
||||
envStr: string;
|
||||
}
|
||||
export interface ComposeOperation {
|
||||
name: string;
|
||||
@ -265,6 +267,7 @@ export namespace Container {
|
||||
name: string;
|
||||
path: string;
|
||||
content: string;
|
||||
env: Array<string>;
|
||||
}
|
||||
|
||||
export interface TemplateCreate {
|
||||
|
@ -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:
|
||||
|
@ -629,6 +629,7 @@ const message = {
|
||||
emptyUser: '為空時,將使用容器默認的用戶登錄',
|
||||
privileged: '特權模式',
|
||||
privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟!',
|
||||
editComposeHelper: '在菜單中手動輸入的環境變量會覆蓋原有的同名變量,若不存在則新增。',
|
||||
|
||||
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
|
||||
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||
|
@ -632,6 +632,7 @@ const message = {
|
||||
emptyUser: '为空时,将使用容器默认的用户登录',
|
||||
privileged: '特权模式',
|
||||
privilegedHelper: '允许容器在主机上执行某些特权操作,可能会增加容器风险,谨慎开启!',
|
||||
editComposeHelper: '菜单中手动输入的环境变量会覆盖原有的同名变量,如果不存在则新增',
|
||||
|
||||
upgradeHelper: '仓库名称/镜像名称:镜像版本',
|
||||
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||
|
@ -86,6 +86,14 @@
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.env')" prop="envStr">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="$t('container.tagHelper')"
|
||||
:rows="3"
|
||||
v-model="form.envStr"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -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) => {
|
||||
|
@ -9,20 +9,37 @@
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('commons.button.edit')" :resource="name" :back="handleClose" />
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
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)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="content"
|
||||
/>
|
||||
<div v-loading="loading" style="padding-bottom: 20px">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form ref="formRef" @submit.prevent label-position="top">
|
||||
<el-form-item>
|
||||
<codemirror
|
||||
:autofocus="true"
|
||||
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)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="content"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.env')" prop="environmentStr">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="$t('container.tagHelper')"
|
||||
:rows="3"
|
||||
v-model="environmentStr"
|
||||
/>
|
||||
</el-form-item>
|
||||
<span class="input-help">{{ $t('container.editComposeHelper') }}</span>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@ -45,6 +62,7 @@ import { composeUpdate } from '@/api/modules/container';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { ElForm } from 'element-plus';
|
||||
|
||||
const loading = ref(false);
|
||||
const composeVisible = ref(false);
|
||||
@ -52,13 +70,18 @@ const extensions = [javascript(), oneDark];
|
||||
const path = ref();
|
||||
const content = ref();
|
||||
const name = ref();
|
||||
const environmentStr = ref();
|
||||
|
||||
const onSubmitEdit = async () => {
|
||||
const param = {
|
||||
name: name.value,
|
||||
path: path.value,
|
||||
content: content.value,
|
||||
env: environmentStr.value,
|
||||
};
|
||||
if (environmentStr.value != undefined) {
|
||||
param.env = environmentStr.value.split('\n');
|
||||
}
|
||||
loading.value = true;
|
||||
await composeUpdate(param)
|
||||
.then(() => {
|
||||
@ -82,6 +105,7 @@ const acceptParams = (props: DialogProps): void => {
|
||||
path.value = props.path;
|
||||
name.value = props.name;
|
||||
content.value = props.content;
|
||||
environmentStr.value = '';
|
||||
};
|
||||
const handleClose = () => {
|
||||
composeVisible.value = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user