Browse Source

fix: 修改镜像构建和编排创建路径限制,增加 config 校验 (#342)

fix: 修改镜像构建和编排创建路径限制,增加 config 校验
pull/355/head
ssongliu 2 years ago committed by GitHub
parent
commit
6ee9789a2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      backend/app/api/v1/container.go
  2. 73
      backend/app/service/container_compose.go
  3. 6
      backend/app/service/image.go
  4. 1
      backend/router/ro_container.go
  5. 42
      cmd/server/docs/docs.go
  6. 42
      cmd/server/docs/swagger.json
  7. 27
      cmd/server/docs/swagger.yaml
  8. 5
      frontend/src/api/modules/container.ts
  9. 2
      frontend/src/lang/modules/en.ts
  10. 3
      frontend/src/lang/modules/zh.ts
  11. 73
      frontend/src/views/container/compose/create/index.vue
  12. 9
      frontend/src/views/container/image/build/index.vue

28
backend/app/api/v1/container.go

@ -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 创建容器编排

73
backend/app/service/container_compose.go

@ -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
}

6
backend/app/service/image.go

@ -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),

1
backend/router/ro_container.go

@ -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)

42
cmd/server/docs/docs.go

@ -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": [

42
cmd/server/docs/swagger.json

@ -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": [

27
cmd/server/docs/swagger.yaml

@ -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:

5
frontend/src/api/modules/container.ts

@ -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);

2
frontend/src/lang/modules/en.ts

@ -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',

3
frontend/src/lang/modules/zh.ts

@ -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: '更新内容',

73
frontend/src/views/container/compose/create/index.vue

@ -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({

9
frontend/src/views/container/image/build/index.vue

@ -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…
Cancel
Save