feat: 容器操作增加制作容器镜像功能

This commit is contained in:
zhoujunhong 2024-06-13 17:58:54 +08:00
parent 68ac4341ac
commit d90241729f
11 changed files with 219 additions and 1 deletions

View file

@ -346,6 +346,26 @@ func (b *BaseApi) ContainerRename(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Container
// @Summary Commit Container
// @Description 容器提交生成新镜像
// @Accept json
// @Param request body dto.ContainerCommit true "request"
// @Success 200
// @Router /containers/commit [post]
func (b *BaseApi) ContainerCommit(c *gin.Context) {
var req dto.ContainerCommit
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.ContainerCommit(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Container
// @Summary Operate Container
// @Description 容器操作

View file

@ -1,6 +1,8 @@
package dto
import "time"
import (
"time"
)
type PageContainer struct {
PageInfo
@ -122,6 +124,15 @@ type ContainerRename struct {
NewName string `json:"newName" validate:"required"`
}
type ContainerCommit struct {
ContainerId string `json:"containerID" validate:"required"`
ContainerName string `json:"containerName"`
NewImageName string `json:"newImageName"`
Comment string `json:"comment"`
Author string `json:"author"`
Pause bool `json:"pause"`
}
type ContainerPrune struct {
PruneType string `json:"pruneType" validate:"required,oneof=container image volume network buildcache"`
WithTagAll bool `json:"withTagAll"`

View file

@ -60,6 +60,7 @@ type IContainerService interface {
ContainerListStats() ([]dto.ContainerListStats, error)
LoadResourceLimit() (*dto.ResourceLimit, error)
ContainerRename(req dto.ContainerRename) error
ContainerCommit(req dto.ContainerCommit) error
ContainerLogClean(req dto.OperationWithName) error
ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(wsConn *websocket.Conn, containerType, container, since, tail string, follow bool) error
@ -597,6 +598,28 @@ func (u *ContainerService) ContainerRename(req dto.ContainerRename) error {
return client.ContainerRename(ctx, req.Name, req.NewName)
}
func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error {
ctx := context.Background()
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
options := container.CommitOptions{
Reference: req.NewImageName,
Comment: req.Comment,
Author: req.Author,
Changes: nil,
Pause: req.Pause,
Config: nil,
}
_, err = client.ContainerCommit(ctx, req.ContainerId, options)
if err != nil {
return fmt.Errorf("failed to commit container, err: %v", err)
}
return nil
}
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
var err error
ctx := context.Background()

View file

@ -31,6 +31,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/load/log", baseApi.LoadContainerLog)
baRouter.POST("/inspect", baseApi.Inspect)
baRouter.POST("/rename", baseApi.ContainerRename)
baRouter.POST("/commit", baseApi.ContainerCommit)
baRouter.POST("/operate", baseApi.ContainerOperation)
baRouter.POST("/prune", baseApi.ContainerPrune)

View file

@ -9,6 +9,14 @@ export namespace Container {
name: string;
newName: string;
}
export interface ContainerCommit {
containerID: string;
containerName: string;
newImageName: string;
comment: string;
author: string;
pause: boolean;
}
export interface ContainerSearch extends ReqPage {
name: string;
state: string;

View file

@ -21,6 +21,9 @@ export const updateContainer = (params: Container.ContainerHelper) => {
export const upgradeContainer = (name: string, image: string, forcePull: boolean) => {
return http.post(`/containers/upgrade`, { name: name, image: image, forcePull: forcePull }, TimeoutEnum.T_10M);
};
export const commitContainer = (params: Container.ContainerCommit) => {
return http.post(`/containers/commit`, params);
};
export const loadContainerInfo = (name: string) => {
return http.post<Container.ContainerHelper>(`/containers/info`, { name: name });
};

View file

@ -811,6 +811,13 @@ const message = {
cleanImagesHelper: '( Clean up all images that are not used by any containers )',
cleanContainersHelper: '( Clean up all stopped containers )',
cleanVolumesHelper: '( Clean up all unused local volumes )',
makeImage: 'Create Image',
newImageName: 'New Image Name',
commitMessage: 'Commit Message',
author: 'Author',
ifPause: 'Pause Container During Creation',
ifMakeImageWithContainer: 'Create New Image from This Container?',
},
cronjob: {
create: 'Create Cronjob',

View file

@ -776,6 +776,13 @@ const message = {
cleanImagesHelper: '( 清理所有未被任何容器使用的鏡像 )',
cleanContainersHelper: '( 清理所有處於停止狀態的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本地存儲卷 )',
makeImage: '製作鏡像',
newImageName: '新鏡像名稱',
commitMessage: '提交信息',
author: '作者',
ifPause: '製作過程中是否暫停容器',
ifMakeImageWithContainer: '是否根據此容器製作新鏡像',
},
cronjob: {
create: '創建計劃任務',

View file

@ -777,6 +777,13 @@ const message = {
cleanImagesHelper: '( 清理所有未被任何容器使用的镜像 )',
cleanContainersHelper: '( 清理所有处于停止状态的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本地存储卷 )',
makeImage: '制作镜像',
newImageName: '新镜像名称',
commitMessage: '提交信息',
author: '作者',
ifPause: '制作过程中是否暂停容器',
ifMakeImageWithContainer: '是否根据此容器制作新镜像',
},
cronjob: {
create: '创建计划任务',

View file

@ -0,0 +1,119 @@
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="50%"
>
<template #header>
<DrawerHeader :header="$t('container.makeImage')" :resource="form.containerName" :back="handleClose" />
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form @submit.prevent ref="formRef" :model="form" label-position="top">
<el-form-item prop="newImageName" :rules="Rules.imageName">
<template #label>
{{ $t('container.newImageName') }}
</template>
<el-input v-model="form.newImageName" />
</el-form-item>
<el-form-item prop="comment">
<template #label>
{{ $t('container.commitMessage') }}
</template>
<el-input v-model="form.comment" />
</el-form-item>
<el-form-item prop="author">
<template #label>
{{ $t('container.author') }}
</template>
<el-input v-model="form.author" />
</el-form-item>
<el-form-item prop="pause">
<el-checkbox v-model="form.pause">
{{ $t('container.ifPause') }}
</el-checkbox>
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="drawerVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { ElForm } from 'element-plus';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { commitContainer } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
const drawerVisible = ref<boolean>(false);
const emit = defineEmits<{ (e: 'search'): void }>();
const loading = ref(false);
const form = reactive({
containerID: '',
containerName: '',
newImageName: '',
comment: '',
author: '',
pause: false,
});
interface DialogProps {
containerID: string;
containerName: string;
}
const acceptParams = (props: DialogProps): void => {
form.containerID = props.containerID;
form.containerName = props.containerName;
drawerVisible.value = true;
};
const formRef = ref<FormInstance>();
type FormInstance = InstanceType<typeof ElForm>;
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(i18n.global.t('container.ifMakeImageWithContainer'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
loading.value = true;
await commitContainer(form)
.then(() => {
loading.value = false;
emit('search');
drawerVisible.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = async () => {
drawerVisible.value = false;
emit('search');
};
defineExpose({
acceptParams,
});
</script>

View file

@ -311,6 +311,7 @@
<ContainerLogDialog ref="dialogContainerLogRef" />
<OperateDialog @search="search" ref="dialogOperateRef" />
<UpgradeDialog @search="search" ref="dialogUpgradeRef" />
<CommitDialog @search="search" ref="dialogCommitRef" />
<MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" />
@ -323,6 +324,7 @@ import PruneDialog from '@/views/container/container/prune/index.vue';
import RenameDialog from '@/views/container/container/rename/index.vue';
import OperateDialog from '@/views/container/container/operate/index.vue';
import UpgradeDialog from '@/views/container/container/upgrade/index.vue';
import CommitDialog from '@/views/container/container/commit/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
import ContainerLogDialog from '@/views/container/container/log/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
@ -363,6 +365,7 @@ const paginationConfig = reactive({
const searchName = ref();
const searchState = ref('all');
const dialogUpgradeRef = ref();
const dialogCommitRef = ref();
const dialogPortJumpRef = ref();
const opRef = ref();
const includeAppStore = ref(true);
@ -715,6 +718,15 @@ const buttons = [
return checkStatus('restart', row);
},
},
{
label: i18n.global.t('container.makeImage'),
click: (row: Container.ContainerInfo) => {
dialogCommitRef.value!.acceptParams({ containerID: row.containerID, containerName: row.name });
},
disabled: (row: any) => {
return checkStatus('commit', row);
},
},
{
label: i18n.global.t('container.kill'),
click: (row: Container.ContainerInfo) => {