mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-18 22:22:59 +08:00
fix: 修改镜像构建和编排创建路径限制,增加 config 校验 (#342)
fix: 修改镜像构建和编排创建路径限制,增加 config 校验
This commit is contained in:
parent
68a457ae89
commit
6ee9789a2f
@ -70,6 +70,34 @@ func (b *BaseApi) SearchCompose(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags Container Compose
|
||||
// @Summary Test compose
|
||||
// @Description 测试 compose 是否可用
|
||||
// @Accept json
|
||||
// @Param request body dto.ComposeCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /containers/compose/test [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"检测 compose [name] 格式","formatEN":"check compose [name]"}
|
||||
func (b *BaseApi) TestCompose(c *gin.Context) {
|
||||
var req dto.ComposeCreate
|
||||
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
|
||||
}
|
||||
|
||||
isOK, err := containerService.TestCompose(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, isOK)
|
||||
}
|
||||
|
||||
// @Tags Container Compose
|
||||
// @Summary Create compose
|
||||
// @Description 创建容器编排
|
||||
|
@ -125,40 +125,28 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
return int64(total), BackDatas, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) {
|
||||
if req.From == "template" {
|
||||
template, err := composeRepo.Get(commonRepo.WithByID(req.Template))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.From = "edit"
|
||||
req.File = template.Content
|
||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||
if err := u.loadPath(&req); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if req.From == "edit" {
|
||||
dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name)
|
||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
cmd := exec.Command("docker-compose", "-f", req.Path, "config")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, errors.New(string(stdout))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
write := bufio.NewWriter(file)
|
||||
_, _ = write.WriteString(string(req.File))
|
||||
write.Flush()
|
||||
req.Path = path
|
||||
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) {
|
||||
if err := u.loadPath(&req); err != nil {
|
||||
return "", err
|
||||
}
|
||||
global.LOG.Infof("docker-compose.yml %s create successful, start to docker-compose up", req.Name)
|
||||
|
||||
if req.From == "path" {
|
||||
req.Name = path.Base(strings.ReplaceAll(req.Path, "/docker-compose.yml", ""))
|
||||
req.Name = path.Base(strings.ReplaceAll(req.Path, "/"+path.Base(req.Path), ""))
|
||||
}
|
||||
logName := strings.ReplaceAll(req.Path, "docker-compose.yml", "compose.log")
|
||||
logName := path.Dir(req.Path) + "/compose.log"
|
||||
file, err := os.OpenFile(logName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -221,3 +209,34 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||
if req.From == "template" {
|
||||
template, err := composeRepo.Get(commonRepo.WithByID(req.Template))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.From = "edit"
|
||||
req.File = template.Content
|
||||
}
|
||||
if req.From == "edit" {
|
||||
dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name)
|
||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
write := bufio.NewWriter(file)
|
||||
_, _ = write.WriteString(string(req.File))
|
||||
write.Flush()
|
||||
req.Path = path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -122,6 +122,7 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileName := "Dockerfile"
|
||||
if req.From == "edit" {
|
||||
dir := fmt.Sprintf("%s/docker/build/%s", constant.DataDir, strings.ReplaceAll(req.Name, ":", "_"))
|
||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||
@ -141,7 +142,8 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
|
||||
write.Flush()
|
||||
req.Dockerfile = dir
|
||||
} else {
|
||||
req.Dockerfile = strings.ReplaceAll(req.Dockerfile, "/Dockerfile", "")
|
||||
fileName = path.Base(req.Dockerfile)
|
||||
req.Dockerfile = path.Dir(req.Dockerfile)
|
||||
}
|
||||
tar, err := archive.TarWithOptions(req.Dockerfile+"/", &archive.TarOptions{})
|
||||
if err != nil {
|
||||
@ -149,7 +151,7 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
|
||||
}
|
||||
|
||||
opts := types.ImageBuildOptions{
|
||||
Dockerfile: "Dockerfile",
|
||||
Dockerfile: fileName,
|
||||
Tags: []string{req.Name},
|
||||
Remove: true,
|
||||
Labels: stringsToMap(req.Tags),
|
||||
|
@ -33,6 +33,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
||||
|
||||
baRouter.POST("/compose/search", baseApi.SearchCompose)
|
||||
baRouter.POST("/compose", baseApi.CreateCompose)
|
||||
baRouter.POST("/compose/test", baseApi.TestCompose)
|
||||
baRouter.POST("/compose/operate", baseApi.OperatorCompose)
|
||||
baRouter.POST("/compose/update", baseApi.ComposeUpdate)
|
||||
|
||||
|
@ -1038,6 +1038,48 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/containers/compose/test": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "测试 compose 是否可用",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Container Compose"
|
||||
],
|
||||
"summary": "Test compose",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ComposeCreate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"x-panel-log": {
|
||||
"BeforeFuntions": [],
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"formatEN": "create compose [name]",
|
||||
"formatZH": "创建 compose [name]",
|
||||
"paramKeys": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/containers/compose/update": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -1031,6 +1031,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/containers/compose/test": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "测试 compose 是否可用",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Container Compose"
|
||||
],
|
||||
"summary": "Test compose",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ComposeCreate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"x-panel-log": {
|
||||
"BeforeFuntions": [],
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
],
|
||||
"formatEN": "create compose [name]",
|
||||
"formatZH": "创建 compose [name]",
|
||||
"paramKeys": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/containers/compose/update": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -3286,6 +3286,33 @@ paths:
|
||||
summary: Page composes
|
||||
tags:
|
||||
- Container Compose
|
||||
/containers/compose/test:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 测试 compose 是否可用
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ComposeCreate'
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Test compose
|
||||
tags:
|
||||
- Container Compose
|
||||
x-panel-log:
|
||||
BeforeFuntions: []
|
||||
bodyKeys:
|
||||
- name
|
||||
formatEN: create compose [name]
|
||||
formatZH: 创建 compose [name]
|
||||
paramKeys: []
|
||||
/containers/compose/update:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -117,7 +117,10 @@ export const searchCompose = (params: SearchWithPage) => {
|
||||
return http.post<ResPage<Container.ComposeInfo>>(`/containers/compose/search`, params);
|
||||
};
|
||||
export const upCompose = (params: Container.ComposeCreate) => {
|
||||
return http.post<string>(`/containers/compose`, params, 600000);
|
||||
return http.post<string>(`/containers/compose`, params);
|
||||
};
|
||||
export const testCompose = (params: Container.ComposeCreate) => {
|
||||
return http.post<boolean>(`/containers/compose/test`, params);
|
||||
};
|
||||
export const composeOperator = (params: Container.ComposeOpration) => {
|
||||
return http.post(`/containers/compose/operate`, params);
|
||||
|
@ -521,6 +521,8 @@ const message = {
|
||||
registrieHelper: 'One in a row, for example:\n172.16.10.111:8081 \n172.16.10.112:8081',
|
||||
|
||||
compose: 'Compose',
|
||||
composeHelper:
|
||||
'The current content has passed the format verification. Please click Submit to complete the creation',
|
||||
apps: 'Apps',
|
||||
local: 'Local',
|
||||
createCompose: 'Create compose',
|
||||
|
@ -532,6 +532,7 @@ const message = {
|
||||
registrieHelper: '一行一个,例:\n172.16.10.111:8081 \n172.16.10.112:8081',
|
||||
|
||||
compose: '编排',
|
||||
composeHelper: '当前内容已通过格式验证,请点击确认完成创建',
|
||||
composePathHelper: '容器编排将保存在: {0}',
|
||||
apps: '应用商店',
|
||||
local: '本地',
|
||||
@ -842,7 +843,7 @@ const message = {
|
||||
versionHelper: '1Panel 版本号命名规则为: [大版本].[功能版本].[Bug 修复版本],示例如下:',
|
||||
versionHelper1: 'v1.0.1 是 v1.0.0 之后的 Bug 修复版本',
|
||||
versionHelper2: 'v1.1.0 是 v1.0.0 之后的功能版本',
|
||||
newVersion: '(Bug fix version)',
|
||||
newVersion: '(Bug 修复版本)',
|
||||
latestVersion: '(功能版本)',
|
||||
upgradeCheck: '检查更新',
|
||||
upgradeNotes: '更新内容',
|
||||
|
@ -9,12 +9,12 @@
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('container.compose')" :back="handleClose" />
|
||||
</template>
|
||||
<div>
|
||||
<div v-loading="loading">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-form ref="formRef" label-position="top" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item :label="$t('container.from')">
|
||||
<el-radio-group v-model="form.from">
|
||||
<el-radio-group v-model="form.from" @change="hasChecked = false">
|
||||
<el-radio label="edit">{{ $t('commons.button.edit') }}</el-radio>
|
||||
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
|
||||
<el-radio label="template">{{ $t('container.composeTemplate') }}</el-radio>
|
||||
@ -37,7 +37,7 @@
|
||||
<span class="input-help">{{ $t('container.composePathHelper', [composeFile]) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.from === 'template'" prop="template">
|
||||
<el-select v-model="form.template">
|
||||
<el-select v-model="form.template" @change="hasChecked = false">
|
||||
<el-option
|
||||
v-for="item in templateOptions"
|
||||
:key="item.id"
|
||||
@ -52,10 +52,11 @@
|
||||
placeholder="#Define or paste the content of your docker-compose file here"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="width: 100%; height: 200px"
|
||||
style="width: 100%; height: 250px"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
@change="hasChecked = false"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="form.file"
|
||||
@ -63,12 +64,28 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<codemirror
|
||||
v-if="logVisiable"
|
||||
v-if="logVisiable && form.from !== 'edit'"
|
||||
:autofocus="true"
|
||||
placeholder="Waiting for build output..."
|
||||
placeholder="Waiting for docker-compose up output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="max-height: calc(100vh - 537px)"
|
||||
style="height: calc(100vh - 370px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
@ready="handleReady"
|
||||
v-model="logInfo"
|
||||
:readOnly="true"
|
||||
/>
|
||||
<codemirror
|
||||
v-if="logVisiable && form.from === 'edit'"
|
||||
:autofocus="true"
|
||||
placeholder="Waiting for docker-compose up output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="height: calc(100vh - 590px)"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
@ -86,7 +103,10 @@
|
||||
<el-button @click="drawerVisiable = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="buttonDisabled" @click="onSubmit(formRef)">
|
||||
<el-button :disabled="buttonDisabled" @click="onTest(formRef)">
|
||||
{{ $t('commons.button.verify') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="buttonDisabled || !hasChecked" @click="onSubmit(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@ -104,10 +124,13 @@ import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
import { listComposeTemplate, upCompose } from '@/api/modules/container';
|
||||
import { listComposeTemplate, testCompose, upCompose } from '@/api/modules/container';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
import { LoadFile } from '@/api/modules/files';
|
||||
import { formatImageStdout } from '@/utils/docker';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
const loading = ref();
|
||||
|
||||
const extensions = [javascript(), oneDark];
|
||||
const view = shallowRef();
|
||||
@ -124,14 +147,9 @@ const buttonDisabled = ref(false);
|
||||
const baseDir = ref();
|
||||
const composeFile = ref();
|
||||
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const hasChecked = ref();
|
||||
|
||||
const varifyPath = (rule: any, value: any, callback: any) => {
|
||||
if (value.indexOf('docker-compose.yml') === -1) {
|
||||
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['docker-compose.yml'])));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
@ -142,7 +160,7 @@ const form = reactive({
|
||||
});
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.imageName],
|
||||
path: [Rules.requiredSelect, { validator: varifyPath, trigger: 'change', required: true }],
|
||||
path: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
const loadTemplates = async () => {
|
||||
@ -160,6 +178,7 @@ const acceptParams = (): void => {
|
||||
form.path = '';
|
||||
form.file = '';
|
||||
logVisiable.value = false;
|
||||
hasChecked.value = false;
|
||||
logInfo.value = '';
|
||||
loadTemplates();
|
||||
loadPath();
|
||||
@ -186,6 +205,25 @@ const changePath = async () => {
|
||||
type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const onTest = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
await testCompose(form)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.data) {
|
||||
MsgSuccess(i18n.global.t('container.composeHelper'));
|
||||
hasChecked.value = true;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
@ -224,6 +262,7 @@ const loadLogs = async (path: string) => {
|
||||
|
||||
const loadDir = async (path: string) => {
|
||||
form.path = path;
|
||||
hasChecked.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
@ -116,16 +116,11 @@ const form = reactive({
|
||||
tagStr: '',
|
||||
tags: [] as Array<string>,
|
||||
});
|
||||
const varifyPath = (rule: any, value: any, callback: any) => {
|
||||
if (value.indexOf('Dockerfile') === -1) {
|
||||
callback(new Error(i18n.global.t('commons.rule.selectHelper', ['Dockerfile'])));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.imageName],
|
||||
from: [Rules.requiredSelect],
|
||||
dockerfile: [Rules.requiredInput, { validator: varifyPath, trigger: 'change', required: true }],
|
||||
dockerfile: [Rules.requiredInput],
|
||||
});
|
||||
const acceptParams = async () => {
|
||||
logVisiable.value = false;
|
||||
|
Loading…
Reference in New Issue
Block a user