mirror of
https://github.com/cuigh/swirl
synced 2025-05-16 09:35:47 +00:00
Add basic support for agent
This commit is contained in:
parent
94127504ff
commit
cb2cb4ab86
@ -33,6 +33,7 @@ func NewContainer(b biz.ContainerBiz) *ContainerHandler {
|
|||||||
|
|
||||||
func containerSearch(b biz.ContainerBiz) web.HandlerFunc {
|
func containerSearch(b biz.ContainerBiz) web.HandlerFunc {
|
||||||
type Args struct {
|
type Args struct {
|
||||||
|
Node string `json:"node" bind:"node"`
|
||||||
Name string `json:"name" bind:"name"`
|
Name string `json:"name" bind:"name"`
|
||||||
Status string `json:"status" bind:"status"`
|
Status string `json:"status" bind:"status"`
|
||||||
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
PageIndex int `json:"pageIndex" bind:"pageIndex"`
|
||||||
@ -47,7 +48,7 @@ func containerSearch(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err = ctx.Bind(args); err == nil {
|
if err = ctx.Bind(args); err == nil {
|
||||||
containers, total, err = b.Search(args.Name, args.Status, args.PageIndex, args.PageSize)
|
containers, total, err = b.Search(args.Node, args.Name, args.Status, args.PageIndex, args.PageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -63,8 +64,9 @@ func containerSearch(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
|
|
||||||
func containerFind(b biz.ContainerBiz) web.HandlerFunc {
|
func containerFind(b biz.ContainerBiz) web.HandlerFunc {
|
||||||
return func(ctx web.Context) error {
|
return func(ctx web.Context) error {
|
||||||
|
node := ctx.Query("node")
|
||||||
id := ctx.Query("id")
|
id := ctx.Query("id")
|
||||||
container, raw, err := b.Find(id)
|
container, raw, err := b.Find(node, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -74,12 +76,13 @@ func containerFind(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
|
|
||||||
func containerDelete(b biz.ContainerBiz) web.HandlerFunc {
|
func containerDelete(b biz.ContainerBiz) web.HandlerFunc {
|
||||||
type Args struct {
|
type Args struct {
|
||||||
|
Node string `json:"node"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
return func(ctx web.Context) (err error) {
|
return func(ctx web.Context) (err error) {
|
||||||
args := &Args{}
|
args := &Args{}
|
||||||
if err = ctx.Bind(args); err == nil {
|
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)
|
return ajax(ctx, err)
|
||||||
}
|
}
|
||||||
@ -87,6 +90,7 @@ func containerDelete(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
|
|
||||||
func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc {
|
func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc {
|
||||||
type Args struct {
|
type Args struct {
|
||||||
|
Node string `json:"node" bind:"node"`
|
||||||
ID string `json:"id" bind:"id"`
|
ID string `json:"id" bind:"id"`
|
||||||
Lines int `json:"lines" bind:"lines"`
|
Lines int `json:"lines" bind:"lines"`
|
||||||
Timestamps bool `json:"timestamps" bind:"timestamps"`
|
Timestamps bool `json:"timestamps" bind:"timestamps"`
|
||||||
@ -98,7 +102,7 @@ func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
stdout, stderr string
|
stdout, stderr string
|
||||||
)
|
)
|
||||||
if err = ctx.Bind(args); err == nil {
|
if err = ctx.Bind(args); err == nil {
|
||||||
stdout, stderr, err = b.FetchLogs(args.ID, args.Lines, args.Timestamps)
|
stdout, stderr, err = b.FetchLogs(args.Node, args.ID, args.Lines, args.Timestamps)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -110,11 +114,12 @@ func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
func containerConnect(b biz.ContainerBiz) web.HandlerFunc {
|
func containerConnect(b biz.ContainerBiz) web.HandlerFunc {
|
||||||
return func(ctx web.Context) error {
|
return func(ctx web.Context) error {
|
||||||
var (
|
var (
|
||||||
|
node = ctx.Query("node")
|
||||||
id = ctx.Query("id")
|
id = ctx.Query("id")
|
||||||
cmd = ctx.Query("cmd")
|
cmd = ctx.Query("cmd")
|
||||||
)
|
)
|
||||||
|
|
||||||
_, _, err := b.Find(id)
|
_, _, err := b.Find(node, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -124,17 +129,17 @@ func containerConnect(b biz.ContainerBiz) web.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
idResp, err := b.ExecCreate(id, cmd)
|
idResp, err := b.ExecCreate(node, id, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.ExecAttach(idResp.ID)
|
resp, err := b.ExecAttach(node, idResp.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = b.ExecStart(idResp.ID)
|
err = b.ExecStart(node, idResp.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
13
api/node.go
13
api/node.go
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
// NodeHandler encapsulates node related handlers.
|
// NodeHandler encapsulates node related handlers.
|
||||||
type NodeHandler struct {
|
type NodeHandler struct {
|
||||||
|
List web.HandlerFunc `path:"/list" auth:"node.view" desc:"list nodes"`
|
||||||
Search web.HandlerFunc `path:"/search" auth:"node.view" desc:"search nodes"`
|
Search web.HandlerFunc `path:"/search" auth:"node.view" desc:"search nodes"`
|
||||||
Find web.HandlerFunc `path:"/find" auth:"node.view" desc:"find node by name"`
|
Find web.HandlerFunc `path:"/find" auth:"node.view" desc:"find node by name"`
|
||||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"node.delete" desc:"delete node"`
|
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"node.delete" desc:"delete node"`
|
||||||
@ -17,6 +18,7 @@ type NodeHandler struct {
|
|||||||
// NewNode creates an instance of NodeHandler
|
// NewNode creates an instance of NodeHandler
|
||||||
func NewNode(nb biz.NodeBiz) *NodeHandler {
|
func NewNode(nb biz.NodeBiz) *NodeHandler {
|
||||||
return &NodeHandler{
|
return &NodeHandler{
|
||||||
|
List: nodeList(nb),
|
||||||
Search: nodeSearch(nb),
|
Search: nodeSearch(nb),
|
||||||
Find: nodeFind(nb),
|
Find: nodeFind(nb),
|
||||||
Delete: nodeDelete(nb),
|
Delete: nodeDelete(nb),
|
||||||
@ -24,6 +26,17 @@ func NewNode(nb biz.NodeBiz) *NodeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nodeList(nb biz.NodeBiz) web.HandlerFunc {
|
||||||
|
return func(ctx web.Context) error {
|
||||||
|
nodes, err := nb.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(ctx, nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func nodeSearch(nb biz.NodeBiz) web.HandlerFunc {
|
func nodeSearch(nb biz.NodeBiz) web.HandlerFunc {
|
||||||
return func(ctx web.Context) error {
|
return func(ctx web.Context) error {
|
||||||
nodes, err := nb.Search()
|
nodes, err := nb.Search()
|
||||||
|
@ -14,8 +14,6 @@ import (
|
|||||||
"github.com/cuigh/swirl/model"
|
"github.com/cuigh/swirl/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//var ErrSystemInitialized = errors.New("system was already initialized")
|
|
||||||
|
|
||||||
// SystemHandler encapsulates system related handlers.
|
// SystemHandler encapsulates system related handlers.
|
||||||
type SystemHandler struct {
|
type SystemHandler struct {
|
||||||
CheckState web.HandlerFunc `path:"/check-state" auth:"*" desc:"check system state"`
|
CheckState web.HandlerFunc `path:"/check-state" auth:"*" desc:"check system state"`
|
||||||
|
@ -12,13 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ContainerBiz interface {
|
type ContainerBiz interface {
|
||||||
Search(name, status string, pageIndex, pageSize int) ([]*Container, int, error)
|
Search(node, name, status string, pageIndex, pageSize int) ([]*Container, int, error)
|
||||||
Find(id string) (container *Container, raw string, err error)
|
Find(node, id string) (container *Container, raw string, err error)
|
||||||
Delete(id string, user web.User) (err error)
|
Delete(node, id string, user web.User) (err error)
|
||||||
FetchLogs(id string, lines int, timestamps bool) (stdout, stderr string, err error)
|
FetchLogs(node, id string, lines int, timestamps bool) (stdout, stderr string, err error)
|
||||||
ExecCreate(id string, cmd string) (resp types.IDResponse, err error)
|
ExecCreate(node, id string, cmd string) (resp types.IDResponse, err error)
|
||||||
ExecAttach(id string) (resp types.HijackedResponse, err error)
|
ExecAttach(node, id string) (resp types.HijackedResponse, err error)
|
||||||
ExecStart(id string) error
|
ExecStart(node, id string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContainer(d *docker.Docker) ContainerBiz {
|
func NewContainer(d *docker.Docker) ContainerBiz {
|
||||||
@ -29,13 +29,13 @@ type containerBiz struct {
|
|||||||
d *docker.Docker
|
d *docker.Docker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) Find(id string) (c *Container, raw string, err error) {
|
func (b *containerBiz) Find(node, id string) (c *Container, raw string, err error) {
|
||||||
var (
|
var (
|
||||||
cj types.ContainerJSON
|
cj types.ContainerJSON
|
||||||
r []byte
|
r []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
if cj, r, err = b.d.ContainerInspect(context.TODO(), id); err == nil {
|
if cj, r, err = b.d.ContainerInspect(context.TODO(), node, id); err == nil {
|
||||||
raw, err = indentJSON(r)
|
raw, err = indentJSON(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +45,8 @@ func (b *containerBiz) Find(id string) (c *Container, raw string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) Search(name, status string, pageIndex, pageSize int) (containers []*Container, total int, err error) {
|
func (b *containerBiz) Search(node, name, status string, pageIndex, pageSize int) (containers []*Container, total int, err error) {
|
||||||
list, total, err := b.d.ContainerList(context.TODO(), name, status, pageIndex, pageSize)
|
list, total, err := b.d.ContainerList(context.TODO(), node, name, status, pageIndex, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@ -58,28 +58,28 @@ func (b *containerBiz) Search(name, status string, pageIndex, pageSize int) (con
|
|||||||
return containers, total, nil
|
return containers, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) Delete(id string, user web.User) (err error) {
|
func (b *containerBiz) Delete(node, id string, user web.User) (err error) {
|
||||||
err = b.d.ContainerRemove(context.TODO(), id)
|
err = b.d.ContainerRemove(context.TODO(), node, id)
|
||||||
//if err == nil {
|
//if err == nil {
|
||||||
// Event.CreateContainer(model.EventActionDelete, id, user)
|
// Event.CreateContainer(model.EventActionDelete, id, user)
|
||||||
//}
|
//}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) ExecCreate(id, cmd string) (resp types.IDResponse, err error) {
|
func (b *containerBiz) ExecCreate(node, id, cmd string) (resp types.IDResponse, err error) {
|
||||||
return b.d.ContainerExecCreate(context.TODO(), id, cmd)
|
return b.d.ContainerExecCreate(context.TODO(), node, id, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) ExecAttach(id string) (resp types.HijackedResponse, err error) {
|
func (b *containerBiz) ExecAttach(node, id string) (resp types.HijackedResponse, err error) {
|
||||||
return b.d.ContainerExecAttach(context.TODO(), id)
|
return b.d.ContainerExecAttach(context.TODO(), node, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) ExecStart(id string) error {
|
func (b *containerBiz) ExecStart(node, id string) error {
|
||||||
return b.d.ContainerExecStart(context.TODO(), id)
|
return b.d.ContainerExecStart(context.TODO(), node, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *containerBiz) FetchLogs(id string, lines int, timestamps bool) (string, string, error) {
|
func (b *containerBiz) FetchLogs(node, id string, lines int, timestamps bool) (string, string, error) {
|
||||||
stdout, stderr, err := b.d.ContainerLogs(context.TODO(), id, lines, timestamps)
|
stdout, stderr, err := b.d.ContainerLogs(context.TODO(), node, id, lines, timestamps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type NodeBiz interface {
|
type NodeBiz interface {
|
||||||
|
List() ([]*docker.Node, error)
|
||||||
Search() ([]*Node, error)
|
Search() ([]*Node, error)
|
||||||
Find(id string) (node *Node, raw string, err error)
|
Find(id string) (node *Node, raw string, err error)
|
||||||
Delete(id, name string, user web.User) (err error)
|
Delete(id, name string, user web.User) (err error)
|
||||||
@ -40,6 +41,10 @@ func (b *nodeBiz) Find(id string) (node *Node, raw string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *nodeBiz) List() ([]*docker.Node, error) {
|
||||||
|
return b.d.NodeListCache()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *nodeBiz) Search() ([]*Node, error) {
|
func (b *nodeBiz) Search() ([]*Node, error) {
|
||||||
list, err := b.d.NodeList(context.TODO())
|
list, err := b.d.NodeList(context.TODO())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -133,6 +133,16 @@ func (b *serviceBiz) Create(s *Service, user web.User) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Mode == "replicated" {
|
||||||
|
spec.Mode.Replicated = &swarm.ReplicatedService{Replicas: &s.Replicas}
|
||||||
|
} else if s.Mode == "replicated-job" {
|
||||||
|
spec.Mode.ReplicatedJob = &swarm.ReplicatedJob{TotalCompletions: &s.Replicas}
|
||||||
|
} else if s.Mode == "global" {
|
||||||
|
spec.Mode.Global = &swarm.GlobalService{}
|
||||||
|
} else if s.Mode == "global-job" {
|
||||||
|
spec.Mode.GlobalJob = &swarm.GlobalJob{}
|
||||||
|
}
|
||||||
|
|
||||||
auth := ""
|
auth := ""
|
||||||
if i := strings.Index(s.Image, "/"); i > 0 {
|
if i := strings.Index(s.Image, "/"); i > 0 {
|
||||||
if host := s.Image[:i]; strings.Contains(host, ".") {
|
if host := s.Image[:i]; strings.Contains(host, ".") {
|
||||||
@ -161,6 +171,12 @@ func (b *serviceBiz) Update(s *Service, user web.User) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Mode == "replicated" && spec.Mode.Replicated != nil {
|
||||||
|
spec.Mode.Replicated.Replicas = &s.Replicas
|
||||||
|
} else if s.Mode == "replicated-job" && spec.Mode.ReplicatedJob != nil {
|
||||||
|
spec.Mode.ReplicatedJob.TotalCompletions = &s.Replicas
|
||||||
|
}
|
||||||
|
|
||||||
if err = b.d.ServiceUpdate(context.TODO(), spec, s.Version); err == nil {
|
if err = b.d.ServiceUpdate(context.TODO(), spec, s.Version); err == nil {
|
||||||
b.eb.CreateService(EventActionUpdate, s.Name, user)
|
b.eb.CreateService(EventActionUpdate, s.Name, user)
|
||||||
}
|
}
|
||||||
@ -490,9 +506,7 @@ func newServiceBase(s *swarm.Service) *ServiceBase {
|
|||||||
UpdatedAt: formatTime(s.UpdatedAt),
|
UpdatedAt: formatTime(s.UpdatedAt),
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ServiceStatus == nil {
|
if s.ServiceStatus != nil {
|
||||||
// TODO: ServiceStatus is valid from docker api v1.41, so we should calculate count manually here.
|
|
||||||
} else {
|
|
||||||
service.RunningTasks = s.ServiceStatus.RunningTasks
|
service.RunningTasks = s.ServiceStatus.RunningTasks
|
||||||
service.DesiredTasks = s.ServiceStatus.DesiredTasks
|
service.DesiredTasks = s.ServiceStatus.DesiredTasks
|
||||||
service.CompletedTasks = s.ServiceStatus.CompletedTasks
|
service.CompletedTasks = s.ServiceStatus.CompletedTasks
|
||||||
@ -625,25 +639,6 @@ func (s *Service) MergeTo(spec *swarm.ServiceSpec) (err error) {
|
|||||||
spec.TaskTemplate.ContainerSpec.Command = parseArgs(s.Command)
|
spec.TaskTemplate.ContainerSpec.Command = parseArgs(s.Command)
|
||||||
spec.TaskTemplate.ContainerSpec.Args = parseArgs(s.Args)
|
spec.TaskTemplate.ContainerSpec.Args = parseArgs(s.Args)
|
||||||
|
|
||||||
// Mode
|
|
||||||
if s.Mode == "replicated" {
|
|
||||||
if spec.Mode.Replicated == nil {
|
|
||||||
spec.Mode.Replicated = &swarm.ReplicatedService{Replicas: &s.Replicas}
|
|
||||||
} else {
|
|
||||||
spec.Mode.Replicated.Replicas = &s.Replicas
|
|
||||||
}
|
|
||||||
} else if s.Mode == "replicated-job" {
|
|
||||||
if spec.Mode.ReplicatedJob == nil {
|
|
||||||
spec.Mode.ReplicatedJob = &swarm.ReplicatedJob{TotalCompletions: &s.Replicas}
|
|
||||||
} else {
|
|
||||||
spec.Mode.ReplicatedJob.TotalCompletions = &s.Replicas
|
|
||||||
}
|
|
||||||
} else if s.Mode == "global" && spec.Mode.Global != nil {
|
|
||||||
spec.Mode.Global = &swarm.GlobalService{}
|
|
||||||
} else if s.Mode == "global-job" && spec.Mode.GlobalJob != nil {
|
|
||||||
spec.Mode.GlobalJob = &swarm.GlobalJob{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Networks
|
// Networks
|
||||||
spec.TaskTemplate.Networks = nil
|
spec.TaskTemplate.Networks = nil
|
||||||
for _, n := range s.Networks {
|
for _, n := range s.Networks {
|
||||||
|
@ -15,8 +15,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ContainerList return containers on the host.
|
// ContainerList return containers on the host.
|
||||||
func (d *Docker) ContainerList(ctx context.Context, name, status string, pageIndex, pageSize int) (containers []types.Container, total int, err error) {
|
func (d *Docker) ContainerList(ctx context.Context, node, name, status string, pageIndex, pageSize int) (containers []types.Container, total int, err error) {
|
||||||
err = d.call(func(c *client.Client) (err error) {
|
var c *client.Client
|
||||||
|
c, err = d.agent(node)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
opts := types.ContainerListOptions{Filters: filters.NewArgs()}
|
opts := types.ContainerListOptions{Filters: filters.NewArgs()}
|
||||||
if status == "" {
|
if status == "" {
|
||||||
opts.All = true
|
opts.All = true
|
||||||
@ -34,29 +39,34 @@ func (d *Docker) ContainerList(ctx context.Context, name, status string, pageInd
|
|||||||
containers = containers[start:end]
|
containers = containers[start:end]
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerInspect return container raw information.
|
// ContainerInspect return container raw information.
|
||||||
func (d *Docker) ContainerInspect(ctx context.Context, id string) (container types.ContainerJSON, raw []byte, err error) {
|
func (d *Docker) ContainerInspect(ctx context.Context, node, id string) (container types.ContainerJSON, raw []byte, err error) {
|
||||||
var c *client.Client
|
var c *client.Client
|
||||||
if c, err = d.client(); err == nil {
|
if c, err = d.agent(node); err == nil {
|
||||||
container, raw, err = c.ContainerInspectWithRaw(ctx, id, true)
|
container, raw, err = c.ContainerInspectWithRaw(ctx, id, true)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerRemove remove a container.
|
// ContainerRemove remove a container.
|
||||||
func (d *Docker) ContainerRemove(ctx context.Context, id string) error {
|
func (d *Docker) ContainerRemove(ctx context.Context, node, id string) (err error) {
|
||||||
return d.call(func(c *client.Client) (err error) {
|
var c *client.Client
|
||||||
return c.ContainerRemove(ctx, id, types.ContainerRemoveOptions{})
|
if c, err = d.agent(node); err == nil {
|
||||||
})
|
err = c.ContainerRemove(ctx, id, types.ContainerRemoveOptions{})
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerExecCreate creates an exec instance.
|
// ContainerExecCreate creates an exec instance.
|
||||||
func (d *Docker) ContainerExecCreate(ctx context.Context, id string, cmd string) (resp types.IDResponse, err error) {
|
func (d *Docker) ContainerExecCreate(ctx context.Context, node, id string, cmd string) (resp types.IDResponse, err error) {
|
||||||
err = d.call(func(c *client.Client) (err error) {
|
var c *client.Client
|
||||||
|
c, err = d.agent(node)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
opts := types.ExecConfig{
|
opts := types.ExecConfig{
|
||||||
AttachStdin: true,
|
AttachStdin: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
@ -65,40 +75,48 @@ func (d *Docker) ContainerExecCreate(ctx context.Context, id string, cmd string)
|
|||||||
//User: "root",
|
//User: "root",
|
||||||
Cmd: strings.Split(cmd, " "),
|
Cmd: strings.Split(cmd, " "),
|
||||||
}
|
}
|
||||||
//c.DialSession()
|
|
||||||
resp, err = c.ContainerExecCreate(ctx, id, opts)
|
resp, err = c.ContainerExecCreate(ctx, id, opts)
|
||||||
return
|
return
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerExecAttach attaches a connection to an exec process in the server.
|
// ContainerExecAttach attaches a connection to an exec process in the server.
|
||||||
func (d *Docker) ContainerExecAttach(ctx context.Context, id string) (resp types.HijackedResponse, err error) {
|
func (d *Docker) ContainerExecAttach(ctx context.Context, node, id string) (resp types.HijackedResponse, err error) {
|
||||||
err = d.call(func(c *client.Client) (err error) {
|
var c *client.Client
|
||||||
|
c, err = d.agent(node)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
opts := types.ExecStartCheck{
|
opts := types.ExecStartCheck{
|
||||||
Detach: false,
|
Detach: false,
|
||||||
Tty: true,
|
Tty: true,
|
||||||
}
|
}
|
||||||
resp, err = c.ContainerExecAttach(ctx, id, opts)
|
resp, err = c.ContainerExecAttach(ctx, id, opts)
|
||||||
return err
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerExecStart starts an exec instance.
|
// ContainerExecStart starts an exec instance.
|
||||||
func (d *Docker) ContainerExecStart(ctx context.Context, id string) error {
|
func (d *Docker) ContainerExecStart(ctx context.Context, node, id string) (err error) {
|
||||||
return d.call(func(c *client.Client) (err error) {
|
c, err := d.agent(node)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
opts := types.ExecStartCheck{
|
opts := types.ExecStartCheck{
|
||||||
Detach: false,
|
Detach: false,
|
||||||
Tty: true,
|
Tty: true,
|
||||||
}
|
}
|
||||||
return c.ContainerExecStart(ctx, id, opts)
|
return c.ContainerExecStart(ctx, id, opts)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerLogs returns the logs generated by a container.
|
// ContainerLogs returns the logs generated by a container.
|
||||||
func (d *Docker) ContainerLogs(ctx context.Context, id string, lines int, timestamps bool) (stdout, stderr *bytes.Buffer, err error) {
|
func (d *Docker) ContainerLogs(ctx context.Context, node, id string, lines int, timestamps bool) (stdout, stderr *bytes.Buffer, err error) {
|
||||||
err = d.call(func(c *client.Client) (err error) {
|
var c *client.Client
|
||||||
|
c, err = d.agent(node)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
rc io.ReadCloser
|
rc io.ReadCloser
|
||||||
opts = types.ContainerLogsOptions{
|
opts = types.ContainerLogsOptions{
|
||||||
@ -117,6 +135,4 @@ func (d *Docker) ContainerLogs(ctx context.Context, id string, lines int, timest
|
|||||||
_, err = stdcopy.StdCopy(stdout, stderr, rc)
|
_, err = stdcopy.StdCopy(stdout, stderr, rc)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
159
docker/docker.go
159
docker/docker.go
@ -1,20 +1,23 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"context"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cuigh/auxo/app/container"
|
"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/log"
|
||||||
|
"github.com/cuigh/auxo/util/lazy"
|
||||||
"github.com/cuigh/swirl/misc"
|
"github.com/cuigh/swirl/misc"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultAPIVersion = "1.41"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newVersion(v uint64) swarm.Version {
|
func newVersion(v uint64) swarm.Version {
|
||||||
return swarm.Version{Index: v}
|
return swarm.Version{Index: v}
|
||||||
}
|
}
|
||||||
@ -23,6 +26,7 @@ type Docker struct {
|
|||||||
c *client.Client
|
c *client.Client
|
||||||
locker sync.Mutex
|
locker sync.Mutex
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
|
agents sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocker() *Docker {
|
func NewDocker() *Docker {
|
||||||
@ -39,22 +43,19 @@ func (d *Docker) call(fn func(c *client.Client) error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Docker) client() (cli *client.Client, err error) {
|
func (d *Docker) client() (c *client.Client, err error) {
|
||||||
if d.c == nil {
|
if d.c == nil {
|
||||||
d.locker.Lock()
|
d.locker.Lock()
|
||||||
defer d.locker.Unlock()
|
defer d.locker.Unlock()
|
||||||
|
|
||||||
if d.c == nil {
|
if d.c == nil {
|
||||||
apiVersion := misc.Options.DockerAPIVersion
|
var opt client.Opt
|
||||||
if apiVersion == "" {
|
|
||||||
apiVersion = defaultAPIVersion
|
|
||||||
}
|
|
||||||
if misc.Options.DockerEndpoint == "" {
|
if misc.Options.DockerEndpoint == "" {
|
||||||
_ = os.Setenv("DOCKER_API_VERSION", apiVersion)
|
opt = client.FromEnv
|
||||||
d.c, err = client.NewClientWithOpts(client.FromEnv)
|
|
||||||
} else {
|
} else {
|
||||||
d.c, err = client.NewClientWithOpts(client.WithHost(misc.Options.DockerEndpoint), client.WithVersion(apiVersion))
|
opt = client.WithHost(misc.Options.DockerEndpoint)
|
||||||
}
|
}
|
||||||
|
d.c, err = client.NewClientWithOpts(opt, client.WithVersion(misc.Options.DockerAPIVersion))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -63,6 +64,138 @@ func (d *Docker) client() (cli *client.Client, err error) {
|
|||||||
return d.c, nil
|
return d.c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Docker) agent(node string) (*client.Client, error) {
|
||||||
|
host, err := d.getAgent(node)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Error("failed to find node agent: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return d.client()
|
||||||
|
}
|
||||||
|
|
||||||
|
value, _ := d.agents.LoadOrStore(node, &lazy.Value{
|
||||||
|
New: func() (interface{}, error) {
|
||||||
|
c, e := client.NewClientWithOpts(
|
||||||
|
client.WithHost("tcp://"+host),
|
||||||
|
client.WithVersion(misc.Options.DockerAPIVersion),
|
||||||
|
)
|
||||||
|
return c, e
|
||||||
|
},
|
||||||
|
})
|
||||||
|
c, err := value.(*lazy.Value).Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.(*client.Client), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) getAgent(node string) (agent string, err error) {
|
||||||
|
if node == "" || node == "@" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := d.getNodes()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, ok := nodes[node]; ok {
|
||||||
|
agent = n.Agent
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
agents, err := d.loadAgents(context.TODO(), c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to load agents")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := d.loadNodes(context.TODO(), c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range nodes {
|
||||||
|
nodes[i].Agent = agents[nodes[i].ID]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) loadNodes(ctx context.Context, c *client.Client) (nodes map[string]*Node, err error) {
|
||||||
|
var list []swarm.Node
|
||||||
|
list, err = c.NodeList(ctx, types.NodeListOptions{})
|
||||||
|
if err == nil {
|
||||||
|
nodes = make(map[string]*Node)
|
||||||
|
for _, n := range list {
|
||||||
|
ni := &Node{
|
||||||
|
ID: n.ID,
|
||||||
|
Name: n.Spec.Name,
|
||||||
|
State: n.Status.State,
|
||||||
|
}
|
||||||
|
if ni.Name == "" {
|
||||||
|
ni.Name = n.Description.Hostname
|
||||||
|
}
|
||||||
|
nodes[n.ID] = ni
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) loadAgents(ctx context.Context, c *client.Client) (agents map[string]string, err error) {
|
||||||
|
var tasks []swarm.Task
|
||||||
|
agents = make(map[string]string)
|
||||||
|
for _, agent := range misc.Options.Agents {
|
||||||
|
pair := strings.SplitN(agent, ":", 2)
|
||||||
|
args := filters.NewArgs(
|
||||||
|
filters.Arg("desired-state", string(swarm.TaskStateRunning)),
|
||||||
|
filters.Arg("service", pair[0]),
|
||||||
|
)
|
||||||
|
tasks, err = c.TaskList(ctx, types.TaskListOptions{Filters: args})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port := "2375"
|
||||||
|
if len(pair) > 1 {
|
||||||
|
port = pair[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range tasks {
|
||||||
|
if len(t.NetworksAttachments) > 0 {
|
||||||
|
pair = strings.SplitN(t.NetworksAttachments[0].Addresses[0], "/", 2)
|
||||||
|
agents[t.NodeID] = pair[0] + ":" + port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
State swarm.NodeState `json:"-"`
|
||||||
|
Agent string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
container.Put(NewDocker)
|
container.Put(NewDocker)
|
||||||
}
|
}
|
||||||
|
@ -59,3 +59,16 @@ func (d *Docker) NodeInspect(ctx context.Context, id string) (node swarm.Node, r
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Docker) NodeListCache() ([]*Node, error) {
|
||||||
|
m, err := d.getNodes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := make([]*Node, 0, len(m))
|
||||||
|
for _, n := range m {
|
||||||
|
nodes = append(nodes, n)
|
||||||
|
}
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
@ -76,13 +76,13 @@ func (d *Docker) ServiceInspect(ctx context.Context, name string, status bool) (
|
|||||||
|
|
||||||
func (d *Docker) fillStatus(ctx context.Context, c *client.Client, services []swarm.Service) (err error) {
|
func (d *Docker) fillStatus(ctx context.Context, c *client.Client, services []swarm.Service) (err error) {
|
||||||
var (
|
var (
|
||||||
activeNodes map[string]struct{}
|
nodes map[string]*Node
|
||||||
m = make(map[string]*swarm.Service)
|
m = make(map[string]*swarm.Service)
|
||||||
tasks []swarm.Task
|
tasks []swarm.Task
|
||||||
opts = types.TaskListOptions{Filters: filters.NewArgs()}
|
opts = types.TaskListOptions{Filters: filters.NewArgs()}
|
||||||
)
|
)
|
||||||
|
|
||||||
activeNodes, err = d.findActiveNodes(ctx, c)
|
nodes, err = d.getNodes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ func (d *Docker) fillStatus(ctx context.Context, c *client.Client, services []sw
|
|||||||
if s.Spec.Mode.Global != nil && task.DesiredState != swarm.TaskStateShutdown {
|
if s.Spec.Mode.Global != nil && task.DesiredState != swarm.TaskStateShutdown {
|
||||||
s.ServiceStatus.DesiredTasks++
|
s.ServiceStatus.DesiredTasks++
|
||||||
}
|
}
|
||||||
if _, ok := activeNodes[task.NodeID]; ok && task.Status.State == swarm.TaskStateRunning {
|
if n, ok := nodes[task.NodeID]; ok && n.State != swarm.NodeStateDown && task.Status.State == swarm.TaskStateRunning {
|
||||||
s.ServiceStatus.RunningTasks++
|
s.ServiceStatus.RunningTasks++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,18 +265,3 @@ func (d *Docker) ServiceSearch(ctx context.Context, args filters.Args) (services
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Docker) findActiveNodes(ctx context.Context, c *client.Client) (map[string]struct{}, error) {
|
|
||||||
nodes, err := c.NodeList(ctx, types.NodeListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
active := make(map[string]struct{})
|
|
||||||
for _, n := range nodes {
|
|
||||||
if n.Status.State != swarm.NodeStateDown {
|
|
||||||
active[n.ID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return active, nil
|
|
||||||
}
|
|
||||||
|
2
main.go
2
main.go
@ -30,7 +30,7 @@ var (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app.Name = "Swirl"
|
app.Name = "Swirl"
|
||||||
app.Version = "1.0.0beta4"
|
app.Version = "1.0.0beta5"
|
||||||
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
||||||
app.Action = func(ctx *app.Context) error {
|
app.Action = func(ctx *app.Context) error {
|
||||||
return run.Pipeline(misc.LoadOptions, initSystem, scaler.Start, startServer)
|
return run.Pipeline(misc.LoadOptions, initSystem, scaler.Start, startServer)
|
||||||
|
@ -16,6 +16,7 @@ var Options = &struct {
|
|||||||
DBAddress string
|
DBAddress string
|
||||||
TokenKey string
|
TokenKey string
|
||||||
TokenExpiry time.Duration
|
TokenExpiry time.Duration
|
||||||
|
Agents []string
|
||||||
}{
|
}{
|
||||||
DBType: "mongo",
|
DBType: "mongo",
|
||||||
DBAddress: "mongodb://localhost:27017/swirl",
|
DBAddress: "mongodb://localhost:27017/swirl",
|
||||||
@ -30,6 +31,7 @@ func bindOptions() {
|
|||||||
"db_address",
|
"db_address",
|
||||||
"token_key",
|
"token_key",
|
||||||
"token_expiry",
|
"token_expiry",
|
||||||
|
"agents",
|
||||||
}
|
}
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
config.BindEnv("swirl."+key, strings.ToUpper(key))
|
config.BindEnv("swirl."+key, strings.ToUpper(key))
|
||||||
|
@ -36,6 +36,7 @@ export interface Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchArgs {
|
export interface SearchArgs {
|
||||||
|
node?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
@ -53,21 +54,22 @@ export interface FindResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchLogsArgs {
|
export interface FetchLogsArgs {
|
||||||
|
node: string;
|
||||||
id: string;
|
id: string;
|
||||||
lines: number;
|
lines: number;
|
||||||
timestamps: boolean;
|
timestamps: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContainerApi {
|
export class ContainerApi {
|
||||||
find(id: string) {
|
find(node: string, id: string) {
|
||||||
return ajax.get<FindResult>('/container/find', { id })
|
return ajax.get<FindResult>('/container/find', { node, id })
|
||||||
}
|
}
|
||||||
|
|
||||||
search(args: SearchArgs) {
|
search(args: SearchArgs) {
|
||||||
return ajax.get<SearchResult>('/container/search', args)
|
return ajax.get<SearchResult>('/container/search', args)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: string, name: string) {
|
delete(node: string, id: string, name: string) {
|
||||||
return ajax.post<Result<Object>>('/container/delete', { id, name })
|
return ajax.post<Result<Object>>('/container/delete', { id, name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,10 @@ export class NodeApi {
|
|||||||
return ajax.get<FindResult>('/node/find', { id })
|
return ajax.get<FindResult>('/node/find', { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
return ajax.get<Node[]>('/node/list')
|
||||||
|
}
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
return ajax.get<Node[]>('/node/search')
|
return ajax.get<Node[]>('/node/search')
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NA } from "naive-ui";
|
import { NA } from "naive-ui";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
|
import type { RouteLocationRaw } from "vue-router";
|
||||||
|
|
||||||
const props = defineProps({
|
interface Props {
|
||||||
url: {
|
url: RouteLocationRaw
|
||||||
type: String,
|
}
|
||||||
required: true,
|
|
||||||
}
|
const props = defineProps<Props>()
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -60,6 +60,9 @@ const props = defineProps({
|
|||||||
type: String as PropType<'task' | 'container' | 'service'>,
|
type: String as PropType<'task' | 'container' | 'service'>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
node: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@ -88,7 +91,7 @@ async function fetchData() {
|
|||||||
var r: Result<Logs>;
|
var r: Result<Logs>;
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'container':
|
case 'container':
|
||||||
r = await containerApi.fetchLogs({ id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
r = await containerApi.fetchLogs({ node: props.node || '', id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
||||||
break
|
break
|
||||||
case 'task':
|
case 'task':
|
||||||
r = await taskApi.fetchLogs({ id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
r = await taskApi.fetchLogs({ id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
<x-page-header />
|
<x-page-header />
|
||||||
<n-space class="page-body" vertical :size="12">
|
<n-space class="page-body" vertical :size="12">
|
||||||
<n-space :size="12">
|
<n-space :size="12">
|
||||||
|
<n-select
|
||||||
|
size="small"
|
||||||
|
: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-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-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@ -21,30 +29,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import {
|
import {
|
||||||
NSpace,
|
NSpace,
|
||||||
NButton,
|
NButton,
|
||||||
NDataTable,
|
NDataTable,
|
||||||
NInput,
|
NInput,
|
||||||
|
NSelect,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
import XPageHeader from "@/components/PageHeader.vue";
|
import XPageHeader from "@/components/PageHeader.vue";
|
||||||
import containerApi from "@/api/container";
|
import containerApi from "@/api/container";
|
||||||
import type { Container } from "@/api/container";
|
import type { Container } from "@/api/container";
|
||||||
|
import nodeApi from "@/api/node";
|
||||||
import { useDataTable } from "@/utils/data-table";
|
import { useDataTable } from "@/utils/data-table";
|
||||||
import { renderButton, renderLink, renderTag } from "@/utils/render";
|
import { renderButton, renderLink, renderTag } from "@/utils/render";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const filter = reactive({
|
const filter = reactive({
|
||||||
name: "",
|
node: '',
|
||||||
|
name: '',
|
||||||
});
|
});
|
||||||
|
const nodes: any = ref([])
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t('fields.name'),
|
title: t('fields.name'),
|
||||||
key: "name",
|
key: "name",
|
||||||
fixed: "left" as const,
|
fixed: "left" as const,
|
||||||
render: (c: Container) => renderLink(`/local/containers/${c.id}`, c.name),
|
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)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('objects.image'),
|
title: t('objects.image'),
|
||||||
@ -65,14 +80,24 @@ const columns = [
|
|||||||
title: t('fields.actions'),
|
title: t('fields.actions'),
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render(i: Container, index: number) {
|
render(i: Container, index: number) {
|
||||||
return renderButton('error', t('buttons.delete'), () => deleteContainer(i.id, index), t('prompts.delete'))
|
return renderButton('error', t('buttons.delete'), () => deleteContainer(i, index), t('prompts.delete'))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { state, pagination, fetchData, changePageSize } = useDataTable(containerApi.search, filter)
|
const { state, pagination, fetchData, changePageSize } = useDataTable(containerApi.search, filter, false)
|
||||||
|
|
||||||
async function deleteContainer(id: string, index: number) {
|
async function deleteContainer(c: Container, index: number) {
|
||||||
await containerApi.delete(id, "");
|
const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id')
|
||||||
|
await containerApi.delete(node?.value || '', c.id, '');
|
||||||
state.data.splice(index, 1)
|
state.data.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const r = await nodeApi.list()
|
||||||
|
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
|
||||||
|
if (r.data?.length) {
|
||||||
|
filter.node = r.data[0].id
|
||||||
|
}
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
@ -52,10 +52,10 @@
|
|||||||
<x-code :code="raw" language="json" />
|
<x-code :code="raw" language="json" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="logs" :tab="t('fields.logs')" display-directive="show:lazy">
|
<n-tab-pane name="logs" :tab="t('fields.logs')" display-directive="show:lazy">
|
||||||
<x-logs type="container" :id="model.id"></x-logs>
|
<x-logs type="container" :node="node" :id="model.id"></x-logs>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="exec" :tab="t('fields.execute')" display-directive="show:lazy">
|
<n-tab-pane name="exec" :tab="t('fields.execute')" display-directive="show:lazy">
|
||||||
<execute :container-id="model.id"></execute>
|
<execute :node="node" :id="model.id"></execute>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
@ -88,10 +88,11 @@ const { t } = useI18n()
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const model = ref({} as Container);
|
const model = ref({} as Container);
|
||||||
const raw = ref('');
|
const raw = ref('');
|
||||||
|
const node = route.params.node as string || '';
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const id = route.params.id as string;
|
const id = route.params.id as string;
|
||||||
let r = await containerApi.find(id);
|
let r = await containerApi.find(node, id);
|
||||||
model.value = r.data?.container as Container;
|
model.value = r.data?.container as Container;
|
||||||
raw.value = r.data?.raw as string;
|
raw.value = r.data?.raw as string;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,11 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
containerId: {
|
node: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@ -47,7 +51,7 @@ function connect() {
|
|||||||
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
||||||
let host = import.meta.env.DEV ? 'localhost:8002' : location.host;
|
let host = import.meta.env.DEV ? 'localhost:8002' : location.host;
|
||||||
let cmd = encodeURIComponent(command.value)
|
let cmd = encodeURIComponent(command.value)
|
||||||
socket = new WebSocket(`${protocol}${host}/api/container/connect?id=${props.containerId}&cmd=${cmd}`);
|
socket = new WebSocket(`${protocol}${host}/api/container/connect?node=${props.node}&id=${props.id}&cmd=${cmd}`);
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
const fit = new FitAddon();
|
const fit = new FitAddon();
|
||||||
term = new Terminal({ fontSize: 14, cursorBlink: true });
|
term = new Terminal({ fontSize: 14, cursorBlink: true });
|
||||||
@ -57,9 +61,9 @@ function connect() {
|
|||||||
fit.fit();
|
fit.fit();
|
||||||
term.focus();
|
term.focus();
|
||||||
};
|
};
|
||||||
socket.onclose = () => {
|
// socket.onclose = () => {
|
||||||
console.log('close socket')
|
// console.log('close socket')
|
||||||
};
|
// };
|
||||||
socket.onerror = (e) => {
|
socket.onerror = (e) => {
|
||||||
console.log('socket error: ' + e)
|
console.log('socket error: ' + e)
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<n-input :placeholder="t('objects.image')" v-model:value="model.image" />
|
<n-input :placeholder="t('objects.image')" v-model:value="model.image" />
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi :label="t('fields.mode')" path="mode">
|
<n-form-item-gi :label="t('fields.mode')" path="mode">
|
||||||
<n-radio-group v-model:value="model.mode">
|
<n-radio-group v-model:value="model.mode" :disabled="Boolean(model.id)">
|
||||||
<n-radio key="replicated" value="replicated">Replicated</n-radio>
|
<n-radio key="replicated" value="replicated">Replicated</n-radio>
|
||||||
<n-radio key="global" value="global">Global</n-radio>
|
<n-radio key="global" value="global">Global</n-radio>
|
||||||
<n-radio key="replicated-job" value="replicated-job">Replicated Job</n-radio>
|
<n-radio key="replicated-job" value="replicated-job">Replicated Job</n-radio>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<x-anchor :url="`/swarm/services/${model.serviceName}`">{{ model.serviceName }}</x-anchor>
|
<x-anchor :url="`/swarm/services/${model.serviceName}`">{{ model.serviceName }}</x-anchor>
|
||||||
</x-description-item>
|
</x-description-item>
|
||||||
<x-description-item :label="t('objects.container')" :span="2">
|
<x-description-item :label="t('objects.container')" :span="2">
|
||||||
<x-anchor :url="`/local/containers/${model.containerId}`">{{ model.containerId }}</x-anchor>
|
<x-anchor :url="`/local/containers/${model.nodeId}/${model.containerId}`">{{ model.containerId }}</x-anchor>
|
||||||
</x-description-item>
|
</x-description-item>
|
||||||
<x-description-item :label="t('objects.node')" :span="2">
|
<x-description-item :label="t('objects.node')" :span="2">
|
||||||
<x-anchor :url="`/swarm/nodes/${model.nodeId}`">{{ model.nodeId }}</x-anchor>
|
<x-anchor :url="`/swarm/nodes/${model.nodeId}`">{{ model.nodeId }}</x-anchor>
|
||||||
|
@ -213,7 +213,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "container_detail",
|
name: "container_detail",
|
||||||
path: "/local/containers/:id",
|
path: "/local/containers/:node/:id",
|
||||||
component: () => import('../pages/container/View.vue'),
|
component: () => import('../pages/container/View.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { h } from "vue";
|
import { h } from "vue";
|
||||||
import Anchor from "../components/Anchor.vue";
|
import type { RouteLocationRaw } from "vue-router";
|
||||||
import { NButton, NPopconfirm, NSpace, NTag, NTime } from "naive-ui";
|
import { NButton, NPopconfirm, NSpace, NTag, NTime } from "naive-ui";
|
||||||
|
import Anchor from "../components/Anchor.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format duration
|
* Format duration
|
||||||
@ -59,7 +60,7 @@ export function formatSize(value: number) {
|
|||||||
return size.toFixed(2) + ' ' + units[index];
|
return size.toFixed(2) + ' ' + units[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderLink(url: string, text: string) {
|
export function renderLink(url: RouteLocationRaw, text: string) {
|
||||||
return h(Anchor, { url }, { default: () => text })
|
return h(Anchor, { url }, { default: () => text })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user