From bb48beec822ce231179b35ad2fa8a40931736718 Mon Sep 17 00:00:00 2001 From: cuigh Date: Thu, 23 Dec 2021 15:25:51 +0800 Subject: [PATCH] Add prune function to image and container --- api/container.go | 27 +++++++++++++++++++++- api/image.go | 24 ++++++++++++++++++++ api/volume.go | 6 ++--- biz/container.go | 27 +++++++++++++++------- biz/event.go | 12 ++++++++++ biz/image.go | 23 ++++++++++++++----- biz/volume.go | 6 ++--- docker/container.go | 9 ++++++++ docker/image.go | 9 ++++++++ ui/src/api/container.ts | 7 ++++++ ui/src/api/image.ts | 7 ++++++ ui/src/api/volume.ts | 10 ++++----- ui/src/locales/en.ts | 10 +++++++++ ui/src/locales/zh.ts | 10 +++++++++ ui/src/pages/container/List.vue | 40 ++++++++++++++++++++++++++++----- ui/src/pages/image/List.vue | 36 ++++++++++++++++++++++++++--- ui/src/pages/volume/List.vue | 12 +++++----- 17 files changed, 234 insertions(+), 41 deletions(-) diff --git a/api/container.go b/api/container.go index 3956949..f6a8948 100644 --- a/api/container.go +++ b/api/container.go @@ -18,6 +18,7 @@ type ContainerHandler struct { Delete web.HandlerFunc `path:"/delete" method:"post" auth:"container.delete" desc:"delete container"` FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"container.logs" desc:"fetch logs of container"` Connect web.HandlerFunc `path:"/connect" auth:"container.execute" desc:"connect to a running container"` + Prune web.HandlerFunc `path:"/prune" method:"post" auth:"container.delete" desc:"delete unused containers"` } // NewContainer creates an instance of ContainerHandler @@ -28,6 +29,7 @@ func NewContainer(b biz.ContainerBiz) *ContainerHandler { Delete: containerDelete(b), FetchLogs: containerFetchLogs(b), Connect: containerConnect(b), + Prune: containerPrune(b), } } @@ -78,11 +80,12 @@ func containerDelete(b biz.ContainerBiz) web.HandlerFunc { type Args struct { Node string `json:"node"` ID string `json:"id"` + Name string `json:"name"` } return func(ctx web.Context) (err error) { args := &Args{} if err = ctx.Bind(args); err == nil { - err = b.Delete(args.Node, args.ID, ctx.User()) + err = b.Delete(args.Node, args.ID, args.Name, ctx.User()) } return ajax(ctx, err) } @@ -214,3 +217,25 @@ func containerConnect(b biz.ContainerBiz) web.HandlerFunc { return nil } } + +func containerPrune(b biz.ContainerBiz) web.HandlerFunc { + type Args struct { + Node string `json:"node"` + } + return func(ctx web.Context) (err error) { + args := &Args{} + if err = ctx.Bind(args); err != nil { + return err + } + + count, size, err := b.Prune(args.Node, ctx.User()) + if err != nil { + return err + } + + return success(ctx, data.Map{ + "count": count, + "size": size, + }) + } +} diff --git a/api/image.go b/api/image.go index 10268df..422b8c9 100644 --- a/api/image.go +++ b/api/image.go @@ -11,6 +11,7 @@ type ImageHandler struct { Search web.HandlerFunc `path:"/search" auth:"image.view" desc:"search images"` Find web.HandlerFunc `path:"/find" auth:"image.view" desc:"find image by id"` Delete web.HandlerFunc `path:"/delete" method:"post" auth:"image.delete" desc:"delete image"` + Prune web.HandlerFunc `path:"/prune" method:"post" auth:"image.delete" desc:"delete unused images"` } // NewImage creates an instance of ImageHandler @@ -19,6 +20,7 @@ func NewImage(b biz.ImageBiz) *ImageHandler { Search: imageSearch(b), Find: imageFind(b), Delete: imageDelete(b), + Prune: imagePrune(b), } } @@ -77,3 +79,25 @@ func imageDelete(b biz.ImageBiz) web.HandlerFunc { return ajax(ctx, err) } } + +func imagePrune(b biz.ImageBiz) web.HandlerFunc { + type Args struct { + Node string `json:"node"` + } + return func(ctx web.Context) (err error) { + args := &Args{} + if err = ctx.Bind(args); err != nil { + return err + } + + count, size, err := b.Prune(args.Node, ctx.User()) + if err != nil { + return err + } + + return success(ctx, data.Map{ + "count": count, + "size": size, + }) + } +} diff --git a/api/volume.go b/api/volume.go index 25ae601..9b2a895 100644 --- a/api/volume.go +++ b/api/volume.go @@ -103,14 +103,14 @@ func volumePrune(b biz.VolumeBiz) web.HandlerFunc { return err } - deletedVolumes, reclaimedSpace, err := b.Prune(args.Node, ctx.User()) + count, size, err := b.Prune(args.Node, ctx.User()) if err != nil { return err } return success(ctx, data.Map{ - "deletedVolumes": deletedVolumes, - "reclaimedSpace": reclaimedSpace, + "count": count, + "size": size, }) } } diff --git a/biz/container.go b/biz/container.go index ce692db..6bc523f 100644 --- a/biz/container.go +++ b/biz/container.go @@ -14,19 +14,21 @@ import ( type ContainerBiz interface { Search(node, name, status string, pageIndex, pageSize int) ([]*Container, int, error) Find(node, id string) (container *Container, raw string, err error) - Delete(node, id string, user web.User) (err error) + Delete(node, id, name string, user web.User) (err error) FetchLogs(node, id string, lines int, timestamps bool) (stdout, stderr string, err error) ExecCreate(node, id string, cmd string) (resp types.IDResponse, err error) ExecAttach(node, id string) (resp types.HijackedResponse, err error) ExecStart(node, id string) error + Prune(node string, user web.User) (count int, size uint64, err error) } -func NewContainer(d *docker.Docker) ContainerBiz { - return &containerBiz{d: d} +func NewContainer(d *docker.Docker, eb EventBiz) ContainerBiz { + return &containerBiz{d: d, eb: eb} } type containerBiz struct { - d *docker.Docker + d *docker.Docker + eb EventBiz } func (b *containerBiz) Find(node, id string) (c *Container, raw string, err error) { @@ -58,11 +60,11 @@ func (b *containerBiz) Search(node, name, status string, pageIndex, pageSize int return containers, total, nil } -func (b *containerBiz) Delete(node, id string, user web.User) (err error) { +func (b *containerBiz) Delete(node, id, name string, user web.User) (err error) { err = b.d.ContainerRemove(context.TODO(), node, id) - //if err == nil { - // Event.CreateContainer(model.EventActionDelete, id, user) - //} + if err == nil { + b.eb.CreateContainer(EventActionDelete, id, name, user) + } return } @@ -86,6 +88,15 @@ func (b *containerBiz) FetchLogs(node, id string, lines int, timestamps bool) (s return stdout.String(), stderr.String(), nil } +func (b *containerBiz) Prune(node string, user web.User) (count int, size uint64, err error) { + var report types.ContainersPruneReport + if report, err = b.d.ContainerPrune(context.TODO(), node); err == nil { + count, size = len(report.ContainersDeleted), report.SpaceReclaimed + b.eb.CreateContainer(EventActionPrune, "", "", user) + } + return +} + type Container struct { ID string `json:"id"` Name string `json:"name"` diff --git a/biz/event.go b/biz/event.go index 1f51122..837cef1 100644 --- a/biz/event.go +++ b/biz/event.go @@ -21,6 +21,8 @@ const ( EventTypeStack EventType = "Stack" EventTypeConfig EventType = "Config" EventTypeSecret EventType = "Secret" + EventTypeImage EventType = "Image" + EventTypeContainer EventType = "Container" EventTypeVolume EventType = "Volume" EventTypeUser EventType = "User" EventTypeRole EventType = "Role" @@ -54,6 +56,8 @@ type EventBiz interface { CreateConfig(action EventAction, id, name string, user web.User) CreateSecret(action EventAction, id, name string, user web.User) CreateStack(action EventAction, name string, user web.User) + CreateImage(action EventAction, id string, user web.User) + CreateContainer(action EventAction, id, name string, user web.User) CreateVolume(action EventAction, name string, user web.User) CreateUser(action EventAction, id, name string, user web.User) CreateRole(action EventAction, id, name string, user web.User) @@ -110,6 +114,14 @@ func (b *eventBiz) CreateNode(action EventAction, id, name string, user web.User b.create(EventTypeNode, action, id, name, user) } +func (b *eventBiz) CreateImage(action EventAction, id string, user web.User) { + b.create(EventTypeImage, action, id, "", user) +} + +func (b *eventBiz) CreateContainer(action EventAction, id, name string, user web.User) { + b.create(EventTypeContainer, action, id, name, user) +} + func (b *eventBiz) CreateVolume(action EventAction, name string, user web.User) { b.create(EventTypeVolume, action, name, name, user) } diff --git a/biz/image.go b/biz/image.go index 3c92fbf..0a5c1bb 100644 --- a/biz/image.go +++ b/biz/image.go @@ -15,14 +15,16 @@ type ImageBiz interface { Search(node, name string, pageIndex, pageSize int) ([]*Image, int, error) Find(node, name string) (image *Image, raw string, err error) Delete(node, id string, user web.User) (err error) + Prune(node string, user web.User) (count int, size uint64, err error) } -func NewImage(d *docker.Docker) ImageBiz { - return &imageBiz{d: d} +func NewImage(d *docker.Docker, eb EventBiz) ImageBiz { + return &imageBiz{d: d, eb: eb} } type imageBiz struct { - d *docker.Docker + d *docker.Docker + eb EventBiz } func (b *imageBiz) Find(node, id string) (img *Image, raw string, err error) { @@ -62,9 +64,18 @@ func (b *imageBiz) Search(node, name string, pageIndex, pageSize int) (images [] func (b *imageBiz) Delete(node, id string, user web.User) (err error) { err = b.d.ImageRemove(context.TODO(), node, id) - //if err == nil { - // Event.CreateImage(model.EventActionDelete, id, user) - //} + if err == nil { + b.eb.CreateImage(EventActionDelete, id, user) + } + return +} + +func (b *imageBiz) Prune(node string, user web.User) (count int, size uint64, err error) { + var report types.ImagesPruneReport + if report, err = b.d.ImagePrune(context.TODO(), node); err == nil { + count, size = len(report.ImagesDeleted), report.SpaceReclaimed + b.eb.CreateImage(EventActionPrune, "", user) + } return } diff --git a/biz/volume.go b/biz/volume.go index dade6ae..fc9a8b6 100644 --- a/biz/volume.go +++ b/biz/volume.go @@ -16,7 +16,7 @@ type VolumeBiz interface { Find(node, name string) (volume *Volume, raw string, err error) Delete(node, name string, user web.User) (err error) Create(volume *Volume, user web.User) (err error) - Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) + Prune(node string, user web.User) (count int, size uint64, err error) } func NewVolume(d *docker.Docker, eb EventBiz) VolumeBiz { @@ -85,11 +85,11 @@ func (b *volumeBiz) Create(vol *Volume, user web.User) (err error) { return } -func (b *volumeBiz) Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) { +func (b *volumeBiz) Prune(node string, user web.User) (count int, size uint64, err error) { var report types.VolumesPruneReport report, err = b.d.VolumePrune(context.TODO(), node) if err == nil { - deletedVolumes, reclaimedSpace = report.VolumesDeleted, report.SpaceReclaimed + count, size = len(report.VolumesDeleted), report.SpaceReclaimed b.eb.CreateVolume(EventActionPrune, "", user) } return diff --git a/docker/container.go b/docker/container.go index e7e1757..120616d 100644 --- a/docker/container.go +++ b/docker/container.go @@ -136,3 +136,12 @@ func (d *Docker) ContainerLogs(ctx context.Context, node, id string, lines int, } return } + +// ContainerPrune remove all unused containers. +func (d *Docker) ContainerPrune(ctx context.Context, node string) (report types.ContainersPruneReport, err error) { + var c *client.Client + if c, err = d.agent(node); err == nil { + report, err = c.ContainersPrune(ctx, filters.NewArgs()) + } + return +} diff --git a/docker/image.go b/docker/image.go index 46e81dd..37a6d58 100644 --- a/docker/image.go +++ b/docker/image.go @@ -59,3 +59,12 @@ func (d *Docker) ImageRemove(ctx context.Context, node, id string) error { } return err } + +// ImagePrune remove all unused images. +func (d *Docker) ImagePrune(ctx context.Context, node string) (report types.ImagesPruneReport, err error) { + var c *client.Client + if c, err = d.agent(node); err == nil { + report, err = c.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "false"))) + } + return +} diff --git a/ui/src/api/container.ts b/ui/src/api/container.ts index 026a37d..0115c32 100644 --- a/ui/src/api/container.ts +++ b/ui/src/api/container.ts @@ -79,6 +79,13 @@ export class ContainerApi { stderr: string; }>('/container/fetch-logs', args) } + + prune(node: string) { + return ajax.post<{ + count: number; + size: number; + }>('/container/prune', { node }) + } } export default new ContainerApi diff --git a/ui/src/api/image.ts b/ui/src/api/image.ts index 21a7be9..67e968f 100644 --- a/ui/src/api/image.ts +++ b/ui/src/api/image.ts @@ -74,6 +74,13 @@ export class ImageApi { delete(node: string, id: string, name: string) { return ajax.post>('/image/delete', { node, id, name }) } + + prune(node: string) { + return ajax.post<{ + count: number; + size: number; + }>('/image/prune', { node }) + } } export default new ImageApi diff --git a/ui/src/api/volume.ts b/ui/src/api/volume.ts index 742106b..51ace43 100644 --- a/ui/src/api/volume.ts +++ b/ui/src/api/volume.ts @@ -36,11 +36,6 @@ export interface FindResult { raw: string; } -export interface PruneResult { - deletedVolumes: string[]; - reclaimedSpace: number; -} - export class VolumeApi { find(node: string, name: string) { return ajax.get('/volume/find', { node, name }) @@ -59,7 +54,10 @@ export class VolumeApi { } prune(node: string) { - return ajax.post('/volume/prune', { node }) + return ajax.post<{ + count: number; + size: number; + }>('/volume/prune', { node }) } } diff --git a/ui/src/locales/en.ts b/ui/src/locales/en.ts index 28e3bda..bc6e53c 100644 --- a/ui/src/locales/en.ts +++ b/ui/src/locales/en.ts @@ -233,6 +233,14 @@ export default { "export_chart": { "title": "Export chart", }, + "prune_image": { + "title": "Prune image", + "body": "Are you sure you want to clean up unused images?", + }, + "prune_container": { + "title": "Prune container", + "body": "Are you sure you want to clean up stopped containers?", + }, "prune_volume": { "title": "Prune volume", "body": "Are you sure you want to clean up unused data volumes?", @@ -384,6 +392,8 @@ export default { "service_notice": "This service belong to '%s' stack, usually you should modify original compose instead of updating it directly.", "password_notice": "You are a LDAP user, can not modify password here.", "setting_notice": "You must restart Swirl to activate modifications.", + "prune_image_success": "A total of {count} images are cleaned up to free up {size} of space.", + "prune_container_success": "A total of {count} containers are cleaned up to free up {size} of space.", "prune_volume_success": "A total of {count} data volumes are cleaned up to free up {size} of space.", "403": "Sorry, you don't have permission to access this page", "404": "The page you are requesting has been moved, deleted, renamed, or may not exist", diff --git a/ui/src/locales/zh.ts b/ui/src/locales/zh.ts index b6ec307..19cdae6 100644 --- a/ui/src/locales/zh.ts +++ b/ui/src/locales/zh.ts @@ -233,6 +233,14 @@ export default { "export_chart": { "title": "导出图表", }, + "prune_image": { + "title": "清理镜像", + "body": "是否确实要清理未使用的镜像?", + }, + "prune_container": { + "title": "清理容器", + "body": "是否确实要清理已停止的容器?", + }, "prune_volume": { "title": "清理数据卷", "body": "是否确实要清理未使用的数据卷?", @@ -384,6 +392,8 @@ export default { "service_notice": "这个服务属于编排 {stack},通常你应该修改原始的编排而不是直接修改此服务信息,否则后续重新部署编排时这些修改将会丢失。", "password_notice": "你是 LDAP 用户,不能在这里修改密码。", "setting_notice": "你必须重启 Swirl 才能让设置生效。", + "prune_image_success": "共清理 {count} 个镜像,释放 {size} 空间。", + "prune_container_success": "共清理 {count} 个容器,释放 {size} 空间。", "prune_volume_success": "共清理 {count} 个数据卷,释放 {size} 空间。", "403": "很抱歉,你没有权限访问此页面", "404": "您要请求的页面已被移动、删除、重命名或可能不存在", diff --git a/ui/src/pages/container/List.vue b/ui/src/pages/container/List.vue index 50ce6f9..12935e5 100644 --- a/ui/src/pages/container/List.vue +++ b/ui/src/pages/container/List.vue @@ -1,5 +1,16 @@