From cb2cb4ab8644dc479a3cd93510a503fb1c5521bb Mon Sep 17 00:00:00 2001 From: cuigh Date: Fri, 17 Dec 2021 20:13:58 +0800 Subject: [PATCH] Add basic support for agent --- api/container.go | 27 ++-- api/node.go | 13 ++ api/system.go | 2 - biz/container.go | 42 +++--- biz/node.go | 5 + biz/service.go | 39 +++-- docker/container.go | 166 +++++++++++---------- docker/docker.go | 159 ++++++++++++++++++-- docker/node.go | 13 ++ docker/service.go | 27 +--- main.go | 2 +- misc/option.go | 2 + ui/src/api/container.ts | 8 +- ui/src/api/node.ts | 4 + ui/src/components/Anchor.vue | 12 +- ui/src/components/Logs.vue | 5 +- ui/src/pages/container/List.vue | 41 ++++- ui/src/pages/container/View.vue | 7 +- ui/src/pages/container/modules/Execute.vue | 14 +- ui/src/pages/service/Edit.vue | 2 +- ui/src/pages/task/View.vue | 2 +- ui/src/router/router.ts | 2 +- ui/src/utils/render.ts | 5 +- 23 files changed, 402 insertions(+), 197 deletions(-) diff --git a/api/container.go b/api/container.go index bbf02ed..d996761 100644 --- a/api/container.go +++ b/api/container.go @@ -33,6 +33,7 @@ func NewContainer(b biz.ContainerBiz) *ContainerHandler { func containerSearch(b biz.ContainerBiz) web.HandlerFunc { type Args struct { + Node string `json:"node" bind:"node"` Name string `json:"name" bind:"name"` Status string `json:"status" bind:"status"` PageIndex int `json:"pageIndex" bind:"pageIndex"` @@ -47,7 +48,7 @@ func containerSearch(b biz.ContainerBiz) web.HandlerFunc { ) 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 { @@ -63,8 +64,9 @@ func containerSearch(b biz.ContainerBiz) web.HandlerFunc { func containerFind(b biz.ContainerBiz) web.HandlerFunc { return func(ctx web.Context) error { + node := ctx.Query("node") id := ctx.Query("id") - container, raw, err := b.Find(id) + container, raw, err := b.Find(node, id) if err != nil { return err } @@ -74,12 +76,13 @@ func containerFind(b biz.ContainerBiz) web.HandlerFunc { func containerDelete(b biz.ContainerBiz) web.HandlerFunc { type Args struct { - ID string `json:"id"` + 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) } @@ -87,6 +90,7 @@ func containerDelete(b biz.ContainerBiz) web.HandlerFunc { func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc { type Args struct { + Node string `json:"node" bind:"node"` ID string `json:"id" bind:"id"` Lines int `json:"lines" bind:"lines"` Timestamps bool `json:"timestamps" bind:"timestamps"` @@ -98,7 +102,7 @@ func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc { stdout, stderr string ) 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 { return err @@ -110,11 +114,12 @@ func containerFetchLogs(b biz.ContainerBiz) web.HandlerFunc { func containerConnect(b biz.ContainerBiz) web.HandlerFunc { return func(ctx web.Context) error { var ( - id = ctx.Query("id") - cmd = ctx.Query("cmd") + node = ctx.Query("node") + id = ctx.Query("id") + cmd = ctx.Query("cmd") ) - _, _, err := b.Find(id) + _, _, err := b.Find(node, id) if err != nil { return err } @@ -124,17 +129,17 @@ func containerConnect(b biz.ContainerBiz) web.HandlerFunc { return err } - idResp, err := b.ExecCreate(id, cmd) + idResp, err := b.ExecCreate(node, id, cmd) if err != nil { return err } - resp, err := b.ExecAttach(idResp.ID) + resp, err := b.ExecAttach(node, idResp.ID) if err != nil { return err } - err = b.ExecStart(idResp.ID) + err = b.ExecStart(node, idResp.ID) if err != nil { return err } diff --git a/api/node.go b/api/node.go index 42b9894..ed542e6 100644 --- a/api/node.go +++ b/api/node.go @@ -8,6 +8,7 @@ import ( // NodeHandler encapsulates node related handlers. 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"` 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"` @@ -17,6 +18,7 @@ type NodeHandler struct { // NewNode creates an instance of NodeHandler func NewNode(nb biz.NodeBiz) *NodeHandler { return &NodeHandler{ + List: nodeList(nb), Search: nodeSearch(nb), Find: nodeFind(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 { return func(ctx web.Context) error { nodes, err := nb.Search() diff --git a/api/system.go b/api/system.go index d0fc079..6f38521 100644 --- a/api/system.go +++ b/api/system.go @@ -14,8 +14,6 @@ import ( "github.com/cuigh/swirl/model" ) -//var ErrSystemInitialized = errors.New("system was already initialized") - // SystemHandler encapsulates system related handlers. type SystemHandler struct { CheckState web.HandlerFunc `path:"/check-state" auth:"*" desc:"check system state"` diff --git a/biz/container.go b/biz/container.go index d8136ee..ce692db 100644 --- a/biz/container.go +++ b/biz/container.go @@ -12,13 +12,13 @@ import ( ) type ContainerBiz interface { - Search(name, status string, pageIndex, pageSize int) ([]*Container, int, error) - Find(id string) (container *Container, raw string, err error) - Delete(id string, user web.User) (err error) - FetchLogs(id string, lines int, timestamps bool) (stdout, stderr string, err error) - ExecCreate(id string, cmd string) (resp types.IDResponse, err error) - ExecAttach(id string) (resp types.HijackedResponse, err error) - ExecStart(id string) error + 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) + 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 } func NewContainer(d *docker.Docker) ContainerBiz { @@ -29,13 +29,13 @@ type containerBiz struct { 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 ( cj types.ContainerJSON 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) } @@ -45,8 +45,8 @@ func (b *containerBiz) Find(id string) (c *Container, raw string, err error) { return } -func (b *containerBiz) Search(name, status string, pageIndex, pageSize int) (containers []*Container, total int, err error) { - list, total, err := b.d.ContainerList(context.TODO(), name, status, pageIndex, pageSize) +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(), node, name, status, pageIndex, pageSize) if err != nil { return nil, 0, err } @@ -58,28 +58,28 @@ func (b *containerBiz) Search(name, status string, pageIndex, pageSize int) (con return containers, total, nil } -func (b *containerBiz) Delete(id string, user web.User) (err error) { - err = b.d.ContainerRemove(context.TODO(), id) +func (b *containerBiz) Delete(node, id string, user web.User) (err error) { + err = b.d.ContainerRemove(context.TODO(), node, id) //if err == nil { // Event.CreateContainer(model.EventActionDelete, id, user) //} return } -func (b *containerBiz) ExecCreate(id, cmd string) (resp types.IDResponse, err error) { - return b.d.ContainerExecCreate(context.TODO(), id, cmd) +func (b *containerBiz) ExecCreate(node, id, cmd string) (resp types.IDResponse, err error) { + return b.d.ContainerExecCreate(context.TODO(), node, id, cmd) } -func (b *containerBiz) ExecAttach(id string) (resp types.HijackedResponse, err error) { - return b.d.ContainerExecAttach(context.TODO(), id) +func (b *containerBiz) ExecAttach(node, id string) (resp types.HijackedResponse, err error) { + return b.d.ContainerExecAttach(context.TODO(), node, id) } -func (b *containerBiz) ExecStart(id string) error { - return b.d.ContainerExecStart(context.TODO(), id) +func (b *containerBiz) ExecStart(node, id string) error { + return b.d.ContainerExecStart(context.TODO(), node, id) } -func (b *containerBiz) FetchLogs(id string, lines int, timestamps bool) (string, string, error) { - stdout, stderr, err := b.d.ContainerLogs(context.TODO(), id, lines, timestamps) +func (b *containerBiz) FetchLogs(node, id string, lines int, timestamps bool) (string, string, error) { + stdout, stderr, err := b.d.ContainerLogs(context.TODO(), node, id, lines, timestamps) if err != nil { return "", "", err } diff --git a/biz/node.go b/biz/node.go index f10db2e..fe94ca0 100644 --- a/biz/node.go +++ b/biz/node.go @@ -10,6 +10,7 @@ import ( ) type NodeBiz interface { + List() ([]*docker.Node, error) Search() ([]*Node, error) Find(id string) (node *Node, raw string, 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 } +func (b *nodeBiz) List() ([]*docker.Node, error) { + return b.d.NodeListCache() +} + func (b *nodeBiz) Search() ([]*Node, error) { list, err := b.d.NodeList(context.TODO()) if err != nil { diff --git a/biz/service.go b/biz/service.go index 2333990..e15d094 100644 --- a/biz/service.go +++ b/biz/service.go @@ -133,6 +133,16 @@ func (b *serviceBiz) Create(s *Service, user web.User) (err error) { 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 := "" if i := strings.Index(s.Image, "/"); i > 0 { if host := s.Image[:i]; strings.Contains(host, ".") { @@ -161,6 +171,12 @@ func (b *serviceBiz) Update(s *Service, user web.User) (err error) { 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 { b.eb.CreateService(EventActionUpdate, s.Name, user) } @@ -490,9 +506,7 @@ func newServiceBase(s *swarm.Service) *ServiceBase { UpdatedAt: formatTime(s.UpdatedAt), } - if s.ServiceStatus == nil { - // TODO: ServiceStatus is valid from docker api v1.41, so we should calculate count manually here. - } else { + if s.ServiceStatus != nil { service.RunningTasks = s.ServiceStatus.RunningTasks service.DesiredTasks = s.ServiceStatus.DesiredTasks 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.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 spec.TaskTemplate.Networks = nil for _, n := range s.Networks { diff --git a/docker/container.go b/docker/container.go index b859cd9..e7e1757 100644 --- a/docker/container.go +++ b/docker/container.go @@ -15,108 +15,124 @@ import ( ) // 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) { - err = d.call(func(c *client.Client) (err error) { - opts := types.ContainerListOptions{Filters: filters.NewArgs()} - if status == "" { - opts.All = true - } else { - opts.Filters.Add("status", status) - } - if name != "" { - opts.Filters.Add("name", name) - } - - containers, err = c.ContainerList(ctx, opts) - if err == nil { - total = len(containers) - start, end := misc.Page(total, pageIndex, pageSize) - containers = containers[start:end] - } +func (d *Docker) ContainerList(ctx context.Context, node, name, status string, pageIndex, pageSize int) (containers []types.Container, total int, err error) { + var c *client.Client + c, err = d.agent(node) + if err != nil { return - }) + } + + opts := types.ContainerListOptions{Filters: filters.NewArgs()} + if status == "" { + opts.All = true + } else { + opts.Filters.Add("status", status) + } + if name != "" { + opts.Filters.Add("name", name) + } + + containers, err = c.ContainerList(ctx, opts) + if err == nil { + total = len(containers) + start, end := misc.Page(total, pageIndex, pageSize) + containers = containers[start:end] + } return } // 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 - if c, err = d.client(); err == nil { + if c, err = d.agent(node); err == nil { container, raw, err = c.ContainerInspectWithRaw(ctx, id, true) } return } // ContainerRemove remove a container. -func (d *Docker) ContainerRemove(ctx context.Context, id string) error { - return d.call(func(c *client.Client) (err error) { - return c.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}) - }) +func (d *Docker) ContainerRemove(ctx context.Context, node, id string) (err error) { + var c *client.Client + if c, err = d.agent(node); err == nil { + err = c.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}) + } + return } // ContainerExecCreate creates an exec instance. -func (d *Docker) ContainerExecCreate(ctx context.Context, id string, cmd string) (resp types.IDResponse, err error) { - err = d.call(func(c *client.Client) (err error) { - opts := types.ExecConfig{ - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Tty: true, - //User: "root", - Cmd: strings.Split(cmd, " "), - } - //c.DialSession() - resp, err = c.ContainerExecCreate(ctx, id, opts) +func (d *Docker) ContainerExecCreate(ctx context.Context, node, id string, cmd string) (resp types.IDResponse, err error) { + var c *client.Client + c, err = d.agent(node) + if err != nil { return - }) + } + + opts := types.ExecConfig{ + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + //User: "root", + Cmd: strings.Split(cmd, " "), + } + resp, err = c.ContainerExecCreate(ctx, id, opts) return } // 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) { - err = d.call(func(c *client.Client) (err error) { - opts := types.ExecStartCheck{ - Detach: false, - Tty: true, - } - resp, err = c.ContainerExecAttach(ctx, id, opts) - return err - }) +func (d *Docker) ContainerExecAttach(ctx context.Context, node, id string) (resp types.HijackedResponse, err error) { + var c *client.Client + c, err = d.agent(node) + if err != nil { + return + } + + opts := types.ExecStartCheck{ + Detach: false, + Tty: true, + } + resp, err = c.ContainerExecAttach(ctx, id, opts) return } // ContainerExecStart starts an exec instance. -func (d *Docker) ContainerExecStart(ctx context.Context, id string) error { - return d.call(func(c *client.Client) (err error) { - opts := types.ExecStartCheck{ - Detach: false, - Tty: true, - } - return c.ContainerExecStart(ctx, id, opts) - }) +func (d *Docker) ContainerExecStart(ctx context.Context, node, id string) (err error) { + c, err := d.agent(node) + if err != nil { + return err + } + + opts := types.ExecStartCheck{ + Detach: false, + Tty: true, + } + return c.ContainerExecStart(ctx, id, opts) } // 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) { - err = d.call(func(c *client.Client) (err error) { - var ( - rc io.ReadCloser - opts = types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Tail: strconv.Itoa(lines), - Timestamps: timestamps, - //Since: (time.Hour * 24).String() - } - ) - if rc, err = c.ContainerLogs(ctx, id, opts); err == nil { - defer rc.Close() - - stdout = &bytes.Buffer{} - stderr = &bytes.Buffer{} - _, err = stdcopy.StdCopy(stdout, stderr, rc) - } +func (d *Docker) ContainerLogs(ctx context.Context, node, id string, lines int, timestamps bool) (stdout, stderr *bytes.Buffer, err error) { + var c *client.Client + c, err = d.agent(node) + if err != nil { return - }) + } + + var ( + rc io.ReadCloser + opts = types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Tail: strconv.Itoa(lines), + Timestamps: timestamps, + //Since: (time.Hour * 24).String() + } + ) + if rc, err = c.ContainerLogs(ctx, id, opts); err == nil { + defer rc.Close() + + stdout = &bytes.Buffer{} + stderr = &bytes.Buffer{} + _, err = stdcopy.StdCopy(stdout, stderr, rc) + } return } diff --git a/docker/docker.go b/docker/docker.go index 9449320..4100143 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -1,20 +1,23 @@ package docker import ( - "os" + "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" "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/client" ) -const ( - defaultAPIVersion = "1.41" -) - func newVersion(v uint64) swarm.Version { return swarm.Version{Index: v} } @@ -23,6 +26,7 @@ type Docker struct { c *client.Client locker sync.Mutex logger log.Logger + agents sync.Map } func NewDocker() *Docker { @@ -39,22 +43,19 @@ func (d *Docker) call(fn func(c *client.Client) error) error { return err } -func (d *Docker) client() (cli *client.Client, err error) { +func (d *Docker) client() (c *client.Client, err error) { if d.c == nil { d.locker.Lock() defer d.locker.Unlock() if d.c == nil { - apiVersion := misc.Options.DockerAPIVersion - if apiVersion == "" { - apiVersion = defaultAPIVersion - } + var opt client.Opt if misc.Options.DockerEndpoint == "" { - _ = os.Setenv("DOCKER_API_VERSION", apiVersion) - d.c, err = client.NewClientWithOpts(client.FromEnv) + opt = client.FromEnv } 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 { return } @@ -63,6 +64,138 @@ func (d *Docker) client() (cli *client.Client, err error) { 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() { container.Put(NewDocker) } diff --git a/docker/node.go b/docker/node.go index 8d5c997..a020204 100644 --- a/docker/node.go +++ b/docker/node.go @@ -59,3 +59,16 @@ func (d *Docker) NodeInspect(ctx context.Context, id string) (node swarm.Node, r }) 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 +} diff --git a/docker/service.go b/docker/service.go index 48be972..01606de 100644 --- a/docker/service.go +++ b/docker/service.go @@ -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) { var ( - activeNodes map[string]struct{} - m = make(map[string]*swarm.Service) - tasks []swarm.Task - opts = types.TaskListOptions{Filters: filters.NewArgs()} + nodes map[string]*Node + m = make(map[string]*swarm.Service) + tasks []swarm.Task + opts = types.TaskListOptions{Filters: filters.NewArgs()} ) - activeNodes, err = d.findActiveNodes(ctx, c) + nodes, err = d.getNodes() if err != nil { 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 { 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++ } } @@ -265,18 +265,3 @@ func (d *Docker) ServiceSearch(ctx context.Context, args filters.Args) (services }) 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 -} diff --git a/main.go b/main.go index 38b1289..422b86a 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ var ( func main() { 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.Action = func(ctx *app.Context) error { return run.Pipeline(misc.LoadOptions, initSystem, scaler.Start, startServer) diff --git a/misc/option.go b/misc/option.go index ef87c72..65175c3 100644 --- a/misc/option.go +++ b/misc/option.go @@ -16,6 +16,7 @@ var Options = &struct { DBAddress string TokenKey string TokenExpiry time.Duration + Agents []string }{ DBType: "mongo", DBAddress: "mongodb://localhost:27017/swirl", @@ -30,6 +31,7 @@ func bindOptions() { "db_address", "token_key", "token_expiry", + "agents", } for _, key := range keys { config.BindEnv("swirl."+key, strings.ToUpper(key)) diff --git a/ui/src/api/container.ts b/ui/src/api/container.ts index 85d810a..026a37d 100644 --- a/ui/src/api/container.ts +++ b/ui/src/api/container.ts @@ -36,6 +36,7 @@ export interface Container { } export interface SearchArgs { + node?: string; name?: string; status?: string; pageIndex: number; @@ -53,21 +54,22 @@ export interface FindResult { } export interface FetchLogsArgs { + node: string; id: string; lines: number; timestamps: boolean; } export class ContainerApi { - find(id: string) { - return ajax.get('/container/find', { id }) + find(node: string, id: string) { + return ajax.get('/container/find', { node, id }) } search(args: SearchArgs) { return ajax.get('/container/search', args) } - delete(id: string, name: string) { + delete(node: string, id: string, name: string) { return ajax.post>('/container/delete', { id, name }) } diff --git a/ui/src/api/node.ts b/ui/src/api/node.ts index e0d7100..78b7015 100644 --- a/ui/src/api/node.ts +++ b/ui/src/api/node.ts @@ -37,6 +37,10 @@ export class NodeApi { return ajax.get('/node/find', { id }) } + list() { + return ajax.get('/node/list') + } + search() { return ajax.get('/node/search') } diff --git a/ui/src/components/Anchor.vue b/ui/src/components/Anchor.vue index 1b5db3e..a9f622e 100644 --- a/ui/src/components/Anchor.vue +++ b/ui/src/components/Anchor.vue @@ -9,11 +9,11 @@ diff --git a/ui/src/components/Logs.vue b/ui/src/components/Logs.vue index d41929d..8cfcf60 100644 --- a/ui/src/components/Logs.vue +++ b/ui/src/components/Logs.vue @@ -60,6 +60,9 @@ const props = defineProps({ type: String as PropType<'task' | 'container' | 'service'>, required: true, }, + node: { + type: String, + }, id: { type: String, required: true, @@ -88,7 +91,7 @@ async function fetchData() { var r: Result; switch (props.type) { 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 case 'task': r = await taskApi.fetchLogs({ id: props.id, lines: filters.lines, timestamps: filters.timestamps }); diff --git a/ui/src/pages/container/List.vue b/ui/src/pages/container/List.vue index 3ba45bb..f0b6080 100644 --- a/ui/src/pages/container/List.vue +++ b/ui/src/pages/container/List.vue @@ -2,6 +2,14 @@ + {{ t('buttons.search') }} @@ -21,33 +29,40 @@ \ No newline at end of file diff --git a/ui/src/pages/container/View.vue b/ui/src/pages/container/View.vue index 0a180c3..bd28121 100644 --- a/ui/src/pages/container/View.vue +++ b/ui/src/pages/container/View.vue @@ -52,10 +52,10 @@ - + - + @@ -88,10 +88,11 @@ const { t } = useI18n() const route = useRoute(); const model = ref({} as Container); const raw = ref(''); +const node = route.params.node as string || ''; async function fetchData() { 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; raw.value = r.data?.raw as string; } diff --git a/ui/src/pages/container/modules/Execute.vue b/ui/src/pages/container/modules/Execute.vue index 5f4a880..5c42694 100644 --- a/ui/src/pages/container/modules/Execute.vue +++ b/ui/src/pages/container/modules/Execute.vue @@ -27,7 +27,11 @@ import { useI18n } from 'vue-i18n' const { t } = useI18n() const props = defineProps({ - containerId: { + node: { + type: String, + required: true, + }, + id: { type: String, required: true, }, @@ -47,7 +51,7 @@ function connect() { let protocol = (location.protocol === "https:") ? "wss://" : "ws://"; let host = import.meta.env.DEV ? 'localhost:8002' : location.host; 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 = () => { const fit = new FitAddon(); term = new Terminal({ fontSize: 14, cursorBlink: true }); @@ -57,9 +61,9 @@ function connect() { fit.fit(); term.focus(); }; - socket.onclose = () => { - console.log('close socket') - }; + // socket.onclose = () => { + // console.log('close socket') + // }; socket.onerror = (e) => { console.log('socket error: ' + e) } diff --git a/ui/src/pages/service/Edit.vue b/ui/src/pages/service/Edit.vue index b969c46..a8d9073 100644 --- a/ui/src/pages/service/Edit.vue +++ b/ui/src/pages/service/Edit.vue @@ -21,7 +21,7 @@ - + Replicated Global Replicated Job diff --git a/ui/src/pages/task/View.vue b/ui/src/pages/task/View.vue index 1a302bc..555441b 100644 --- a/ui/src/pages/task/View.vue +++ b/ui/src/pages/task/View.vue @@ -22,7 +22,7 @@ {{ model.serviceName }} - {{ model.containerId }} + {{ model.containerId }} {{ model.nodeId }} diff --git a/ui/src/router/router.ts b/ui/src/router/router.ts index d272519..9bf181d 100644 --- a/ui/src/router/router.ts +++ b/ui/src/router/router.ts @@ -213,7 +213,7 @@ const routes: RouteRecordRaw[] = [ }, { name: "container_detail", - path: "/local/containers/:id", + path: "/local/containers/:node/:id", component: () => import('../pages/container/View.vue'), }, { diff --git a/ui/src/utils/render.ts b/ui/src/utils/render.ts index 5616058..95b79fa 100644 --- a/ui/src/utils/render.ts +++ b/ui/src/utils/render.ts @@ -1,6 +1,7 @@ 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 Anchor from "../components/Anchor.vue"; /** * Format duration @@ -59,7 +60,7 @@ export function formatSize(value: number) { 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 }) }