1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-01 03:24:14 +08:00

feat: docker 配置自定义路径

This commit is contained in:
ssongliu 2022-12-07 17:28:14 +08:00 committed by ssongliu
parent 2fb0b663d0
commit 004b997288
14 changed files with 248 additions and 54 deletions

View File

@ -4,16 +4,17 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func (b *BaseApi) LoadDaemonJson(c *gin.Context) { func (b *BaseApi) LoadDockerStatus(c *gin.Context) {
conf, err := dockerService.LoadDockerConf() status := dockerService.LoadDockerStatus()
if err != nil { helper.SuccessWithData(c, status)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
} }
func (b *BaseApi) LoadDaemonJson(c *gin.Context) {
conf := dockerService.LoadDockerConf()
helper.SuccessWithData(c, conf) helper.SuccessWithData(c, conf)
} }
@ -38,6 +39,10 @@ func (b *BaseApi) UpdateDaemonJsonByFile(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return return
} }
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := dockerService.UpdateConfByFile(req); err != nil { if err := dockerService.UpdateConfByFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
@ -46,3 +51,22 @@ func (b *BaseApi) UpdateDaemonJsonByFile(c *gin.Context) {
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, nil)
} }
func (b *BaseApi) OperateDocker(c *gin.Context) {
var req dto.DockerOperation
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := dockerService.OperateDocker(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -21,6 +21,15 @@ func (b *BaseApi) GetSettingInfo(c *gin.Context) {
helper.SuccessWithData(c, setting) helper.SuccessWithData(c, setting)
} }
func (b *BaseApi) GetDaemonjson(c *gin.Context) {
value, err := settingService.GetDaemonjson()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, value)
}
func (b *BaseApi) UpdateSetting(c *gin.Context) { func (b *BaseApi) UpdateSetting(c *gin.Context) {
var req dto.SettingUpdate var req dto.SettingUpdate
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -13,3 +13,7 @@ type DaemonJsonConf struct {
LiveRestore bool `json:"liveRestore"` LiveRestore bool `json:"liveRestore"`
CgroupDriver string `json:"cgroupDriver"` CgroupDriver string `json:"cgroupDriver"`
} }
type DockerOperation struct {
Operation string `json:"operation" validate:"required,oneof=start restart stop"`
}

View File

@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -18,7 +19,9 @@ type DockerService struct{}
type IDockerService interface { type IDockerService interface {
UpdateConf(req dto.DaemonJsonConf) error UpdateConf(req dto.DaemonJsonConf) error
UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error UpdateConfByFile(info dto.DaemonJsonUpdateByFile) error
LoadDockerConf() (*dto.DaemonJsonConf, error) LoadDockerStatus() string
LoadDockerConf() *dto.DaemonJsonConf
OperateDocker(req dto.DockerOperation) error
} }
func NewIDockerService() IDockerService { func NewIDockerService() IDockerService {
@ -34,22 +37,44 @@ type daemonJsonItem struct {
ExecOpts []string `json:"exec-opts"` ExecOpts []string `json:"exec-opts"`
} }
func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) { func (u *DockerService) LoadDockerStatus() string {
file, err := ioutil.ReadFile(constant.DaemonJsonDir) status := constant.StatusRunning
if _, err := docker.NewDockerClient(); err != nil {
status = constant.Stopped
}
return status
}
func (u *DockerService) LoadDockerConf() *dto.DaemonJsonConf {
status := constant.StatusRunning
if _, err := docker.NewDockerClient(); err != nil {
status = constant.Stopped
}
fileSetting, err := settingRepo.Get(settingRepo.WithByKey("DaemonJsonPath"))
if err != nil { if err != nil {
return nil, err return &dto.DaemonJsonConf{Status: status}
}
if len(fileSetting.Value) == 0 {
return &dto.DaemonJsonConf{Status: status}
}
if _, err := os.Stat(fileSetting.Value); err != nil {
return &dto.DaemonJsonConf{Status: status}
}
file, err := ioutil.ReadFile(fileSetting.Value)
if err != nil {
return &dto.DaemonJsonConf{Status: status}
} }
var conf daemonJsonItem var conf daemonJsonItem
deamonMap := make(map[string]interface{}) deamonMap := make(map[string]interface{})
if err := json.Unmarshal(file, &deamonMap); err != nil { if err := json.Unmarshal(file, &deamonMap); err != nil {
return nil, err return &dto.DaemonJsonConf{Status: status}
} }
arr, err := json.Marshal(deamonMap) arr, err := json.Marshal(deamonMap)
if err != nil { if err != nil {
return nil, err return &dto.DaemonJsonConf{Status: status}
} }
if err := json.Unmarshal(arr, &conf); err != nil { if err := json.Unmarshal(arr, &conf); err != nil {
return nil, err return &dto.DaemonJsonConf{Status: status}
} }
driver := "cgroupfs" driver := "cgroupfs"
for _, opt := range conf.ExecOpts { for _, opt := range conf.ExecOpts {
@ -59,7 +84,7 @@ func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
} }
} }
data := dto.DaemonJsonConf{ data := dto.DaemonJsonConf{
Status: conf.Status, Status: status,
Mirrors: conf.Mirrors, Mirrors: conf.Mirrors,
Registries: conf.Registries, Registries: conf.Registries,
Bip: conf.Bip, Bip: conf.Bip,
@ -67,18 +92,32 @@ func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
CgroupDriver: driver, CgroupDriver: driver,
} }
return &data, nil return &data
} }
func (u *DockerService) UpdateConf(req dto.DaemonJsonConf) error { func (u *DockerService) UpdateConf(req dto.DaemonJsonConf) error {
file, err := ioutil.ReadFile(constant.DaemonJsonDir) fileSetting, err := settingRepo.Get(settingRepo.WithByKey("DaemonJsonPath"))
if err != nil {
return err
}
if len(fileSetting.Value) == 0 {
return errors.New("error daemon.json path in request")
}
if _, err := os.Stat(fileSetting.Value); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(fileSetting.Value, os.ModePerm); err != nil {
if err != nil {
return err
}
}
}
file, err := ioutil.ReadFile(fileSetting.Value)
if err != nil { if err != nil {
return err return err
} }
deamonMap := make(map[string]interface{}) deamonMap := make(map[string]interface{})
if err := json.Unmarshal(file, &deamonMap); err != nil { _ = json.Unmarshal(file, &deamonMap)
return err
}
if len(req.Registries) == 0 { if len(req.Registries) == 0 {
delete(deamonMap, "insecure-registries") delete(deamonMap, "insecure-registries")
} else { } else {
@ -119,7 +158,7 @@ func (u *DockerService) UpdateConf(req dto.DaemonJsonConf) error {
if err != nil { if err != nil {
return err return err
} }
if err := ioutil.WriteFile(constant.DaemonJsonDir, newJson, 0640); err != nil { if err := ioutil.WriteFile(fileSetting.Value, newJson, 0640); err != nil {
return err return err
} }
@ -148,3 +187,12 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
} }
return nil return nil
} }
func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
cmd := exec.Command("systemctl", req.Operation, "docker")
stdout, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(stdout))
}
return nil
}

View File

@ -16,6 +16,7 @@ type SettingService struct{}
type ISettingService interface { type ISettingService interface {
GetSettingInfo() (*dto.SettingInfo, error) GetSettingInfo() (*dto.SettingInfo, error)
GetDaemonjson() (string, error)
Update(c *gin.Context, key, value string) error Update(c *gin.Context, key, value string) error
UpdatePassword(c *gin.Context, old, new string) error UpdatePassword(c *gin.Context, old, new string) error
HandlePasswordExpired(c *gin.Context, old, new string) error HandlePasswordExpired(c *gin.Context, old, new string) error
@ -48,6 +49,14 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
return &info, err return &info, err
} }
func (u *SettingService) GetDaemonjson() (string, error) {
setting, err := settingRepo.Get(settingRepo.WithByKey("DaemonJsonPath"))
if err != nil {
return "", err
}
return setting.Value, nil
}
func (u *SettingService) Update(c *gin.Context, key, value string) error { func (u *SettingService) Update(c *gin.Context, key, value string) error {
if key == "ExpirationDays" { if key == "ExpirationDays" {
timeout, _ := strconv.Atoi(value) timeout, _ := strconv.Atoi(value)

View File

@ -107,6 +107,10 @@ var AddTableSetting = &gormigrate.Migration{
return err return err
} }
if err := tx.Create(&model.Setting{Key: "DaemonJsonPath", Value: "/opt/1Panel/docker/conf/daemon.json"}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "MessageType", Value: "none"}).Error; err != nil { if err := tx.Create(&model.Setting{Key: "MessageType", Value: "none"}).Error; err != nil {
return err return err
} }

View File

@ -65,6 +65,8 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/volume", baseApi.CreateVolume) baRouter.POST("/volume", baseApi.CreateVolume)
baRouter.GET("/daemonjson", baseApi.LoadDaemonJson) baRouter.GET("/daemonjson", baseApi.LoadDaemonJson)
baRouter.GET("docker/status", baseApi.LoadDockerStatus)
baRouter.POST("/docker/operate", baseApi.OperateDocker)
baRouter.POST("/daemonjson/update", baseApi.UpdateDaemonJson) baRouter.POST("/daemonjson/update", baseApi.UpdateDaemonJson)
baRouter.POST("/daemonjson/update/byfile", baseApi.UpdateDaemonJsonByFile) baRouter.POST("/daemonjson/update/byfile", baseApi.UpdateDaemonJsonByFile)
} }

View File

@ -28,6 +28,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/time/sync", baseApi.SyncTime)
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
settingRouter.GET("/mfa", baseApi.GetMFA) settingRouter.GET("/mfa", baseApi.GetMFA)
settingRouter.GET("/daemonjson", baseApi.GetDaemonjson)
settingRouter.POST("/mfa/bind", baseApi.MFABind) settingRouter.POST("/mfa/bind", baseApi.MFABind)
} }
} }

View File

@ -234,7 +234,9 @@ export namespace Container {
path: string; path: string;
file: string; file: string;
} }
export interface DockerOperate {
operation: string;
}
export interface DaemonJsonConf { export interface DaemonJsonConf {
status: string; status: string;
bip: string; bip: string;

View File

@ -124,6 +124,9 @@ export const composeUpdate = (params: Container.ComposeUpdate) => {
}; };
// docker // docker
export const dockerOperate = (params: Container.DockerOperate) => {
return http.post(`/containers/docker/operate`, params);
};
export const loadDaemonJson = () => { export const loadDaemonJson = () => {
return http.get<Container.DaemonJsonConf>(`/containers/daemonjson`); return http.get<Container.DaemonJsonConf>(`/containers/daemonjson`);
}; };

View File

@ -29,6 +29,10 @@ export const getMFA = () => {
return http.get<Setting.MFAInfo>(`/settings/mfa`, {}); return http.get<Setting.MFAInfo>(`/settings/mfa`, {});
}; };
export const loadDaemonJsonPath = () => {
return http.get<string>(`/settings/daemonjson`, {});
};
export const bindMFA = (param: Setting.MFABind) => { export const bindMFA = (param: Setting.MFABind) => {
return http.post(`/settings/mfa/bind`, param); return http.post(`/settings/mfa/bind`, param);
}; };

View File

@ -449,6 +449,16 @@ export default {
composeDetailHelper: composeDetailHelper:
'The compose is created external to 1Panel. The start and stop operations are not supported.', 'The compose is created external to 1Panel. The start and stop operations are not supported.',
composeOperatorHelper: '{1} operation will be performed on {0}. Do you want to continue?', composeOperatorHelper: '{1} operation will be performed on {0}. Do you want to continue?',
setting: 'Setting',
dockerStatus: 'Docker Service',
daemonJsonPathHelper: 'Ensure that the configuration path is the same as that specified in docker.service.',
mirrors: 'Registry mirrors',
mirrorsHelper:
'If empty, mirror acceleration is disabled. The accelerated URL is used first for the operation, and will skipped when the request times out',
registries: 'Insecure registries',
liveHelper: 'Whether to close all containers when stopping the docker service',
daemonJsonPath: 'Conf Path',
}, },
cronjob: { cronjob: {
cronTask: 'Task', cronTask: 'Task',

View File

@ -458,11 +458,14 @@ export default {
composeDetailHelper: ' compose 1Panel 编排外部创建暂不支持启停操作', composeDetailHelper: ' compose 1Panel 编排外部创建暂不支持启停操作',
composeOperatorHelper: '将对 {0} 进行 {1} 操作是否继续', composeOperatorHelper: '将对 {0} 进行 {1} 操作是否继续',
setting: '容器配置', setting: '配置',
dockerStatus: 'Docker 服务',
daemonJsonPathHelper: '请保证配置路径与 docker.service 中指定的配置路径保持一致',
mirrors: '镜像加速', mirrors: '镜像加速',
mirrorsHelper: '为空则关闭镜像加速优先使用加速 URL 执行操作请求超时将跳过使用默认加速方式', mirrorsHelper: '为空则关闭镜像加速优先使用加速 URL 执行操作请求超时将跳过使用默认加速方式',
registries: '私有仓库', registries: '私有仓库',
liveHelper: '停止 docker 服务时是否关闭所有容器', liveHelper: '停止 docker 服务时是否关闭所有容器',
daemonJsonPath: '配置路径',
}, },
cronjob: { cronjob: {
cronTask: '计划任务', cronTask: '计划任务',

View File

@ -1,13 +1,51 @@
<template> <template>
<div v-loading="loading"> <div v-loading="loading">
<Submenu activeName="setting" /> <Submenu activeName="setting" />
<el-form :model="form" ref="formRef" label-width="120px">
<el-card style="margin-top: 20px"> <el-card style="margin-top: 20px">
<el-row style="margin-top: 20px">
<el-col :span="1"><br /></el-col>
<el-col :span="10">
<el-form-item :label="$t('container.dockerStatus')">
<div v-if="form.status === 'Running'">
<el-tag type="success">{{ $t('commons.status.running') }}</el-tag>
<el-button type="primary" @click="onOperator('stop')" link style="margin-left: 20px">
{{ $t('container.stop') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperator('restart')" link>
{{ $t('container.restart') }}
</el-button>
</div>
<div v-if="form.status === 'Stopped'">
<el-tag type="info">{{ $t('commons.status.stopped') }}</el-tag>
<el-button type="primary" @click="onOperator('start')" link style="margin-left: 20px">
{{ $t('container.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperator('restart')" link>
{{ $t('container.restart') }}
</el-button>
</div>
</el-form-item>
<el-form-item :label="$t('container.daemonJsonPath')">
<el-input clearable v-model="daemonJsonPath">
<template #append>
<FileList @choose="loadLoadDir" :dir="false"></FileList>
</template>
</el-input>
<span class="input-help">{{ $t('container.daemonJsonPathHelper') }}</span>
<el-button type="primary" @click="savePath">{{ $t('commons.button.save') }}</el-button>
</el-form-item>
</el-col>
</el-row>
</el-card>
<el-card style="margin-top: 10px">
<el-radio-group v-model="confShowType" @change="changeMode"> <el-radio-group v-model="confShowType" @change="changeMode">
<el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button> <el-radio-button label="base">{{ $t('database.baseConf') }}</el-radio-button>
<el-radio-button label="all">{{ $t('database.allConf') }}</el-radio-button> <el-radio-button label="all">{{ $t('database.allConf') }}</el-radio-button>
</el-radio-group> </el-radio-group>
<el-form v-if="confShowType === 'base'" :model="form" ref="formRef" label-width="120px"> <el-row style="margin-top: 20px" v-if="confShowType === 'base'">
<el-row style="margin-top: 20px">
<el-col :span="1"><br /></el-col> <el-col :span="1"><br /></el-col>
<el-col :span="10"> <el-col :span="10">
<el-form-item :label="$t('container.mirrors')" prop="mirrors"> <el-form-item :label="$t('container.mirrors')" prop="mirrors">
@ -47,14 +85,14 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</el-form>
<div v-if="confShowType === 'all'"> <div v-if="confShowType === 'all'">
<codemirror <codemirror
:autofocus="true" :autofocus="true"
placeholder="None data" placeholder="None data"
:indent-with-tab="true" :indent-with-tab="true"
:tabSize="4" :tabSize="4"
style="margin-top: 10px; height: calc(100vh - 280px)" style="margin-top: 10px; height: calc(100vh - 380px)"
:lineWrapping="true" :lineWrapping="true"
:matchBrackets="true" :matchBrackets="true"
theme="cobalt" theme="cobalt"
@ -68,6 +106,7 @@
</el-button> </el-button>
</div> </div>
</el-card> </el-card>
</el-form>
<ConfirmDialog ref="confirmDialogRef" @confirm="onSubmitSave"></ConfirmDialog> <ConfirmDialog ref="confirmDialogRef" @confirm="onSubmitSave"></ConfirmDialog>
</div> </div>
@ -75,6 +114,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage, FormInstance } from 'element-plus'; import { ElMessage, FormInstance } from 'element-plus';
import FileList from '@/components/file-list/index.vue';
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import Submenu from '@/views/container/index.vue'; import Submenu from '@/views/container/index.vue';
import { Codemirror } from 'vue-codemirror'; import { Codemirror } from 'vue-codemirror';
@ -83,13 +123,19 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { LoadFile } from '@/api/modules/files'; import { LoadFile } from '@/api/modules/files';
import ConfirmDialog from '@/components/confirm-dialog/index.vue'; import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { loadDaemonJson, updateDaemonJson, updateDaemonJsonByfile } from '@/api/modules/container'; import { dockerOperate, loadDaemonJson, updateDaemonJson, updateDaemonJsonByfile } from '@/api/modules/container';
import { loadDaemonJsonPath, updateSetting } from '@/api/modules/setting';
const loading = ref(false); const loading = ref(false);
const extensions = [javascript(), oneDark]; const extensions = [javascript(), oneDark];
const confShowType = ref('base'); const confShowType = ref('base');
const daemonJsonPath = ref();
const loadLoadDir = async (path: string) => {
daemonJsonPath.value = path;
};
const form = reactive({ const form = reactive({
status: '', status: '',
bip: '', bip: '',
@ -125,11 +171,30 @@ const onSaveFile = async () => {
confirmDialogRef.value!.acceptParams(params); confirmDialogRef.value!.acceptParams(params);
}; };
const onOperator = async (operation: string) => {
let param = {
operation: operation,
};
await dockerOperate(param);
changeMode();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
};
const savePath = async () => {
let param = {
key: 'DaemonJsonPath',
value: daemonJsonPath.value,
};
await updateSetting(param);
changeMode();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
};
const onSubmitSave = async () => { const onSubmitSave = async () => {
if (confShowType.value === 'all') { if (confShowType.value === 'all') {
let param = { let param = {
file: dockerConf.value, file: dockerConf.value,
path: '/opt/1Panel/docker/conf/daemon.json', path: daemonJsonPath.value,
}; };
loading.value = true; loading.value = true;
await updateDaemonJsonByfile(param) await updateDaemonJsonByfile(param)
@ -169,10 +234,15 @@ const onSubmitSave = async () => {
}; };
const loadMysqlConf = async () => { const loadMysqlConf = async () => {
const res = await LoadFile({ path: '/opt/1Panel/docker/conf/daemon.json' }); const res = await LoadFile({ path: daemonJsonPath.value });
dockerConf.value = res.data; dockerConf.value = res.data;
}; };
const loadPath = async () => {
const res = await loadDaemonJsonPath();
daemonJsonPath.value = res.data;
};
const changeMode = async () => { const changeMode = async () => {
if (confShowType.value === 'all') { if (confShowType.value === 'all') {
loadMysqlConf(); loadMysqlConf();
@ -187,11 +257,12 @@ const search = async () => {
form.status = res.data.status; form.status = res.data.status;
form.cgroupDriver = res.data.cgroupDriver; form.cgroupDriver = res.data.cgroupDriver;
form.liveRestore = res.data.liveRestore; form.liveRestore = res.data.liveRestore;
form.mirrors = res.data.registryMirrors.join('\n'); form.mirrors = res.data.registryMirrors ? res.data.registryMirrors.join('\n') : '';
form.registries = res.data.insecureRegistries.join('\n'); form.registries = res.data.insecureRegistries ? res.data.insecureRegistries.join('\n') : '';
}; };
onMounted(() => { onMounted(() => {
loadPath();
search(); search();
}); });
</script> </script>