| @@ -90,11 +90,12 @@ func (b *BaseApi) CreateCompose(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := containerService.CreateCompose(req); err != nil { | 	log, err := containerService.CreateCompose(req) | ||||||
|  | 	if err != nil { | ||||||
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) | 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	helper.SuccessWithData(c, nil) | 	helper.SuccessWithData(c, log) | ||||||
| } | } | ||||||
|  |  | ||||||
| // @Tags Container Compose | // @Tags Container Compose | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ type IContainerService interface { | |||||||
| 	PageVolume(req dto.SearchWithPage) (int64, interface{}, error) | 	PageVolume(req dto.SearchWithPage) (int64, interface{}, error) | ||||||
| 	ListVolume() ([]dto.Options, error) | 	ListVolume() ([]dto.Options, error) | ||||||
| 	PageCompose(req dto.SearchWithPage) (int64, interface{}, error) | 	PageCompose(req dto.SearchWithPage) (int64, interface{}, error) | ||||||
| 	CreateCompose(req dto.ComposeCreate) error | 	CreateCompose(req dto.ComposeCreate) (string, error) | ||||||
| 	ComposeOperation(req dto.ComposeOperation) error | 	ComposeOperation(req dto.ComposeOperation) error | ||||||
| 	ContainerCreate(req dto.ContainerCreate) error | 	ContainerCreate(req dto.ContainerCreate) error | ||||||
| 	ContainerOperation(req dto.ContainerOperation) error | 	ContainerOperation(req dto.ContainerOperation) error | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"os/exec" | ||||||
| 	"path" | 	"path" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -124,11 +125,11 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface | |||||||
| 	return int64(total), BackDatas, nil | 	return int64(total), BackDatas, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error { | func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) { | ||||||
| 	if req.From == "template" { | 	if req.From == "template" { | ||||||
| 		template, err := composeRepo.Get(commonRepo.WithByID(req.Template)) | 		template, err := composeRepo.Get(commonRepo.WithByID(req.Template)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return "", err | ||||||
| 		} | 		} | ||||||
| 		req.From = "edit" | 		req.From = "edit" | ||||||
| 		req.File = template.Content | 		req.File = template.Content | ||||||
| @@ -137,14 +138,14 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error { | |||||||
| 		dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name) | 		dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name) | ||||||
| 		if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { | 		if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { | ||||||
| 			if err = os.MkdirAll(dir, os.ModePerm); err != nil { | 			if err = os.MkdirAll(dir, os.ModePerm); err != nil { | ||||||
| 				return err | 				return "", err | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		path := fmt.Sprintf("%s/docker-compose.yml", dir) | 		path := fmt.Sprintf("%s/docker-compose.yml", dir) | ||||||
| 		file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) | 		file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return "", err | ||||||
| 		} | 		} | ||||||
| 		defer file.Close() | 		defer file.Close() | ||||||
| 		write := bufio.NewWriter(file) | 		write := bufio.NewWriter(file) | ||||||
| @@ -157,13 +158,28 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error { | |||||||
| 	if req.From == "path" { | 	if req.From == "path" { | ||||||
| 		req.Name = path.Base(strings.ReplaceAll(req.Path, "/docker-compose.yml", "")) | 		req.Name = path.Base(strings.ReplaceAll(req.Path, "/docker-compose.yml", "")) | ||||||
| 	} | 	} | ||||||
| 	if stdout, err := compose.Up(req.Path); err != nil { | 	logName := strings.ReplaceAll(req.Path, "docker-compose.yml", "compose.log") | ||||||
| 		_, _ = compose.Down(req.Path) | 	file, err := os.OpenFile(logName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) | ||||||
| 		return errors.New(stdout) | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
| 	} | 	} | ||||||
|  | 	go func() { | ||||||
|  | 		defer file.Close() | ||||||
|  | 		cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d") | ||||||
|  | 		stdout, err := cmd.CombinedOutput() | ||||||
|  | 		_, _ = file.Write(stdout) | ||||||
|  | 		if err != nil { | ||||||
|  | 			global.LOG.Errorf("docker-compose up %s failed, err: %v", req.Name, err) | ||||||
|  | 			_, _ = compose.Down(req.Path) | ||||||
|  | 			_, _ = file.WriteString("docker-compose up failed!") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		global.LOG.Infof("docker-compose up %s successful!", req.Name) | ||||||
| 		_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name}) | 		_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name}) | ||||||
|  | 		_, _ = file.WriteString("docker-compose up successful!") | ||||||
|  | 	}() | ||||||
|  |  | ||||||
| 	return nil | 	return logName, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { | func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func Up(filePath string) (string, error) { | func Up(filePath string) (string, error) { | ||||||
| 	stdout, err := cmd.Execf("docker-compose -f %s up -d --quiet-pull", filePath) | 	stdout, err := cmd.Execf("docker-compose -f %s up -d", filePath) | ||||||
| 	return stdout, err | 	return stdout, err | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ export const searchCompose = (params: SearchWithPage) => { | |||||||
|     return http.post<ResPage<Container.ComposeInfo>>(`/containers/compose/search`, params); |     return http.post<ResPage<Container.ComposeInfo>>(`/containers/compose/search`, params); | ||||||
| }; | }; | ||||||
| export const upCompose = (params: Container.ComposeCreate) => { | export const upCompose = (params: Container.ComposeCreate) => { | ||||||
|     return http.post(`/containers/compose`, params, 600000); |     return http.post<string>(`/containers/compose`, params, 600000); | ||||||
| }; | }; | ||||||
| export const composeOperator = (params: Container.ComposeOpration) => { | export const composeOperator = (params: Container.ComposeOpration) => { | ||||||
|     return http.post(`/containers/compose/operate`, params); |     return http.post(`/containers/compose/operate`, params); | ||||||
|   | |||||||
| @@ -1,12 +1,18 @@ | |||||||
| <template> | <template> | ||||||
|     <el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%"> |     <el-drawer | ||||||
|  |         v-model="drawerVisiable" | ||||||
|  |         @close="handleClose" | ||||||
|  |         :destroy-on-close="true" | ||||||
|  |         :close-on-click-modal="false" | ||||||
|  |         size="50%" | ||||||
|  |     > | ||||||
|         <template #header> |         <template #header> | ||||||
|             <DrawerHeader :header="$t('container.compose')" :back="handleClose" /> |             <DrawerHeader :header="$t('container.compose')" :back="handleClose" /> | ||||||
|         </template> |         </template> | ||||||
|         <div v-loading="loading"> |         <div> | ||||||
|             <el-form ref="formRef" label-position="top" :model="form" :rules="rules" label-width="80px"> |  | ||||||
|             <el-row type="flex" justify="center"> |             <el-row type="flex" justify="center"> | ||||||
|                 <el-col :span="22"> |                 <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-form-item :label="$t('container.from')"> | ||||||
|                             <el-radio-group v-model="form.from"> |                             <el-radio-group v-model="form.from"> | ||||||
|                                 <el-radio label="edit">{{ $t('commons.button.edit') }}</el-radio> |                                 <el-radio label="edit">{{ $t('commons.button.edit') }}</el-radio> | ||||||
| @@ -46,7 +52,7 @@ | |||||||
|                                 placeholder="#Define or paste the content of your docker-compose file here" |                                 placeholder="#Define or paste the content of your docker-compose file here" | ||||||
|                                 :indent-with-tab="true" |                                 :indent-with-tab="true" | ||||||
|                                 :tabSize="4" |                                 :tabSize="4" | ||||||
|                                 style="width: 100%; height: calc(100vh - 340px)" |                                 style="width: 100%; height: 200px" | ||||||
|                                 :lineWrapping="true" |                                 :lineWrapping="true" | ||||||
|                                 :matchBrackets="true" |                                 :matchBrackets="true" | ||||||
|                                 theme="cobalt" |                                 theme="cobalt" | ||||||
| @@ -55,16 +61,32 @@ | |||||||
|                                 v-model="form.file" |                                 v-model="form.file" | ||||||
|                             /> |                             /> | ||||||
|                         </el-form-item> |                         </el-form-item> | ||||||
|  |                     </el-form> | ||||||
|  |                     <codemirror | ||||||
|  |                         v-if="logVisiable" | ||||||
|  |                         :autofocus="true" | ||||||
|  |                         placeholder="Waiting for build output..." | ||||||
|  |                         :indent-with-tab="true" | ||||||
|  |                         :tabSize="4" | ||||||
|  |                         style="max-height: calc(100vh - 537px)" | ||||||
|  |                         :lineWrapping="true" | ||||||
|  |                         :matchBrackets="true" | ||||||
|  |                         theme="cobalt" | ||||||
|  |                         :styleActiveLine="true" | ||||||
|  |                         :extensions="extensions" | ||||||
|  |                         @ready="handleReady" | ||||||
|  |                         v-model="logInfo" | ||||||
|  |                         :readOnly="true" | ||||||
|  |                     /> | ||||||
|                 </el-col> |                 </el-col> | ||||||
|             </el-row> |             </el-row> | ||||||
|             </el-form> |  | ||||||
|         </div> |         </div> | ||||||
|         <template #footer> |         <template #footer> | ||||||
|             <span class="dialog-footer"> |             <span class="dialog-footer"> | ||||||
|                 <el-button :disabled="loading" @click="drawerVisiable = false"> |                 <el-button @click="drawerVisiable = false"> | ||||||
|                     {{ $t('commons.button.cancel') }} |                     {{ $t('commons.button.cancel') }} | ||||||
|                 </el-button> |                 </el-button> | ||||||
|                 <el-button type="primary" :disabled="loading" @click="onSubmit(formRef)"> |                 <el-button type="primary" :disabled="buttonDisabled" @click="onSubmit(formRef)"> | ||||||
|                     {{ $t('commons.button.confirm') }} |                     {{ $t('commons.button.confirm') }} | ||||||
|                 </el-button> |                 </el-button> | ||||||
|             </span> |             </span> | ||||||
| @@ -73,7 +95,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { reactive, ref } from 'vue'; | import { nextTick, reactive, ref, shallowRef } from 'vue'; | ||||||
| import FileList from '@/components/file-list/index.vue'; | import FileList from '@/components/file-list/index.vue'; | ||||||
| import { Codemirror } from 'vue-codemirror'; | import { Codemirror } from 'vue-codemirror'; | ||||||
| import { javascript } from '@codemirror/lang-javascript'; | import { javascript } from '@codemirror/lang-javascript'; | ||||||
| @@ -83,23 +105,34 @@ import i18n from '@/lang'; | |||||||
| import { ElForm } from 'element-plus'; | import { ElForm } from 'element-plus'; | ||||||
| import DrawerHeader from '@/components/drawer-header/index.vue'; | import DrawerHeader from '@/components/drawer-header/index.vue'; | ||||||
| import { listComposeTemplate, upCompose } from '@/api/modules/container'; | import { listComposeTemplate, upCompose } from '@/api/modules/container'; | ||||||
| import { MsgSuccess } from '@/utils/message'; |  | ||||||
| import { loadBaseDir } from '@/api/modules/setting'; | import { loadBaseDir } from '@/api/modules/setting'; | ||||||
|  | import { LoadFile } from '@/api/modules/files'; | ||||||
|  | import { formatImageStdout } from '@/utils/docker'; | ||||||
|  |  | ||||||
| const extensions = [javascript(), oneDark]; | const extensions = [javascript(), oneDark]; | ||||||
|  | const view = shallowRef(); | ||||||
|  | const handleReady = (payload) => { | ||||||
|  |     view.value = payload.view; | ||||||
|  | }; | ||||||
|  | const logVisiable = ref(); | ||||||
|  | const logInfo = ref(); | ||||||
|  |  | ||||||
| const drawerVisiable = ref(false); | const drawerVisiable = ref(false); | ||||||
| const templateOptions = ref(); | const templateOptions = ref(); | ||||||
|  | const buttonDisabled = ref(false); | ||||||
|  |  | ||||||
| const loading = ref(false); |  | ||||||
| const baseDir = ref(); | const baseDir = ref(); | ||||||
| const composeFile = ref(); | const composeFile = ref(); | ||||||
|  |  | ||||||
|  | let timer: NodeJS.Timer | null = null; | ||||||
|  |  | ||||||
| const varifyPath = (rule: any, value: any, callback: any) => { | const varifyPath = (rule: any, value: any, callback: any) => { | ||||||
|     if (value.indexOf('docker-compose.yml') === -1) { |     if (value.indexOf('docker-compose.yml') === -1) { | ||||||
|         callback(new Error(i18n.global.t('commons.rule.selectHelper', ['docker-compose.yml']))); |         callback(new Error(i18n.global.t('commons.rule.selectHelper', ['docker-compose.yml']))); | ||||||
|     } |     } | ||||||
|     callback(); |     callback(); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const form = reactive({ | const form = reactive({ | ||||||
|     name: '', |     name: '', | ||||||
|     from: 'edit', |     from: 'edit', | ||||||
| @@ -126,12 +159,17 @@ const acceptParams = (): void => { | |||||||
|     form.from = 'edit'; |     form.from = 'edit'; | ||||||
|     form.path = ''; |     form.path = ''; | ||||||
|     form.file = ''; |     form.file = ''; | ||||||
|  |     logVisiable.value = false; | ||||||
|  |     logInfo.value = ''; | ||||||
|     loadTemplates(); |     loadTemplates(); | ||||||
|     loadPath(); |     loadPath(); | ||||||
| }; | }; | ||||||
| const emit = defineEmits<{ (e: 'search'): void }>(); | const emit = defineEmits<{ (e: 'search'): void }>(); | ||||||
|  |  | ||||||
| const handleClose = () => { | const handleClose = () => { | ||||||
|  |     emit('search'); | ||||||
|  |     clearInterval(Number(timer)); | ||||||
|  |     timer = null; | ||||||
|     drawerVisiable.value = false; |     drawerVisiable.value = false; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -152,18 +190,36 @@ const onSubmit = async (formEl: FormInstance | undefined) => { | |||||||
|     if (!formEl) return; |     if (!formEl) return; | ||||||
|     formEl.validate(async (valid) => { |     formEl.validate(async (valid) => { | ||||||
|         if (!valid) return; |         if (!valid) return; | ||||||
|         loading.value = true; |         const res = await upCompose(form); | ||||||
|         upCompose(form) |         logInfo.value = ''; | ||||||
|             .then(() => { |         buttonDisabled.value = true; | ||||||
|                 MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); |         logVisiable.value = true; | ||||||
|                 loading.value = false; |         loadLogs(res.data); | ||||||
|                 emit('search'); |     }); | ||||||
|                 drawerVisiable.value = false; | }; | ||||||
|             }) |  | ||||||
|             .finally(() => { | const loadLogs = async (path: string) => { | ||||||
|                 loading.value = false; |     timer = setInterval(async () => { | ||||||
|  |         if (logVisiable.value) { | ||||||
|  |             const res = await LoadFile({ path: path }); | ||||||
|  |             logInfo.value = formatImageStdout(res.data); | ||||||
|  |             nextTick(() => { | ||||||
|  |                 const state = view.value.state; | ||||||
|  |                 view.value.dispatch({ | ||||||
|  |                     selection: { anchor: state.doc.length, head: state.doc.length }, | ||||||
|  |                     scrollIntoView: true, | ||||||
|                 }); |                 }); | ||||||
|             }); |             }); | ||||||
|  |             if ( | ||||||
|  |                 logInfo.value.endsWith('docker-compose up failed!') || | ||||||
|  |                 logInfo.value.endsWith('docker-compose up successful!') | ||||||
|  |             ) { | ||||||
|  |                 clearInterval(Number(timer)); | ||||||
|  |                 timer = null; | ||||||
|  |                 buttonDisabled.value = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }, 1000 * 3); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const loadDir = async (path: string) => { | const loadDir = async (path: string) => { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|     <el-drawer |     <el-drawer | ||||||
|         v-model="drawerVisiable" |         v-model="drawerVisiable" | ||||||
|         :destroy-on-close="true" |         :destroy-on-close="true" | ||||||
|         @close="onCloseLog" |         @close="handleClose" | ||||||
|         :close-on-click-modal="false" |         :close-on-click-modal="false" | ||||||
|         size="50%" |         size="50%" | ||||||
|     > |     > | ||||||
| @@ -141,6 +141,9 @@ const emit = defineEmits<{ (e: 'search'): void }>(); | |||||||
|  |  | ||||||
| const handleClose = () => { | const handleClose = () => { | ||||||
|     drawerVisiable.value = false; |     drawerVisiable.value = false; | ||||||
|  |     emit('search'); | ||||||
|  |     clearInterval(Number(timer)); | ||||||
|  |     timer = null; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| type FormInstance = InstanceType<typeof ElForm>; | type FormInstance = InstanceType<typeof ElForm>; | ||||||
| @@ -181,11 +184,6 @@ const loadLogs = async (path: string) => { | |||||||
|         } |         } | ||||||
|     }, 1000 * 3); |     }, 1000 * 3); | ||||||
| }; | }; | ||||||
| const onCloseLog = async () => { |  | ||||||
|     emit('search'); |  | ||||||
|     clearInterval(Number(timer)); |  | ||||||
|     timer = null; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const loadBuildDir = async (path: string) => { | const loadBuildDir = async (path: string) => { | ||||||
|     form.dockerfile = path; |     form.dockerfile = path; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 ssongliu
					ssongliu