feat: 容器镜像支持多选删除 (#6748)

Refs #6562
This commit is contained in:
ssongliu 2024-10-17 15:27:31 +08:00 committed by GitHub
parent 9c5da23a38
commit f50c473cf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 55 deletions

View file

@ -150,12 +150,13 @@ func (b *BaseApi) ImageRemove(c *gin.Context) {
return
}
if err := imageService.ImageRemove(req); err != nil {
data, err := imageService.ImageRemove(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
helper.SuccessWithData(c, data)
}
// @Tags Container Image

View file

@ -319,13 +319,30 @@ func (u *ContainerService) ContainerCreateByCommand(req dto.ContainerCreateByCom
if cmd.CheckIllegal(req.Command) {
return buserr.New(constant.ErrCmdIllegal)
}
taskItem, err := task.NewTaskWithOps("-", task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if !strings.HasPrefix(strings.TrimSpace(req.Command), "docker run ") {
return errors.New("error command format")
}
containerName := ""
commands := strings.Split(req.Command, " ")
for index, val := range commands {
if val == "--name" && len(commands) > index+1 {
containerName = commands[index+1]
}
}
if !strings.Contains(req.Command, " -d ") {
req.Command = strings.ReplaceAll(req.Command, "docker run", "docker run -d")
}
if len(containerName) == 0 {
containerName = fmt.Sprintf("1Panel-%s-%s", common.RandStr(5), common.RandStrAndNum(4))
req.Command += fmt.Sprintf(" --name %s", containerName)
}
taskItem, err := task.NewTaskWithOps(containerName, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
go func() {
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", "-"), func(t *task.Task) error {
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", containerName), func(t *task.Task) error {
logPath := path.Join(constant.LogDir, task.TaskScopeContainer, req.TaskID+".log")
return cmd.ExecShell(logPath, 5*time.Minute, "bash", "-c", req.Command)
}, nil)

View file

@ -40,7 +40,7 @@ type IImageService interface {
ImageLoad(req dto.ImageLoad) error
ImageSave(req dto.ImageSave) error
ImagePush(req dto.ImagePush) error
ImageRemove(req dto.BatchDelete) error
ImageRemove(req dto.BatchDelete) (dto.ContainerPruneReport, error)
ImageTag(req dto.ImageTag) error
}
@ -399,24 +399,31 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
return nil
}
func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
func (u *ImageService) ImageRemove(req dto.BatchDelete) (dto.ContainerPruneReport, error) {
report := dto.ContainerPruneReport{}
client, err := docker.NewDockerClient()
if err != nil {
return err
return report, err
}
defer client.Close()
for _, id := range req.Names {
imageItem, _, err := client.ImageInspectWithRaw(context.TODO(), id)
if err != nil {
return report, err
}
if _, err := client.ImageRemove(context.TODO(), id, image.RemoveOptions{Force: req.Force, PruneChildren: true}); err != nil {
if strings.Contains(err.Error(), "image is being used") || strings.Contains(err.Error(), "is using") {
if strings.Contains(id, "sha256:") {
return buserr.New(constant.ErrObjectInUsed)
return report, buserr.New(constant.ErrObjectInUsed)
}
return buserr.WithDetail(constant.ErrInUsed, id, nil)
return report, buserr.WithDetail(constant.ErrInUsed, id, nil)
}
return err
return report, err
}
report.DeletedNumber++
report.SpaceReclaimed += int(imageItem.Size)
}
return nil
return report, nil
}
func formatFileSize(fileSize int64) (size string) {

View file

@ -94,7 +94,7 @@ export const imageTag = (params: Container.ImageTag) => {
return http.post(`/containers/image/tag`, params);
};
export const imageRemove = (params: Container.BatchDelete) => {
return http.post(`/containers/image/remove`, params);
return http.post<Container.ContainerPruneReport>(`/containers/image/remove`, params);
};
// network

View file

@ -1,67 +1,63 @@
<template>
<el-dialog v-model="dialogVisible" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
<template #header>
<div class="card-header">
<span>{{ $t('container.imagePrune') }}</span>
</div>
</template>
<DrawerPro v-model="dialogVisible" :header="$t('container.imagePrune')" :back="handleClose" size="small">
<el-form ref="deleteForm" v-loading="loading">
<el-form-item>
<el-radio-group v-model="withTagAll">
<el-radio :value="false">{{ $t('container.imagePruneSome') }}</el-radio>
<el-radio :value="true">{{ $t('container.imagePruneAll') }}</el-radio>
<el-radio-group class="w-full" v-model="scope" @change="changeScope">
<el-radio value="untag">{{ $t('container.imagePruneSome') }}</el-radio>
<el-radio value="unused">{{ $t('container.imagePruneAll') }}</el-radio>
</el-radio-group>
<span class="input-help">{{ showMsg }}</span>
<el-checkbox
class="w-full"
v-if="data.length !== 0"
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('commons.table.all') }}
</el-checkbox>
<el-checkbox-group v-model="checkedLists" @change="handleCheckedChange">
<el-checkbox class="w-full" v-for="(item, index) in data" :key="index" :value="item.id">
{{
item.tags && item.tags[0]
? item.tags[0]
: item.id.replaceAll('sha256:', '').substring(0, 12)
}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<span v-if="withTagAll">
{{ unUsedList.length !== 0 ? $t('container.imagePruneAllHelper') : $t('container.imagePruneAllEmpty') }}
</span>
<span v-else>
{{
unTagList.length !== 0 ? $t('container.imagePruneSomeHelper') : $t('container.imagePruneSomeEmpty')
}}
</span>
<div v-if="!withTagAll">
<ul v-for="(item, index) in unTagList" :key="index">
<li v-if="item.tags && item.tags[0]">
{{ item.tags[0] }}
</li>
<li v-else>
{{ item.id.replaceAll('sha256:', '').substring(0, 12) }}
</li>
</ul>
</div>
<div v-else>
<ul v-for="(item, index) in unUsedList" :key="index">
<li v-if="item.tags && item.tags[0]">{{ item.tags.join(', ') }}</li>
<li v-else>{{ item.id.replaceAll('sha256:', '').substring(0, 12) }}</li>
</ul>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" :disabled="buttonDisable() || loading" @click="onClean">
<el-button type="primary" :disabled="data.length === 0 || loading" @click="onClean">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</DrawerPro>
</template>
<script lang="ts" setup>
import { containerPrune, listAllImage } from '@/api/modules/container';
import { containerPrune, imageRemove, listAllImage } from '@/api/modules/container';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { computeSize } from '@/utils/util';
import { ref } from 'vue';
const dialogVisible = ref(false);
const withTagAll = ref(false);
const scope = ref('untag');
const showMsg = ref();
const loading = ref();
const unTagList = ref();
const unUsedList = ref();
const unTagList = ref([]);
const unUsedList = ref([]);
const data = ref([]);
const checkAll = ref(false);
const isIndeterminate = ref(false);
const checkedLists = ref([]);
const acceptParams = async (): Promise<void> => {
const res = await listAllImage();
@ -81,20 +77,62 @@ const acceptParams = async (): Promise<void> => {
}
}
dialogVisible.value = true;
withTagAll.value = false;
scope.value = 'untag';
changeScope();
};
const emit = defineEmits<{ (e: 'search'): void }>();
const buttonDisable = () => {
return withTagAll.value ? unUsedList.value.length === 0 : unTagList.value.length === 0;
const changeScope = () => {
if (scope.value === 'untag') {
data.value = unTagList.value || [];
showMsg.value =
data.value.length === 0
? i18n.global.t('container.imagePruneSomeHelper')
: i18n.global.t('container.imagePruneSomeEmpty');
return;
}
data.value = unUsedList.value || [];
showMsg.value =
data.value.length === 0
? i18n.global.t('container.imagePruneAllHelper')
: i18n.global.t('container.imagePruneAllEmpty');
return;
};
const handleCheckAllChange = (val: boolean) => {
checkedLists.value = [];
if (!val) {
isIndeterminate.value = false;
return;
}
for (const item of data.value) {
checkedLists.value.push(item.id);
}
};
const handleCheckedChange = (value: string[]) => {
const checkedCount = value.length;
checkAll.value = checkedCount === unUsedList.value.length;
isIndeterminate.value = checkedCount > 0 && checkedCount < unUsedList.value.length;
};
const handleClose = () => {
dialogVisible.value = false;
};
const onClean = async () => {
loading.value = true;
if (checkAll.value) {
prune();
return;
}
removeImage();
};
const prune = async () => {
let params = {
pruneType: 'image',
withTagAll: withTagAll.value,
withTagAll: scope.value === 'unused',
};
await containerPrune(params)
.then((res) => {
@ -113,6 +151,27 @@ const onClean = async () => {
});
};
const removeImage = async () => {
let params = {
names: checkedLists.value,
};
await imageRemove(params)
.then((res) => {
loading.value = false;
dialogVisible.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
emit('search');
})
.catch(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});