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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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<FindResult>('/container/find', { id })
find(node: string, id: string) {
return ajax.get<FindResult>('/container/find', { node, id })
}
search(args: SearchArgs) {
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 })
}

View File

@ -37,6 +37,10 @@ export class NodeApi {
return ajax.get<FindResult>('/node/find', { id })
}
list() {
return ajax.get<Node[]>('/node/list')
}
search() {
return ajax.get<Node[]>('/node/search')
}

View File

@ -9,11 +9,11 @@
<script setup lang="ts">
import { NA } from "naive-ui";
import { RouterLink } from "vue-router";
import type { RouteLocationRaw } from "vue-router";
const props = defineProps({
url: {
type: String,
required: true,
}
})
interface Props {
url: RouteLocationRaw
}
const props = defineProps<Props>()
</script>

View File

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

View File

@ -2,6 +2,14 @@
<x-page-header />
<n-space class="page-body" vertical :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-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
</n-space>
@ -21,33 +29,40 @@
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { onMounted, reactive, ref } from "vue";
import {
NSpace,
NButton,
NDataTable,
NInput,
NSelect,
} from "naive-ui";
import XPageHeader from "@/components/PageHeader.vue";
import containerApi from "@/api/container";
import type { Container } from "@/api/container";
import nodeApi from "@/api/node";
import { useDataTable } from "@/utils/data-table";
import { renderButton, renderLink, renderTag } from "@/utils/render";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const filter = reactive({
name: "",
node: '',
name: '',
});
const nodes: any = ref([])
const columns = [
{
title: t('fields.name'),
key: "name",
fixed: "left" as const,
render: (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'),
key: "image",
},
{
@ -65,14 +80,24 @@ const columns = [
title: t('fields.actions'),
key: "actions",
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) {
await containerApi.delete(id, "");
async function deleteContainer(c: Container, index: number) {
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)
}
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>

View File

@ -52,10 +52,10 @@
<x-code :code="raw" language="json" />
</n-tab-pane>
<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 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-tabs>
</div>
@ -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;
}

View File

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

View File

@ -21,7 +21,7 @@
<n-input :placeholder="t('objects.image')" v-model:value="model.image" />
</n-form-item-gi>
<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="global" value="global">Global</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-description-item>
<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 :label="t('objects.node')" :span="2">
<x-anchor :url="`/swarm/nodes/${model.nodeId}`">{{ model.nodeId }}</x-anchor>

View File

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

View File

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