mirror of
https://github.com/cuigh/swirl
synced 2025-04-10 15:35:38 +00:00
Add agent support for image and volume
This commit is contained in:
parent
cb2cb4ab86
commit
dfe15524a2
21
api/image.go
21
api/image.go
@ -8,22 +8,23 @@ import (
|
||||
|
||||
// ImageHandler encapsulates image related handlers.
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// NewImage creates an instance of ImageHandler
|
||||
func NewImage(b biz.ImageBiz) *ImageHandler {
|
||||
return &ImageHandler{
|
||||
Search: imageSearch(b),
|
||||
Find: imageFind(b),
|
||||
Delete: imageDelete(b),
|
||||
Search: imageSearch(b),
|
||||
Find: imageFind(b),
|
||||
Delete: imageDelete(b),
|
||||
}
|
||||
}
|
||||
|
||||
func imageSearch(b biz.ImageBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Node string `json:"node" bind:"node"`
|
||||
Name string `json:"name" bind:"name"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
@ -37,7 +38,7 @@ func imageSearch(b biz.ImageBiz) web.HandlerFunc {
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
images, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
|
||||
images, total, err = b.Search(args.Node, args.Name, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -53,8 +54,9 @@ func imageSearch(b biz.ImageBiz) web.HandlerFunc {
|
||||
|
||||
func imageFind(b biz.ImageBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
node := ctx.Query("node")
|
||||
id := ctx.Query("id")
|
||||
image, raw, err := b.Find(id)
|
||||
image, raw, err := b.Find(node, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -64,12 +66,13 @@ func imageFind(b biz.ImageBiz) web.HandlerFunc {
|
||||
|
||||
func imageDelete(b biz.ImageBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Node string `json:"node"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.ID, ctx.User())
|
||||
err = b.Delete(args.Node, args.ID, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
|
11
api/node.go
11
api/node.go
@ -33,6 +33,17 @@ func nodeList(nb biz.NodeBiz) web.HandlerFunc {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Query("agent") == "true" {
|
||||
i := 0
|
||||
for j := 0; j < len(nodes); j++ {
|
||||
if nodes[j].Agent != "" {
|
||||
nodes[i] = nodes[j]
|
||||
i++
|
||||
}
|
||||
}
|
||||
nodes = nodes[:i]
|
||||
}
|
||||
|
||||
return success(ctx, nodes)
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ func NewVolume(b biz.VolumeBiz) *VolumeHandler {
|
||||
|
||||
func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Node string `json:"node" bind:"node"`
|
||||
Name string `json:"name" bind:"name"`
|
||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||
PageSize int `json:"pageSize" bind:"pageSize"`
|
||||
@ -41,7 +42,7 @@ func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
volumes, total, err = b.Search(args.Name, args.PageIndex, args.PageSize)
|
||||
volumes, total, err = b.Search(args.Node, args.Name, args.PageIndex, args.PageSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -57,8 +58,9 @@ func volumeSearch(b biz.VolumeBiz) web.HandlerFunc {
|
||||
|
||||
func volumeFind(b biz.VolumeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
node := ctx.Query("node")
|
||||
name := ctx.Query("name")
|
||||
volume, raw, err := b.Find(name)
|
||||
volume, raw, err := b.Find(node, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -68,12 +70,13 @@ func volumeFind(b biz.VolumeBiz) web.HandlerFunc {
|
||||
|
||||
func volumeDelete(b biz.VolumeBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Node string `json:"node"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
args := &Args{}
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
err = b.Delete(args.Name, ctx.User())
|
||||
err = b.Delete(args.Node, args.Name, ctx.User())
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
@ -91,11 +94,20 @@ func volumeSave(b biz.VolumeBiz) web.HandlerFunc {
|
||||
}
|
||||
|
||||
func volumePrune(b biz.VolumeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
deletedVolumes, reclaimedSpace, err := b.Prune(ctx.User())
|
||||
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
|
||||
}
|
||||
|
||||
deletedVolumes, reclaimedSpace, err := b.Prune(args.Node, ctx.User())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"deletedVolumes": deletedVolumes,
|
||||
"reclaimedSpace": reclaimedSpace,
|
||||
|
20
biz/image.go
20
biz/image.go
@ -12,9 +12,9 @@ import (
|
||||
)
|
||||
|
||||
type ImageBiz interface {
|
||||
Search(name string, pageIndex, pageSize int) ([]*Image, int, error)
|
||||
Find(name string) (image *Image, raw string, err error)
|
||||
Delete(id string, user web.User) (err error)
|
||||
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)
|
||||
}
|
||||
|
||||
func NewImage(d *docker.Docker) ImageBiz {
|
||||
@ -25,7 +25,7 @@ type imageBiz struct {
|
||||
d *docker.Docker
|
||||
}
|
||||
|
||||
func (b *imageBiz) Find(id string) (img *Image, raw string, err error) {
|
||||
func (b *imageBiz) Find(node, id string) (img *Image, raw string, err error) {
|
||||
var (
|
||||
i types.ImageInspect
|
||||
r []byte
|
||||
@ -33,12 +33,12 @@ func (b *imageBiz) Find(id string) (img *Image, raw string, err error) {
|
||||
ctx = context.TODO()
|
||||
)
|
||||
|
||||
if i, r, err = b.d.ImageInspect(ctx, id); err == nil {
|
||||
if i, r, err = b.d.ImageInspect(ctx, node, id); err == nil {
|
||||
raw, err = indentJSON(r)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
histories, err = b.d.ImageHistory(ctx, id)
|
||||
histories, err = b.d.ImageHistory(ctx, node, id)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@ -47,8 +47,8 @@ func (b *imageBiz) Find(id string) (img *Image, raw string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *imageBiz) Search(name string, pageIndex, pageSize int) (images []*Image, total int, err error) {
|
||||
list, total, err := b.d.ImageList(context.TODO(), name, pageIndex, pageSize)
|
||||
func (b *imageBiz) Search(node, name string, pageIndex, pageSize int) (images []*Image, total int, err error) {
|
||||
list, total, err := b.d.ImageList(context.TODO(), node, name, pageIndex, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -60,8 +60,8 @@ func (b *imageBiz) Search(name string, pageIndex, pageSize int) (images []*Image
|
||||
return images, total, nil
|
||||
}
|
||||
|
||||
func (b *imageBiz) Delete(id string, user web.User) (err error) {
|
||||
err = b.d.ImageRemove(context.TODO(), id)
|
||||
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)
|
||||
//}
|
||||
|
15
biz/node.go
15
biz/node.go
@ -2,6 +2,7 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
@ -42,7 +43,19 @@ func (b *nodeBiz) Find(id string) (node *Node, raw string, err error) {
|
||||
}
|
||||
|
||||
func (b *nodeBiz) List() ([]*docker.Node, error) {
|
||||
return b.d.NodeListCache()
|
||||
m, err := b.d.NodeMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes := make([]*docker.Node, 0, len(m))
|
||||
for _, n := range m {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Name < nodes[j].Name
|
||||
})
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (b *nodeBiz) Search() ([]*Node, error) {
|
||||
|
25
biz/task.go
25
biz/task.go
@ -28,12 +28,17 @@ func (b *taskBiz) Find(id string) (task *Task, raw string, err error) {
|
||||
s swarm.Service
|
||||
r []byte
|
||||
)
|
||||
|
||||
t, r, err = b.d.TaskInspect(context.TODO(), id)
|
||||
if err == nil {
|
||||
raw, err = indentJSON(r)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
task = newTask(&t)
|
||||
m, _ := b.d.NodeMap()
|
||||
task = newTask(&t, m)
|
||||
|
||||
// Fill service name
|
||||
if s, _, _ = b.d.ServiceInspect(context.TODO(), t.ServiceID, false); s.Spec.Name == "" {
|
||||
task.ServiceName = task.ServiceID
|
||||
} else {
|
||||
@ -50,9 +55,15 @@ func (b *taskBiz) Search(node, service, state string, pageIndex, pageSize int) (
|
||||
return
|
||||
}
|
||||
|
||||
m, _ := b.d.NodeMap()
|
||||
tasks = make([]*Task, len(list))
|
||||
for i, t := range list {
|
||||
tasks[i] = newTask(&t)
|
||||
tasks[i] = newTask(&t, m)
|
||||
if m != nil {
|
||||
if n, ok := m[t.NodeID]; ok {
|
||||
tasks[i].NodeName = n.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -75,6 +86,7 @@ type Task struct {
|
||||
ServiceID string `json:"serviceId"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
NodeID string `json:"nodeId"`
|
||||
NodeName string `json:"nodeName"`
|
||||
ContainerID string `json:"containerId"`
|
||||
PID int `json:"pid"`
|
||||
ExitCode int `json:"exitCode"`
|
||||
@ -93,7 +105,7 @@ type TaskNetwork struct {
|
||||
IPs []string `json:"ips"`
|
||||
}
|
||||
|
||||
func newTask(t *swarm.Task) *Task {
|
||||
func newTask(t *swarm.Task, nodes map[string]*docker.Node) *Task {
|
||||
task := &Task{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
@ -103,6 +115,7 @@ func newTask(t *swarm.Task) *Task {
|
||||
State: t.Status.State,
|
||||
ServiceID: t.ServiceID,
|
||||
NodeID: t.NodeID,
|
||||
NodeName: t.NodeID,
|
||||
Message: t.Status.Message,
|
||||
Error: t.Status.Err,
|
||||
Env: envToOptions(t.Spec.ContainerSpec.Env),
|
||||
@ -122,5 +135,11 @@ func newTask(t *swarm.Task) *Task {
|
||||
IPs: n.Addresses,
|
||||
})
|
||||
}
|
||||
// Fill node name
|
||||
if nodes != nil {
|
||||
if n, ok := nodes[t.NodeID]; ok {
|
||||
task.NodeName = n.Name
|
||||
}
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
@ -12,11 +12,11 @@ import (
|
||||
)
|
||||
|
||||
type VolumeBiz interface {
|
||||
Search(name string, pageIndex, pageSize int) ([]*Volume, int, error)
|
||||
Find(name string) (volume *Volume, raw string, err error)
|
||||
Delete(name string, user web.User) (err error)
|
||||
Search(node, name string, pageIndex, pageSize int) ([]*Volume, int, error)
|
||||
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(user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error)
|
||||
Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error)
|
||||
}
|
||||
|
||||
func NewVolume(d *docker.Docker, eb EventBiz) VolumeBiz {
|
||||
@ -28,13 +28,13 @@ type volumeBiz struct {
|
||||
eb EventBiz
|
||||
}
|
||||
|
||||
func (b *volumeBiz) Find(name string) (volume *Volume, raw string, err error) {
|
||||
func (b *volumeBiz) Find(node, name string) (volume *Volume, raw string, err error) {
|
||||
var (
|
||||
v types.Volume
|
||||
r []byte
|
||||
)
|
||||
|
||||
if v, r, err = b.d.VolumeInspect(context.TODO(), name); err == nil {
|
||||
if v, r, err = b.d.VolumeInspect(context.TODO(), node, name); err == nil {
|
||||
raw, err = indentJSON(r)
|
||||
}
|
||||
|
||||
@ -44,8 +44,8 @@ func (b *volumeBiz) Find(name string) (volume *Volume, raw string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *volumeBiz) Search(name string, pageIndex, pageSize int) (volumes []*Volume, total int, err error) {
|
||||
list, total, err := b.d.VolumeList(context.TODO(), name, pageIndex, pageSize)
|
||||
func (b *volumeBiz) Search(node, name string, pageIndex, pageSize int) (volumes []*Volume, total int, err error) {
|
||||
list, total, err := b.d.VolumeList(context.TODO(), node, name, pageIndex, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -57,8 +57,8 @@ func (b *volumeBiz) Search(name string, pageIndex, pageSize int) (volumes []*Vol
|
||||
return volumes, total, nil
|
||||
}
|
||||
|
||||
func (b *volumeBiz) Delete(name string, user web.User) (err error) {
|
||||
err = b.d.VolumeRemove(context.TODO(), name)
|
||||
func (b *volumeBiz) Delete(node, name string, user web.User) (err error) {
|
||||
err = b.d.VolumeRemove(context.TODO(), node, name)
|
||||
if err == nil {
|
||||
b.eb.CreateVolume(EventActionDelete, name, user)
|
||||
}
|
||||
@ -78,16 +78,16 @@ func (b *volumeBiz) Create(vol *Volume, user web.User) (err error) {
|
||||
options.Driver = vol.Driver
|
||||
}
|
||||
|
||||
err = b.d.VolumeCreate(context.TODO(), options)
|
||||
err = b.d.VolumeCreate(context.TODO(), vol.Node, options)
|
||||
if err != nil {
|
||||
b.eb.CreateVolume(EventActionDelete, vol.Name, user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *volumeBiz) Prune(user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) {
|
||||
func (b *volumeBiz) Prune(node string, user web.User) (deletedVolumes []string, reclaimedSpace uint64, err error) {
|
||||
var report types.VolumesPruneReport
|
||||
report, err = b.d.VolumePrune(context.TODO())
|
||||
report, err = b.d.VolumePrune(context.TODO(), node)
|
||||
if err == nil {
|
||||
deletedVolumes, reclaimedSpace = report.VolumesDeleted, report.SpaceReclaimed
|
||||
b.eb.CreateVolume(EventActionPrune, "", user)
|
||||
@ -96,6 +96,7 @@ func (b *volumeBiz) Prune(user web.User) (deletedVolumes []string, reclaimedSpac
|
||||
}
|
||||
|
||||
type Volume struct {
|
||||
Node string `json:"node"`
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver,omitempty"`
|
||||
CustomDriver string `json:"customDriver,omitempty"`
|
||||
|
@ -4,10 +4,8 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/cache"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/util/lazy"
|
||||
@ -91,11 +89,11 @@ func (d *Docker) agent(node string) (*client.Client, error) {
|
||||
}
|
||||
|
||||
func (d *Docker) getAgent(node string) (agent string, err error) {
|
||||
if node == "" || node == "@" {
|
||||
if node == "" || node == "-" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
nodes, err := d.getNodes()
|
||||
nodes, err := d.NodeMap()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -106,18 +104,6 @@ func (d *Docker) getAgent(node string) (agent string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Docker) getNodes() (map[string]*Node, error) {
|
||||
v := cache.Value{
|
||||
TTL: 30 * time.Minute,
|
||||
Load: func() (interface{}, error) { return d.loadCache() },
|
||||
}
|
||||
value, err := v.Get(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value.(map[string]*Node), nil
|
||||
}
|
||||
|
||||
func (d *Docker) loadCache() (interface{}, error) {
|
||||
c, err := d.client()
|
||||
if err != nil {
|
||||
|
@ -11,8 +11,8 @@ import (
|
||||
)
|
||||
|
||||
// ImageList return images on the host.
|
||||
func (d *Docker) ImageList(ctx context.Context, name string, pageIndex, pageSize int) (images []types.ImageSummary, total int, err error) {
|
||||
c, err := d.client()
|
||||
func (d *Docker) ImageList(ctx context.Context, node, name string, pageIndex, pageSize int) (images []types.ImageSummary, total int, err error) {
|
||||
c, err := d.agent(node)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -34,27 +34,28 @@ func (d *Docker) ImageList(ctx context.Context, name string, pageIndex, pageSize
|
||||
}
|
||||
|
||||
// ImageInspect returns image information.
|
||||
func (d *Docker) ImageInspect(ctx context.Context, id string) (image types.ImageInspect, raw []byte, err error) {
|
||||
func (d *Docker) ImageInspect(ctx context.Context, node, id string) (image types.ImageInspect, raw []byte, err error) {
|
||||
var c *client.Client
|
||||
if c, err = d.client(); err == nil {
|
||||
if c, err = d.agent(node); err == nil {
|
||||
return c.ImageInspectWithRaw(ctx, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ImageHistory returns the changes in an image in history format.
|
||||
func (d *Docker) ImageHistory(ctx context.Context, id string) (histories []image.HistoryResponseItem, err error) {
|
||||
func (d *Docker) ImageHistory(ctx context.Context, node, id string) (histories []image.HistoryResponseItem, err error) {
|
||||
var c *client.Client
|
||||
if c, err = d.client(); err == nil {
|
||||
if c, err = d.agent(node); err == nil {
|
||||
return c.ImageHistory(ctx, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ImageRemove remove a image.
|
||||
func (d *Docker) ImageRemove(ctx context.Context, id string) error {
|
||||
return d.call(func(c *client.Client) (err error) {
|
||||
func (d *Docker) ImageRemove(ctx context.Context, node, id string) error {
|
||||
c, err := d.agent(node)
|
||||
if err == nil {
|
||||
_, err = c.ImageRemove(ctx, id, types.ImageRemoveOptions{})
|
||||
return
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ package docker
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/cache"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
@ -60,15 +62,14 @@ func (d *Docker) NodeInspect(ctx context.Context, id string) (node swarm.Node, r
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Docker) NodeListCache() ([]*Node, error) {
|
||||
m, err := d.getNodes()
|
||||
func (d *Docker) NodeMap() (map[string]*Node, error) {
|
||||
v := cache.Value{
|
||||
TTL: 30 * time.Minute,
|
||||
Load: func() (interface{}, error) { return d.loadCache() },
|
||||
}
|
||||
value, err := v.Get(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes := make([]*Node, 0, len(m))
|
||||
for _, n := range m {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
return nodes, nil
|
||||
return value.(map[string]*Node), nil
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ func (d *Docker) fillStatus(ctx context.Context, c *client.Client, services []sw
|
||||
opts = types.TaskListOptions{Filters: filters.NewArgs()}
|
||||
)
|
||||
|
||||
nodes, err = d.getNodes()
|
||||
nodes, err = d.NodeMap()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -12,13 +12,13 @@ import (
|
||||
)
|
||||
|
||||
// VolumeList return volumes on the host.
|
||||
func (d *Docker) VolumeList(ctx context.Context, name string, pageIndex, pageSize int) (volumes []*types.Volume, total int, err error) {
|
||||
func (d *Docker) VolumeList(ctx context.Context, node, name string, pageIndex, pageSize int) (volumes []*types.Volume, total int, err error) {
|
||||
var (
|
||||
c *client.Client
|
||||
resp volume.VolumeListOKBody
|
||||
)
|
||||
|
||||
c, err = d.client()
|
||||
c, err = d.agent(node)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -44,34 +44,37 @@ func (d *Docker) VolumeList(ctx context.Context, name string, pageIndex, pageSiz
|
||||
}
|
||||
|
||||
// VolumeCreate create a volume.
|
||||
func (d *Docker) VolumeCreate(ctx context.Context, options *volume.VolumeCreateBody) error {
|
||||
return d.call(func(c *client.Client) (err error) {
|
||||
func (d *Docker) VolumeCreate(ctx context.Context, node string, options *volume.VolumeCreateBody) (err error) {
|
||||
var c *client.Client
|
||||
if c, err = d.agent(node); err == nil {
|
||||
_, err = c.VolumeCreate(ctx, *options)
|
||||
return
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// VolumeRemove remove a volume.
|
||||
func (d *Docker) VolumeRemove(ctx context.Context, name string) error {
|
||||
return d.call(func(c *client.Client) (err error) {
|
||||
return c.VolumeRemove(ctx, name, false)
|
||||
})
|
||||
func (d *Docker) VolumeRemove(ctx context.Context, node, name string) (err error) {
|
||||
c, err := d.agent(node)
|
||||
if err == nil {
|
||||
err = c.VolumeRemove(ctx, name, false)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// VolumePrune remove all unused volumes.
|
||||
func (d *Docker) VolumePrune(ctx context.Context, ) (report types.VolumesPruneReport, err error) {
|
||||
err = d.call(func(c *client.Client) (err error) {
|
||||
func (d *Docker) VolumePrune(ctx context.Context, node string) (report types.VolumesPruneReport, err error) {
|
||||
var c *client.Client
|
||||
if c, err = d.agent(node); err == nil {
|
||||
report, err = c.VolumesPrune(ctx, filters.NewArgs())
|
||||
return
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// VolumeInspect return volume raw information.
|
||||
func (d *Docker) VolumeInspect(ctx context.Context, name string) (vol types.Volume, raw []byte, err error) {
|
||||
err = d.call(func(c *client.Client) error {
|
||||
func (d *Docker) VolumeInspect(ctx context.Context, node, name string) (vol types.Volume, raw []byte, err error) {
|
||||
var c *client.Client
|
||||
if c, err = d.agent(node); err == nil {
|
||||
vol, raw, err = c.VolumeInspectWithRaw(ctx, name)
|
||||
return err
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ export interface Image {
|
||||
}
|
||||
|
||||
export interface SearchArgs {
|
||||
node?: string;
|
||||
name?: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
@ -62,16 +63,16 @@ export interface FindResult {
|
||||
}
|
||||
|
||||
export class ImageApi {
|
||||
find(id: string) {
|
||||
return ajax.get<FindResult>('/image/find', { id })
|
||||
find(node: string, id: string) {
|
||||
return ajax.get<FindResult>('/image/find', { node, id })
|
||||
}
|
||||
|
||||
search(args: SearchArgs) {
|
||||
return ajax.get<SearchResult>('/image/search', args)
|
||||
}
|
||||
|
||||
delete(id: string, name: string) {
|
||||
return ajax.post<Result<Object>>('/image/delete', { id, name })
|
||||
delete(node: string, id: string, name: string) {
|
||||
return ajax.post<Result<Object>>('/image/delete', { node, id, name })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,8 @@ export class NodeApi {
|
||||
return ajax.get<FindResult>('/node/find', { id })
|
||||
}
|
||||
|
||||
list() {
|
||||
return ajax.get<Node[]>('/node/list')
|
||||
list(agent: boolean) {
|
||||
return ajax.get<Node[]>('/node/list', { agent })
|
||||
}
|
||||
|
||||
search() {
|
||||
|
@ -10,6 +10,7 @@ export interface Task {
|
||||
serviceId: string;
|
||||
serviceName: string;
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
containerId?: string;
|
||||
pid?: number;
|
||||
exitCode?: number;
|
||||
|
@ -20,6 +20,7 @@ export interface Volume {
|
||||
}
|
||||
|
||||
export interface SearchArgs {
|
||||
node?: string;
|
||||
name?: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
@ -41,24 +42,24 @@ export interface PruneResult {
|
||||
}
|
||||
|
||||
export class VolumeApi {
|
||||
find(name: string) {
|
||||
return ajax.get<FindResult>('/volume/find', { name })
|
||||
find(node: string, name: string) {
|
||||
return ajax.get<FindResult>('/volume/find', { node, name })
|
||||
}
|
||||
|
||||
search(args: SearchArgs) {
|
||||
return ajax.get<SearchResult>('/volume/search', args)
|
||||
}
|
||||
|
||||
delete(name: string) {
|
||||
return ajax.post<Result<Object>>('/volume/delete', { name })
|
||||
delete(node: string, name: string) {
|
||||
return ajax.post<Result<Object>>('/volume/delete', { node, name })
|
||||
}
|
||||
|
||||
save(v: Volume) {
|
||||
return ajax.post<Result<Object>>('/volume/save', v)
|
||||
}
|
||||
|
||||
prune() {
|
||||
return ajax.post<PruneResult>('/volume/prune')
|
||||
prune(node: string) {
|
||||
return ajax.post<PruneResult>('/volume/prune', { node })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,9 @@
|
||||
<n-space class="page-body" vertical :size="12">
|
||||
<n-space :size="12">
|
||||
<n-select
|
||||
filterable
|
||||
size="small"
|
||||
:consistent-menu-width="false"
|
||||
:placeholder="t('objects.node')"
|
||||
v-model:value="filter.node"
|
||||
:options="nodes"
|
||||
@ -58,7 +60,7 @@ const columns = [
|
||||
fixed: "left" as const,
|
||||
render: (c: Container) => {
|
||||
const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id')
|
||||
return renderLink({ name: 'container_detail', params: { id: c.id, node: node?.value || '@' } }, c.name)
|
||||
return renderLink({ name: 'container_detail', params: { id: c.id, node: node?.value || '-' } }, c.name)
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -93,7 +95,7 @@ async function deleteContainer(c: Container, index: number) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const r = await nodeApi.list()
|
||||
const r = await nodeApi.list(true)
|
||||
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
|
||||
if (r.data?.length) {
|
||||
filter.node = r.data[0].id
|
||||
|
@ -2,6 +2,16 @@
|
||||
<x-page-header />
|
||||
<n-space class="page-body" vertical :size="12">
|
||||
<n-space :size="12">
|
||||
<n-select
|
||||
filterable
|
||||
size="small"
|
||||
:consistent-menu-width="false"
|
||||
:placeholder="t('objects.node')"
|
||||
v-model:value="filter.node"
|
||||
:options="nodes"
|
||||
style="width: 200px"
|
||||
v-if="nodes && nodes.length"
|
||||
/>
|
||||
<n-input size="small" v-model:value="filter.name" :placeholder="t('fields.name')" clearable />
|
||||
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
|
||||
</n-space>
|
||||
@ -21,30 +31,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import {
|
||||
NSpace,
|
||||
NButton,
|
||||
NDataTable,
|
||||
NInput,
|
||||
NSelect,
|
||||
} from "naive-ui";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import imageApi from "@/api/image";
|
||||
import type { Image } from "@/api/image";
|
||||
import nodeApi from "@/api/node";
|
||||
import { useDataTable } from "@/utils/data-table";
|
||||
import { formatSize, renderButton, renderLink, renderTags } from "@/utils/render";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const filter = reactive({
|
||||
name: "",
|
||||
node: '',
|
||||
name: '',
|
||||
});
|
||||
const nodes: any = ref([])
|
||||
const columns = [
|
||||
{
|
||||
title: t('fields.id'),
|
||||
key: "id",
|
||||
fixed: "left" as const,
|
||||
render: (i: Image) => renderLink(`/local/images/${i.id}`, i.id.substring(7, 19)),
|
||||
render: (i: Image) => renderLink({ name: 'image_detail', params: { node: filter.node || '-', id: i.id } }, i.id.substring(7, 19)),
|
||||
},
|
||||
{
|
||||
title: t('fields.tags'),
|
||||
@ -76,10 +90,19 @@ const columns = [
|
||||
},
|
||||
},
|
||||
];
|
||||
const { state, pagination, fetchData, changePageSize } = useDataTable(imageApi.search, filter)
|
||||
const { state, pagination, fetchData, changePageSize } = useDataTable(imageApi.search, filter, false)
|
||||
|
||||
async function deleteImage(id: string, index: number) {
|
||||
await imageApi.delete(id, "");
|
||||
await imageApi.delete(filter.node, id, "");
|
||||
state.data.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const r = await nodeApi.list(true)
|
||||
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
|
||||
if (r.data?.length) {
|
||||
filter.node = r.data[0].id
|
||||
}
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
@ -92,6 +92,7 @@ const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const model = ref({} as Image);
|
||||
const raw = ref('');
|
||||
const node = route.params.node as string || '';
|
||||
const columns = [
|
||||
{
|
||||
title: t('fields.sn'),
|
||||
@ -137,7 +138,7 @@ const columns = [
|
||||
|
||||
async function fetchData() {
|
||||
const id = route.params.id as string;
|
||||
let r = await imageApi.find(id);
|
||||
let r = await imageApi.find(node, id);
|
||||
model.value = r.data?.image as Image;
|
||||
raw.value = r.data?.raw as string;
|
||||
}
|
||||
|
@ -444,7 +444,7 @@
|
||||
<tbody>
|
||||
<tr v-for="t in tasks">
|
||||
<td>
|
||||
<x-anchor :url="`/swarm/tasks/${t.id}`">{{ t.id }}</x-anchor>
|
||||
<x-anchor :url="{ name: 'task_detail', params: { id: t.id } }">{{ t.id }}</x-anchor>
|
||||
</td>
|
||||
<td>
|
||||
<n-tag
|
||||
@ -454,7 +454,7 @@
|
||||
>{{ t.state }}</n-tag>
|
||||
</td>
|
||||
<td>
|
||||
<x-anchor :url="`/swarm/nodes/${t.nodeId}`">{{ t.nodeId }}</x-anchor>
|
||||
<x-anchor :url="{ name: 'node_detail', params: { id: t.nodeId } }">{{ t.nodeName }}</x-anchor>
|
||||
</td>
|
||||
<td>
|
||||
<n-space :size="4">
|
||||
@ -481,7 +481,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, h, computed } from "vue";
|
||||
import { onMounted, ref, h, computed } from "vue";
|
||||
import {
|
||||
NButton,
|
||||
NTag,
|
||||
|
@ -2,7 +2,12 @@
|
||||
<x-page-header />
|
||||
<n-space class="page-body" vertical :size="12">
|
||||
<n-space :size="12">
|
||||
<n-input size="small" v-model:value="filter.service" :placeholder="t('fields.name')" clearable />
|
||||
<n-input
|
||||
size="small"
|
||||
v-model:value="filter.service"
|
||||
:placeholder="t('fields.name')"
|
||||
clearable
|
||||
/>
|
||||
<n-select
|
||||
size="small"
|
||||
:placeholder="t('fields.state')"
|
||||
@ -60,12 +65,12 @@ const columns = [
|
||||
title: t('fields.id'),
|
||||
key: "id",
|
||||
fixed: "left" as const,
|
||||
render: (s: Task) => renderLink(`/swarm/tasks/${s.id}`, s.id),
|
||||
render: (s: Task) => renderLink({ name: 'task_detail', params: { id: s.id } }, s.id),
|
||||
},
|
||||
{
|
||||
title: t('fields.service_id'),
|
||||
key: "service",
|
||||
render: (s: Task) => renderLink(`/swarm/services/${s.serviceId}`, s.serviceId),
|
||||
render: (s: Task) => renderLink({ name: 'service_detail', params: { name: s.serviceId } }, s.serviceId),
|
||||
},
|
||||
{
|
||||
title: t('objects.image'),
|
||||
@ -74,7 +79,7 @@ const columns = [
|
||||
{
|
||||
title: t('fields.node_id'),
|
||||
key: "image",
|
||||
render: (s: Task) => renderLink(`/swarm/nodes/${s.nodeId}`, s.nodeId),
|
||||
render: (s: Task) => renderLink({ name: 'node_detail', params: { id: s.nodeId } }, s.nodeName),
|
||||
},
|
||||
{
|
||||
title: t('fields.state'),
|
||||
|
@ -18,14 +18,20 @@
|
||||
<x-description label-placement="left" label-align="right" :label-width="90">
|
||||
<x-description-item :label="t('fields.id')">{{ model.id }}</x-description-item>
|
||||
<x-description-item :label="t('objects.image')">{{ model.image }}</x-description-item>
|
||||
<x-description-item :label="t('objects.service')" :span="2">
|
||||
<x-anchor :url="`/swarm/services/${model.serviceName}`">{{ model.serviceName }}</x-anchor>
|
||||
<x-description-item :label="t('objects.service')" :span="2" v-if="model.serviceName">
|
||||
<x-anchor
|
||||
:url="{ name: 'service_detail', params: { name: model.serviceName } }"
|
||||
>{{ model.serviceName }}</x-anchor>
|
||||
</x-description-item>
|
||||
<x-description-item :label="t('objects.container')" :span="2">
|
||||
<x-anchor :url="`/local/containers/${model.nodeId}/${model.containerId}`">{{ model.containerId }}</x-anchor>
|
||||
<x-description-item :label="t('objects.container')" :span="2" v-if="model.containerId">
|
||||
<x-anchor
|
||||
:url="{ name: 'container_detail', params: { id: model.containerId, node: model.nodeId || '-' } }"
|
||||
>{{ model.containerId }}</x-anchor>
|
||||
</x-description-item>
|
||||
<x-description-item :label="t('objects.node')" :span="2">
|
||||
<x-anchor :url="`/swarm/nodes/${model.nodeId}`">{{ model.nodeId }}</x-anchor>
|
||||
<x-description-item :label="t('objects.node')" :span="2" v-if="model.nodeId">
|
||||
<x-anchor
|
||||
:url="{ name: 'node_detail', params: { id: model.nodeId } }"
|
||||
>{{ model.nodeName }}</x-anchor>
|
||||
</x-description-item>
|
||||
<x-description-item :label="t('fields.created_at')">{{ model.createdAt }}</x-description-item>
|
||||
<x-description-item :label="t('fields.updated_at')">{{ model.updatedAt }}</x-description-item>
|
||||
@ -70,7 +76,7 @@
|
||||
<tbody>
|
||||
<tr v-for="n in model.networks">
|
||||
<td>
|
||||
<x-anchor :url="`/swarm/networks/${n.name}`">{{ n.name }}</x-anchor>
|
||||
<x-anchor :url="{ name: 'network_detail', params: { name: n.name } }">{{ n.name }}</x-anchor>
|
||||
</td>
|
||||
<td>
|
||||
<n-space :size="4">
|
||||
|
@ -9,7 +9,7 @@
|
||||
</template>
|
||||
{{ t('buttons.prune') }}
|
||||
</n-button>
|
||||
<n-button secondary size="small" @click="$router.push('/local/volumes/new')">
|
||||
<n-button secondary size="small" @click="$router.push({name: 'volume_new', params: {node: filter.node || '-'}})">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<add-icon />
|
||||
@ -21,6 +21,16 @@
|
||||
</x-page-header>
|
||||
<n-space class="page-body" vertical :size="12">
|
||||
<n-space :size="12">
|
||||
<n-select
|
||||
filterable
|
||||
size="small"
|
||||
:consistent-menu-width="false"
|
||||
:placeholder="t('objects.node')"
|
||||
v-model:value="filter.node"
|
||||
:options="nodes"
|
||||
style="width: 200px"
|
||||
v-if="nodes && nodes.length"
|
||||
/>
|
||||
<n-input size="small" v-model:value="filter.name" :placeholder="t('fields.name')" clearable />
|
||||
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
|
||||
</n-space>
|
||||
@ -40,32 +50,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import {
|
||||
NSpace,
|
||||
NButton,
|
||||
NDataTable,
|
||||
NInput,
|
||||
NIcon,
|
||||
NSelect,
|
||||
} from "naive-ui";
|
||||
import { AddOutline as AddIcon, CloseOutline as CloseIcon } from "@vicons/ionicons5";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import volumeApi from "@/api/volume";
|
||||
import type { Volume } from "@/api/volume";
|
||||
import nodeApi from "@/api/node";
|
||||
import { useDataTable } from "@/utils/data-table";
|
||||
import { formatSize, renderButton, renderLink, renderTag } from "@/utils/render";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const filter = reactive({
|
||||
name: "",
|
||||
node: '',
|
||||
name: '',
|
||||
});
|
||||
const nodes: any = ref([])
|
||||
const columns = [
|
||||
{
|
||||
title: t('fields.name'),
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
render: (v: Volume) => renderLink(`/local/volumes/${v.name}`, v.name),
|
||||
render: (v: Volume) => renderLink({ name: 'volume_detail', params: { node: filter.node || '-', name: v.name } }, v.name),
|
||||
},
|
||||
{
|
||||
title: t('fields.driver'),
|
||||
@ -97,10 +111,10 @@ const columns = [
|
||||
},
|
||||
},
|
||||
];
|
||||
const { state, pagination, fetchData, changePageSize } = useDataTable(volumeApi.search, filter)
|
||||
const { state, pagination, fetchData, changePageSize } = useDataTable(volumeApi.search, filter, false)
|
||||
|
||||
async function deleteVolume(name: string, index: number) {
|
||||
await volumeApi.delete(name);
|
||||
await volumeApi.delete(filter.node, name);
|
||||
state.data.splice(index, 1)
|
||||
}
|
||||
|
||||
@ -111,7 +125,7 @@ async function pruneVolume() {
|
||||
positiveText: t('buttons.confirm'),
|
||||
negativeText: t('buttons.cancel'),
|
||||
onPositiveClick: async () => {
|
||||
const r = await volumeApi.prune();
|
||||
const r = await volumeApi.prune(filter.node);
|
||||
window.message.info(t('texts.prune_volume_success', {
|
||||
count: r.data?.deletedVolumes.length,
|
||||
size: formatSize(r.data?.reclaimedSpace as number)
|
||||
@ -120,4 +134,13 @@ async function pruneVolume() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const r = await nodeApi.list(true)
|
||||
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
|
||||
if (r.data?.length) {
|
||||
filter.node = r.data[0].id
|
||||
}
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
@ -97,10 +97,11 @@ const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const model = ref({} as Volume);
|
||||
const raw = ref('');
|
||||
const node = route.params.node as string || '';
|
||||
|
||||
async function fetchData() {
|
||||
const name = route.params.name as string;
|
||||
let r = await volumeApi.find(name);
|
||||
let r = await volumeApi.find(node, name);
|
||||
model.value = r.data?.volume as Volume;
|
||||
raw.value = r.data?.raw as string;
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
{
|
||||
name: "image_detail",
|
||||
path: "/local/images/:id",
|
||||
path: "/local/images/:node/:id",
|
||||
component: () => import('../pages/image/View.vue'),
|
||||
},
|
||||
{
|
||||
@ -223,12 +223,12 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
{
|
||||
name: "volume_detail",
|
||||
path: "/local/volumes/:name",
|
||||
path: "/local/volumes/:node/:name",
|
||||
component: () => import('../pages/volume/View.vue'),
|
||||
},
|
||||
{
|
||||
name: "volume_new",
|
||||
path: "/local/volumes/new",
|
||||
path: "/local/volumes/:node/new",
|
||||
component: () => import('../pages/volume/New.vue'),
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user