mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-06 05:24:33 +08:00
parent
9c5da23a38
commit
f50c473cf6
5 changed files with 139 additions and 55 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue