mirror of https://github.com/1Panel-dev/1Panel
feat: 容器操作增加制作容器镜像功能 (#5447)
parent
584234c4f1
commit
2513c10d22
|
@ -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 容器操作
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -776,6 +776,13 @@ const message = {
|
|||
cleanImagesHelper: '( 清理所有未被任何容器使用的鏡像 )',
|
||||
cleanContainersHelper: '( 清理所有處於停止狀態的容器 )',
|
||||
cleanVolumesHelper: '( 清理所有未被使用的本地存儲卷 )',
|
||||
|
||||
makeImage: '製作鏡像',
|
||||
newImageName: '新鏡像名稱',
|
||||
commitMessage: '提交信息',
|
||||
author: '作者',
|
||||
ifPause: '製作過程中是否暫停容器',
|
||||
ifMakeImageWithContainer: '是否根據此容器製作新鏡像?',
|
||||
},
|
||||
cronjob: {
|
||||
create: '創建計劃任務',
|
||||
|
|
|
@ -777,6 +777,13 @@ const message = {
|
|||
cleanImagesHelper: '( 清理所有未被任何容器使用的镜像 )',
|
||||
cleanContainersHelper: '( 清理所有处于停止状态的容器 )',
|
||||
cleanVolumesHelper: '( 清理所有未被使用的本地存储卷 )',
|
||||
|
||||
makeImage: '制作镜像',
|
||||
newImageName: '新镜像名称',
|
||||
commitMessage: '提交信息',
|
||||
author: '作者',
|
||||
ifPause: '制作过程中是否暂停容器',
|
||||
ifMakeImageWithContainer: '是否根据此容器制作新镜像?',
|
||||
},
|
||||
cronjob: {
|
||||
create: '创建计划任务',
|
||||
|
|
|
@ -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>
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue