feat: 完成 docker 配置功能

pull/37/head
ssongliu 2022-11-14 19:19:42 +08:00 committed by ssongliu
parent 13bc73ac3c
commit 0dfb9bd5c7
15 changed files with 449 additions and 5 deletions

View File

@ -0,0 +1,48 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) LoadDaemonJson(c *gin.Context) {
conf, err := dockerService.LoadDockerConf()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, conf)
}
func (b *BaseApi) UpdateDaemonJson(c *gin.Context) {
var req dto.DaemonJsonConf
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := dockerService.UpdateConf(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateDaemonJsonByFile(c *gin.Context) {
var req dto.DaemonJsonUpdateByFile
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := dockerService.UpdateConfByFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -18,6 +18,7 @@ var (
composeTemplateService = service.ServiceGroupApp.ComposeTemplateService
imageRepoService = service.ServiceGroupApp.ImageRepoService
imageService = service.ServiceGroupApp.ImageService
dockerService = service.ServiceGroupApp.DockerService
mysqlService = service.ServiceGroupApp.MysqlService
redisService = service.ServiceGroupApp.RedisService

15
backend/app/dto/docker.go Normal file
View File

@ -0,0 +1,15 @@
package dto
type DaemonJsonUpdateByFile struct {
Path string `json:"path" validate:"required"`
File string `json:"file"`
}
type DaemonJsonConf struct {
Status string `json:"status"`
Mirrors []string `json:"registryMirrors"`
Registries []string `json:"insecureRegistries"`
Bip string `json:"bip"`
LiveRestore bool `json:"liveRestore"`
CgroupDriver string `json:"cgroupDriver"`
}

View File

@ -0,0 +1,125 @@
package service
import (
"bufio"
"encoding/json"
"io/ioutil"
"os"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto"
)
type DockerService struct{}
type IDockerService interface {
UpdateConf(req dto.DaemonJsonConf) error
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
LoadDockerConf() (*dto.DaemonJsonConf, error)
}
func NewIDockerService() IDockerService {
return &DockerService{}
}
type daemonJsonItem struct {
Status string `json:"status"`
Mirrors []string `json:"registry-mirrors"`
Registries []string `json:"insecure-registries"`
Bip string `json:"bip"`
LiveRestore bool `json:"live-restore"`
ExecOpts []string `json:"exec-opts"`
}
func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
file, err := ioutil.ReadFile("/opt/1Panel/docker/daemon.json")
if err != nil {
return nil, err
}
var conf daemonJsonItem
deamonMap := make(map[string]interface{})
if err := json.Unmarshal(file, &deamonMap); err != nil {
return nil, err
}
arr, err := json.Marshal(deamonMap)
if err != nil {
return nil, err
}
if err := json.Unmarshal(arr, &conf); err != nil {
return nil, err
}
driver := "cgroupfs"
for _, opt := range conf.ExecOpts {
if strings.HasPrefix(opt, "native.cgroupdriver=") {
driver = strings.ReplaceAll(opt, "native.cgroupdriver=", "")
break
}
}
data := dto.DaemonJsonConf{
Status: conf.Status,
Mirrors: conf.Mirrors,
Registries: conf.Registries,
Bip: conf.Bip,
LiveRestore: conf.LiveRestore,
CgroupDriver: driver,
}
return &data, nil
}
func (u *DockerService) UpdateConf(req dto.DaemonJsonConf) error {
file, err := ioutil.ReadFile("/opt/1Panel/docker/daemon.json")
if err != nil {
return err
}
deamonMap := make(map[string]interface{})
if err := json.Unmarshal(file, &deamonMap); err != nil {
return err
}
deamonMap["registry-mirrors"] = req.Mirrors
deamonMap["insecure-registries"] = req.Registries
deamonMap["bip"] = req.Bip
deamonMap["live-restore"] = req.LiveRestore
if opts, ok := deamonMap["exec-opts"]; ok {
if optsValue, isArray := opts.([]interface{}); isArray {
for i := 0; i < len(optsValue); i++ {
if opt, isStr := optsValue[i].(string); isStr {
if strings.HasPrefix(opt, "native.cgroupdriver=") {
optsValue[i] = "native.cgroupdriver=" + req.CgroupDriver
break
}
}
}
}
} else {
if req.CgroupDriver == "systemd" {
deamonMap["exec-opts"] = []string{"native.cgroupdriver=systemd"}
}
}
newJson, err := json.MarshalIndent(deamonMap, "", "\t")
if err != nil {
return err
}
if err := ioutil.WriteFile("/opt/1Panel/docker/daemon.json", newJson, 0640); err != nil {
return err
}
return nil
}
func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
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.File)
write.Flush()
// cmd := exec.Command("systemctl", "restart", "docker")
// stdout, err := cmd.CombinedOutput()
// if err != nil {
// return errors.New(string(stdout))
// }
return nil
}

View File

@ -0,0 +1,32 @@
package service
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"testing"
)
func TestDocker(t *testing.T) {
file, err := ioutil.ReadFile("/opt/1Panel/docker/daemon.json")
if err != nil {
fmt.Println(err)
}
var conf daemonJsonItem
deamonMap := make(map[string]interface{})
if err := json.Unmarshal(file, &deamonMap); err != nil {
fmt.Println(err)
}
arr, err := json.Marshal(deamonMap)
if err != nil {
fmt.Println(err)
}
_ = json.Unmarshal(arr, &conf)
for _, opt := range conf.ExecOpts {
if strings.HasPrefix(opt, "native.cgroupdriver=") {
fmt.Println(strings.ReplaceAll(opt, "native.cgroupdriver=", ""))
}
}
}

View File

@ -12,6 +12,7 @@ type ServiceGroup struct {
ImageService
ImageRepoService
ComposeTemplateService
DockerService
MysqlService
RedisService

View File

@ -75,7 +75,7 @@ func (u *ImageRepoService) Create(imageRepoDto dto.ImageRepoCreate) error {
if err != nil {
return err
}
if err := ioutil.WriteFile(constant.DaemonJsonDir, newJson, 0777); err != nil {
if err := ioutil.WriteFile(constant.DaemonJsonDir, newJson, 0640); err != nil {
return err
}
}

View File

@ -62,5 +62,9 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/volume/search", baseApi.SearchVolume)
baRouter.GET("/volume", baseApi.ListVolume)
baRouter.POST("/volume", baseApi.CreateVolume)
baRouter.GET("/daemonjson", baseApi.LoadDaemonJson)
baRouter.POST("/daemonjson/update", baseApi.UpdateDaemonJson)
baRouter.POST("/daemonjson/update/byfile", baseApi.UpdateDaemonJsonByFile)
}
}

View File

@ -223,4 +223,18 @@ export namespace Container {
export interface BatchDelete {
ids: Array<string>;
}
export interface DaemonJsonUpdateByFile {
path: string;
file: string;
}
export interface DaemonJsonConf {
status: string;
bip: string;
registryMirrors: Array<string>;
insecureRegistries: Array<string>;
liveRestore: boolean;
cgroupDriver: string;
}
}

View File

@ -119,3 +119,14 @@ export const upCompose = (params: Container.ComposeCreate) => {
export const ComposeOperator = (params: Container.ComposeOpration) => {
return http.post(`/containers/compose/operate`, params);
};
// docker
export const loadDaemonJson = () => {
return http.get<Container.DaemonJsonConf>(`/containers/daemonjson`);
};
export const updateDaemonJson = (params: Container.DaemonJsonConf) => {
return http.post(`/containers/daemonjson/update`, params);
};
export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) => {
return http.post(`/containers/daemonjson/update/byfile`, params);
};

View File

@ -387,6 +387,12 @@ export default {
down: '',
up: '',
operatorComposeHelper: ' Compose {0} ',
setting: '',
mirrors: '',
mirrorsHelper: '使 URL 使',
registries: '',
liveHelper: ' docker ',
},
cronjob: {
cronTask: '',

View File

@ -67,13 +67,22 @@ const containerRouter = {
},
{
path: 'template',
name: 'composeTemplate',
name: 'ComposeTemplate',
component: () => import('@/views/container/template/index.vue'),
hidden: true,
meta: {
activeMenu: '/containers',
},
},
{
path: 'setting',
name: 'ContainerSetting',
component: () => import('@/views/container/setting/index.vue'),
hidden: true,
meta: {
activeMenu: '/containers',
},
},
],
};

View File

@ -38,6 +38,14 @@
>
{{ $t('container.composeTemplate') }}
</el-radio-button>
<el-radio-button
class="topButton"
size="large"
@click="routerTo('/containers/setting')"
label="setting"
>
{{ $t('container.setting') }}
</el-radio-button>
</el-radio-group>
</el-card>
</div>

View File

@ -0,0 +1,172 @@
<template>
<div>
<Submenu activeName="setting" />
<el-card style="margin-top: 20px">
<el-radio-group v-model="confShowType">
<el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button>
<el-radio-button label="all">{{ $t('database.allConf') }}</el-radio-button>
</el-radio-group>
<el-form v-if="confShowType === 'base'" :model="form" ref="formRef" :rules="rules" label-width="120px">
<el-row style="margin-top: 20px">
<el-col :span="1"><br /></el-col>
<el-col :span="10">
<el-form-item :label="$t('container.mirrors')" prop="mirrors">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 10 }"
v-model="form.mirrors"
/>
<span class="input-help">{{ $t('container.mirrorsHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.registries')" prop="registries">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 10 }"
v-model="form.registries"
/>
</el-form-item>
<el-form-item label="bip" prop="bip">
<el-input clearable v-model="form.bip" />
</el-form-item>
<el-form-item label="live-restore" prop="liveRestore">
<el-switch v-model="form.liveRestore"></el-switch>
<span class="input-help">{{ $t('container.liveHelper') }}</span>
</el-form-item>
<el-form-item label="cgroup-driver" prop="cgroupDriver">
<el-radio-group v-model="form.cgroupDriver">
<el-radio label="cgroupfs">cgroupfs</el-radio>
<el-radio label="systemd">systemd</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave(formRef)" style="width: 90px; margin-top: 5px">
{{ $t('commons.button.save') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div v-if="confShowType === 'all'">
<codemirror
:autofocus="true"
placeholder="None data"
:indent-with-tab="true"
:tabSize="4"
style="margin-top: 10px; height: calc(100vh - 280px)"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="dockerConf"
:readOnly="true"
/>
<el-button type="primary" @click="onSaveFile" style="width: 90px; margin-top: 5px">
{{ $t('commons.button.save') }}
</el-button>
</div>
</el-card>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmitSave"></ConfirmDialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, FormInstance } from 'element-plus';
import { onMounted, reactive, ref } from 'vue';
import Submenu from '@/views/container/index.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { LoadFile } from '@/api/modules/files';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import i18n from '@/lang';
import { loadDaemonJson, updateDaemonJson, updateDaemonJsonByfile } from '@/api/modules/container';
import { Rules } from '@/global/form-rules';
const extensions = [javascript(), oneDark];
const confShowType = ref('base');
const form = reactive({
status: '',
bip: '',
mirrors: '',
registries: '',
liveRestore: false,
cgroupDriver: '',
});
const rules = reactive({
bip: [Rules.requiredInput],
});
const formRef = ref<FormInstance>();
const dockerConf = ref();
const confirmDialogRef = ref();
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (!valid) return;
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
});
};
const onSaveFile = async () => {
let params = {
header: i18n.global.t('database.confChange'),
operationInfo: i18n.global.t('database.restartNowHelper1'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialogRef.value!.acceptParams(params);
};
const onSubmitSave = async () => {
if (confShowType.value === 'all') {
let param = {
file: dockerConf.value,
path: '/opt/1Panel/docker/daemon.json',
};
await updateDaemonJsonByfile(param);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
return;
}
let param = {
status: form.status,
bip: form.bip,
registryMirrors: form.mirrors.split('\n'),
insecureRegistries: form.registries.split('\n'),
liveRestore: form.liveRestore,
cgroupDriver: form.cgroupDriver,
};
await updateDaemonJson(param);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
};
const loadMysqlConf = async () => {
const res = await LoadFile({ path: '/opt/1Panel/docker/daemon.json' });
dockerConf.value = res.data;
};
const search = async () => {
const res = await loadDaemonJson();
form.bip = res.data.bip;
form.status = res.data.status;
form.cgroupDriver = res.data.cgroupDriver;
form.liveRestore = res.data.liveRestore;
form.mirrors = res.data.registryMirrors.join('\n');
form.registries = res.data.insecureRegistries.join('\n');
};
onMounted(() => {
search();
loadMysqlConf();
});
</script>

View File

@ -83,7 +83,6 @@ import { ChangePort } from '@/api/modules/app';
const extensions = [javascript(), oneDark];
const confShowType = ref('base');
const restartNow = ref(false);
const form = reactive({
name: '',
port: 6379,
@ -166,10 +165,9 @@ const onSaveFile = async () => {
const onSubmitSave = async () => {
let param = {
file: mysqlConf.value,
restartNow: restartNow.value,
restartNow: true,
};
await updateRedisConfByFile(param);
restartNow.value = false;
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
};