feat: 工具箱的缓存清理添加容器垃圾清理功能 (#5024)
Some checks failed
Build / SonarCloud (push) Failing after -4m17s
sync2gitee / repo-sync (push) Failing after -4m20s
Build Test / build-linux-binary (push) Failing after 7s

This commit is contained in:
John Bro 2024-05-17 14:02:37 +08:00 committed by GitHub
parent 2e47afef3b
commit c31b3e9ddc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 278 additions and 3 deletions

View file

@ -165,6 +165,7 @@ type CleanData struct {
UploadClean []CleanTree `json:"uploadClean"`
DownloadClean []CleanTree `json:"downloadClean"`
SystemLogClean []CleanTree `json:"systemLogClean"`
ContainerClean []CleanTree `json:"containerClean"`
}
type CleanTree struct {

View file

@ -337,7 +337,9 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneRepo
report.DeletedNumber = len(rep.VolumesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
case "buildcache":
rep, err := client.BuildCachePrune(context.Background(), types.BuildCachePruneOptions{})
opts := types.BuildCachePruneOptions{}
opts.All = true
rep, err := client.BuildCachePrune(context.Background(), opts)
if err != nil {
return report, err
}

View file

@ -1,7 +1,11 @@
package service
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"os"
"path"
"sort"
@ -130,6 +134,9 @@ func (u *DeviceService) Scan() dto.CleanData {
logTree := loadLogTree(fileOp)
SystemClean.SystemLogClean = append(SystemClean.SystemLogClean, logTree...)
containerTree := loadContainerTree()
SystemClean.ContainerClean = append(SystemClean.ContainerClean, containerTree...)
return SystemClean
}
@ -259,6 +266,14 @@ func (u *DeviceService) Clean(req []dto.Clean) {
} else {
_ = cronjobRepo.DeleteRecord(cronjobRepo.WithByRecordFile(pathItem))
}
case "images":
dropImages()
case "containers":
dropContainers()
case "volumes":
dropVolumes()
case "build_cache":
dropBuildCache()
}
}
@ -496,6 +511,82 @@ func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
return treeData
}
func loadContainerTree() []dto.CleanTree {
var treeData []dto.CleanTree
client, err := docker.NewDockerClient()
diskUsage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
if err != nil {
return treeData
}
var listImage []dto.CleanTree
imageSize := uint64(0)
for _, file := range diskUsage.Images {
if file.Containers == 0 {
name := "none"
if file.RepoTags != nil {
name = file.RepoTags[0]
}
item := dto.CleanTree{
ID: file.ID,
Label: name,
Type: "images",
Size: uint64(file.Size),
Name: name,
IsCheck: false,
IsRecommend: true,
}
imageSize += item.Size
listImage = append(listImage, item)
}
}
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: listImage, Type: "images", IsRecommend: true})
var listContainer []dto.CleanTree
containerSize := uint64(0)
for _, file := range diskUsage.Containers {
if file.State != "running" {
item := dto.CleanTree{
ID: file.ID,
Label: file.Names[0],
Type: "containers",
Size: uint64(file.SizeRw),
Name: file.Names[0],
IsCheck: false,
IsRecommend: true,
}
containerSize += item.Size
listContainer = append(listContainer, item)
}
}
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: listContainer, Type: "containers", IsRecommend: true})
var listVolume []dto.CleanTree
volumeSize := uint64(0)
for _, file := range diskUsage.Volumes {
if file.UsageData.RefCount <= 0 {
item := dto.CleanTree{
ID: uuid.NewString(),
Label: file.Name,
Type: "volumes",
Size: uint64(file.UsageData.Size),
Name: file.Name,
IsCheck: false,
IsRecommend: true,
}
volumeSize += item.Size
listVolume = append(listVolume, item)
}
}
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, Children: listVolume, Type: "volumes", IsRecommend: true})
var buildCacheTotalSize int64
for _, cache := range diskUsage.BuildCache {
buildCacheTotalSize += cache.Size
}
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), Type: "build_cache", IsRecommend: true})
return treeData
}
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
var lists []dto.CleanTree
files, err := os.ReadDir(pathItem)
@ -586,6 +677,63 @@ func dropFileOrDir(itemPath string) {
}
}
func dropBuildCache() {
client, err := docker.NewDockerClient()
if err != nil {
global.LOG.Errorf("do not get docker client")
}
opts := types.BuildCachePruneOptions{}
opts.All = true
_, err = client.BuildCachePrune(context.Background(), opts)
if err != nil {
global.LOG.Errorf("drop build cache failed, err %v", err)
}
}
func dropImages() {
client, err := docker.NewDockerClient()
if err != nil {
global.LOG.Errorf("do not get docker client")
}
pruneFilters := filters.NewArgs()
pruneFilters.Add("dangling", "false")
_, err = client.ImagesPrune(context.Background(), pruneFilters)
if err != nil {
global.LOG.Errorf("drop images failed, err %v", err)
}
}
func dropContainers() {
client, err := docker.NewDockerClient()
if err != nil {
global.LOG.Errorf("do not get docker client")
}
pruneFilters := filters.NewArgs()
_, err = client.ContainersPrune(context.Background(), pruneFilters)
if err != nil {
global.LOG.Errorf("drop containers failed, err %v", err)
}
}
func dropVolumes() {
client, err := docker.NewDockerClient()
if err != nil {
global.LOG.Errorf("do not get docker client")
}
pruneFilters := filters.NewArgs()
versions, err := client.ServerVersion(context.Background())
if err != nil {
global.LOG.Errorf("do not get docker api versions")
}
if common.ComparePanelVersion(versions.APIVersion, "1.42") {
pruneFilters.Add("all", "true")
}
_, err = client.VolumesPrune(context.Background(), pruneFilters)
if err != nil {
global.LOG.Errorf("drop volumes failed, err %v", err)
}
}
func dropFileOrDirWithLog(itemPath string, log *string, size *int64, count *int) {
itemSize := int64(0)
itemCount := 0

View file

@ -36,6 +36,7 @@ export namespace Toolbox {
uploadClean: Array<CleanTree>;
downloadClean: Array<CleanTree>;
systemLogClean: Array<CleanTree>;
containerClean: Array<CleanTree>;
}
export interface CleanTree {
id: string;

View file

@ -796,6 +796,11 @@ const message = {
sockPathErr: 'Please select or enter the correct Docker sock file path',
related: 'Related resources',
includeAppstore: 'Show app store container',
cleanDockerDiskZone: 'Clean up disk space used by Docker',
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 )',
},
cronjob: {
create: 'Create Cronjob',
@ -1601,6 +1606,12 @@ const message = {
shell: 'Shell script scheduled tasks',
containerShell: 'Container internal Shell script scheduled tasks',
curl: 'CURL scheduled tasks',
containerTrash: 'Container Trash',
images: 'Images',
containers: 'Containers',
volumes: 'Volumes',
buildCache: 'Container Build Cache',
},
app: {
app: 'Application',

View file

@ -763,6 +763,11 @@ const message = {
sockPathErr: '請選擇或輸入正確的 Docker sock 文件路徑',
related: '相關資源',
includeAppstore: '顯示應用程式商店容器',
cleanDockerDiskZone: '清理 Docker 使用的磁碟空間',
cleanImagesHelper: '( 清理所有未被任何容器使用的鏡像 )',
cleanContainersHelper: '( 清理所有處於停止狀態的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本地存儲卷 )',
},
cronjob: {
create: '創建計劃任務',
@ -1493,6 +1498,12 @@ const message = {
shell: 'Shell 腳本計劃任務',
containerShell: '容器內執行 Shell 腳本計劃任務',
curl: 'CURL 計劃任務',
containerTrash: '容器垃圾',
images: '鏡像',
containers: '容器',
volumes: '存儲卷',
buildCache: '容器建置快取',
},
app: {
app: '應用',

View file

@ -764,6 +764,11 @@ const message = {
sockPathErr: '请选择或输入正确的 Docker sock 文件路径',
related: '关联资源',
includeAppstore: '显示应用商店容器',
cleanDockerDiskZone: '清理 Docker 使用的磁盘空间',
cleanImagesHelper: '( 清理所有未被任何容器使用的镜像 )',
cleanContainersHelper: '( 清理所有处于停止状态的容器 )',
cleanVolumesHelper: '( 清理所有未被使用的本地存储卷 )',
},
cronjob: {
create: '创建计划任务',
@ -1493,6 +1498,12 @@ const message = {
shell: 'Shell 脚本计划任务',
containerShell: '容器内执行 Shell 脚本计划任务',
curl: 'CURL 计划任务',
containerTrash: '容器垃圾',
images: '镜像',
containers: '容器',
volumes: '存储卷',
buildCache: '构建缓存',
},
app: {
app: '应用',

View file

@ -33,7 +33,9 @@
</div>
<div else>
<el-text class="clean_title">
<el-icon><MagicStick /></el-icon>
<el-icon>
<MagicStick />
</el-icon>
{{ $t('clean.scanHelper') }}
</el-text>
</div>
@ -53,6 +55,24 @@
</el-col>
</el-row>
</el-card>
<el-card class="e-card">
<el-row>
<el-col :span="4">
<svg-icon iconName="p-docker" class="svg-icon"></svg-icon>
<el-button link class="card_icon" />
</el-col>
<el-col :span="20">
<div>
<el-text class="mx-1 card_title" type="primary">
{{ $t('clean.containerTrash') }}
</el-text>
</div>
<span class="input-help">
{{ $t('container.cleanDockerDiskZone') }}
</span>
</el-col>
</el-row>
</el-card>
<el-card class="e-card">
<el-row>
<el-col :span="4">
@ -156,6 +176,30 @@
</template>
</el-tree>
</el-collapse-item>
<el-collapse-item :title="$t('clean.containerTrash')" name="container_trash">
<el-tree
ref="containerRef"
:data="cleanData.containerClean"
node-key="id"
:default-checked-keys="containerDefaultCheck"
show-checkbox
:props="defaultProps"
@check-change="onChange"
>
<template #default="{ node, data }">
<div class="float-left">
<span>{{ load18n(data.label) }}</span>
</div>
<div class="ml-4 float-left">
<span v-if="data.size">{{ computeSize(data.size) }}</span>
</div>
<div class="ml-4 float-left">
<span>{{ loadTag(node, data) }}</span>
</div>
</template>
</el-tree>
</el-collapse-item>
<el-collapse-item :title="$t('clean.upload')" name="upload">
<el-tree
ref="uploadRef"
@ -241,6 +285,7 @@ import { clean, scan } from '@/api/modules/toolbox';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const loading = ref();
@ -258,6 +303,7 @@ const cleanData = reactive({
uploadClean: [],
downloadClean: [],
systemLogClean: [],
containerClean: [],
});
const systemRef = ref();
const systemDefaultCheck = ref([]);
@ -267,7 +313,9 @@ const downloadRef = ref();
const downloadDefaultCheck = ref([]);
const systemLogRef = ref();
const systemLogDefaultCheck = ref([]);
const activeNames = ref(['system', 'upload', 'download', 'system_log']);
const containerRef = ref();
const containerDefaultCheck = ref([]);
const activeNames = ref(['system', 'upload', 'download', 'system_log', 'container_trash']);
const submitCleans = ref();
@ -300,10 +348,15 @@ const scanData = async () => {
for (const item of cleanData.systemLogClean) {
totalSize.value += item.size;
}
cleanData.containerClean = res.data.containerClean || [];
for (const item of cleanData.containerClean) {
totalSize.value += item.size;
}
loadCheck(cleanData.systemClean, systemDefaultCheck.value);
loadCheck(cleanData.uploadClean, uploadDefaultCheck.value);
loadCheck(cleanData.downloadClean, downloadDefaultCheck.value);
loadCheck(cleanData.systemLogClean, systemLogDefaultCheck.value);
loadCheck(cleanData.containerClean, containerDefaultCheck.value);
scanStatus.value = 'scanned';
})
.catch(() => {
@ -324,6 +377,7 @@ const onSubmitClean = async () => {
loadSubmitCheck(cleanData.uploadClean);
loadSubmitCheck(cleanData.downloadClean);
loadSubmitCheck(cleanData.systemLogClean);
loadSubmitCheck(cleanData.containerClean);
for (const item of submitCleans.value) {
if (item.treeType === 'cache') {
restart = true;
@ -416,6 +470,12 @@ function onChange(data: any, isCheck: boolean) {
selectSize.value = selectSize.value + Number(item.size);
}
}
let containerSelects = containerRef.value.getCheckedNodes(false, true);
for (const item of containerSelects) {
if (item.children === null) {
selectSize.value = selectSize.value + Number(item.size);
}
}
}
function loadCheck(data: any, checkList: any) {
@ -444,6 +504,15 @@ function loadTag(node: any, data: any) {
if (data.size === 0) {
return i18n.global.t('clean.statusClean');
}
if (data.label === 'container_images') {
return i18n.global.t('container.cleanImagesHelper');
}
if (data.label === 'container_containers') {
return i18n.global.t('container.cleanContainersHelper');
}
if (data.label === 'container_volumes') {
return i18n.global.t('container.cleanVolumesHelper');
}
if (data.label === 'upgrade') {
return i18n.global.t('clean.upgradeHelper');
}
@ -509,6 +578,14 @@ function load18n(label: string) {
return i18n.global.t('clean.containerShell');
case 'curl':
return i18n.global.t('clean.curl');
case 'container_images':
return i18n.global.t('clean.images');
case 'container_containers':
return i18n.global.t('clean.containers');
case 'container_volumes':
return i18n.global.t('clean.volumes');
case 'build_cache':
return i18n.global.t('clean.buildCache');
default:
return label;
}
@ -524,32 +601,45 @@ onMounted(() => {
.app-card {
cursor: pointer;
width: 100%;
&:hover .app-icon {
transform: scale(1.2);
}
.e-card {
margin-top: 20px;
cursor: pointer;
border: var(--panel-border) !important;
&:hover {
cursor: pointer;
border: 1px solid var(--el-color-primary) !important;
}
}
}
.card_icon {
font-size: 36px;
float: right;
margin-right: 15px;
}
.card_title {
font-size: 18px;
}
.clean_title {
font-size: 22px;
}
.large_button {
float: right;
margin-top: -40px;
}
.svg-icon {
font-size: 14px;
float: right;
margin-right: 15px;
}
</style>