mirror of
https://github.com/cuigh/swirl
synced 2025-05-09 22:30:33 +00:00
Add basic support for agent
This commit is contained in:
parent
94127504ff
commit
cb2cb4ab86
@ -33,6 +33,7 @@ func NewContainer(b biz.ContainerBiz) *ContainerHandler {
|
||||
|
||||
func containerSearch(b biz.ContainerBiz) web.HandlerFunc {
|
||||
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
|
||||
}
|
||||
|
13
api/node.go
13
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()
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
159
docker/docker.go
159
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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
2
main.go
2
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)
|
||||
|
@ -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))
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 });
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'),
|
||||
},
|
||||
{
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user