Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jun 14, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/app/api/v1/container.go
Original file line number Diff line number Diff line change
@@ -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 容器操作
13 changes: 12 additions & 1 deletion backend/app/dto/container.go
Original file line number Diff line number Diff line change
@@ -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"`
23 changes: 23 additions & 0 deletions backend/app/service/container.go
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions backend/router/ro_container.go
Original file line number Diff line number Diff line change
@@ -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)

8 changes: 8 additions & 0 deletions frontend/src/api/interface/container.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions frontend/src/api/modules/container.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
7 changes: 7 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
@@ -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',
7 changes: 7 additions & 0 deletions frontend/src/lang/modules/tw.ts
Original file line number Diff line number Diff line change
@@ -776,6 +776,13 @@ const message = {
cleanImagesHelper: '( 清理所有未被任何容器使用的鏡像 )',
cleanContainersHelper: '( 清理所有處於停止狀態的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本地存儲卷 )',

makeImage: '製作鏡像',
newImageName: '新鏡像名稱',
commitMessage: '提交信息',
author: '作者',
ifPause: '製作過程中是否暫停容器',
ifMakeImageWithContainer: '是否根據此容器製作新鏡像?',
},
cronjob: {
create: '創建計劃任務',
7 changes: 7 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
@@ -777,6 +777,13 @@ const message = {
cleanImagesHelper: '( 清理所有未被任何容器使用的镜像 )',
cleanContainersHelper: '( 清理所有处于停止状态的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本地存储卷 )',

makeImage: '制作镜像',
newImageName: '新镜像名称',
commitMessage: '提交信息',
author: '作者',
ifPause: '制作过程中是否暂停容器',
ifMakeImageWithContainer: '是否根据此容器制作新镜像?',
},
cronjob: {
create: '创建计划任务',
119 changes: 119 additions & 0 deletions frontend/src/views/container/container/commit/index.vue
Original file line number Diff line number Diff line change
@@ -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>
12 changes: 12 additions & 0 deletions frontend/src/views/container/container/index.vue
Original file line number Diff line number Diff line change
@@ -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);
@@ -688,6 +691,15 @@ const buttons = [
return row.isFromCompose;
},
},
{
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.start'),
click: (row: Container.ContainerInfo) => {