diff --git a/backend/app/api/v1/container.go b/backend/app/api/v1/container.go index 753ed4a7d..542abbd25 100644 --- a/backend/app/api/v1/container.go +++ b/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 创建容器编排 diff --git a/backend/app/service/container_compose.go b/backend/app/service/container_compose.go index 3e1321bed..e415ec9e7 100644 --- a/backend/app/service/container_compose.go +++ b/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 +} diff --git a/backend/app/service/image.go b/backend/app/service/image.go index b0c217c4d..75acd08b1 100644 --- a/backend/app/service/image.go +++ b/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), diff --git a/backend/router/ro_container.go b/backend/router/ro_container.go index 2e8008bda..213264780 100644 --- a/backend/router/ro_container.go +++ b/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) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 332eb766c..70a3043da 100644 --- a/cmd/server/docs/docs.go +++ b/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": [ diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 86c00a5b0..dd967cce9 100644 --- a/cmd/server/docs/swagger.json +++ b/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": [ diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 4203c329f..e8bbdfb1c 100644 --- a/cmd/server/docs/swagger.yaml +++ b/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: diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 43ece62e9..5718ce2b0 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -117,7 +117,10 @@ export const searchCompose = (params: SearchWithPage) => { return http.post>(`/containers/compose/search`, params); }; export const upCompose = (params: Container.ComposeCreate) => { - return http.post(`/containers/compose`, params, 600000); + return http.post(`/containers/compose`, params); +}; +export const testCompose = (params: Container.ComposeCreate) => { + return http.post(`/containers/compose/test`, params); }; export const composeOperator = (params: Container.ComposeOpration) => { return http.post(`/containers/compose/operate`, params); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a0c2487b4..c15c7c9e4 100644 --- a/frontend/src/lang/modules/en.ts +++ b/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', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 17cf3d7f6..8931410da 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/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: '更新内容', diff --git a/frontend/src/views/container/compose/create/index.vue b/frontend/src/views/container/compose/create/index.vue index 8f7baac8f..f36c4f373 100644 --- a/frontend/src/views/container/compose/create/index.vue +++ b/frontend/src/views/container/compose/create/index.vue @@ -9,12 +9,12 @@ -
+
- + {{ $t('commons.button.edit') }} {{ $t('container.pathSelect') }} {{ $t('container.composeTemplate') }} @@ -37,7 +37,7 @@ {{ $t('container.composePathHelper', [composeFile]) }} - + + {{ $t('commons.button.cancel') }} - + + {{ $t('commons.button.verify') }} + + {{ $t('commons.button.confirm') }} @@ -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; const formRef = ref(); +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({ diff --git a/frontend/src/views/container/image/build/index.vue b/frontend/src/views/container/image/build/index.vue index 1851563d8..6fd4fba26 100644 --- a/frontend/src/views/container/image/build/index.vue +++ b/frontend/src/views/container/image/build/index.vue @@ -116,16 +116,11 @@ const form = reactive({ tagStr: '', tags: [] as Array, }); -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;