Add basic support for agent

This commit is contained in:
cuigh 2021-12-17 20:13:58 +08:00
parent 94127504ff
commit cb2cb4ab86
23 changed files with 402 additions and 197 deletions

View File

@ -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
} }

View File

@ -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()

View File

@ -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"`

View File

@ -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
} }

View File

@ -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 {

View File

@ -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 {

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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))

View File

@ -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 })
} }

View File

@ -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')
} }

View File

@ -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>

View File

@ -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 });

View File

@ -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>

View File

@ -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;
} }

View File

@ -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)
} }

View File

@ -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>

View File

@ -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>

View File

@ -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'),
}, },
{ {

View File

@ -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 })
} }