fix: Optimize container cleanup mechanism (#9795)

Refs #7444

涉及到容器、镜像、网络、存储卷、构建缓存清理
This commit is contained in:
ssongliu 2025-08-01 16:07:24 +08:00 committed by GitHub
parent ba4307c1dc
commit 9c292bebf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 280 additions and 169 deletions

View file

@ -315,7 +315,7 @@ func (b *BaseApi) ContainerUpgrade(c *gin.Context) {
// @Summary Clean container
// @Accept json
// @Param request body dto.ContainerPrune true "request"
// @Success 200 {object} dto.ContainerPruneReport
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/prune [post]
@ -326,12 +326,11 @@ func (b *BaseApi) ContainerPrune(c *gin.Context) {
return
}
report, err := containerService.Prune(req)
if err != nil {
if err := containerService.Prune(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, report)
helper.Success(c)
}
// @Tags Container

View file

@ -138,7 +138,7 @@ func (b *BaseApi) ImagePush(c *gin.Context) {
// @Summary Delete image
// @Accept json
// @Param request body dto.BatchDelete true "request"
// @Success 200 {object} dto.ContainerPruneReport
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/image/remove [post]
@ -149,13 +149,12 @@ func (b *BaseApi) ImageRemove(c *gin.Context) {
return
}
data, err := imageService.ImageRemove(req)
if err != nil {
if err := imageService.ImageRemove(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
helper.Success(c)
}
// @Tags Container Image

View file

@ -174,15 +174,11 @@ type ContainerCommit struct {
}
type ContainerPrune struct {
TaskID string `json:"taskID"`
PruneType string `json:"pruneType" validate:"required,oneof=container image volume network buildcache"`
WithTagAll bool `json:"withTagAll"`
}
type ContainerPruneReport struct {
DeletedNumber int `json:"deletedNumber"`
SpaceReclaimed int `json:"spaceReclaimed"`
}
type Network struct {
ID string `json:"id"`
Name string `json:"name"`
@ -227,8 +223,9 @@ type VolumeCreate struct {
}
type BatchDelete struct {
Force bool `json:"force"`
Names []string `json:"names" validate:"required"`
TaskID string `json:"taskID"`
Force bool `json:"force"`
Names []string `json:"names" validate:"required"`
}
type ComposeInfo struct {

View file

@ -82,7 +82,7 @@ type IContainerService interface {
CreateVolume(req dto.VolumeCreate) error
TestCompose(req dto.ComposeCreate) (bool, error)
ComposeUpdate(req dto.ComposeUpdate) error
Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error)
Prune(req dto.ContainerPrune) error
LoadUsers(req dto.OperationWithName) []string
@ -417,66 +417,93 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
return string(bytes), nil
}
func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneReport, error) {
report := dto.ContainerPruneReport{}
func (u *ContainerService) Prune(req dto.ContainerPrune) error {
client, err := docker.NewDockerClient()
if err != nil {
return report, err
return err
}
defer client.Close()
pruneFilters := filters.NewArgs()
if req.WithTagAll {
pruneFilters.Add("dangling", "false")
if req.PruneType != "image" {
pruneFilters.Add("until", "24h")
}
}
name := ""
switch req.PruneType {
case "container":
rep, err := client.ContainersPrune(context.Background(), pruneFilters)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.ContainersDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
name = "Container"
case "image":
rep, err := client.ImagesPrune(context.Background(), pruneFilters)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.ImagesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
case "network":
rep, err := client.NetworksPrune(context.Background(), pruneFilters)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.NetworksDeleted)
name = "Image"
case "volume":
versions, err := client.ServerVersion(context.Background())
if err != nil {
return report, err
}
if common.ComparePanelVersion(versions.APIVersion, "1.42") {
pruneFilters.Add("all", "true")
}
rep, err := client.VolumesPrune(context.Background(), pruneFilters)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.VolumesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
name = "Volume"
case "buildcache":
opts := build.CachePruneOptions{}
opts.All = true
rep, err := client.BuildCachePrune(context.Background(), opts)
if err != nil {
return report, err
}
report.DeletedNumber = len(rep.CachesDeleted)
report.SpaceReclaimed = int(rep.SpaceReclaimed)
name = "BuildCache"
case "network":
name = "Network"
}
return report, nil
taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey(name), task.TaskClean, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
pruneFilters := filters.NewArgs()
if req.WithTagAll {
pruneFilters.Add("dangling", "false")
if req.PruneType != "image" {
pruneFilters.Add("until", "24h")
}
}
DeletedNumber := 0
SpaceReclaimed := 0
switch req.PruneType {
case "container":
rep, err := client.ContainersPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.ContainersDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
case "image":
rep, err := client.ImagesPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.ImagesDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
case "network":
rep, err := client.NetworksPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.NetworksDeleted)
case "volume":
versions, err := client.ServerVersion(context.Background())
if err != nil {
return err
}
if common.ComparePanelVersion(versions.APIVersion, "1.42") {
pruneFilters.Add("all", "true")
}
rep, err := client.VolumesPrune(context.Background(), pruneFilters)
if err != nil {
return err
}
DeletedNumber = len(rep.VolumesDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
case "buildcache":
opts := build.CachePruneOptions{}
opts.All = true
rep, err := client.BuildCachePrune(context.Background(), opts)
if err != nil {
return err
}
DeletedNumber = len(rep.CachesDeleted)
SpaceReclaimed = int(rep.SpaceReclaimed)
}
taskItem.Log(i18n.GetMsgWithMap("PruneHelper", map[string]interface{}{"name": i18n.GetMsgByKey(name), "count": DeletedNumber, "size": common.LoadSizeUnit2F(float64(SpaceReclaimed))}))
return nil
}, nil)
go func() {
_ = taskItem.Execute()
}()
return nil
}
func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) {
@ -1241,29 +1268,29 @@ func checkImageExist(client *client.Client, imageItem string) bool {
}
func checkImageLike(client *client.Client, imageName string) bool {
if client == nil {
var err error
client, err = docker.NewDockerClient()
if err != nil {
return false
}
}
images, err := client.ImageList(context.Background(), image.ListOptions{})
if err != nil {
return false
}
if client == nil {
var err error
client, err = docker.NewDockerClient()
if err != nil {
return false
}
}
images, err := client.ImageList(context.Background(), image.ListOptions{})
if err != nil {
return false
}
for _, img := range images {
for _, tag := range img.RepoTags {
parts := strings.Split(tag, "/")
imageNameWithTag := parts[len(parts)-1]
for _, img := range images {
for _, tag := range img.RepoTags {
parts := strings.Split(tag, "/")
imageNameWithTag := parts[len(parts)-1]
if imageNameWithTag == imageName {
return true
}
}
}
return false
if imageNameWithTag == imageName {
return true
}
}
}
return false
}
func pullImages(task *task.Task, client *client.Client, imageName string) error {

View file

@ -21,6 +21,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
@ -41,7 +42,7 @@ type IImageService interface {
ImageLoad(req dto.ImageLoad) error
ImageSave(req dto.ImageSave) error
ImagePush(req dto.ImagePush) error
ImageRemove(req dto.BatchDelete) (dto.ContainerPruneReport, error)
ImageRemove(req dto.BatchDelete) error
ImageTag(req dto.ImageTag) error
}
@ -386,34 +387,44 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
return nil
}
func (u *ImageService) ImageRemove(req dto.BatchDelete) (dto.ContainerPruneReport, error) {
report := dto.ContainerPruneReport{}
func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return report, err
return 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 report, buserr.New("ErrObjectInUsed")
}
return report, buserr.WithDetail("ErrInUsed", id, nil)
}
if strings.Contains(err.Error(), "image has dependent") {
return report, buserr.New("ErrObjectBeDependent")
}
return report, err
}
report.DeletedNumber++
report.SpaceReclaimed += int(imageItem.Size)
taskItem, err := task.NewTaskWithOps(task.TaskScopeImage, task.TaskDelete, task.TaskScopeContainer, req.TaskID, 1)
if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err)
return err
}
return report, nil
for _, id := range req.Names {
taskItem.AddSubTask(i18n.GetMsgByKey("TaskDelete")+id, func(t *task.Task) error {
imageItem, err := client.ImageInspect(context.TODO(), id)
if err != nil {
return 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("ErrObjectInUsed")
}
return buserr.WithDetail("ErrInUsed", id, nil)
}
if strings.Contains(err.Error(), "image has dependent") {
return buserr.New("ErrObjectBeDependent")
}
return err
}
taskItem.Log(i18n.GetMsgWithMap("ImageRemoveHelper", map[string]interface{}{"name": id, "size": common.LoadSizeUnit2F(float64(imageItem.Size))}))
return nil
}, nil)
}
go func() {
_ = taskItem.Execute()
}()
return nil
}
func formatFileSize(fileSize int64) (size string) {

View file

@ -65,6 +65,7 @@ const (
TaskPull = "TaskPull"
TaskCommit = "TaskCommit"
TaskPush = "TaskPush"
TaskClean = "TaskClean"
TaskHandle = "TaskHandle"
)

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'The object is in use and cannot be deleted'
ErrObjectBeDependent: 'This image depends on other images and cannot be deleted'
ErrPortRules: 'Port number does not match, please re-enter!'
ErrPgImagePull: 'Image pull timed out, please configure image acceleration or manually pull the {{ .name }} image and try again'
PruneHelper: "This cleanup removed {{ .name }} {{ .count }} items, freeing {{ .size }} disk space"
ImageRemoveHelper: "Deleted image {{ .name }}, freeing {{ .size }} disk space"
BuildCache: "Build cache"
Volume: "Storage volume"
Network: "Network"
#runtime
ErrFileNotExist: '{{ .detail }} file does not exist! Please check the integrity of the source file!'
@ -290,6 +295,7 @@ TaskPull: 'Pull'
TaskCommit: 'Commit'
TaskBuild: 'Build'
TaskPush: 'Push'
TaskClean: "Cleanup"
TaskHandle: 'Execute'
Website: 'Website'
App: 'Application'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'オブジェクトは使用中のため削除できません'
ErrObjectBeDependent: 'このイメージは他のイメージに依存しているため、削除できません'
ErrPortRules: 'ポート番号が一致しません。再入力してください。'
ErrPgImagePull: 'イメージのプルがタイムアウトしました。イメージのアクセラレーションを設定するか、{{ .name }} イメージを手動でプルして再試行してください'
PruneHelper: "今回のクリーンアップで{{ .name }} {{ .count }}個を削除し、{{ .size }}のディスク領域を解放しました"
ImageRemoveHelper: "イメージ{{ .name }}を削除し、{{ .size }}のディスク領域を解放しました"
BuildCache: "ビルドキャッシュ"
Volume: "ストレージボリューム"
Network: "ネットワーク"
#runtime
ErrFileNotExist: '{{ .detail }} ファイルが存在しません。ソース ファイルの整合性を確認してください。'
@ -290,6 +295,7 @@ TaskPull: 'プル'
TaskCommit: 'コミット'
TaskBuild: 'ビルド'
TaskPush: 'プッシュ'
TaskClean: "クリーンアップ"
TaskHandle: '実行'
Website: 'ウェブサイト'
App: 'アプリケーション'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: '개체가 사용 중이므로 삭제할 수 없습니다'
ErrObjectBeDependent: '이 이미지는 다른 이미지에 의존하므로 삭제할 수 없습니다'
ErrPortRules: '포트 번호가 일치하지 않습니다. 다시 입력하세요!'
ErrPgImagePull: '이미지 풀링 시간이 초과되었습니다. 이미지 가속을 구성하거나 {{ .name }} 이미지를 수동으로 풀링한 다음 다시 시도하세요.'
PruneHelper: "이번 정리에서 {{ .name }} {{ .count }}개를 제거하여 {{ .size }} 디스크 공간을 확보했습니다"
ImageRemoveHelper: "이미지 {{ .name }} 삭제, {{ .size }} 디스크 공간 확보"
BuildCache: "빌드 캐시"
Volume: "스토리지 볼륨"
Network: "네트워크"
#실행 시간
ErrFileNotExist: '{{ .detail }} 파일이 존재하지 않습니다! 소스 파일의 무결성을 확인하세요!'
@ -290,6 +295,7 @@ TaskPull: '당기기'
TaskCommit: '커밋'
TaskBuild: '빌드'
TaskPush: '푸시'
TaskClean: "정리"
TaskHandle: '실행'
Website: '웹사이트'
App: '애플리케이션'

View file

@ -172,6 +172,11 @@ ErrObjectInUsed: 'Objek sedang digunakan dan tidak boleh dipadamkan'
ErrObjectBeDependent: 'Imej ini bergantung pada imej lain dan tidak boleh dipadamkan'
ErrPortRules: 'Nombor port tidak sepadan, sila masukkan semula!'
ErrPgImagePull: 'Tarikh imej tamat masa, sila konfigurasikan pecutan imej atau tarik imej {{ .name }} secara manual dan cuba lagi'
PruneHelper: "Pembersihan ini membuang {{ .name }} {{ .count }} item, membebaskan {{ .size }} ruang cakera"
ImageRemoveHelper: "Padam imej {{ .name }}, membebaskan {{ .size }} ruang cakera"
BuildCache: "Cache binaan"
Volume: "Jilid storan"
Network: "Rangkaian"
#masa berjalan
ErrFileNotExist: 'Fail {{ .detail }} tidak wujud! Sila semak integriti fail sumber!'
@ -289,6 +294,7 @@ TaskPull: 'Tarik'
TaskCommit: 'Komit'
TaskBuild: 'Bina'
TaskPush: 'Tolak'
TaskClean: "Pembersihan"
TaskHandle: 'Laksanakan'
Website: 'Laman web'
App: 'Aplikasi'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'O objeto está em uso e não pode ser excluído'
ErrObjectBeDependent: 'Esta imagem depende de outras imagens e não pode ser excluída'
ErrPortRules: 'O número da porta não corresponde, digite novamente!'
ErrPgImagePull: 'Tempo limite para extração de imagem. Configure a aceleração de imagem ou extraia manualmente a imagem {{ .name }} e tente novamente'
PruneHelper: "Esta limpeza removeu {{ .name }} {{ .count }} itens, liberando {{ .size }} de espaço em disco"
ImageRemoveHelper: "Excluída a imagem {{ .name }}, liberando {{ .size }} de espaço em disco"
BuildCache: "Cache de construção"
Volume: "Volume de armazenamento"
Network: "Rede"
#tempo de execução
ErrFileNotExist: 'O arquivo {{ .detail }} não existe! Verifique a integridade do arquivo de origem!'
@ -290,6 +295,7 @@ TaskPull: 'Puxar'
TaskCommit: 'Commit'
TaskBuild: 'Construir'
TaskPush: 'Empurrar'
TaskClean: "Limpeza"
TaskHandle: 'Executar'
Website: 'Site'
App: 'Aplicativo'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'Объект используется и не может бы
ErrObjectBeDependent: 'Это изображение зависит от других изображений и не может быть удалено'
ErrPortRules: 'Номер порта не совпадает, введите заново!'
ErrPgImagePull: 'Время извлечения изображения истекло. Настройте ускорение изображения или вручную извлеките изображение {{ .name }} и повторите попытку'
PruneHelper: "Очистка удалила {{ .name }} в количестве {{ .count }}, освободив {{ .size }} дискового пространства"
ImageRemoveHelper: "Удалён образ {{ .name }}, освобождено {{ .size }} дискового пространства"
BuildCache: "Кэш сборки"
Volume: "Том хранилища"
Network: "Сеть"
#время выполнения
ErrFileNotExist: 'Файл {{ .detail }} не существует! Проверьте целостность исходного файла!'
@ -290,6 +295,7 @@ TaskPull: 'Вытянуть'
TaskCommit: 'Kоммит'
ЗадачаСборка: 'Сборка'
TaskPush: 'Push'
TaskClean: "Очистка"
TaskHandle: 'Выполнить'
Website: 'Be6-сайт'
App: 'Приложение'

View file

@ -173,6 +173,11 @@ ErrObjectInUsed: 'Nesne kullanımda ve silinemez'
ErrObjectBeDependent: 'Bu image diğer imagelere bağlı ve silinemez'
ErrPortRules: 'Port numarası eşleşmiyor, lütfen yeniden girin!'
ErrPgImagePull: 'Image çekme zaman aşımı, lütfen image hızlandırma yapılandırın veya manuel olarak {{ .name }} imageını çekin ve tekrar deneyin'
PruneHelper: "Bu temizleme {{ .name }} {{ .count }} öğe kaldırdı, {{ .size }} disk alanı boşalttı"
ImageRemoveHelper: "{{ .name }} imajı silindi, {{ .size }} disk alanı boşalttı"
BuildCache: "Derleme önbelleği"
Volume: "Depolama hacmi"
Network: "Ağ"
#runtime
ErrFileNotExist: '{{ .detail }} dosyası mevcut değil! Lütfen kaynak dosyanın bütünlüğünü kontrol edin!'
@ -288,6 +293,7 @@ TaskPull: 'Çek'
TaskCommit: 'işleme'
TaskBuild: 'Yapı'
TaskPush: 'Gönder'
TaskClean: "Temizleme"
TaskHandle: 'Yürüt'
Website: 'Web Sitesi'
App: 'Uygulama'

View file

@ -172,6 +172,11 @@ ErrObjectInUsed: '該物件正被使用,無法刪除'
ErrObjectBeDependent: '此鏡像依賴其他鏡像,無法刪除'
ErrPortRules: '連接埠數目不匹配,請重新輸入!'
ErrPgImagePull: '鏡像拉取逾時,請配置鏡像加速或手動拉取{{ .name }} 鏡像後重試'
PruneHelper: "本次清理 {{ .name }} {{ .count }} 個,釋放磁盤空間 {{ .size }}"
ImageRemoveHelper: "刪除鏡像 {{ .name }} ,釋放磁盤空間 {{ .size }}"
BuildCache: "構建緩存"
Volume: "存儲卷"
Network: "網絡"
#runtime
ErrFileNotExist: '{{ .detail }} 檔案不存在!請檢查來源檔案完整性!'
@ -289,6 +294,7 @@ TaskPull: '拉取'
TaskCommit: '制作'
TaskBuild: '建置'
TaskPush: '推送'
TaskClean: "清理"
TaskHandle: '執行'
Website: '網站'
App: '應用程式'

View file

@ -172,6 +172,11 @@ ErrObjectInUsed: "该对象正被使用,无法删除"
ErrObjectBeDependent: "该镜像依赖于其他镜像,无法删除"
ErrPortRules: "端口数目不匹配,请重新输入!"
ErrPgImagePull: "镜像拉取超时,请配置镜像加速或手动拉取 {{ .name }} 镜像后重试"
PruneHelper: "本次清理 {{ .name }} {{ .count }} 个,释放磁盘空间 {{ .size }}"
ImageRemoveHelper: "删除镜像 {{ .name }} ,释放磁盘空间 {{ .size }}"
BuildCache: "构建缓存"
Volume: "存储卷"
Network: "网络"
#runtime
ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!"
@ -289,6 +294,7 @@ TaskPull: "拉取"
TaskCommit: "制作"
TaskBuild: "构建"
TaskPush: "推送"
TaskClean: "清理"
TaskHandle: "执行"
Website: "网站"
App: "应用"

View file

@ -1,6 +1,12 @@
import { ReqPage } from '.';
export namespace Cronjob {
export interface Search extends ReqPage {
info: string;
groupIDs: Array<number>;
orderBy?: string;
order?: string;
}
export interface CronjobInfo {
id: number;
name: string;

View file

@ -20,7 +20,6 @@ export interface ReqPage {
}
export interface SearchWithPage {
info: string;
groupIDs: Array<number>;
page: number;
pageSize: number;
orderBy?: string;

View file

@ -53,7 +53,7 @@ export const containerOperator = (params: Container.ContainerOperate) => {
return http.post(`/containers/operate`, params);
};
export const containerPrune = (params: Container.ContainerPrune) => {
return http.post<Container.ContainerPruneReport>(`/containers/prune`, params);
return http.post(`/containers/prune`, params);
};
export const inspect = (params: Container.ContainerInspect) => {
return http.post<string>(`/containers/inspect`, params);
@ -95,7 +95,7 @@ export const imageTag = (params: Container.ImageTag) => {
return http.post(`/containers/image/tag`, params);
};
export const imageRemove = (params: Container.BatchDelete) => {
return http.post<Container.ContainerPruneReport>(`/containers/image/remove`, params);
return http.post(`/containers/image/remove`, params);
};
// network

View file

@ -3,7 +3,7 @@ import { ResPage, SearchWithPage } from '../interface';
import { Cronjob } from '../interface/cronjob';
import { TimeoutEnum } from '@/enums/http-enum';
export const searchCronjobPage = (params: SearchWithPage) => {
export const searchCronjobPage = (params: Cronjob.Search) => {
return http.post<ResPage<Cronjob.CronjobInfo>>(`/cronjobs/search`, params);
};

View file

@ -18,42 +18,45 @@
</span>
</template>
</DialogPro>
<TaskLog ref="taskLogRef" width="70%" @close="onSearch" />
</template>
<script lang="ts" setup>
import { containerPrune } from '@/api/modules/container';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import TaskLog from '@/components/log/task/index.vue';
import { ref } from 'vue';
import { computeSize } from '@/utils/util';
import { newUUID } from '@/utils/util';
const loading = ref(false);
const open = ref<boolean>(false);
const taskLogRef = ref();
const emit = defineEmits<{ (e: 'search'): void }>();
const onClean = async () => {
loading.value = true;
let params = {
taskID: newUUID(),
pruneType: 'container',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
.then(() => {
loading.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
open.value = false;
emit('search');
openTaskLog(params.taskID);
})
.catch(() => {
loading.value = false;
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const onSearch = () => {
emit('search');
};
const acceptParams = (): void => {
open.value = true;

View file

@ -33,7 +33,7 @@
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" :heightDiff="300">
<el-table-column label="ID" prop="id" width="140" show-overflow-tooltip>
<el-table-column label="ID" prop="id" width="140">
<template #default="{ row }">
<el-text type="primary" class="cursor-pointer" @click="onInspect(row.id)">
{{ row.id.replaceAll('sha256:', '').substring(0, 12) }}
@ -92,12 +92,13 @@
<Build ref="dialogBuildRef" @search="search" />
<Delete ref="dialogDeleteRef" @search="search" />
<Prune ref="dialogPruneRef" @search="search" />
<TaskLog ref="taskLogRef" width="70%" />
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, computed } from 'vue';
import { dateFormat } from '@/utils/util';
import { dateFormat, newUUID } from '@/utils/util';
import { Container } from '@/api/interface/container';
import Pull from '@/views/container/image/pull/index.vue';
import Tag from '@/views/container/image/tag/index.vue';
@ -109,13 +110,14 @@ import Delete from '@/views/container/image/delete/index.vue';
import Prune from '@/views/container/image/prune/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import TaskLog from '@/components/log/task/index.vue';
import { searchImage, listImageRepo, imageRemove, inspect, containerPrune } from '@/api/modules/container';
import i18n from '@/lang';
import { GlobalStore } from '@/store';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
const globalStore = GlobalStore();
const taskLogRef = ref();
const mobile = computed(() => {
return globalStore.isMobile();
});
@ -220,13 +222,14 @@ const onOpenBuildCache = () => {
}).then(async () => {
loading.value = true;
let params = {
taskID: newUUID(),
pruneType: 'buildcache',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
openTaskLog(params.taskID);
search();
})
.catch(() => {
@ -234,6 +237,9 @@ const onOpenBuildCache = () => {
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const onOpenload = () => {
dialogLoadRef.value!.acceptParams();

View file

@ -38,13 +38,14 @@
</span>
</template>
</DrawerPro>
<TaskLog ref="taskLogRef" width="70%" @close="onSearch" />
</template>
<script lang="ts" setup>
import { containerPrune, imageRemove, listAllImage } from '@/api/modules/container';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { computeSize } from '@/utils/util';
import { newUUID } from '@/utils/util';
import { ref } from 'vue';
const dialogVisible = ref(false);
@ -54,6 +55,7 @@ const loading = ref();
const unTagList = ref([]);
const unUsedList = ref([]);
const data = ref([]);
const taskLogRef = ref();
const checkAll = ref(false);
const isIndeterminate = ref(false);
@ -120,6 +122,10 @@ const handleClose = () => {
dialogVisible.value = false;
};
const onSearch = () => {
emit('search');
};
const onClean = async () => {
loading.value = true;
if (checkAll.value) {
@ -131,41 +137,34 @@ const onClean = async () => {
const prune = async () => {
let params = {
taskID: newUUID(),
pruneType: 'image',
withTagAll: scope.value === 'unused',
};
await containerPrune(params)
.then((res) => {
.then(() => {
loading.value = false;
dialogVisible.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
emit('search');
openTaskLog(params.taskID);
})
.catch(() => {
loading.value = false;
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const removeImage = async () => {
let params = {
taskID: newUUID(),
names: checkedLists.value,
};
await imageRemove(params)
.then((res) => {
.then(() => {
loading.value = false;
dialogVisible.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
emit('search');
openTaskLog(params.taskID);
})
.catch(() => {
loading.value = false;

View file

@ -88,6 +88,7 @@
<OpDialog ref="opRef" @search="search" />
<CodemirrorDrawer ref="myDetail" />
<CreateDialog @search="search" ref="dialogCreateRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div>
</template>
@ -95,16 +96,17 @@
import CreateDialog from '@/views/container/network/create/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import { reactive, ref } from 'vue';
import { dateFormat } from '@/utils/util';
import { dateFormat, newUUID } from '@/utils/util';
import { deleteNetwork, searchNetwork, inspect, containerPrune } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import DockerStatus from '@/views/container/docker-status/index.vue';
const loading = ref();
const myDetail = ref();
const taskLogRef = ref();
const data = ref();
const selects = ref<any>([]);
@ -136,20 +138,23 @@ const onClean = () => {
}).then(async () => {
loading.value = true;
let params = {
taskID: newUUID(),
pruneType: 'network',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
search();
openTaskLog(params.taskID);
})
.catch(() => {
loading.value = false;
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
function selectable(row) {
return !row.isSystem;

View file

@ -83,6 +83,7 @@
<CodemirrorDrawer ref="myDetail" />
<CreateDialog @search="search" ref="dialogCreateRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div>
</template>
@ -91,16 +92,17 @@ import CreateDialog from '@/views/container/volume/create/index.vue';
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
import { reactive, ref, computed } from 'vue';
import { computeSize, dateFormat } from '@/utils/util';
import { dateFormat, newUUID } from '@/utils/util';
import { deleteVolume, searchVolume, inspect, containerPrune } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const taskLogRef = ref();
const mobile = computed(() => {
return globalStore.isMobile();
});
@ -174,25 +176,23 @@ const onClean = () => {
}).then(async () => {
loading.value = true;
let params = {
taskID: newUUID(),
pruneType: 'volume',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
.then(() => {
loading.value = false;
MsgSuccess(
i18n.global.t('container.cleanSuccessWithSpace', [
res.data.deletedNumber,
computeSize(res.data.spaceReclaimed),
]),
);
search();
openTaskLog(params.taskID);
})
.catch(() => {
loading.value = false;
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const batchDelete = async (row: Container.VolumeInfo | null) => {
let names = [];

View file

@ -125,6 +125,7 @@
<Config ref="configRef" />
<Supervisor ref="supervisorRef" />
<Terminal ref="terminalRef" />
<TaskLog ref="taskLogRef" width="70%" @close="search" />
</div>
</template>
@ -132,10 +133,10 @@
import { onMounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, RuntimeDeleteCheck, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat } from '@/utils/util';
import { dateFormat, newUUID } from '@/utils/util';
import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang';
import ExtManagement from './extension-management/index.vue';
import Extensions from './extension-template/index.vue';
@ -158,6 +159,7 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
const taskLogRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
@ -354,20 +356,23 @@ const onOpenBuildCache = () => {
}).then(async () => {
loading.value = true;
let params = {
taskID: newUUID(),
pruneType: 'buildcache',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
search();
openTaskLog(params.taskID);
})
.catch(() => {
loading.value = false;
});
});
};
const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });